if ($this->format == 'xml') {
$this->initDocument('xml');
- $this->showTwitterXmlUser($twitter_user);
+ $this->showTwitterXmlUser($twitter_user, 'user', true);
$this->endDocument('xml');
} elseif ($this->format == 'json') {
$this->initDocument('json');
if ($this->format == 'xml') {
$this->initDocument('xml');
- $this->showTwitterXmlUser($twitter_user);
+ $this->showTwitterXmlUser($twitter_user, 'user', true);
$this->endDocument('xml');
} elseif ($this->format == 'json') {
$this->initDocument('json');
if ($this->format == 'xml') {
$this->initDocument('xml');
- $this->showTwitterXmlUser($twitter_user);
+ $this->showTwitterXmlUser($twitter_user, 'user', true);
$this->endDocument('xml');
} elseif ($this->format == 'json') {
$this->initDocument('json');
if ($this->format == 'xml') {
$this->initDocument('xml');
- $this->showTwitterXmlUser($twitter_user);
+ $this->showTwitterXmlUser($twitter_user, 'user', true);
$this->endDocument('xml');
} elseif ($this->format == 'json') {
$this->initDocument('json');
return;
}
+ $type = $imagefile->preferredType();
$filename = Avatar::filename(
$user->id,
- image_type_to_extension($imagefile->type),
+ image_type_to_extension($type),
null,
'tmp'.common_timestamp()
);
$filepath = Avatar::path($filename);
- move_uploaded_file($imagefile->filepath, $filepath);
+ $imagefile->copyTo($filepath);
$profile = $this->user->getProfile();
if ($this->format == 'xml') {
$this->initDocument('xml');
- $this->showTwitterXmlUser($twitter_user);
+ $this->showTwitterXmlUser($twitter_user, 'user', true);
$this->endDocument('xml');
} elseif ($this->format == 'json') {
$this->initDocument('json');
parent::prepare($args);
$this->group = $this->getTargetGroup($this->arg('id'));
+ if (empty($this->group)) {
+ // TRANS: Client error displayed trying to show group membership on a non-existing group.
+ $this->clientError(_('Group not found.'), 404, $this->format);
+ return false;
+ }
+
$this->profiles = $this->getProfiles();
return true;
{
parent::handle($args);
- if (empty($this->group)) {
- // TRANS: Client error displayed trying to show group membership on a non-existing group.
- $this->clientError(_('Group not found.'), 404, $this->format);
- return false;
- }
-
// XXX: RSS and Atom
switch($this->format) {
function supported($cmd)
{
static $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand',
- 'FavCommand', 'OnCommand', 'OffCommand');
+ 'FavCommand', 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand');
if (in_array(get_class($cmd), $cmdlist)) {
return true;
$profile = Profile::fromURI($uri);
if (!empty($profile)) {
- $options['replies'] = $uri;
+ $options['replies'][] = $uri;
} else {
$group = User_group::staticGet('uri', $uri);
if (!empty($group)) {
- $options['groups'] = $uri;
+ $options['groups'][] = $uri;
} else {
// @fixme: hook for discovery here
common_log(LOG_WARNING, sprintf('AtomPub post with unknown attention URI %s', $uri));
return;
}
+ if (Subscription::exists($this->_profile, $profile)) {
+ // 409 Conflict
+ $this->clientError(sprintf(_('Already subscribed to %s'),
+ $person->id),
+ 409);
+ return;
+ }
+
if (Subscription::start($this->_profile, $profile)) {
$sub = Subscription::pkeyGet(array('subscriber' => $this->_profile->id,
'subscribed' => $profile->id));
}
$cur = common_current_user();
-
+ $type = $imagefile->preferredType();
$filename = Avatar::filename($cur->id,
- image_type_to_extension($imagefile->type),
+ image_type_to_extension($type),
null,
'tmp'.common_timestamp());
$filepath = Avatar::path($filename);
-
- move_uploaded_file($imagefile->filepath, $filepath);
+ $imagefile->copyTo($filepath);
$filedata = array('filename' => $filename,
'filepath' => $filepath,
'width' => $imagefile->width,
'height' => $imagefile->height,
- 'type' => $imagefile->type);
+ 'type' => $type);
$_SESSION['FILEDATA'] = $filedata;
if ($this->trimmed('iamsure') != $iamsure ) {
// TRANS: Notification for user about the text that must be input to be able to delete a user account.
// TRANS: %s is the text that needs to be input.
- $this->_error = sprintf(_('You must write "%s" exactly in the box.', $iamsure));
+ $this->_error = sprintf(_('You must write "%s" exactly in the box.'), $iamsure);
$this->showPage();
return;
}
return;
}
+ $type = $imagefile->preferredType();
$filename = Avatar::filename($this->group->id,
- image_type_to_extension($imagefile->type),
+ image_type_to_extension($type),
null,
'group-temp-'.common_timestamp());
$filepath = Avatar::path($filename);
- move_uploaded_file($imagefile->filepath, $filepath);
+ $imagefile->copyTo($filepath);
$filedata = array('filename' => $filename,
'filepath' => $filepath,
'width' => $imagefile->width,
'height' => $imagefile->height,
- 'type' => $imagefile->type);
+ 'type' => $type);
$_SESSION['FILEDATA'] = $filedata;
}
}
- $mainpage = common_local_url('showgroup', array('nickname' => $nickname));
-
$cur = common_current_user();
// Checked in prepare() above
'location' => $location,
'aliases' => $aliases,
'userid' => $cur->id,
- 'mainpage' => $mainpage,
'local' => true));
common_redirect($group->homeUrl(), 303);
$this->elementStart('div', array('id' => 'aside_primary',
'class' => 'aside'));
+
+ $this->elementStart('div', array('id' => 'account_actions',
+ 'class' => 'section'));
$this->elementStart('ul');
if (Event::handle('StartProfileSettingsActions', array($this))) {
if ($user->hasRight(Right::BACKUPACCOUNT)) {
}
$this->elementEnd('ul');
$this->elementEnd('div');
+ $this->elementEnd('div');
}
}
}
}
- $subscribers->free();
-
$this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
$this->page, 'subscribers',
array('nickname' => $this->user->nickname));
}
}
- $subscriptions->free();
-
$this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
$this->page, 'subscriptions',
array('nickname' => $this->user->nickname));
// Exclude any deleted, non-local, or blocking recipients.
$profile = $this->getProfile();
+ $originalProfile = null;
+ if ($this->repeat_of) {
+ // Check blocks against the original notice's poster as well.
+ $original = Notice::staticGet('id', $this->repeat_of);
+ if ($original) {
+ $originalProfile = $original->getProfile();
+ }
+ }
foreach ($ni as $id => $source) {
$user = User::staticGet('id', $id);
- if (empty($user) || $user->hasBlocked($profile)) {
+ if (empty($user) || $user->hasBlocked($profile) ||
+ ($originalProfile && $user->hasBlocked($originalProfile))) {
unset($ni[$id]);
}
}
}
// MAGICALLY put fields into current scope
+ // @fixme kill extract(); it makes debugging absurdly hard
extract($fields);
// fill in later...
$uri = null;
}
+ if (empty($mainpage)) {
+ $mainpage = common_local_url('showgroup', array('nickname' => $nickname));
+ }
$group->nickname = $nickname;
$group->fullname = $fullname;
('twitvim','TwitVim','http://vim.sourceforge.net/scripts/script.php?script_id=2204', now()),
('Updating.Me','Updating.Me','http://updating.me/', now()),
('urfastr','urfastr','http://urfastr.net/', now()),
- ('yatca','Yatca','http://www.yatca.com/', now());
+ ('yatca','Yatca','http://www.yatca.com/', now()),
+ ('rss.me', 'rss.me', 'http://rss.me/', now());
--- /dev/null
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A class for moving an account to a new server
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Account
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Moves an account from this server to another
+ *
+ * @category Account
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class AccountMover extends QueueHandler
+{
+ function transport()
+ {
+ return 'acctmove';
+ }
+
+ function handle($object)
+ {
+ list($user, $remote, $password) = $object;
+
+ $remote = Discovery::normalize($remote);
+
+ $oprofile = Ostatus_profile::ensureProfileURI($remote);
+
+ if (empty($oprofile)) {
+ throw new Exception("Can't locate account {$remote}");
+ }
+
+ list($svcDocUrl, $username) = self::getServiceDocument($remote);
+
+ $sink = new ActivitySink($svcDocUrl, $username, $password);
+
+ $this->log(LOG_INFO,
+ "Moving user {$user->nickname} ".
+ "to {$remote}.");
+
+ $stream = new UserActivityStream($user);
+
+ // Reverse activities to run in correct chron order
+
+ $acts = array_reverse($stream->activities);
+
+ $this->log(LOG_INFO,
+ "Got ".count($acts)." activities ".
+ "for {$user->nickname}.");
+
+ $qm = QueueManager::get();
+
+ foreach ($acts as $act) {
+ $qm->enqueue(array($act, $sink, $user->uri, $remote), 'actmove');
+ }
+
+ $this->log(LOG_INFO,
+ "Finished moving user {$user->nickname} ".
+ "to {$remote}.");
+ }
+
+ static function getServiceDocument($remote)
+ {
+ $discovery = new Discovery();
+
+ $xrd = $discovery->lookup($remote);
+
+ if (empty($xrd)) {
+ throw new Exception("Can't find XRD for $remote");
+ }
+
+ $svcDocUrl = null;
+ $username = null;
+
+ foreach ($xrd->links as $link) {
+ if ($link['rel'] == 'http://apinamespace.org/atom' &&
+ $link['type'] == 'application/atomsvc+xml') {
+ $svcDocUrl = $link['href'];
+ if (!empty($link['property'])) {
+ foreach ($link['property'] as $property) {
+ if ($property['type'] == 'http://apinamespace.org/atom/username') {
+ $username = $property['value'];
+ break;
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ if (empty($svcDocUrl)) {
+ throw new Exception("No AtomPub API service for $remote.");
+ }
+
+ return array($svcDocUrl, $username);
+ }
+
+ /**
+ * Log some data
+ *
+ * Add a header for our class so we know who did it.
+ *
+ * @param int $level Log level, like LOG_ERR or LOG_INFO
+ * @param string $message Message to log
+ *
+ * @return void
+ */
+
+ protected function log($level, $message)
+ {
+ common_log($level, "AccountMover: " . $message);
+ }
+}
$action_name = $this->action->trimmed('action');
$this->action->elementStart('ul', array('class' => 'nav'));
- if (Event::handle('StartAccountSettingsNav', array(&$this->action))) {
+ if (Event::handle('StartAccountSettingsNav', array($this->action))) {
$user = common_current_user();
if(Event::handle('StartAccountSettingsProfileMenuItem', array($this, &$menu))){
Event::handle('EndAccountSettingsOtherMenuItem', array($this, &$menu));
}
- Event::handle('EndAccountSettingsNav', array(&$this->action));
+ Event::handle('EndAccountSettingsNav', array($this->action));
}
$this->action->elementEnd('ul');
$actorEl = $this->_child($entry, self::ACTOR);
if (!empty($actorEl)) {
+ // Standalone <activity:actor> elements are a holdover from older
+ // versions of ActivityStreams. Newer feeds should have this data
+ // integrated straight into <atom:author>.
$this->actor = new ActivityObject($actorEl);
$this->actor->id = $authorObj->id;
}
}
- } else if (!empty($feed) &&
- $subjectEl = $this->_child($feed, self::SUBJECT)) {
-
- $this->actor = new ActivityObject($subjectEl);
-
} else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
+ // An <atom:author> in the entry overrides any author info on
+ // the surrounding feed.
$this->actor = new ActivityObject($authorEl);
} else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
self::ATOM)) {
+ // If there's no <atom:author> on the entry, it's safe to assume
+ // the containing feed's authorship info applies.
$this->actor = new ActivityObject($authorEl);
+ } else if (!empty($feed) &&
+ $subjectEl = $this->_child($feed, self::SUBJECT)) {
+
+ // Feed subject is used for things like groups.
+ // Should actually possibly not be interpreted as an actor...?
+ $this->actor = new ActivityObject($subjectEl);
}
$contextEl = $this->_child($entry, self::CONTEXT);
*
* @return DOMElement Atom entry
*/
+
function toAtomEntry()
{
return null;
function asString($namespace=false, $author=true, $source=false)
{
$xs = new XMLStringer(true);
+ $this->outputTo($xs, $namespace, $author, $source);
+ return $xs->getString();
+ }
+ function outputTo($xs, $namespace=false, $author=true, $source=false)
+ {
if ($namespace) {
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
$xs->elementEnd('entry');
- $str = $xs->getString();
-
- return $str;
+ return;
}
private function _child($element, $tag, $namespace=self::SPEC)
--- /dev/null
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Title of module
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Cache
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Class comment
+ *
+ * @category General
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class ActivityMover extends QueueHandler
+{
+ function transport()
+ {
+ return 'actmove';
+ }
+
+ function handle($data)
+ {
+ list ($act, $sink, $userURI, $remoteURI) = $data;
+
+ $user = User::staticGet('uri', $userURI);
+ $remote = Profile::fromURI($remoteURI);
+
+ try {
+ $this->moveActivity($act, $sink, $user, $remote);
+ } catch (ClientException $cex) {
+ $this->log(LOG_WARNING,
+ $cex->getMessage());
+ // "don't retry me"
+ return true;
+ } catch (ServerException $sex) {
+ $this->log(LOG_WARNING,
+ $sex->getMessage());
+ // "retry me" (because we think the server might handle it next time)
+ return false;
+ } catch (Exception $ex) {
+ $this->log(LOG_WARNING,
+ $ex->getMessage());
+ // "don't retry me"
+ return true;
+ }
+ }
+
+ function moveActivity($act, $sink, $user, $remote)
+ {
+ if (empty($user)) {
+ throw new Exception("No such user {$act->actor->id}");
+ }
+
+ switch ($act->verb) {
+ case ActivityVerb::FAVORITE:
+ $this->log(LOG_INFO,
+ "Moving favorite of {$act->objects[0]->id} by ".
+ "{$act->actor->id} to {$remote->nickname}.");
+ // push it, then delete local
+ $sink->postActivity($act);
+ $notice = Notice::staticGet('uri', $act->objects[0]->id);
+ if (!empty($notice)) {
+ $fave = Fave::pkeyGet(array('user_id' => $user->id,
+ 'notice_id' => $notice->id));
+ $fave->delete();
+ }
+ break;
+ case ActivityVerb::POST:
+ $this->log(LOG_INFO,
+ "Moving notice {$act->objects[0]->id} by ".
+ "{$act->actor->id} to {$remote->nickname}.");
+ // XXX: send a reshare, not a post
+ $sink->postActivity($act);
+ $notice = Notice::staticGet('uri', $act->objects[0]->id);
+ if (!empty($notice)) {
+ $notice->delete();
+ }
+ break;
+ case ActivityVerb::JOIN:
+ $this->log(LOG_INFO,
+ "Moving group join of {$act->objects[0]->id} by ".
+ "{$act->actor->id} to {$remote->nickname}.");
+ $sink->postActivity($act);
+ $group = User_group::staticGet('uri', $act->objects[0]->id);
+ if (!empty($group)) {
+ Group_member::leave($group->id, $user->id);
+ }
+ break;
+ case ActivityVerb::FOLLOW:
+ if ($act->actor->id == $user->uri) {
+ $this->log(LOG_INFO,
+ "Moving subscription to {$act->objects[0]->id} by ".
+ "{$act->actor->id} to {$remote->nickname}.");
+ $sink->postActivity($act);
+ $other = Profile::fromURI($act->objects[0]->id);
+ if (!empty($other)) {
+ Subscription::cancel($user->getProfile(), $other);
+ }
+ } else {
+ $otherUser = User::staticGet('uri', $act->actor->id);
+ if (!empty($otherUser)) {
+ $this->log(LOG_INFO,
+ "Changing sub to {$act->objects[0]->id}".
+ "by {$act->actor->id} to {$remote->nickname}.");
+ $otherProfile = $otherUser->getProfile();
+ Subscription::start($otherProfile, $remote);
+ Subscription::cancel($otherProfile, $user->getProfile());
+ } else {
+ $this->log(LOG_NOTICE,
+ "Not changing sub to {$act->objects[0]->id}".
+ "by remote {$act->actor->id} ".
+ "to {$remote->nickname}.");
+ }
+ }
+ break;
+ }
+ }
+
+ /**
+ * Log some data
+ *
+ * Add a header for our class so we know who did it.
+ *
+ * @param int $level Log level, like LOG_ERR or LOG_INFO
+ * @param string $message Message to log
+ *
+ * @return void
+ */
+
+ protected function log($level, $message)
+ {
+ common_log($level, "ActivityMover: " . $message);
+ }
+}
--- /dev/null
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A remote, atompub-receiving service
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category AtomPub
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * A remote service that supports AtomPub
+ *
+ * @category AtomPub
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class ActivitySink
+{
+ protected $svcDocUrl = null;
+ protected $username = null;
+ protected $password = null;
+ protected $collections = array();
+
+ function __construct($svcDocUrl, $username, $password)
+ {
+ $this->svcDocUrl = $svcDocUrl;
+ $this->username = $username;
+ $this->password = $password;
+
+ $this->_parseSvcDoc();
+ }
+
+ private function _parseSvcDoc()
+ {
+ $client = new HTTPClient();
+ $response = $client->get($this->svcDocUrl);
+
+ if ($response->getStatus() != 200) {
+ throw new Exception("Can't get {$this->svcDocUrl}; response status " . $response->getStatus());
+ }
+
+ $xml = $response->getBody();
+
+ $dom = new DOMDocument();
+
+ // We don't want to bother with white spaces
+ $dom->preserveWhiteSpace = false;
+
+ // Don't spew XML warnings to output
+ $old = error_reporting();
+ error_reporting($old & ~E_WARNING);
+ $ok = $dom->loadXML($xml);
+ error_reporting($old);
+
+ $path = new DOMXPath($dom);
+
+ $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
+ $path->registerNamespace('app', 'http://www.w3.org/2007/app');
+ $path->registerNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
+
+ $collections = $path->query('//app:collection');
+
+ for ($i = 0; $i < $collections->length; $i++) {
+ $collection = $collections->item($i);
+ $url = $collection->getAttribute('href');
+ $takesEntries = false;
+ $accepts = $path->query('app:accept', $collection);
+ for ($j = 0; $j < $accepts->length; $j++) {
+ $accept = $accepts->item($j);
+ $acceptValue = $accept->nodeValue;
+ if (preg_match('#application/atom\+xml(;\s*type=entry)?#', $acceptValue)) {
+ $takesEntries = true;
+ break;
+ }
+ }
+ if (!$takesEntries) {
+ continue;
+ }
+ $verbs = $path->query('activity:verb', $collection);
+ if ($verbs->length == 0) {
+ $this->_addCollection(ActivityVerb::POST, $url);
+ } else {
+ for ($k = 0; $k < $verbs->length; $k++) {
+ $verb = $verbs->item($k);
+ $this->_addCollection($verb->nodeValue, $url);
+ }
+ }
+ }
+ }
+
+ private function _addCollection($verb, $url)
+ {
+ if (array_key_exists($verb, $this->collections)) {
+ $this->collections[$verb][] = $url;
+ } else {
+ $this->collections[$verb] = array($url);
+ }
+ return;
+ }
+
+ function postActivity($activity)
+ {
+ if (!array_key_exists($activity->verb, $this->collections)) {
+ throw new Exception("No collection for verb {$activity->verb}");
+ } else {
+ if (count($this->collections[$activity->verb]) > 1) {
+ common_log(LOG_NOTICE, "More than one collection for verb {$activity->verb}");
+ }
+ $this->postToCollection($this->collections[$activity->verb][0], $activity);
+ }
+ }
+
+ function postToCollection($url, $activity)
+ {
+ $client = new HTTPClient($url);
+
+ $client->setMethod('POST');
+ $client->setAuth($this->username, $this->password);
+ $client->setHeader('Content-Type', 'application/atom+xml;type=entry');
+ $client->setBody($activity->asString(true, true, true));
+
+ $response = $client->send();
+
+ $status = $response->getStatus();
+ $reason = $response->getReasonPhrase();
+
+ if ($status >= 200 && $status < 300) {
+ return true;
+ } else if ($status >= 400 && $status < 500) {
+ throw new ClientException("{$url} {$status} {$reason}");
+ } else if ($status >= 500 && $status < 600) {
+ throw new ServerException("{$url} {$status} {$reason}");
+ } else {
+ // That's unexpected.
+ throw new Exception("{$url} {$status} {$reason}");
+ }
+ }
+}
{
if (empty($id)) {
if (self::is_decimal($this->arg('id'))) {
- return User_group::staticGet($this->arg('id'));
+ return User_group::staticGet('id', $this->arg('id'));
} else if ($this->arg('id')) {
- $nickname = common_canonical_nickname($this->arg('id'));
- $local = Local_group::staticGet('nickname', $nickname);
- if (empty($local)) {
- return null;
- } else {
- return User_group::staticGet('id', $local->id);
- }
+ return User_group::getForNickname($this->arg('id'));
} else if ($this->arg('group_id')) {
- // This is to ensure that a non-numeric user_id still
- // overrides screen_name even if it doesn't get used
+ // This is to ensure that a non-numeric group_id still
+ // overrides group_name even if it doesn't get used
if (self::is_decimal($this->arg('group_id'))) {
return User_group::staticGet('id', $this->arg('group_id'));
}
} else if ($this->arg('group_name')) {
- $nickname = common_canonical_nickname($this->arg('group_name'));
- $local = Local_group::staticGet('nickname', $nickname);
- if (empty($local)) {
- return null;
- } else {
- return User_group::staticGet('id', $local->group_id);
- }
+ return User_group::getForNickname($this->arg('group_name'));
}
} else if (self::is_decimal($id)) {
- return User_group::staticGet($id);
+ return User_group::staticGet('id', $id);
} else {
- $nickname = common_canonical_nickname($id);
- $local = Local_group::staticGet('nickname', $nickname);
- if (empty($local)) {
- return null;
- } else {
- return User_group::staticGet('id', $local->group_id);
- }
+ return User_group::getForNickname($id);
}
}
function __call($name, $args)
{
$item =& $this->_items[$this->_i];
+ if (!is_object($item)) {
+ common_log(LOG_ERR, "Invalid entry " . var_export($item, true) . " at index $this->_i of $this->N; calling $name()");
+ throw new ServerException("Internal error: bad entry in array wrapper list.");
+ }
return call_user_func_array(array($item, $name), $args);
}
}
//exit with 200 response, if this is checking fancy from the installer
if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') { exit; }
-define('STATUSNET_BASE_VERSION', '0.9.8');
-define('STATUSNET_LIFECYCLE', 'dev'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
+define('STATUSNET_BASE_VERSION', '0.9.7');
+define('STATUSNET_LIFECYCLE', 'alpha1'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('STATUSNET_VERSION', STATUSNET_BASE_VERSION . STATUSNET_LIFECYCLE);
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
-define('STATUSNET_CODENAME', 'Letter Never Sent');
+define('STATUSNET_CODENAME', 'World Leader Pretend');
define('AVATAR_PROFILE_SIZE', 96);
define('AVATAR_STREAM_SIZE', 48);
return StatusNet::haveConfig();
}
-function __autoload($cls)
+/**
+ * Wrapper for class autoloaders.
+ * This used to be the special function name __autoload(), but that causes bugs with PHPUnit 3.5+
+ */
+function autoload_sn($cls)
{
if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) {
require_once(INSTALLDIR.'/classes/' . $cls . '.php');
}
}
+spl_autoload_register('autoload_sn');
+
// XXX: how many of these could be auto-loaded on use?
// XXX: note that these files should not use config options
// at compile time since DB config options are not yet loaded.
$action_name = $this->action->trimmed('action');
$this->action->elementStart('ul', array('class' => 'nav'));
- if (Event::handle('StartConnectSettingsNav', array(&$this->action))) {
+ if (Event::handle('StartConnectSettingsNav', array($this->action))) {
# action => array('prompt', 'title')
$menu = array();
$action_name === $menuaction);
}
- Event::handle('EndConnectSettingsNav', array(&$this->action));
+ Event::handle('EndConnectSettingsNav', array($this->action));
}
$this->action->elementEnd('ul');
--- /dev/null
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Use Hammer discovery stack to find out interesting things about an URI
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Discovery
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * This class implements LRDD-based service discovery based on the "Hammer Draft"
+ * (including webfinger)
+ *
+ * @category Discovery
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ *
+ * @see http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf
+ */
+
+class Discovery
+{
+ const LRDD_REL = 'lrdd';
+ const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
+ const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
+ const HCARD = 'http://microformats.org/profile/hcard';
+
+ public $methods = array();
+
+ /**
+ * Constructor for a discovery object
+ *
+ * Registers different discovery methods.
+ *
+ * @return Discovery this
+ */
+
+ public function __construct()
+ {
+ $this->registerMethod('Discovery_LRDD_Host_Meta');
+ $this->registerMethod('Discovery_LRDD_Link_Header');
+ $this->registerMethod('Discovery_LRDD_Link_HTML');
+ }
+
+ /**
+ * Register a discovery class
+ *
+ * @param string $class Class name
+ *
+ * @return void
+ */
+
+ public function registerMethod($class)
+ {
+ $this->methods[] = $class;
+ }
+
+ /**
+ * Given a "user id" make sure it's normalized to either a webfinger
+ * acct: uri or a profile HTTP URL.
+ *
+ * @param string $user_id User ID to normalize
+ *
+ * @return string normalized acct: or http(s)?: URI
+ */
+
+ public static function normalize($user_id)
+ {
+ if (substr($user_id, 0, 5) == 'http:' ||
+ substr($user_id, 0, 6) == 'https:' ||
+ substr($user_id, 0, 5) == 'acct:') {
+ return $user_id;
+ }
+
+ if (strpos($user_id, '@') !== false) {
+ return 'acct:' . $user_id;
+ }
+
+ return 'http://' . $user_id;
+ }
+
+ /**
+ * Determine if a string is a Webfinger ID
+ *
+ * Webfinger IDs look like foo@example.com or acct:foo@example.com
+ *
+ * @param string $user_id ID to check
+ *
+ * @return boolean true if $user_id is a Webfinger, else false
+ */
+
+ public static function isWebfinger($user_id)
+ {
+ $uri = Discovery::normalize($user_id);
+
+ return (substr($uri, 0, 5) == 'acct:');
+ }
+
+ /**
+ * Given a user ID, return the first available XRD
+ *
+ * @param string $id User ID URI
+ *
+ * @return XRD XRD object for the user
+ */
+
+ public function lookup($id)
+ {
+ // Normalize the incoming $id to make sure we have a uri
+ $uri = $this->normalize($id);
+
+ foreach ($this->methods as $class) {
+ $links = call_user_func(array($class, 'discover'), $uri);
+ if ($link = Discovery::getService($links, Discovery::LRDD_REL)) {
+ // Load the LRDD XRD
+ if (!empty($link['template'])) {
+ $xrd_uri = Discovery::applyTemplate($link['template'], $uri);
+ } else {
+ $xrd_uri = $link['href'];
+ }
+
+ $xrd = $this->fetchXrd($xrd_uri);
+ if ($xrd) {
+ return $xrd;
+ }
+ }
+ }
+
+ // TRANS: Exception.
+ throw new Exception(sprintf(_('Unable to find services for %s.'), $id));
+ }
+
+ /**
+ * Given an array of links, returns the matching service
+ *
+ * @param array $links Links to check
+ * @param string $service Service to find
+ *
+ * @return array $link assoc array representing the link
+ */
+
+ public static function getService($links, $service)
+ {
+ if (!is_array($links)) {
+ return false;
+ }
+
+ foreach ($links as $link) {
+ if ($link['rel'] == $service) {
+ return $link;
+ }
+ }
+ }
+
+ /**
+ * Apply a template using an ID
+ *
+ * Replaces {uri} in template string with the ID given.
+ *
+ * @param string $template Template to match
+ * @param string $id User ID to replace with
+ *
+ * @return string replaced values
+ */
+
+ public static function applyTemplate($template, $id)
+ {
+ $template = str_replace('{uri}', urlencode($id), $template);
+
+ return $template;
+ }
+
+ /**
+ * Fetch an XRD file and parse
+ *
+ * @param string $url URL of the XRD
+ *
+ * @return XRD object representing the XRD file
+ */
+
+ public static function fetchXrd($url)
+ {
+ try {
+ $client = new HTTPClient();
+ $response = $client->get($url);
+ } catch (HTTP_Request2_Exception $e) {
+ return false;
+ }
+
+ if ($response->getStatus() != 200) {
+ return false;
+ }
+
+ return XRD::parse($response->getBody());
+ }
+}
+
+/**
+ * Abstract interface for discovery
+ *
+ * Objects that implement this interface can retrieve an array of
+ * XRD links for the URI.
+ *
+ * @category Discovery
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+interface Discovery_LRDD
+{
+ /**
+ * Discover interesting info about the URI
+ *
+ * @param string $uri URI to inquire about
+ *
+ * @return array Links in the XRD file
+ */
+
+ public function discover($uri);
+}
+
+/**
+ * Implementation of discovery using host-meta file
+ *
+ * Discovers XRD file for a user by going to the organization's
+ * host-meta file and trying to find a template for LRDD.
+ *
+ * @category Discovery
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class Discovery_LRDD_Host_Meta implements Discovery_LRDD
+{
+ /**
+ * Discovery core method
+ *
+ * For Webfinger and HTTP URIs, fetch the host-meta file
+ * and look for LRDD templates
+ *
+ * @param string $uri URI to inquire about
+ *
+ * @return array Links in the XRD file
+ */
+
+ public function discover($uri)
+ {
+ if (Discovery::isWebfinger($uri)) {
+ // We have a webfinger acct: - start with host-meta
+ list($name, $domain) = explode('@', $uri);
+ } else {
+ $domain = parse_url($uri, PHP_URL_HOST);
+ }
+
+ $url = 'http://'. $domain .'/.well-known/host-meta';
+
+ $xrd = Discovery::fetchXrd($url);
+
+ if ($xrd) {
+ if ($xrd->host != $domain) {
+ return false;
+ }
+
+ return $xrd->links;
+ }
+ }
+}
+
+/**
+ * Implementation of discovery using HTTP Link header
+ *
+ * Discovers XRD file for a user by fetching the URL and reading any
+ * Link: headers in the HTTP response.
+ *
+ * @category Discovery
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class Discovery_LRDD_Link_Header implements Discovery_LRDD
+{
+ /**
+ * Discovery core method
+ *
+ * For HTTP IDs fetch the URL and look for Link headers.
+ *
+ * @param string $uri URI to inquire about
+ *
+ * @return array Links in the XRD file
+ *
+ * @todo fail out of Webfinger URIs faster
+ */
+
+ public function discover($uri)
+ {
+ try {
+ $client = new HTTPClient();
+ $response = $client->get($uri);
+ } catch (HTTP_Request2_Exception $e) {
+ return false;
+ }
+
+ if ($response->getStatus() != 200) {
+ return false;
+ }
+
+ $link_header = $response->getHeader('Link');
+ if (!$link_header) {
+ // return false;
+ }
+
+ return array(Discovery_LRDD_Link_Header::parseHeader($link_header));
+ }
+
+ /**
+ * Given a string or array of headers, returns XRD-like assoc array
+ *
+ * @param string|array $header string or array of strings for headers
+ *
+ * @return array Link header in XRD-like format
+ */
+
+ protected static function parseHeader($header)
+ {
+ $lh = new LinkHeader($header);
+
+ return array('href' => $lh->href,
+ 'rel' => $lh->rel,
+ 'type' => $lh->type);
+ }
+}
+
+/**
+ * Implementation of discovery using HTML <link> element
+ *
+ * Discovers XRD file for a user by fetching the URL and reading any
+ * <link> elements in the HTML response.
+ *
+ * @category Discovery
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class Discovery_LRDD_Link_HTML implements Discovery_LRDD
+{
+ /**
+ * Discovery core method
+ *
+ * For HTTP IDs, fetch the URL and look for <link> elements
+ * in the HTML response.
+ *
+ * @param string $uri URI to inquire about
+ *
+ * @return array Links in XRD-ish assoc array
+ *
+ * @todo fail out of Webfinger URIs faster
+ */
+
+ public function discover($uri)
+ {
+ try {
+ $client = new HTTPClient();
+ $response = $client->get($uri);
+ } catch (HTTP_Request2_Exception $e) {
+ return false;
+ }
+
+ if ($response->getStatus() != 200) {
+ return false;
+ }
+
+ return Discovery_LRDD_Link_HTML::parse($response->getBody());
+ }
+
+ /**
+ * Parse HTML and return <link> elements
+ *
+ * Given an HTML string, scans the string for <link> elements
+ *
+ * @param string $html HTML to scan
+ *
+ * @return array array of associative arrays in XRD-ish format
+ */
+
+ public function parse($html)
+ {
+ $links = array();
+
+ preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
+ $head_html = $head_matches[2];
+
+ preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
+
+ foreach ($link_matches[0] as $link_html) {
+ $link_url = null;
+ $link_rel = null;
+ $link_type = null;
+
+ preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
+ if ( isset($rel_matches[3]) ) {
+ $link_rel = $rel_matches[3];
+ } else if ( isset($rel_matches[1]) ) {
+ $link_rel = $rel_matches[1];
+ }
+
+ preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
+ if ( isset($href_matches[3]) ) {
+ $link_uri = $href_matches[3];
+ } else if ( isset($href_matches[1]) ) {
+ $link_uri = $href_matches[1];
+ }
+
+ preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
+ if ( isset($type_matches[3]) ) {
+ $link_type = $type_matches[3];
+ } else if ( isset($type_matches[1]) ) {
+ $link_type = $type_matches[1];
+ }
+
+ $links[] = array(
+ 'href' => $link_url,
+ 'rel' => $link_rel,
+ 'type' => $link_type,
+ );
+ }
+
+ return $links;
+ }
+}
*/
function resize($size, $x = 0, $y = 0, $w = null, $h = null)
{
- $targetType = $this->preferredType($this->type);
+ $targetType = $this->preferredType();
$outname = Avatar::filename($this->id,
image_type_to_extension($targetType),
$size,
return $outname;
}
+ /**
+ * Copy the image file to the given destination.
+ * For obscure formats, this will automatically convert to PNG;
+ * otherwise the original file will be copied as-is.
+ *
+ * @param string $outpath
+ * @return string filename
+ */
+ function copyTo($outpath)
+ {
+ return $this->resizeTo($outpath, $this->width, $this->height);
+ }
+
/**
* Create and save a thumbnail image.
*
{
$w = ($w === null) ? $this->width:$w;
$h = ($h === null) ? $this->height:$h;
- $targetType = $this->preferredType($this->type);
+ $targetType = $this->preferredType();
if (!file_exists($this->filepath)) {
throw new Exception(_('Lost our file.'));
/**
* Several obscure file types should be normalized to PNG on resize.
*
- * @param int $type
+ * @fixme consider flattening anything not GIF or JPEG to PNG
* @return int
*/
- function preferredType($type)
+ function preferredType()
{
- if($type == IMAGETYPE_BMP) {
+ if($this->type == IMAGETYPE_BMP) {
//we don't want to save BMP... it's an inefficient, rare, antiquated format
//save png instead
return IMAGETYPE_PNG;
- } else if($type == IMAGETYPE_WBMP) {
+ } else if($this->type == IMAGETYPE_WBMP) {
//we don't want to save WBMP... it's a rare format that we can't guarantee clients will support
//save png instead
return IMAGETYPE_PNG;
- } else if($type == IMAGETYPE_XBM) {
+ } else if($this->type == IMAGETYPE_XBM) {
//we don't want to save XBM... it's a rare format that we can't guarantee clients will support
//save png instead
return IMAGETYPE_PNG;
}
- return $type;
+ return $this->type;
}
function unlink()
--- /dev/null
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Parse HTTP response for interesting Link: headers
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Discovery
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Class to represent Link: headers in an HTTP response
+ *
+ * Since these are a fairly important part of Hammer-stack discovery, they're
+ * reified and implemented here.
+ *
+ * @category Discovery
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ *
+ * @see Discovery
+ */
+
+class LinkHeader
+{
+ var $href;
+ var $rel;
+ var $type;
+
+ /**
+ * Initialize from a string
+ *
+ * @param string $str Link: header value
+ *
+ * @return LinkHeader self
+ */
+
+ function __construct($str)
+ {
+ preg_match('/^<[^>]+>/', $str, $uri_reference);
+ //if (empty($uri_reference)) return;
+
+ $this->href = trim($uri_reference[0], '<>');
+ $this->rel = array();
+ $this->type = null;
+
+ // remove uri-reference from header
+ $str = substr($str, strlen($uri_reference[0]));
+
+ // parse link-params
+ $params = explode(';', $str);
+
+ foreach ($params as $param) {
+ if (empty($param)) {
+ continue;
+ }
+ list($param_name, $param_value) = explode('=', $param, 2);
+
+ $param_name = trim($param_name);
+ $param_value = preg_replace('(^"|"$)', '', trim($param_value));
+
+ // for now we only care about 'rel' and 'type' link params
+ // TODO do something with the other links-params
+ switch ($param_name) {
+ case 'rel':
+ $this->rel = trim($param_value);
+ break;
+
+ case 'type':
+ $this->type = trim($param_value);
+ }
+ }
+ }
+
+ /**
+ * Given an HTTP response, return the requested Link: header
+ *
+ * @param HTTP_Request2_Response $response response to check
+ * @param string $rel relationship to look for
+ * @param string $type media type to look for
+ *
+ * @return LinkHeader discovered header, or null on failure
+ */
+
+ static function getLink($response, $rel=null, $type=null)
+ {
+ $headers = $response->getHeader('Link');
+ if ($headers) {
+ // Can get an array or string, so try to simplify the path
+ if (!is_array($headers)) {
+ $headers = array($headers);
+ }
+
+ foreach ($headers as $header) {
+ $lh = new LinkHeader($header);
+
+ if ((is_null($rel) || $lh->rel == $rel) &&
+ (is_null($type) || $lh->type == $type)) {
+ return $lh->href;
+ }
+ }
+ }
+ return null;
+ }
+}
$this->action->elementStart('ul', array('class' => 'nav'));
- if (Event::handle('StartLoginGroupNav', array(&$this->action))) {
+ if (Event::handle('StartLoginGroupNav', array($this->action))) {
$this->action->menuItem(common_local_url('login'),
_('Login'),
$action_name === 'register');
}
- Event::handle('EndLoginGroupNav', array(&$this->action));
+ Event::handle('EndLoginGroupNav', array($this->action));
}
$this->action->elementEnd('ul');
$domain = mail_domain();
- $notifyfrom = '"'.common_config('site', 'name') .'" <noreply@'.$domain.'>';
+ $notifyfrom = '"'. str_replace('"', '\\"', common_config('site', 'name')) .'" <noreply@'.$domain.'>';
}
return $notifyfrom;
$this->showStatistics();
}
+ /**
+ * Convenience function for common pattern of links to subscription/groups sections.
+ *
+ * @param string $actionClass
+ * @param string $title
+ * @param string $cssClass
+ */
+ private function statsSectionLink($actionClass, $title, $cssClass='')
+ {
+ $this->element('a', array('href' => common_local_url($actionClass,
+ array('nickname' => $this->profile->nickname)),
+ 'class' => $cssClass),
+ $title);
+ }
+
function showSubscriptions()
{
$profile = $this->profile->getSubscriptions(0, PROFILES_PER_MINILIST + 1);
$this->elementStart('div', array('id' => 'entity_subscriptions',
'class' => 'section'));
if (Event::handle('StartShowSubscriptionsMiniList', array($this))) {
- $this->element('h2', null, _('Subscriptions'));
+ $this->elementStart('h2');
+ $this->statsSectionLink('subscriptions', _('Subscriptions'));
+ $this->elementEnd('h2');
$cnt = 0;
if ($cnt > PROFILES_PER_MINILIST) {
$this->elementStart('p');
- $this->element('a', array('href' => common_local_url('subscriptions',
- array('nickname' => $this->profile->nickname)),
- 'class' => 'more'),
- _('All subscriptions'));
+ $this->statsSectionLink('subscriptions', _('All subscriptions'), 'more');
$this->elementEnd('p');
}
if (Event::handle('StartShowSubscribersMiniList', array($this))) {
- $this->element('h2', null, _('Subscribers'));
+ $this->elementStart('h2');
+ $this->statsSectionLink('subscribers', _('Subscribers'));
+ $this->elementEnd('h2');
$cnt = 0;
if ($cnt > PROFILES_PER_MINILIST) {
$this->elementStart('p');
- $this->element('a', array('href' => common_local_url('subscribers',
- array('nickname' => $this->profile->nickname)),
- 'class' => 'more'),
- _('All subscribers'));
+ $this->statsSectionLink('subscribers', _('All subscribers'), 'more');
$this->elementEnd('p');
}
function showStatistics()
{
- $subs_count = $this->profile->subscriptionCount();
- $subbed_count = $this->profile->subscriberCount();
$notice_count = $this->profile->noticeCount();
- $group_count = $this->profile->getGroups()->N;
$age_days = (time() - strtotime($this->profile->created)) / 86400;
if ($age_days < 1) {
// Rather than extrapolating out to a bajillion...
$this->element('h2', null, _('Statistics'));
- // Other stats...?
- $this->elementStart('dl', 'entity_user-id');
- $this->element('dt', null, _('User ID'));
- $this->element('dd', null, $this->profile->id);
- $this->elementEnd('dl');
-
- $this->elementStart('dl', 'entity_member-since');
- $this->element('dt', null, _('Member since'));
- $this->element('dd', null, date('j M Y',
- strtotime($this->profile->created)));
- $this->elementEnd('dl');
-
- $this->elementStart('dl', 'entity_subscriptions');
- $this->elementStart('dt');
- $this->element('a', array('href' => common_local_url('subscriptions',
- array('nickname' => $this->profile->nickname))),
- _('Subscriptions'));
- $this->elementEnd('dt');
- $this->element('dd', null, $subs_count);
- $this->elementEnd('dl');
-
- $this->elementStart('dl', 'entity_subscribers');
- $this->elementStart('dt');
- $this->element('a', array('href' => common_local_url('subscribers',
- array('nickname' => $this->profile->nickname))),
- _('Subscribers'));
- $this->elementEnd('dt');
- $this->element('dd', 'subscribers', $subbed_count);
- $this->elementEnd('dl');
+ $profile = $this->profile;
+ $actionParams = array('nickname' => $profile->nickname);
+ $stats = array(
+ array(
+ 'id' => 'user-id',
+ 'label' => _('User ID'),
+ 'value' => $profile->id,
+ ),
+ array(
+ 'id' => 'member-since',
+ 'label' => _('Member since'),
+ 'value' => date('j M Y', strtotime($profile->created))
+ ),
+ array(
+ 'id' => 'subscriptions',
+ 'label' => _('Subscriptions'),
+ 'link' => common_local_url('subscriptions', $actionParams),
+ 'value' => $profile->subscriptionCount(),
+ ),
+ array(
+ 'id' => 'subscribers',
+ 'label' => _('Subscribers'),
+ 'link' => common_local_url('subscribers', $actionParams),
+ 'value' => $profile->subscriberCount(),
+ ),
+ array(
+ 'id' => 'groups',
+ 'label' => _('Groups'),
+ 'link' => common_local_url('usergroups', $actionParams),
+ 'value' => $profile->getGroups()->N,
+ ),
+ array(
+ 'id' => 'notices',
+ 'label' => _('Notices'),
+ 'value' => $notice_count,
+ ),
+ array(
+ 'id' => 'daily_notices',
+ // TRANS: Average count of posts made per day since account registration
+ 'label' => _('Daily average'),
+ 'value' => $daily_count
+ )
+ );
+
+ // Give plugins a chance to add stats entries
+ Event::handle('ProfileStats', array($profile, &$stats));
+
+ foreach ($stats as $row) {
+ $this->showStatsRow($row);
+ }
+ $this->elementEnd('div');
+ }
- $this->elementStart('dl', 'entity_groups');
+ private function showStatsRow($row)
+ {
+ $this->elementStart('dl', 'entity_' . $row['id']);
$this->elementStart('dt');
- $this->element('a', array('href' => common_local_url('usergroups',
- array('nickname' => $this->profile->nickname))),
- _('Groups'));
+ if (!empty($row['link'])) {
+ $this->element('a', array('href' => $row['link']), $row['label']);
+ } else {
+ $this->text($row['label']);
+ }
$this->elementEnd('dt');
- $this->element('dd', 'groups', $group_count);
+ $this->element('dd', null, $row['value']);
$this->elementEnd('dl');
-
- $this->elementStart('dl', 'entity_notices');
- $this->element('dt', null, _('Notices'));
- $this->element('dd', null, $notice_count);
- $this->elementEnd('dl');
-
- $this->elementStart('dl', 'entity_daily_notices');
- // TRANS: Average count of posts made per day since account registration
- $this->element('dt', null, _('Daily average'));
- $this->element('dd', null, $daily_count);
- $this->elementEnd('dl');
-
- $this->elementEnd('div');
}
function showGroups()
$this->elementStart('div', array('id' => 'entity_groups',
'class' => 'section'));
if (Event::handle('StartShowGroupsMiniList', array($this))) {
- $this->element('h2', null, _('Groups'));
+ $this->elementStart('h2');
+ $this->statsSectionLink('usergroups', _('Groups'));
+ $this->elementEnd('h2');
if ($groups) {
$gml = new GroupMiniList($groups, $this->profile, $this);
if ($cnt > GROUPS_PER_MINILIST) {
$this->elementStart('p');
- $this->element('a', array('href' => common_local_url('usergroups',
- array('nickname' => $this->profile->nickname)),
- 'class' => 'more'),
- _('All groups'));
+ $this->statsSectionLink('usergroups', _('All groups'), 'more');
$this->elementEnd('p');
}
$this->connect('deluser', 'DelUserQueueHandler');
$this->connect('feedimp', 'FeedImporter');
$this->connect('actimp', 'ActivityImporter');
+ $this->connect('acctmove', 'AccountMover');
+ $this->connect('actmove', 'ActivityMover');
// Broadcasting profile updates to OMB remote subscribers
$this->connect('profile', 'ProfileQueueHandler');
class UserActivityStream extends AtomUserNoticeFeed
{
+ public $activities = array();
+
function __construct($user, $indent = true)
{
parent::__construct($user, null, $indent);
usort($objs, 'UserActivityStream::compareObject');
foreach ($objs as $obj) {
- $act = $obj->asActivity();
+ $this->activities[] = $obj->asActivity();
+ }
+ }
+
+ function renderEntries()
+ {
+ foreach ($this->activities as $act) {
// Only show the author sub-element if it's different from default user
- $str = $act->asString(false, ($act->actor->id != $this->user->uri));
- $this->addEntryRaw($str);
+ $act->outputTo($this, false, ($act->actor->id != $this->user->uri));
}
}
$this->out->elementEnd('div');
return;
}
- if (Event::handle('StartProfilePageActionsSection', array(&$this->out, $this->profile))) {
+ if (Event::handle('StartProfilePageActionsSection', array($this->out, $this->profile))) {
$cur = common_current_user();
$this->out->element('h2', null, _('User actions'));
$this->out->elementStart('ul');
- if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) {
+ if (Event::handle('StartProfilePageActionsElements', array($this->out, $this->profile))) {
if (empty($cur)) { // not logged in
- if (Event::handle('StartProfileRemoteSubscribe', array(&$this->out, $this->profile))) {
+ if (Event::handle('StartProfileRemoteSubscribe', array($this->out, $this->profile))) {
$this->out->elementStart('li', 'entity_subscribe');
$this->showRemoteSubscribeLink();
$this->out->elementEnd('li');
- Event::handle('EndProfileRemoteSubscribe', array(&$this->out, $this->profile));
+ Event::handle('EndProfileRemoteSubscribe', array($this->out, $this->profile));
}
} else {
if ($cur->id == $this->profile->id) { // your own page
}
}
- Event::handle('EndProfilePageActionsElements', array(&$this->out, $this->profile));
+ Event::handle('EndProfilePageActionsElements', array($this->out, $this->profile));
}
$this->out->elementEnd('ul');
$this->out->elementEnd('div');
- Event::handle('EndProfilePageActionsSection', array(&$this->out, $this->profile));
+ Event::handle('EndProfilePageActionsSection', array($this->out, $this->profile));
}
}
switch($node->tagName) {
case 'Title':
$link['title'][] = $node->nodeValue;
+ break;
+ case 'Property':
+ $link['property'][] = array('type' => $node->getAttribute('type'),
+ 'value' => $node->nodeValue);
+ break;
+ default:
+ common_log(LOG_NOTICE, "Unexpected tag name {$node->tagName} found in XRD file.");
}
}
}
$xrd->links[] = array('rel' => 'http://apinamespace.org/atom',
'type' => 'application/atomsvc+xml',
- 'href' => common_local_url('ApiAtomService', array('id' => $nick)));
+ 'href' => common_local_url('ApiAtomService', array('id' => $nick)),
+ 'property' => array(array('type' => 'http://apinamespace.org/atom/username',
+ 'value' => $nick)));
if (common_config('site', 'fancy')) {
$apiRoot = common_path('api/', true);
}
if (is_string($rawtags)) {
- $rawtags = preg_split('/[\s,]+/', $rawtags);
+ if (empty($rawtags)) {
+ $rawtags = array();
+ } else {
+ $rawtags = preg_split('/[\s,]+/', $rawtags);
+ }
}
$nb = new Bookmark();
// Use user's preferences for short URLs, if possible
- $user = User::staticGet('id', $profile->id);
+ try {
+ $user = User::staticGet('id', $profile->id);
- $shortUrl = File_redirection::makeShort($url,
- empty($user) ? null : $user);
+ $shortUrl = File_redirection::makeShort($url,
+ empty($user) ? null : $user);
+ } catch (Exception $e) {
+ // Don't let this stop us.
+ $shortUrl = $url;
+ }
$content = sprintf(_('"%s" %s %s %s'),
$title,
return true;
}
- function onEndLoginGroupNav(&$action)
+ function onEndLoginGroupNav($action)
{
$action_name = $action->trimmed('action');
/*
* Add a login tab for Facebook Connect
*
- * @param Action &action the current action
+ * @param Action $action the current action
*
* @return void
*/
- function onEndLoginGroupNav(&$action)
+ function onEndLoginGroupNav($action)
{
if (self::hasKeys()) {
$action_name = $action->trimmed('action');
/*
* Add a tab for managing Facebook Connect settings
*
- * @param Action &action the current action
+ * @param Action $action the current action
*
* @return void
*/
- function onEndConnectSettingsNav(&$action)
+ function onEndConnectSettingsNav($action)
{
if (self::hasKeys()) {
$action_name = $action->trimmed('action');
* Add a login tab for Facebook, but only if there's a Facebook
* application defined for the plugin to use.
*
- * @param Action &action the current action
+ * @param Action $action the current action
*
* @return void
*/
- function onEndLoginGroupNav(&$action)
+ function onEndLoginGroupNav($action)
{
$action_name = $action->trimmed('action');
* Add a tab for user-level Facebook settings if the user
* has a link to Facebook
*
- * @param Action &action the current action
+ * @param Action $action the current action
*
* @return void
*/
- function onEndConnectSettingsNav(&$action)
+ function onEndConnectSettingsNav($action)
{
if ($this->hasApplication()) {
$action_name = $action->trimmed('action');
$this->element('p', null,
_m('Create a new user with this nickname.'));
$this->elementStart('ul', 'form_data');
+
+ // Hook point for captcha etc
+ Event::handle('StartRegistrationFormData', array($this));
+
$this->elementStart('li');
// TRANS: Field label.
$this->input('newname', _m('New nickname'),
($this->username) ? $this->username : '',
_m('1-64 lowercase letters or numbers, no punctuation or spaces'));
$this->elementEnd('li');
+
+ // Hook point for captcha etc
+ Event::handle('EndRegistrationFormData', array($this));
+
$this->elementEnd('ul');
// TRANS: Submit button.
$this->submit('create', _m('BUTTON','Create'));
contentSelector : "#notices_primary ol.notices",
itemSelector : "#notices_primary ol.notices li"
},function(){
- SN.Init.Notices();
+ // Reply button and attachment magic need to be set up
+ // for each new notice.
+ // DO NOT run SN.Init.Notices() which will duplicate stuff.
+ $(this).find('.notice').each(function() {
+ SN.U.NoticeReplyTo($(this));
+ SN.U.NoticeWithAttachment($(this));
+ });
});
});
// grab each selector option and see if any fail.
function areSelectorsValid(opts){
for (var key in opts){
- if (key.indexOf && key.indexOf('Selector') && $(opts[key]).length === 0){
+ if (key.indexOf && (key.indexOf('Selector') != -1) && $(opts[key]).length === 0){
debug('Your ' + key + ' found no elements.');
return false;
}
{
public $loadCSS = false;
- /**
- * Load related modules when needed
- *
- * @param string $cls Name of the class to be loaded
- *
- * @return boolean hook value; true means continue processing, false means stop.
- */
-
- function onAutoload($cls)
- {
- $dir = dirname(__FILE__);
-
- switch ($cls)
- {
- case 'HelloAction':
- include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
- return false;
- case 'User_greeting_count':
- include_once $dir . '/'.$cls.'.php';
- return false;
- default:
- return true;
- }
- }
-
/**
* Modify the default menu
*
return true;
}
- function onStartAccountSettingsNav(&$action)
+ function onStartAccountSettingsNav($action)
{
$this->_settingsMenu($action);
return false;
}
- function onStartConnectSettingsNav(&$action)
+ function onStartConnectSettingsNav($action)
{
$this->_settingsMenu($action);
return false;
}
- private function _settingsMenu(&$action)
+ private function _settingsMenu($action)
{
$actionName = $action->trimmed('action');
_('Other options'),
$actionName == 'othersettings');
- Event::handle('EndAccountSettingsNav', array(&$action));
+ Event::handle('EndAccountSettingsNav', array($action));
if (common_config('xmpp', 'enabled')) {
$action->menuItem(common_local_url('imsettings'),
_('Authorized connected applications'),
$actionName == 'oauthconnectionsettings');
- Event::handle('EndConnectSettingsNav', array(&$action));
+ Event::handle('EndConnectSettingsNav', array($action));
}
function onEndShowStyles($action)
return false;
}
- function onStartGetProfileFromURI($uri, &$profile) {
+ function onStartGetProfileFromURI($uri, &$profile)
+ {
+ // Don't want to do Web-based discovery on our own server,
+ // so we check locally first.
- // XXX: do discovery here instead (OStatus_profile::ensureProfileURI($uri))
+ $user = User::staticGet('uri', $uri);
+
+ if (!empty($user)) {
+ $profile = $user->getProfile();
+ return false;
+ }
- $oprofile = Ostatus_profile::staticGet('uri', $uri);
+ // Now, check remotely
- if (!empty($oprofile) && !$oprofile->isGroup()) {
+ $oprofile = Ostatus_profile::ensureProfileURI($uri);
+
+ if (!empty($oprofile)) {
$profile = $oprofile->localProfile();
return false;
}
+ // Still not a hit, so give up.
+
return true;
}
public $__table = 'magicsig';
+ /**
+ * Key to user.id/profile.id for the local user whose key we're storing.
+ *
+ * @var int
+ */
public $user_id;
+
+ /**
+ * Flattened string representation of the key pair; callers should
+ * usually use $this->publicKey and $this->privateKey directly,
+ * which hold live Crypt_RSA key objects.
+ *
+ * @var string
+ */
public $keypair;
+
+ /**
+ * Crypto algorithm used for this key; currently only RSA-SHA256 is supported.
+ *
+ * @var string
+ */
public $alg;
+ /**
+ * Public RSA key; gets serialized in/out via $this->keypair string.
+ *
+ * @var Crypt_RSA
+ */
public $publicKey;
+
+ /**
+ * PrivateRSA key; gets serialized in/out via $this->keypair string.
+ *
+ * @var Crypt_RSA
+ */
public $privateKey;
public function __construct($alg = 'RSA-SHA256')
$this->alg = $alg;
}
+ /**
+ * Fetch a Magicsig object from the cache or database on a field match.
+ *
+ * @param string $k
+ * @param mixed $v
+ * @return Magicsig
+ */
public /*static*/ function staticGet($k, $v=null)
{
$obj = parent::staticGet(__CLASS__, $k, $v);
return array(false, false, false);
}
+ /**
+ * Save this keypair into the database.
+ *
+ * Overloads default insert behavior to encode the live key objects
+ * as a flat string for storage.
+ *
+ * @return mixed
+ */
function insert()
{
$this->keypair = $this->toString();
return parent::insert();
}
+ /**
+ * Generate a new keypair for a local user and store in the database.
+ *
+ * Warning: this can be very slow on systems without the GMP module.
+ * Runtimes of 20-30 seconds are not unheard-of.
+ *
+ * @param int $user_id id of local user we're creating a key for
+ */
public function generate($user_id)
{
$rsa = new Crypt_RSA();
$this->insert();
}
+ /**
+ * Encode the keypair or public key as a string.
+ *
+ * @param boolean $full_pair set to false to leave out the private key.
+ * @return string
+ */
public function toString($full_pair = true)
{
$mod = Magicsig::base64_url_encode($this->publicKey->modulus->toBytes());
return 'RSA.' . $mod . '.' . $exp . $private_exp;
}
+ /**
+ * Decode a string representation of an RSA public key or keypair
+ * as a Magicsig object which can be used to sign or verify.
+ *
+ * @param string $text
+ * @return Magicsig
+ */
public static function fromString($text)
{
$magic_sig = new Magicsig();
return $magic_sig;
}
+ /**
+ * Fill out $this->privateKey or $this->publicKey with a Crypt_RSA object
+ * representing the give key (as mod/exponent pair).
+ *
+ * @param string $mod base64-encoded
+ * @param string $exp base64-encoded exponent
+ * @param string $type one of 'public' or 'private'
+ */
public function loadKey($mod, $exp, $type = 'public')
{
common_log(LOG_DEBUG, "Adding ".$type." key: (".$mod .', '. $exp .")");
}
}
+ /**
+ * Returns the name of the crypto algorithm used for this key.
+ *
+ * @return string
+ */
public function getName()
{
return $this->alg;
}
+ /**
+ * Returns the name of a hash function to use for signing with this key.
+ *
+ * @return string
+ * @fixme is this used? doesn't seem to be called by name.
+ */
public function getHash()
{
switch ($this->alg) {
}
}
+ /**
+ * Generate base64-encoded signature for the given byte string
+ * using our private key.
+ *
+ * @param string $bytes as raw byte string
+ * @return string base64-encoded signature
+ */
public function sign($bytes)
{
$sig = $this->privateKey->sign($bytes);
return Magicsig::base64_url_encode($sig);
}
+ /**
+ *
+ * @param string $signed_bytes as raw byte string
+ * @param string $signature as base64
+ * @return boolean
+ */
public function verify($signed_bytes, $signature)
{
$signature = Magicsig::base64_url_decode($signature);
return $this->publicKey->verify($signed_bytes, $signature);
}
-
+ /**
+ * URL-encoding-friendly base64 variant encoding.
+ *
+ * @param string $input
+ * @return string
+ */
public static function base64_url_encode($input)
{
return strtr(base64_encode($input), '+/', '-_');
}
+ /**
+ * URL-encoding-friendly base64 variant decoding.
+ *
+ * @param string $input
+ * @return string
+ */
public static function base64_url_decode($input)
{
return base64_decode(strtr($input, '-_', '+/'));
* an acceptable response from the remote site.
*
* @param mixed $entry XML string, Notice, or Activity
+ * @param Profile $actor
* @return boolean success
*/
public function notifyActivity($entry, $actor)
case 'mailto':
$rest = $match[2];
$oprofile = Ostatus_profile::ensureWebfinger($rest);
+ break;
default:
- common_log("Unrecognized URI protocol for profile: $protocol ($uri)");
+ common_log(LOG_WARNING,
+ "Unrecognized URI protocol for profile: $protocol ($uri)");
break;
}
}
+++ /dev/null
-<?php
-/**
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * A sample module to show best practices for StatusNet plugins
- *
- * PHP version 5
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- * @package StatusNet
- * @author James Walker <james@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
- * @link http://status.net/
- */
-
-/**
- * This class implements LRDD-based service discovery based on the "Hammer Draft"
- * (including webfinger)
- *
- * @see http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf
- */
-class Discovery
-{
-
- const LRDD_REL = 'lrdd';
- const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
- const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
- const HCARD = 'http://microformats.org/profile/hcard';
-
- public $methods = array();
-
- public function __construct()
- {
- $this->registerMethod('Discovery_LRDD_Host_Meta');
- $this->registerMethod('Discovery_LRDD_Link_Header');
- $this->registerMethod('Discovery_LRDD_Link_HTML');
- }
-
- public function registerMethod($class)
- {
- $this->methods[] = $class;
- }
-
- /**
- * Given a "user id" make sure it's normalized to either a webfinger
- * acct: uri or a profile HTTP URL.
- */
- public static function normalize($user_id)
- {
- if (substr($user_id, 0, 5) == 'http:' ||
- substr($user_id, 0, 6) == 'https:' ||
- substr($user_id, 0, 5) == 'acct:') {
- return $user_id;
- }
-
- if (strpos($user_id, '@') !== FALSE) {
- return 'acct:' . $user_id;
- }
-
- return 'http://' . $user_id;
- }
-
- public static function isWebfinger($user_id)
- {
- $uri = Discovery::normalize($user_id);
-
- return (substr($uri, 0, 5) == 'acct:');
- }
-
- /**
- * This implements the actual lookup procedure
- */
- public function lookup($id)
- {
- // Normalize the incoming $id to make sure we have a uri
- $uri = $this->normalize($id);
-
- foreach ($this->methods as $class) {
- $links = call_user_func(array($class, 'discover'), $uri);
- if ($link = Discovery::getService($links, Discovery::LRDD_REL)) {
- // Load the LRDD XRD
- if (!empty($link['template'])) {
- $xrd_uri = Discovery::applyTemplate($link['template'], $uri);
- } else {
- $xrd_uri = $link['href'];
- }
-
- $xrd = $this->fetchXrd($xrd_uri);
- if ($xrd) {
- return $xrd;
- }
- }
- }
-
- // TRANS: Exception.
- throw new Exception(sprintf(_m('Unable to find services for %s.'),$id));
- }
-
- public static function getService($links, $service) {
- if (!is_array($links)) {
- return false;
- }
-
- foreach ($links as $link) {
- if ($link['rel'] == $service) {
- return $link;
- }
- }
- }
-
- public static function applyTemplate($template, $id)
- {
- $template = str_replace('{uri}', urlencode($id), $template);
-
- return $template;
- }
-
- public static function fetchXrd($url)
- {
- try {
- $client = new HTTPClient();
- $response = $client->get($url);
- } catch (HTTP_Request2_Exception $e) {
- return false;
- }
-
- if ($response->getStatus() != 200) {
- return false;
- }
-
- return XRD::parse($response->getBody());
- }
-}
-
-interface Discovery_LRDD
-{
- public function discover($uri);
-}
-
-class Discovery_LRDD_Host_Meta implements Discovery_LRDD
-{
- public function discover($uri)
- {
- if (Discovery::isWebfinger($uri)) {
- // We have a webfinger acct: - start with host-meta
- list($name, $domain) = explode('@', $uri);
- } else {
- $domain = parse_url($uri, PHP_URL_HOST);
- }
-
- $url = 'http://'. $domain .'/.well-known/host-meta';
-
- $xrd = Discovery::fetchXrd($url);
-
- if ($xrd) {
- if ($xrd->host != $domain) {
- return false;
- }
-
- return $xrd->links;
- }
- }
-}
-
-class Discovery_LRDD_Link_Header implements Discovery_LRDD
-{
- public function discover($uri)
- {
- try {
- $client = new HTTPClient();
- $response = $client->get($uri);
- } catch (HTTP_Request2_Exception $e) {
- return false;
- }
-
- if ($response->getStatus() != 200) {
- return false;
- }
-
- $link_header = $response->getHeader('Link');
- if (!$link_header) {
- // return false;
- }
-
- return array(Discovery_LRDD_Link_Header::parseHeader($link_header));
- }
-
- protected static function parseHeader($header)
- {
- $lh = new LinkHeader($header);
-
- return array('href' => $lh->href,
- 'rel' => $lh->rel,
- 'type' => $lh->type);
- }
-}
-
-class Discovery_LRDD_Link_HTML implements Discovery_LRDD
-{
- public function discover($uri)
- {
- try {
- $client = new HTTPClient();
- $response = $client->get($uri);
- } catch (HTTP_Request2_Exception $e) {
- return false;
- }
-
- if ($response->getStatus() != 200) {
- return false;
- }
-
- return Discovery_LRDD_Link_HTML::parse($response->getBody());
- }
-
- public function parse($html)
- {
- $links = array();
-
- preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
- $head_html = $head_matches[2];
-
- preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
-
- foreach ($link_matches[0] as $link_html) {
- $link_url = null;
- $link_rel = null;
- $link_type = null;
-
- preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
- if ( isset($rel_matches[3]) ) {
- $link_rel = $rel_matches[3];
- } else if ( isset($rel_matches[1]) ) {
- $link_rel = $rel_matches[1];
- }
-
- preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
- if ( isset($href_matches[3]) ) {
- $link_uri = $href_matches[3];
- } else if ( isset($href_matches[1]) ) {
- $link_uri = $href_matches[1];
- }
-
- preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
- if ( isset($type_matches[3]) ) {
- $link_type = $type_matches[3];
- } else if ( isset($type_matches[1]) ) {
- $link_type = $type_matches[1];
- }
-
- $links[] = array(
- 'href' => $link_url,
- 'rel' => $link_rel,
- 'type' => $link_type,
- );
- }
-
- return $links;
- }
-}
+++ /dev/null
-<?php
-/**
- * @todo Add file header and documentation.
- */
-
-class LinkHeader
-{
- var $href;
- var $rel;
- var $type;
-
- function __construct($str)
- {
- preg_match('/^<[^>]+>/', $str, $uri_reference);
- //if (empty($uri_reference)) return;
-
- $this->href = trim($uri_reference[0], '<>');
- $this->rel = array();
- $this->type = null;
-
- // remove uri-reference from header
- $str = substr($str, strlen($uri_reference[0]));
-
- // parse link-params
- $params = explode(';', $str);
-
- foreach ($params as $param) {
- if (empty($param)) continue;
- list($param_name, $param_value) = explode('=', $param, 2);
- $param_name = trim($param_name);
- $param_value = preg_replace('(^"|"$)', '', trim($param_value));
-
- // for now we only care about 'rel' and 'type' link params
- // TODO do something with the other links-params
- switch ($param_name) {
- case 'rel':
- $this->rel = trim($param_value);
- break;
-
- case 'type':
- $this->type = trim($param_value);
- }
- }
- }
-
- static function getLink($response, $rel=null, $type=null)
- {
- $headers = $response->getHeader('Link');
- if ($headers) {
- // Can get an array or string, so try to simplify the path
- if (!is_array($headers)) {
- $headers = array($headers);
- }
-
- foreach ($headers as $header) {
- $lh = new LinkHeader($header);
-
- if ((is_null($rel) || $lh->rel == $rel) &&
- (is_null($type) || $lh->type == $type)) {
- return $lh->href;
- }
- }
- }
- return null;
- }
-}
throw new Exception(_m('Unable to locate signer public key.'));
}
+ /**
+ * The current MagicEnvelope spec as used in StatusNet 0.9.7 and later
+ * includes both the original data and some signing metadata fields as
+ * the input plaintext for the signature hash.
+ *
+ * @param array $env
+ * @return string
+ */
+ public function signingText($env) {
+ return implode('.', array($env['data'], // this field is pre-base64'd
+ Magicsig::base64_url_encode($env['data_type']),
+ Magicsig::base64_url_encode($env['encoding']),
+ Magicsig::base64_url_encode($env['alg'])));
+ }
+ /**
+ *
+ * @param <type> $text
+ * @param <type> $mimetype
+ * @param <type> $keypair
+ * @return array: associative array of envelope properties
+ * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+ */
public function signMessage($text, $mimetype, $keypair)
{
$signature_alg = Magicsig::fromString($keypair);
$armored_text = Magicsig::base64_url_encode($text);
-
- return array(
+ $env = array(
'data' => $armored_text,
'encoding' => MagicEnvelope::ENCODING,
'data_type' => $mimetype,
- 'sig' => $signature_alg->sign($armored_text),
+ 'sig' => '',
'alg' => $signature_alg->getName()
);
+
+ $env['sig'] = $signature_alg->sign($this->signingText($env));
+
+ return $env;
}
+ /**
+ * Create an <me:env> XML representation of the envelope.
+ *
+ * @param array $env associative array with envelope data
+ * @return string representation of XML document
+ * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+ */
public function toXML($env) {
$xs = new XMLStringer();
$xs->startXML();
return $string;
}
+ /**
+ * Extract the contained XML payload, and insert a copy of the envelope
+ * signature data as an <me:provenance> section.
+ *
+ * @param array $env associative array with envelope data
+ * @return string representation of modified XML document
+ *
+ * @fixme in case of XML parsing errors, this will spew to the error log or output
+ * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+ */
public function unfold($env)
{
$dom = new DOMDocument();
return $dom->saveXML();
}
+ /**
+ * Find the author URI referenced in the given Atom entry.
+ *
+ * @param string $text string containing Atom entry XML
+ * @return mixed URI string or false if XML parsing fails, or null if no author URI can be found
+ *
+ * @fixme XML parsing failures will spew to error logs/output
+ */
public function getAuthor($text) {
$doc = new DOMDocument();
if (!$doc->loadXML($text)) {
}
}
+ /**
+ * Check if the author in the Atom entry fragment claims to match
+ * the given identifier URI.
+ *
+ * @param string $text string containing Atom entry XML
+ * @param string $signer_uri
+ * @return boolean
+ */
public function checkAuthor($text, $signer_uri)
{
return ($this->getAuthor($text) == $signer_uri);
}
+ /**
+ * Attempt to verify cryptographic signing for parsed envelope data.
+ * Requires network access to retrieve public key referenced by the envelope signer.
+ *
+ * Details of failure conditions are dumped to output log and not exposed to caller.
+ *
+ * @param array $env array representation of magic envelope data, as returned from MagicEnvelope::parse()
+ * @return boolean
+ *
+ * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+ */
public function verify($env)
{
if ($env['alg'] != 'RSA-SHA256') {
return false;
}
- return $verifier->verify($env['data'], $env['sig']);
+ return $verifier->verify($this->signingText($env), $env['sig']);
}
+ /**
+ * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
+ *
+ * @param string XML source
+ * @return mixed associative array of envelope data, or false on unrecognized input
+ *
+ * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+ * @fixme will spew errors to logs or output in case of XML parse errors
+ * @fixme may give fatal errors if some elements are missing or invalid XML
+ * @fixme calling DOMDocument::loadXML statically triggers warnings in strict mode
+ */
public function parse($text)
{
$dom = DOMDocument::loadXML($text);
return $this->fromDom($dom);
}
+ /**
+ * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
+ *
+ * @param DOMDocument $dom
+ * @return mixed associative array of envelope data, or false on unrecognized input
+ *
+ * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
+ * @fixme may give fatal errors if some elements are missing
+ */
public function fromDom($dom)
{
$env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'env')->item(0);
);
}
}
+
+/**
+ * Variant of MagicEnvelope using the earlier signature form listed in the MagicEnvelope
+ * spec in early 2010; this was used in StatusNet up through 0.9.6, so for backwards compatiblity
+ * we still need to accept and sometimes send this format.
+ */
+class MagicEnvelopeCompat extends MagicEnvelope {
+
+ /**
+ * StatusNet through 0.9.6 used an earlier version of the MagicEnvelope spec
+ * which used only the input data, without the additional fields, as the plaintext
+ * for signing.
+ *
+ * @param array $env
+ * @return string
+ */
+ public function signingText($env) {
+ return $env['data'];
+ }
+}
+
/**
* Sign and post the given Atom entry as a Salmon message.
*
- * @fixme pass through the actor for signing?
+ * Side effects: may generate a keypair on-demand for the given user,
+ * which can be very slow on some systems.
*
* @param string $endpoint_uri
- * @param string $xml
+ * @param string $xml string representation of payload
+ * @param Profile $actor local user profile whose keys to sign with
* @return boolean success
*/
public function post($endpoint_uri, $xml, $actor)
return false;
}
- try {
- $xml = $this->createMagicEnv($xml, $actor);
- } catch (Exception $e) {
- common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
- return false;
- }
-
- $headers = array('Content-Type: application/magic-envelope+xml');
+ foreach ($this->formatClasses() as $class) {
+ try {
+ $envelope = $this->createMagicEnv($xml, $actor, $class);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
+ return false;
+ }
+
+ $headers = array('Content-Type: application/magic-envelope+xml');
+
+ try {
+ $client = new HTTPClient();
+ $client->setBody($envelope);
+ $response = $client->post($endpoint_uri, $headers);
+ } catch (HTTP_Request2_Exception $e) {
+ common_log(LOG_ERR, "Salmon ($class) post to $endpoint_uri failed: " . $e->getMessage());
+ continue;
+ }
+ if ($response->getStatus() != 200) {
+ common_log(LOG_ERR, "Salmon ($class) at $endpoint_uri returned status " .
+ $response->getStatus() . ': ' . $response->getBody());
+ continue;
+ }
- try {
- $client = new HTTPClient();
- $client->setBody($xml);
- $response = $client->post($endpoint_uri, $headers);
- } catch (HTTP_Request2_Exception $e) {
- common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage());
- return false;
- }
- if ($response->getStatus() != 200) {
- common_log(LOG_ERR, "Salmon at $endpoint_uri returned status " .
- $response->getStatus() . ': ' . $response->getBody());
- return false;
+ // Success!
+ return true;
}
- return true;
+ return false;
+ }
+
+ /**
+ * List the magic envelope signature class variants in the order we try them.
+ * Multiples are needed for backwards-compat with StatusNet prior to 0.9.7,
+ * which used a draft version of the magic envelope spec.
+ */
+ protected function formatClasses() {
+ return array('MagicEnvelope', 'MagicEnvelopeCompat');
}
- public function createMagicEnv($text, $actor)
+ /**
+ * Encode the given string as a signed MagicEnvelope XML document,
+ * using the keypair for the given local user profile.
+ *
+ * Side effects: will create and store a keypair on-demand if one
+ * hasn't already been generated for this user. This can be very slow
+ * on some systems.
+ *
+ * @param string $text XML fragment to sign, assumed to be Atom
+ * @param Profile $actor Profile of a local user to use as signer
+ * @param string $class to override the magic envelope signature version, pass a MagicEnvelope subclass here
+ *
+ * @return string XML string representation of magic envelope
+ *
+ * @throws Exception on bad profile input or key generation problems
+ * @fixme if signing fails, this seems to return the original text without warning. Is there a reason for this?
+ */
+ public function createMagicEnv($text, $actor, $class='MagicEnvelope')
{
- $magic_env = new MagicEnvelope();
+ $magic_env = new $class();
$user = User::staticGet('id', $actor->id);
if ($user->id) {
return $magic_env->toXML($env);
}
+ /**
+ * Check if the given magic envelope is well-formed and correctly signed.
+ * Needs to have network access to fetch public keys over the web.
+ * Both current and back-compat signature formats will be checked.
+ *
+ * Side effects: exceptions and caching updates may occur during network
+ * fetches.
+ *
+ * @param string $text XML fragment of magic envelope
+ * @return boolean
+ *
+ * @throws Exception on bad profile input or key generation problems
+ * @fixme could hit fatal errors or spew output on invalid XML
+ */
public function verifyMagicEnv($text)
{
- $magic_env = new MagicEnvelope();
+ foreach ($this->formatClasses() as $class) {
+ $magic_env = new $class();
- $env = $magic_env->parse($text);
+ $env = $magic_env->parse($text);
+
+ if ($magic_env->verify($env)) {
+ return true;
+ }
+ }
- return $magic_env->verify($env);
+ return false;
}
}
--- /dev/null
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ print "This script must be run from the command line\n";
+ exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+define('STATUSNET', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+
+class MagicEnvelopeTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * Test that MagicEnvelope builds the correct plaintext for signing.
+ * @dataProvider provider
+ */
+ public function testSignatureText($env, $expected)
+ {
+ $magic = new MagicEnvelope;
+ $text = $magic->signingText($env);
+
+ $this->assertEquals($expected, $text, "'$text' should be '$expected'");
+ }
+
+ static public function provider()
+ {
+ return array(
+ array(
+ // Sample case given in spec:
+ // http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-00.html#signing
+ array(
+ 'data' => 'Tm90IHJlYWxseSBBdG9t',
+ 'data_type' => 'application/atom+xml',
+ 'encoding' => 'base64url',
+ 'alg' => 'RSA-SHA256'
+ ),
+ 'Tm90IHJlYWxseSBBdG9t.YXBwbGljYXRpb24vYXRvbSt4bWw=.YmFzZTY0dXJs.UlNBLVNIQTI1Ng=='
+ )
+ );
+ }
+
+
+ /**
+ * Test that MagicEnvelope builds the correct plaintext for signing.
+ * @dataProvider provider
+ */
+ public function testSignatureTextCompat($env, $expected)
+ {
+ // Our old code didn't add the extra fields, just used the armored text.
+ $alt = $env['data'];
+
+ $magic = new MagicEnvelopeCompat;
+ $text = $magic->signingText($env);
+
+ $this->assertEquals($alt, $text, "'$text' should be '$alt'");
+ }
+
+}
$base = 'test' . mt_rand(1, 1000000);
$this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);
$this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);
+
+ $this->group = 'group' . $base;
}
function run()
$this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
}
+ function testCreateGroup()
+ {
+ $this->groupUrl = $this->pub->createGroup($this->group);
+ $this->assertTrue(!empty($this->groupUrl));
+ }
+
+ function testJoinGroup()
+ {
+ #$this->assertFalse($this->sub->inGroup($this->groupUrl));
+ $this->sub->joinGroup($this->groupUrl);
+ #$this->assertTrue($this->sub->inGroup($this->groupUrl));
+ }
+
+ function testLocalGroupPost()
+ {
+ $post = $this->pub->post("Group post from local to !{$this->group}, should go out over push.");
+ $this->assertNotEqual('', $post);
+ $this->sub->assertReceived($post);
+ }
+
+ function testRemoteGroupPost()
+ {
+ $post = $this->sub->post("Group post from remote to !{$this->group}, should come in over salmon.");
+ $this->assertNotEqual('', $post);
+ $this->pub->assertReceived($post);
+ }
+
+ function testLeaveGroup()
+ {
+ #$this->assertTrue($this->sub->inGroup($this->groupUrl));
+ $this->sub->leaveGroup($this->groupUrl);
+ #$this->assertFalse($this->sub->inGroup($this->groupUrl));
+ }
}
class SNTestClient extends TestBase
return false;
}
+ /**
+ * Create a group on this site.
+ *
+ * @param string $nickname
+ * @param array $options
+ * @return string: profile URL for the group
+ */
+ function createGroup($nickname, $options=array()) {
+ $this->log("Creating group as %s on %s: %s",
+ $this->username,
+ $this->basepath,
+ $nickname);
+
+ $data = $this->api('statusnet/groups/create', 'json',
+ array_merge(array('nickname' => $nickname), $options));
+ $url = $data['url'];
+
+ if ($url) {
+ $this->log(' created as %s', $url);
+ } else {
+ $this->log(' failed? %s', var_export($data, true));
+ }
+ return $url;
+ }
+
+ function groupInfo($nickname) {
+ $data = $this->api('statusnet/groups/show', 'json', array(
+ 'id' => $nickname
+ ));
+ }
+
+ /**
+ * Join a group.
+ *
+ * @param string $group nickname or URL
+ */
+ function joinGroup($group) {
+ $this->post('join ' . $group);
+ }
+
+ /**
+ * Leave a group.
+ *
+ * @param string $group nickname or URL
+ */
+ function leaveGroup($group) {
+ $this->post('drop ' . $group);
+ }
+
+ /**
+ *
+ * @param string $nickname
+ * @return
+ */
+ function inGroup($nickname) {
+ // @todo
+ }
}
// @fixme switch to commandline.inc?
--- /dev/null
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - a distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+
+$longoptions = array('verify', 'slap=', 'notice=');
+
+$helptext = <<<END_OF_HELP
+slap.php [options]
+
+Test generation and sending of magic envelopes for Salmon slaps.
+
+ --notice=N generate entry for this notice number
+ --verify send signed magic envelope to Tuomas Koski's test service
+ --slap=<url> send signed Salmon slap to the destination endpoint
+
+
+END_OF_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+if (!have_option('--notice')) {
+ print "$helptext";
+ exit(1);
+}
+
+$notice_id = get_option_value('--notice');
+
+$notice = Notice::staticGet('id', $notice_id);
+$profile = $notice->getProfile();
+$entry = $notice->asAtomEntry(true);
+
+echo "== Original entry ==\n\n";
+print $entry;
+print "\n\n";
+
+$salmon = new Salmon();
+$envelope = $salmon->createMagicEnv($entry, $profile);
+
+echo "== Signed envelope ==\n\n";
+print $envelope;
+print "\n\n";
+
+echo "== Testing local verification ==\n\n";
+$ok = $salmon->verifyMagicEnv($envelope);
+if ($ok) {
+ print "OK\n\n";
+} else {
+ print "FAIL\n\n";
+}
+
+if (have_option('--verify')) {
+ $url = 'http://www.madebymonsieur.com/ostatus_discovery/magic_env/validate/';
+ echo "== Testing remote verification ==\n\n";
+ print "Sending for verification to $url ...\n";
+
+ $client = new HTTPClient();
+ $response = $client->post($url, array(), array('magic_env' => $envelope));
+
+ print $response->getStatus() . "\n\n";
+ print $response->getBody() . "\n\n";
+}
+
+if (have_option('--slap')) {
+ $url = get_option_value('--slap');
+ echo "== Remote salmon slap ==\n\n";
+ print "Sending signed Salmon slap to $url ...\n";
+
+ $ok = $salmon->post($url, $entry, $profile);
+ if ($ok) {
+ print "OK\n\n";
+ } else {
+ print "FAIL\n\n";
+ }
+}
*
* If we're in openidOnly mode, we disable the menu for all other login.
*
- * @param Action &$action Action being executed
+ * @param Action $action Action being executed
*
* @return boolean hook return
*/
- function onStartLoginGroupNav(&$action)
+ function onStartLoginGroupNav($action)
{
if (common_config('site', 'openidonly')) {
$this->showOpenIDLoginTab($action);
/**
* Menu item for login
*
- * @param Action &$action Action being executed
+ * @param Action $action Action being executed
*
* @return boolean hook return
*/
- function onEndLoginGroupNav(&$action)
+ function onEndLoginGroupNav($action)
{
$this->showOpenIDLoginTab($action);
/**
* Menu item for OpenID settings
*
- * @param Action &$action Action being executed
+ * @param Action $action Action being executed
*
* @return boolean hook return
*/
- function onEndAccountSettingsNav(&$action)
+ function onEndAccountSettingsNav($action)
{
$action_name = $action->trimmed('action');
$this->element('p', null,
_m('Create a new user with this nickname.'));
$this->elementStart('ul', 'form_data');
+
+ // Hook point for captcha etc
+ Event::handle('StartRegistrationFormData', array($this));
+
$this->elementStart('li');
$this->input('newname', _m('New nickname'),
($this->username) ? $this->username : '',
_m('1-64 lowercase letters or numbers, no punctuation or spaces'));
$this->elementEnd('li');
+ $this->elementStart('li');
+ $this->input('email', _('Email'), $this->getEmail(),
+ _('Used only for updates, announcements, '.
+ 'and password recovery'));
+ $this->elementEnd('li');
+
+ // Hook point for captcha etc
+ Event::handle('EndRegistrationFormData', array($this));
+
$this->elementStart('li');
$this->element('input', array('type' => 'checkbox',
'id' => 'license',
$this->elementEnd('form');
}
+ /**
+ * Get specified e-mail from the form, or the OpenID sreg info, or the
+ * invite code.
+ *
+ * @return string
+ */
+ function getEmail()
+ {
+ $email = $this->trimmed('email');
+ if (!empty($email)) {
+ return $email;
+ }
+
+ // Pull from openid thingy
+ list($display, $canonical, $sreg) = $this->getSavedValues();
+ if (!empty($sreg['email'])) {
+ return $sreg['email'];
+ }
+
+ // Terrible hack for invites...
+ if (common_config('site', 'inviteonly')) {
+ $code = $_SESSION['invitecode'];
+ if ($code) {
+ $invite = Invitation::staticGet($code);
+
+ if ($invite && $invite->address_type == 'email') {
+ return $invite->address;
+ }
+ }
+ }
+ return '';
+ }
+
function tryLogin()
{
$consumer = oid_consumer();
$fullname = '';
}
- if (!empty($sreg['email']) && Validate::email($sreg['email'], common_config('email', 'check_domain'))) {
- $email = $sreg['email'];
- } else {
- $email = '';
- }
+ $email = $this->getEmail();
# XXX: add language
# XXX: add timezone
}
return true;
}
+
+ /**
+ * Prevent unvalidated folks from creating spam groups.
+ *
+ * @param Profile $profile User profile we're checking
+ * @param string $right rights key
+ * @param boolean $result if overriding, set to true/false has right
+ * @return boolean hook result value
+ */
+ function onUserRightsCheck(Profile $profile, $right, &$result)
+ {
+ if ($right == Right::CREATEGROUP) {
+ $user = User::staticGet('id', $profile->id);
+ if ($user && !$this->validated($user)) {
+ $result = false;
+ return false;
+ }
+ }
+ return true;
+ }
}
}
/**
- * Menu item for settings
+ * Menu item for personal subscriptions/groups area
*
- * @param Action &$action Action being executed
+ * @param Widget $widget Widget being executed
*
* @return boolean hook return
*/
- function onEndAccountSettingsNav(&$action)
+ function onEndSubGroupNav($widget)
{
+ $action = $widget->out;
$action_name = $action->trimmed('action');
$action->menuItem(common_local_url('mirrorsettings'),
}
return true;
}
+
+ /**
+ * Add a count of mirrored feeds into a user's profile sidebar stats.
+ *
+ * @param Profile $profile
+ * @param array $stats
+ * @return boolean hook return value
+ */
+ function onProfileStats($profile, &$stats)
+ {
+ $cur = common_current_user();
+ if (!empty($cur) && $cur->id == $profile->id) {
+ $mirror = new SubMirror();
+ $mirror->subscriber = $profile->id;
+ $entry = array(
+ 'id' => 'mirrors',
+ 'label' => _m('Mirrored feeds'),
+ 'link' => common_local_url('mirrorsettings'),
+ 'value' => $mirror->count(),
+ );
+
+ $insertAt = count($stats);
+ foreach ($stats as $i => $row) {
+ if ($row['id'] == 'groups') {
+ // Slip us in after them.
+ $insertAt = $i + 1;
+ break;
+ }
+ }
+ array_splice($stats, $insertAt, 0, array($entry));
+ }
+ return true;
+ }
}
function handlePost()
{
}
+
+ function showLocalNav()
+ {
+ $nav = new SubGroupNav($this, common_current_user());
+ $nav->show();
+ }
}
/*
* Add a login tab for 'Sign in with Twitter'
*
- * @param Action &action the current action
+ * @param Action $action the current action
*
* @return void
*/
- function onEndLoginGroupNav(&$action)
+ function onEndLoginGroupNav($action)
{
$action_name = $action->trimmed('action');
/**
* Add the Twitter Settings page to the Connect Settings menu
*
- * @param Action &$action The calling page
+ * @param Action $action The calling page
*
* @return boolean hook return
*/
- function onEndConnectSettingsNav(&$action)
+ function onEndConnectSettingsNav($action)
{
if (self::hasKeys()) {
$action_name = $action->trimmed('action');
$this->twuid = $twitter_user->id;
$this->tw_fields = array("screen_name" => $twitter_user->screen_name,
- "name" => $twitter_user->name);
+ "fullname" => $twitter_user->name);
$this->access_token = $atok;
$this->tryLogin();
}
$this->hidden('access_token_secret', $this->access_token->secret);
$this->hidden('twuid', $this->twuid);
$this->hidden('tw_fields_screen_name', $this->tw_fields['screen_name']);
- $this->hidden('tw_fields_name', $this->tw_fields['name']);
+ $this->hidden('tw_fields_name', $this->tw_fields['fullname']);
$this->elementStart('fieldset');
$this->hidden('token', common_session_token());
$this->element('p', null,
_m('Create a new user with this nickname.'));
$this->elementStart('ul', 'form_data');
+
+ // Hook point for captcha etc
+ Event::handle('StartRegistrationFormData', array($this));
+
$this->elementStart('li');
$this->input('newname', _m('New nickname'),
($this->username) ? $this->username : '',
_m('1-64 lowercase letters or numbers, no punctuation or spaces'));
$this->elementEnd('li');
+ $this->elementStart('li');
+ $this->input('email', _('Email'), $this->getEmail(),
+ _('Used only for updates, announcements, '.
+ 'and password recovery'));
+ $this->elementEnd('li');
+
+ // Hook point for captcha etc
+ Event::handle('EndRegistrationFormData', array($this));
+
$this->elementEnd('ul');
$this->submit('create', _m('Create'));
$this->elementEnd('fieldset');
$this->elementEnd('form');
}
+ /**
+ * Get specified e-mail from the form, or the invite code.
+ *
+ * @return string
+ */
+ function getEmail()
+ {
+ $email = $this->trimmed('email');
+ if (!empty($email)) {
+ return $email;
+ }
+
+ // Terrible hack for invites...
+ if (common_config('site', 'inviteonly')) {
+ $code = $_SESSION['invitecode'];
+ if ($code) {
+ $invite = Invitation::staticGet($code);
+
+ if ($invite && $invite->address_type == 'email') {
+ return $invite->address;
+ }
+ }
+ }
+ return '';
+ }
+
function message($msg)
{
$this->message_text = $msg;
return;
}
- $fullname = trim($this->tw_fields['name']);
+ $fullname = trim($this->tw_fields['fullname']);
$args = array('nickname' => $nickname, 'fullname' => $fullname);
$args['code'] = $invite->code;
}
+ $email = $this->getEmail();
+ if (!empty($email)) {
+ $args['email'] = $email;
+ }
+
$user = User::register($args);
if (empty($user)) {
function bestNewNickname()
{
- if (!empty($this->tw_fields['name'])) {
- $nickname = $this->nicknamize($this->tw_fields['name']);
+ if (!empty($this->tw_fields['fullname'])) {
+ $nickname = $this->nicknamize($this->tw_fields['fullname']);
if ($this->isNewNickname($nickname)) {
return $nickname;
}
/**
* Add a 'flag' button to profile page
*
- * @param Action &$action The action being called
+ * @param Action $action The action being called
* @param Profile $profile Profile being shown
*
* @return boolean hook result
*/
- function onEndProfilePageActionsElements(&$action, $profile)
+ function onEndProfilePageActionsElements($action, $profile)
{
$this->showFlagButton($action, $profile,
array('action' => 'showstream',
--- /dev/null
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010 StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+
+$shortoptions = 'i:n:r:w:y';
+$longoptions = array('id=', 'nickname=', 'remote=', 'password=');
+
+$helptext = <<<END_OF_MOVEUSER_HELP
+moveuser.php [options]
+Move a local user to a remote account.
+
+ -i --id ID of user to move
+ -n --nickname nickname of the user to move
+ -r --remote Full ID of remote users
+ -w --password Password of remote user
+ -y --yes do not wait for confirmation
+
+Remote user identity must be a Webfinger (nickname@example.com) or
+an HTTP or HTTPS URL (http://example.com/social/site/user/nickname).
+
+END_OF_MOVEUSER_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+try {
+
+ $user = getUser();
+
+ $remote = get_option_value('r', 'remote');
+
+ if (empty($remote)) {
+ show_help();
+ exit(1);
+ }
+
+ $password = get_option_value('w', 'password');
+
+ if (!have_option('y', 'yes')) {
+ print "WARNING: EXPERIMENTAL FEATURE! Moving accounts will delete data from the source site.\n";
+ print "\n";
+ print "About to PERMANENTLY move user '{$user->nickname}' to $remote. Are you sure? [y/N] ";
+ $response = fgets(STDIN);
+ if (strtolower(trim($response)) != 'y') {
+ print "Aborting.\n";
+ exit(0);
+ }
+ }
+
+ $qm = QueueManager::get();
+
+ $qm->enqueue(array($user, $remote, $password), 'acctmove');
+
+} catch (Exception $e) {
+ print $e->getMessage()."\n";
+ exit(1);
+}
}
}
+ public function testExample10()
+ {
+ global $_example10;
+ $dom = new DOMDocument();
+ $dom->loadXML($_example10);
+
+ // example 10 is a PuSH item of a post on a group feed, as generated
+ // by 0.9.7 code after migration away from <activity:actor> to <author>
+ $feed = $dom->documentElement;
+ $entry = $dom->getElementsByTagName('entry')->item(0);
+ $expected = 'http://lazarus.local/mublog/user/557';
+
+ // Reading just the entry alone should pick up its own <author>
+ // as the actor.
+ $act = new Activity($entry);
+ $this->assertEquals($act->actor->id, $expected);
+
+ // Reading the entry in feed context used to be buggy, picking up
+ // the feed's <activity:subject> which referred to the group.
+ // It should now be returning the expected author entry...
+ $act = new Activity($entry, $feed);
+ $this->assertEquals($act->actor->id, $expected);
+ }
}
$_example1 = <<<EXAMPLE1
</entry>
</feed>
EXAMPLE9;
+
+// Sample PuSH entry from a group feed in 0.9.7
+// Old <activity:actor> has been removed from entries in this version.
+// A bug in the order of input processing meant that we were incorrectly
+// reading the feed's <activity:subject> instead of the entry's <author>,
+// causing the entry to get rejected as malformed (groups can't post on
+// their own; we want to see the actual author's info here).
+$_example10 = <<<EXAMPLE10
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
+ <generator uri="http://status.net" version="0.9.7alpha1">StatusNet</generator>
+ <id>http://lazarus.local/mublog/api/statusnet/groups/timeline/22.atom</id>
+ <title>grouptest316173 timeline</title>
+ <subtitle>Updates from grouptest316173 on Blaguette!</subtitle>
+ <logo>http://lazarus.local/mublog/theme/default/default-avatar-profile.png</logo>
+ <updated>2011-01-06T22:44:18+00:00</updated>
+<author>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/group</activity:object-type>
+ <uri>http://lazarus.local/mublog/group/22/id</uri>
+ <name>grouptest316173</name>
+ <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/group/22/id"/>
+ <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/>
+ <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/>
+ <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/>
+ <poco:preferredUsername>grouptest316173</poco:preferredUsername>
+ <poco:displayName>grouptest316173</poco:displayName>
+</author>
+<activity:subject>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/group</activity:object-type>
+ <id>http://lazarus.local/mublog/group/22/id</id>
+ <title>grouptest316173</title>
+ <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/group/22/id"/>
+ <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/>
+ <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/>
+ <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/>
+ <poco:preferredUsername>grouptest316173</poco:preferredUsername>
+ <poco:displayName>grouptest316173</poco:displayName>
+</activity:subject>
+ <link href="http://lazarus.local/mublog/group/grouptest316173" rel="alternate" type="text/html"/>
+ <link href="http://lazarus.local/mublog/main/push/hub" rel="hub"/>
+ <link href="http://lazarus.local/mublog/main/salmon/group/22" rel="salmon"/>
+ <link href="http://lazarus.local/mublog/main/salmon/group/22" rel="http://salmon-protocol.org/ns/salmon-replies"/>
+ <link href="http://lazarus.local/mublog/main/salmon/group/22" rel="http://salmon-protocol.org/ns/salmon-mention"/>
+ <link href="http://lazarus.local/mublog/api/statusnet/groups/timeline/22.atom" rel="self" type="application/atom+xml"/>
+ <statusnet:group_info member_count="2"></statusnet:group_info>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <id>http://lazarus.local/mublog/notice/1243</id>
+ <title>Group post from local to !grouptest316173, should go out over push.</title>
+ <content type="html">Group post from local to !<span class="vcard"><a href="http://lazarus.local/mublog/group/22/id" class="url"><span class="fn nickname">grouptest316173</span></a></span>, should go out over push.</content>
+ <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/notice/1243"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2011-01-06T22:44:18+00:00</published>
+ <updated>2011-01-06T22:44:18+00:00</updated>
+ <author>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+ <uri>http://lazarus.local/mublog/user/557</uri>
+ <name>Pubtest316173 Smith</name>
+ <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/pubtest316173"/>
+ <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/>
+ <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/>
+ <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/>
+ <poco:preferredUsername>pubtest316173</poco:preferredUsername>
+ <poco:displayName>Pubtest316173 Smith</poco:displayName>
+ <poco:note>Stub account for OStatus tests.</poco:note>
+ <poco:urls>
+ <poco:type>homepage</poco:type>
+ <poco:value>http://example.org/pubtest316173</poco:value>
+ <poco:primary>true</poco:primary>
+ </poco:urls>
+ </author>
+ <link rel="ostatus:conversation" href="http://lazarus.local/mublog/conversation/1131"/>
+ <link rel="ostatus:attention" href="http://lazarus.local/mublog/group/22/id"/>
+ <link rel="mentioned" href="http://lazarus.local/mublog/group/22/id"/>
+ <category term="grouptest316173"></category>
+ <source>
+ <id>http://lazarus.local/mublog/api/statuses/user_timeline/557.atom</id>
+ <title>Pubtest316173 Smith</title>
+ <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/pubtest316173"/>
+ <link rel="self" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/user_timeline/557.atom"/>
+ <link rel="license" href="http://creativecommons.org/licenses/by/3.0/"/>
+ <icon>http://lazarus.local/mublog/theme/default/default-avatar-profile.png</icon>
+ <updated>2011-01-06T22:44:18+00:00</updated>
+ </source>
+ <link rel="self" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/show/1243.atom"/>
+ <link rel="edit" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/show/1243.atom"/>
+ <statusnet:notice_info local_id="1243" source="api"></statusnet:notice_info>
+</entry>
+</feed>
+EXAMPLE10;