diff options
30 files changed, 1461 insertions, 169 deletions
diff --git a/actions/apifavoritecreate.php b/actions/apifavoritecreate.php index 3618f9401..00b6349b0 100644 --- a/actions/apifavoritecreate.php +++ b/actions/apifavoritecreate.php @@ -123,7 +123,7 @@ class ApiFavoriteCreateAction extends ApiAuthAction return; } - $fave = Fave::addNew($this->user, $this->notice); + $fave = Fave::addNew($this->user->getProfile(), $this->notice); if (empty($fave)) { $this->clientError( diff --git a/actions/foaf.php b/actions/foaf.php index fc2ec9b12..fc56e19b4 100644 --- a/actions/foaf.php +++ b/actions/foaf.php @@ -162,40 +162,29 @@ class FoafAction extends Action if ($sub->find()) { while ($sub->fetch()) { - if ($sub->token) { - $other = Remote_profile::staticGet('id', $sub->subscriber); - $profile = Profile::staticGet('id', $sub->subscriber); - } else { - $other = User::staticGet('id', $sub->subscriber); - $profile = Profile::staticGet('id', $sub->subscriber); - } - if (!$other) { + $profile = Profile::staticGet('id', $sub->subscriber); + if (empty($profile)) { common_debug('Got a bad subscription: '.print_r($sub,true)); continue; } - if (array_key_exists($other->uri, $person)) { - $person[$other->uri][0] = BOTH; + $user = $profile->getUser(); + $other_uri = $profile->getUri(); + if (array_key_exists($other_uri, $person)) { + $person[$other_uri][0] = BOTH; } else { - $person[$other->uri] = array(LISTENER, - $other->id, - $profile->nickname, - (empty($sub->token)) ? 'User' : 'Remote_profile'); + $person[$other_uri] = array(LISTENER, + $profile->id, + $profile->nickname, + $user ? 'local' : 'remote'); } - $other->free(); - $other = null; - unset($other); - $profile->free(); - $profile = null; unset($profile); } } - $sub->free(); - $sub = null; unset($sub); foreach ($person as $uri => $p) { - list($type, $id, $nickname, $cls) = $p; + list($type, $id, $nickname, $local) = $p; if ($type == BOTH) { $this->element('knows', array('rdf:resource' => $uri)); } @@ -206,8 +195,8 @@ class FoafAction extends Action foreach ($person as $uri => $p) { $foaf_url = null; - list($type, $id, $nickname, $cls) = $p; - if ($cls == 'User') { + list($type, $id, $nickname, $local) = $p; + if ($local == 'local') { $foaf_url = common_local_url('foaf', array('nickname' => $nickname)); } $profile = Profile::staticGet($id); @@ -216,7 +205,7 @@ class FoafAction extends Action $this->element('knows', array('rdf:resource' => $this->user->uri)); } $this->showMicrobloggingAccount($profile, - ($cls == 'User') ? common_root_url() : null, + ($local == 'local') ? common_root_url() : null, $uri, true); if ($foaf_url) { @@ -275,33 +264,22 @@ class FoafAction extends Action if ($sub->find()) { while ($sub->fetch()) { - if (!empty($sub->token)) { - $other = Remote_profile::staticGet('id', $sub->subscribed); - $profile = Profile::staticGet('id', $sub->subscribed); - } else { - $other = User::staticGet('id', $sub->subscribed); - $profile = Profile::staticGet('id', $sub->subscribed); - } - if (empty($other)) { + $profile = Profile::staticGet('id', $sub->subscribed); + if (empty($profile)) { common_debug('Got a bad subscription: '.print_r($sub,true)); continue; } - $this->element('sioc:follows', array('rdf:resource' => $other->uri.'#acct')); - $person[$other->uri] = array(LISTENEE, - $other->id, - $profile->nickname, - (empty($sub->token)) ? 'User' : 'Remote_profile'); - $other->free(); - $other = null; - unset($other); - $profile->free(); - $profile = null; + $user = $profile->getUser(); + $other_uri = $profile->getUri(); + $this->element('sioc:follows', array('rdf:resource' => $other_uri.'#acct')); + $person[$other_uri] = array(LISTENEE, + $profile->id, + $profile->nickname, + $user ? 'local' : 'remote'); unset($profile); } } - $sub->free(); - $sub = null; unset($sub); } diff --git a/classes/Conversation.php b/classes/Conversation.php index ea8bd87b5..f540004ef 100755 --- a/classes/Conversation.php +++ b/classes/Conversation.php @@ -63,7 +63,8 @@ class Conversation extends Memcached_DataObject } $orig = clone($conv); - $orig->uri = common_local_url('conversation', array('id' => $id)); + $orig->uri = common_local_url('conversation', array('id' => $id), + null, null, false); $result = $orig->update($conv); if (empty($result)) { diff --git a/classes/Fave.php b/classes/Fave.php index a04f15e9c..7ca9ade7f 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -21,7 +21,15 @@ class Fave extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - static function addNew($profile, $notice) { + /** + * Save a favorite record. + * @fixme post-author notification should be moved here + * + * @param Profile $profile the local or remote user who likes + * @param Notice $notice the notice that is liked + * @return mixed false on failure, or Fave record on success + */ + static function addNew(Profile $profile, Notice $notice) { $fave = null; diff --git a/classes/Notice.php b/classes/Notice.php index f7194e339..be3e9ca2a 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -421,7 +421,9 @@ class Notice extends Memcached_DataObject } $profile = Profile::staticGet($this->profile_id); - $profile->blowNoticeCount(); + if (!empty($profile)) { + $profile->blowNoticeCount(); + } } /** @@ -324,10 +324,10 @@ function main() $cac = new ClientErrorAction($cex->getMessage(), $cex->getCode()); $cac->showPage(); } catch (ServerException $sex) { // snort snort guffaw - $sac = new ServerErrorAction($sex->getMessage(), $sex->getCode()); + $sac = new ServerErrorAction($sex->getMessage(), $sex->getCode(), $sex); $sac->showPage(); } catch (Exception $ex) { - $sac = new ServerErrorAction($ex->getMessage()); + $sac = new ServerErrorAction($ex->getMessage(), 500, $ex); $sac->showPage(); } } diff --git a/lib/activity.php b/lib/activity.php index bd1d5d56c..f9192c6b8 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -53,6 +53,7 @@ class Activity { const SPEC = 'http://activitystrea.ms/spec/1.0/'; const SCHEMA = 'http://activitystrea.ms/schema/1.0/'; + const MEDIA = 'http://purl.org/syndication/atommedia'; const VERB = 'verb'; const OBJECT = 'object'; @@ -85,7 +86,7 @@ class Activity public $actor; // an ActivityObject public $verb; // a string (the URL) - public $object; // an ActivityObject + public $objects = array(); // an array of ActivityObjects public $target; // an ActivityObject public $context; // an ActivityObject public $time; // Time of the activity @@ -161,12 +162,15 @@ class Activity // XXX: do other implied stuff here } - $objectEl = $this->_child($entry, self::OBJECT); + $objectEls = $entry->getElementsByTagNameNS(self::SPEC, self::OBJECT); - if (!empty($objectEl)) { - $this->object = new ActivityObject($objectEl); + if ($objectEls->length > 0) { + for ($i = 0; $i < $objectEls->length; $i++) { + $objectEl = $objectEls->item($i); + $this->objects[] = new ActivityObject($objectEl); + } } else { - $this->object = new ActivityObject($entry); + $this->objects[] = new ActivityObject($entry); } $actorEl = $this->_child($entry, self::ACTOR); @@ -280,8 +284,8 @@ class Activity } } - $this->object = new ActivityObject($item); - $this->context = new ActivityContext($item); + $this->objects[] = new ActivityObject($item); + $this->context = new ActivityContext($item); } /** @@ -339,8 +343,10 @@ class Activity $xs->element('activity:verb', null, $this->verb); - if ($this->object) { - $xs->raw($this->object->asString()); + if (!empty($this->objects)) { + foreach($this->objects as $object) { + $xs->raw($object->asString()); + } } if ($this->target) { diff --git a/lib/activityobject.php b/lib/activityobject.php index 0a358ccab..34d1b9170 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -100,6 +100,13 @@ class ActivityObject public $poco; public $displayName; + // @todo move this stuff to it's own PHOTO activity object + const MEDIA_DESCRIPTION = 'description'; + + public $thumbnail; + public $largerImage; + public $description; + /** * Constructor * @@ -150,6 +157,19 @@ class ActivityObject $this->poco = new PoCo($element); } + + if ($this->type == self::PHOTO) { + + $this->thumbnail = ActivityUtils::getLink($element, 'preview'); + $this->largerImage = ActivityUtils::getLink($element, 'enclosure'); + + $this->description = ActivityUtils::childContent( + $element, + ActivityObject::MEDIA_DESCRIPTION, + Activity::MEDIA + ); + + } } private function _fromAuthor($element) diff --git a/lib/apiaction.php b/lib/apiaction.php index e6aaf9316..9fc1a0779 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -1273,7 +1273,7 @@ class ApiAction extends Action if (empty($local)) { return null; } else { - return User_group::staticGet('id', $local->id); + return User_group::staticGet('id', $local->group_id); } } diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index 51ceca857..fe38281af 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -306,7 +306,7 @@ class Attachment extends AttachmentListItem function showRepresentation() { if (empty($this->oembed->type)) { if (empty($this->attachment->mimetype)) { - $this->out->element('pre', null, 'oh well... not sure how to handle the following: ' . print_r($this->attachment, true)); + $this->showFallback(); } else { switch ($this->attachment->mimetype) { case 'image/gif': @@ -332,6 +332,8 @@ class Attachment extends AttachmentListItem $this->out->element('param', array('name' => 'autoStart', 'value' => 1)); $this->out->elementEnd('object'); break; + default: + $this->showFallback(); } } } else { @@ -354,9 +356,23 @@ class Attachment extends AttachmentListItem break; default: - $this->out->element('pre', null, 'oh well... not sure how to handle the following oembed: ' . print_r($this->oembed, true)); + $this->showFallback(); } } } + + function showFallback() + { + // If we don't know how to display an attachment inline, we probably + // shouldn't have gotten to this point. + // + // But, here we are... displaying details on a file or remote URL + // either on the main view or in an ajax-loaded lightbox. As a lesser + // of several evils, we'll try redirecting to the actual target via + // client-side JS. + + common_log(LOG_ERR, "Empty or unknown type for file id {$this->attachment->id}; falling back to client-side redirect."); + $this->out->raw('<script>window.location = ' . json_encode($this->attachment->url) . ';</script>'); + } } diff --git a/lib/command.php b/lib/command.php index f7421269d..216f9e649 100644 --- a/lib/command.php +++ b/lib/command.php @@ -273,7 +273,7 @@ class FavCommand extends Command function handle($channel) { $notice = $this->getNotice($this->other); - $fave = Fave::addNew($this->user, $notice); + $fave = Fave::addNew($this->user->getProfile(), $notice); if (!$fave) { $channel->error($this->user, _('Could not create favorite.')); diff --git a/lib/router.php b/lib/router.php index a48ee875e..a9d07276f 100644 --- a/lib/router.php +++ b/lib/router.php @@ -33,6 +33,33 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { require_once 'Net/URL/Mapper.php'; +class StatusNet_URL_Mapper extends Net_URL_Mapper { + + private static $_singleton = null; + + private function __construct() + { + } + + public static function getInstance($id = '__default__') + { + if (empty(self::$_singleton)) { + self::$_singleton = new StatusNet_URL_Mapper(); + } + return self::$_singleton; + } + + public function connect($path, $defaults = array(), $rules = array()) + { + $result = null; + if (Event::handle('StartConnectPath', array(&$path, &$defaults, &$rules, &$result))) { + $result = parent::connect($path, $defaults, $rules); + Event::handle('EndConnectPath', array($path, $defaults, $rules, $result)); + } + return $result; + } +} + /** * URL Router * @@ -69,7 +96,7 @@ class Router function initialize() { - $m = Net_URL_Mapper::getInstance(); + $m = StatusNet_URL_Mapper::getInstance(); if (Event::handle('StartInitializeRouter', array(&$m))) { diff --git a/lib/servererroraction.php b/lib/servererroraction.php index 0993a63bc..9b5a553dc 100644 --- a/lib/servererroraction.php +++ b/lib/servererroraction.php @@ -62,15 +62,18 @@ class ServerErrorAction extends ErrorAction 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported'); - function __construct($message='Error', $code=500) + function __construct($message='Error', $code=500, $ex=null) { parent::__construct($message, $code); $this->default = 500; // Server errors must be logged. - - common_log(LOG_ERR, "ServerErrorAction: $code $message"); + $log = "ServerErrorAction: $code $message"; + if ($ex) { + $log .= "\n" . $ex->getTraceAsString(); + } + common_log(LOG_ERR, $log); } // XXX: Should these error actions even be invokable via URI? diff --git a/lib/util.php b/lib/util.php index a30d69100..795997868 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1529,7 +1529,8 @@ function common_user_uri(&$user) function common_notice_uri(&$notice) { return common_local_url('shownotice', - array('notice' => $notice->id)); + array('notice' => $notice->id), + null, null, false); } // 36 alphanums - lookalikes (0, O, 1, I) = 32 chars = 5 bits diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php index 29377b5fa..d60725a71 100644 --- a/plugins/OStatus/actions/groupsalmon.php +++ b/plugins/OStatus/actions/groupsalmon.php @@ -60,7 +60,8 @@ class GroupsalmonAction extends SalmonAction function handlePost() { - switch ($this->act->object->type) { + // @fixme process all objects? + switch ($this->act->objects[0]->type) { case ActivityObject::ARTICLE: case ActivityObject::BLOGENTRY: case ActivityObject::NOTE: diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php index 15e8c1869..6c360c49f 100644 --- a/plugins/OStatus/actions/usersalmon.php +++ b/plugins/OStatus/actions/usersalmon.php @@ -55,9 +55,10 @@ class UsersalmonAction extends SalmonAction */ function handlePost() { - common_log(LOG_INFO, "Received post of '{$this->act->object->id}' from '{$this->act->actor->id}'"); + common_log(LOG_INFO, "Received post of '{$this->act->objects[0]->id}' from '{$this->act->actor->id}'"); - switch ($this->act->object->type) { + // @fixme: process all activity objects? + switch ($this->act->objects[0]->type) { case ActivityObject::ARTICLE: case ActivityObject::BLOGENTRY: case ActivityObject::NOTE: @@ -91,7 +92,7 @@ class UsersalmonAction extends SalmonAction throw new ClientException("Not to anyone in reply to anything!"); } - $existing = Notice::staticGet('uri', $this->act->object->id); + $existing = Notice::staticGet('uri', $this->act->objects[0]->id); if (!empty($existing)) { common_log(LOG_ERR, "Not saving notice '{$existing->uri}'; already exists."); @@ -142,7 +143,7 @@ class UsersalmonAction extends SalmonAction function handleFavorite() { - $notice = $this->getNotice($this->act->object); + $notice = $this->getNotice($this->act->objects[0]); $profile = $this->ensureProfile()->localProfile(); $old = Fave::pkeyGet(array('user_id' => $profile->id, @@ -163,7 +164,7 @@ class UsersalmonAction extends SalmonAction */ function handleUnfavorite() { - $notice = $this->getNotice($this->act->object); + $notice = $this->getNotice($this->act->objects[0]); $profile = $this->ensureProfile()->localProfile(); $fave = Fave::pkeyGet(array('user_id' => $profile->id, diff --git a/plugins/OStatus/actions/userxrd.php b/plugins/OStatus/actions/userxrd.php index eb80a5ad4..6a6886eb8 100644 --- a/plugins/OStatus/actions/userxrd.php +++ b/plugins/OStatus/actions/userxrd.php @@ -35,9 +35,13 @@ class UserxrdAction extends XrdAction $this->uri = Discovery::normalize($this->uri); if (Discovery::isWebfinger($this->uri)) { - list($nick, $domain) = explode('@', substr(urldecode($this->uri), 5)); - $nick = common_canonical_nickname($nick); - $this->user = User::staticGet('nickname', $nick); + $parts = explode('@', substr(urldecode($this->uri), 5)); + if (count($parts) == 2) { + list($nick, $domain) = $parts; + // @fixme confirm the domain too + $nick = common_canonical_nickname($nick); + $this->user = User::staticGet('nickname', $nick); + } } else { $this->user = User::staticGet('uri', $this->uri); } diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php index 5705ecc11..864fef628 100644 --- a/plugins/OStatus/classes/Magicsig.php +++ b/plugins/OStatus/classes/Magicsig.php @@ -52,7 +52,15 @@ class Magicsig extends Memcached_DataObject { $obj = parent::staticGet(__CLASS__, $k, $v); if (!empty($obj)) { - return Magicsig::fromString($obj->keypair); + $obj = Magicsig::fromString($obj->keypair); + + // Double check keys: Crypt_RSA did not + // consistently generate good keypairs. + // We've also moved to 1024 bit keys. + if (strlen($obj->publicKey->modulus->toBits()) != 1024) { + $obj->delete(); + return false; + } } return $obj; diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 0eb5b8b82..c7e3b0509 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -442,7 +442,8 @@ class Ostatus_profile extends Memcached_DataObject { $activity = new Activity($entry, $feed); - switch ($activity->object->type) { + // @todo process all activity objects + switch ($activity->objects[0]->type) { case ActivityObject::ARTICLE: case ActivityObject::BLOGENTRY: case ActivityObject::NOTE: @@ -494,7 +495,7 @@ class Ostatus_profile extends Memcached_DataObject // It's not always an ActivityObject::NOTE, but... let's just say it is. - $note = $activity->object; + $note = $activity->objects[0]; // The id URI will be used as a unique identifier for for the notice, // protecting against duplicate saves. It isn't required to be a URL; diff --git a/plugins/OStatus/extlib/Math/BigInteger.php b/plugins/OStatus/extlib/Math/BigInteger.php index 9733351d4..4373805f9 100644 --- a/plugins/OStatus/extlib/Math/BigInteger.php +++ b/plugins/OStatus/extlib/Math/BigInteger.php @@ -67,7 +67,7 @@ * @author Jim Wigginton <terrafrost@php.net> * @copyright MMVI Jim Wigginton * @license http://www.gnu.org/licenses/lgpl.txt - * @version $Id: BigInteger.php,v 1.31 2010/03/01 17:28:19 terrafrost Exp $ + * @version $Id: BigInteger.php,v 1.33 2010/03/22 22:32:03 terrafrost Exp $ * @link http://pear.php.net/package/Math_BigInteger */ @@ -294,7 +294,7 @@ class Math_BigInteger { $this->value = array(); } - if ($x === 0) { + if (empty($x)) { return; } @@ -718,7 +718,7 @@ class Math_BigInteger { * * Will be called, automatically, when serialize() is called on a Math_BigInteger object. * - * @see __wakeup + * @see __wakeup() * @access public */ function __sleep() @@ -740,7 +740,7 @@ class Math_BigInteger { * * Will be called, automatically, when unserialize() is called on a Math_BigInteger object. * - * @see __sleep + * @see __sleep() * @access public */ function __wakeup() diff --git a/plugins/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php index 44fad62fb..7187c1f3e 100644 --- a/plugins/OStatus/lib/discovery.php +++ b/plugins/OStatus/lib/discovery.php @@ -195,7 +195,7 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD // return false; } - return Discovery_LRDD_Link_Header::parseHeader($link_header); + return array(Discovery_LRDD_Link_Header::parseHeader($link_header)); } protected static function parseHeader($header) diff --git a/plugins/OStatus/lib/linkheader.php b/plugins/OStatus/lib/linkheader.php index afcd66d26..cd78d31ce 100644 --- a/plugins/OStatus/lib/linkheader.php +++ b/plugins/OStatus/lib/linkheader.php @@ -11,7 +11,7 @@ class LinkHeader preg_match('/^<[^>]+>/', $str, $uri_reference); //if (empty($uri_reference)) return; - $this->uri = trim($uri_reference[0], '<>'); + $this->href = trim($uri_reference[0], '<>'); $this->rel = array(); $this->type = null; diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index 9266cab5c..799b5e307 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -59,12 +59,21 @@ class MagicEnvelope } if ($xrd->links) { if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) { - list($type, $keypair) = explode(',', $link['href']); - if (empty($keypair)) { + $keypair = false; + $parts = explode(',', $link['href']); + if (count($parts) == 2) { + $keypair = $parts[1]; + } else { // Backwards compatibility check for separator bug in 0.9.0 - list($type, $keypair) = explode(';', $link['href']); + $parts = explode(';', $link['href']); + if (count($parts) == 2) { + $keypair = $parts[1]; + } + } + + if ($keypair) { + return $keypair; } - return $keypair; } } throw new Exception('Unable to locate signer public key'); diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php index aa13ef024..34b28790b 100644 --- a/plugins/OStatus/lib/xrd.php +++ b/plugins/OStatus/lib/xrd.php @@ -53,7 +53,14 @@ class XRD $xrd = new XRD(); $dom = new DOMDocument(); - if (!$dom->loadXML($xml)) { + + // Don't spew XML warnings to output + $old = error_reporting(); + error_reporting($old & ~E_WARNING); + $ok = $dom->loadXML($xml); + error_reporting($old); + + if (!$ok) { throw new Exception("Invalid XML"); } $xrd_element = $dom->getElementsByTagName('XRD')->item(0); diff --git a/plugins/OStatus/tests/remote-tests.php b/plugins/OStatus/tests/remote-tests.php new file mode 100644 index 000000000..24b4b1660 --- /dev/null +++ b/plugins/OStatus/tests/remote-tests.php @@ -0,0 +1,555 @@ +<?php + +if (php_sapi_name() != 'cli') { + die('not for web'); +} + +define('INSTALLDIR', dirname(dirname(dirname(dirname(__FILE__))))); +set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path()); + +require_once 'PEAR.php'; +require_once 'Net/URL2.php'; +require_once 'HTTP/Request2.php'; + + +// ostatus test script, client-side :) + +class TestBase +{ + function log($str) + { + $args = func_get_args(); + array_shift($args); + + $msg = vsprintf($str, $args); + print $msg . "\n"; + } + + function assertEqual($a, $b) + { + if ($a != $b) { + throw new Exception("Failed to assert equality: expected $a, got $b"); + } + return true; + } + + function assertNotEqual($a, $b) + { + if ($a == $b) { + throw new Exception("Failed to assert inequality: expected not $a, got $b"); + } + return true; + } + + function assertTrue($a) + { + if (!$a) { + throw new Exception("Failed to assert true: got false"); + } + } + + function assertFalse($a) + { + if ($a) { + throw new Exception("Failed to assert false: got true"); + } + } +} + +class OStatusTester extends TestBase +{ + /** + * @param string $a base URL of test site A (eg http://localhost/mublog) + * @param string $b base URL of test site B (eg http://localhost/mublog2) + */ + function __construct($a, $b) { + $this->a = $a; + $this->b = $b; + + $base = 'test' . mt_rand(1, 1000000); + $this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000)); + $this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000)); + } + + function run() + { + $this->setup(); + + $methods = get_class_methods($this); + foreach ($methods as $method) { + if (strtolower(substr($method, 0, 4)) == 'test') { + print "\n"; + print "== $method ==\n"; + call_user_func(array($this, $method)); + } + } + + print "\n"; + $this->log("DONE!"); + } + + function setup() + { + $this->pub->register(); + $this->pub->assertRegistered(); + + $this->sub->register(); + $this->sub->assertRegistered(); + } + + function testLocalPost() + { + $post = $this->pub->post("Local post, no subscribers yet."); + $this->assertNotEqual('', $post); + + $post = $this->sub->post("Local post, no subscriptions yet."); + $this->assertNotEqual('', $post); + } + + /** + * pub posts: @b/sub + */ + function testMentionUrl() + { + $bits = parse_url($this->b); + $base = $bits['host']; + if (isset($bits['path'])) { + $base .= $bits['path']; + } + $name = $this->sub->username; + + $post = $this->pub->post("@$base/$name should have this in home and replies"); + $this->sub->assertReceived($post); + } + + function testSubscribe() + { + $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri())); + $this->sub->subscribe($this->pub->getProfileLink()); + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri())); + } + + function testPush() + { + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri())); + + $name = $this->sub->username; + $post = $this->pub->post("Regular post, which $name should get via PuSH"); + $this->sub->assertReceived($post); + } + + function testMentionSubscribee() + { + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri())); + + $name = $this->pub->username; + $post = $this->sub->post("Just a quick note back to my remote subscribee @$name"); + $this->pub->assertReceived($post); + } + + function testUnsubscribe() + { + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri())); + $this->sub->unsubscribe($this->pub->getProfileLink()); + $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri())); + } + +} + +class SNTestClient extends TestBase +{ + function __construct($base, $username, $password) + { + $this->basepath = $base; + $this->username = $username; + $this->password = $password; + + $this->fullname = ucfirst($username) . ' Smith'; + $this->homepage = 'http://example.org/' . $username; + $this->bio = 'Stub account for OStatus tests.'; + $this->location = 'Montreal, QC'; + } + + /** + * Make a low-level web hit to this site, with authentication. + * @param string $path URL fragment for something under the base path + * @param array $params POST parameters to send + * @param boolean $auth whether to include auth data + * @return string + * @throws Exception on low-level error conditions + */ + protected function hit($path, $params=array(), $auth=false, $cookies=array()) + { + $url = $this->basepath . '/' . $path; + + $http = new HTTP_Request2($url, 'POST'); + if ($auth) { + $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC); + } + foreach ($cookies as $name => $val) { + $http->addCookie($name, $val); + } + $http->addPostParameter($params); + $response = $http->send(); + + $code = $response->getStatus(); + if ($code < '200' || $code >= '400') { + throw new Exception("Failed API hit to $url: $code\n" . $response->getBody()); + } + + return $response; + } + + /** + * Make a hit to a web form, without authentication but with a session. + * @param string $path URL fragment relative to site base + * @param string $form id of web form to pull initial parameters from + * @param array $params POST parameters, will be merged with defaults in form + */ + protected function web($path, $form, $params=array()) + { + $url = $this->basepath . '/' . $path; + $http = new HTTP_Request2($url, 'GET'); + $response = $http->send(); + + $dom = $this->checkWeb($url, 'GET', $response); + $cookies = array(); + foreach ($response->getCookies() as $cookie) { + // @fixme check for expirations etc + $cookies[$cookie['name']] = $cookie['value']; + } + + $form = $dom->getElementById($form); + if (!$form) { + throw new Exception("Form $form not found on $url"); + } + $inputs = $form->getElementsByTagName('input'); + foreach ($inputs as $item) { + $type = $item->getAttribute('type'); + if ($type != 'check') { + $name = $item->getAttribute('name'); + $val = $item->getAttribute('value'); + if ($name && $val && !isset($params[$name])) { + $params[$name] = $val; + } + } + } + + $response = $this->hit($path, $params, false, $cookies); + $dom = $this->checkWeb($url, 'POST', $response); + + return $dom; + } + + protected function checkWeb($url, $method, $response) + { + $dom = new DOMDocument(); + if (!$dom->loadHTML($response->getBody())) { + throw new Exception("Invalid HTML from $method to $url"); + } + + $xpath = new DOMXPath($dom); + $error = $xpath->query('//p[@class="error"]'); + if ($error && $error->length) { + throw new Exception("Error on $method to $url: " . + $error->item(0)->textContent); + } + + return $dom; + } + + protected function parseXml($path, $body) + { + $dom = new DOMDocument(); + if ($dom->loadXML($body)) { + return $dom; + } else { + throw new Exception("Bogus XML data from $path:\n$body"); + } + } + + /** + * Make a hit to a REST-y XML page on the site, without authentication. + * @param string $path URL fragment for something relative to base + * @param array $params POST parameters to send + * @return DOMDocument + * @throws Exception on low-level error conditions + */ + protected function xml($path, $params=array()) + { + $response = $this->hit($path, $params, true); + $body = $response->getBody(); + return $this->parseXml($path, $body); + } + + protected function parseJson($path, $body) + { + $data = json_decode($body, true); + if ($data !== null) { + if (!empty($data['error'])) { + throw new Exception("JSON API returned error: " . $data['error']); + } + return $data; + } else { + throw new Exception("Bogus JSON data from $path:\n$body"); + } + } + + /** + * Make an API hit to this site, with authentication. + * @param string $path URL fragment for something under 'api' folder + * @param string $style one of 'json', 'xml', or 'atom' + * @param array $params POST parameters to send + * @return mixed associative array for JSON, DOMDocument for XML/Atom + * @throws Exception on low-level error conditions + */ + protected function api($path, $style, $params=array()) + { + $response = $this->hit("api/$path.$style", $params, true); + $body = $response->getBody(); + if ($style == 'json') { + return $this->parseJson($path, $body); + } else if ($style == 'xml' || $style == 'atom') { + return $this->parseXml($path, $body); + } else { + throw new Exception("API needs to be JSON, XML, or Atom"); + } + } + + /** + * Register the account. + * + * Unfortunately there's not an API method for registering, so we fake it. + */ + function register() + { + $this->log("Registering user %s on %s", + $this->username, + $this->basepath); + $ret = $this->web('main/register', 'form_register', + array('nickname' => $this->username, + 'password' => $this->password, + 'confirm' => $this->password, + 'fullname' => $this->fullname, + 'homepage' => $this->homepage, + 'bio' => $this->bio, + 'license' => 1, + 'submit' => 'Register')); + } + + /** + * @return string canonical URI/URL to profile page + */ + function getProfileUri() + { + $data = $this->api('account/verify_credentials', 'json'); + $id = $data['id']; + return $this->basepath . '/user/' . $id; + } + + /** + * @return string human-friendly URL to profile page + */ + function getProfileLink() + { + return $this->basepath . '/' . $this->username; + } + + /** + * Check that the account has been registered and can be used. + * On failure, throws a test failure exception. + */ + function assertRegistered() + { + $this->log("Confirming %s is registered on %s", + $this->username, + $this->basepath); + $data = $this->api('account/verify_credentials', 'json'); + $this->assertEqual($this->username, $data['screen_name']); + $this->assertEqual($this->fullname, $data['name']); + $this->assertEqual($this->homepage, $data['url']); + $this->assertEqual($this->bio, $data['description']); + $this->log(" looks good!"); + } + + /** + * Post a given message from this account + * @param string $message + * @return string URL/URI of notice + * @todo reply, location options + */ + function post($message) + { + $this->log("Posting notice as %s on %s: %s", + $this->username, + $this->basepath, + $message); + $data = $this->api('statuses/update', 'json', + array('status' => $message)); + + $url = $this->basepath . '/notice/' . $data['id']; + return $url; + } + + /** + * Check that this account has received the notice. + * @param string $notice_uri URI for the notice to check for + */ + function assertReceived($notice_uri) + { + $timeout = 5; + $tries = 6; + while ($tries) { + $ok = $this->checkReceived($notice_uri); + if ($ok) { + return true; + } + $tries--; + if ($tries) { + $this->log(" didn't see it yet, waiting $timeout seconds"); + sleep($timeout); + } + } + throw new Exception(" message $notice_uri not received by $this->username"); + } + + /** + * Pull the user's home timeline to check if a notice with the given + * source URL has been received recently. + * If we don't see it, we'll try a couple more times up to 10 seconds. + * + * @param string $notice_uri + */ + function checkReceived($notice_uri) + { + $this->log("Checking if %s on %s received notice %s", + $this->username, + $this->basepath, + $notice_uri); + $params = array(); + $dom = $this->api('statuses/home_timeline', 'atom', $params); + + $xml = simplexml_import_dom($dom); + if (!$xml->entry) { + return false; + } + if (is_array($xml->entry)) { + $entries = $xml->entry; + } else { + $entries = array($xml->entry); + } + foreach ($entries as $entry) { + if ($entry->id == $notice_uri) { + $this->log(" found it $notice_uri"); + return true; + } + } + return false; + } + + /** + * @param string $profile user page link or webfinger + */ + function subscribe($profile) + { + // This uses the command interface, since there's not currently + // a friendly Twit-API way to do a fresh remote subscription and + // the web form's a pain to use. + $this->post('follow ' . $profile); + } + + /** + * @param string $profile user page link or webfinger + */ + function unsubscribe($profile) + { + // This uses the command interface, since there's not currently + // a friendly Twit-API way to do a fresh remote subscription and + // the web form's a pain to use. + $this->post('leave ' . $profile); + } + + /** + * Check that this account is subscribed to the given profile. + * @param string $profile_uri URI for the profile to check for + * @return boolean + */ + function hasSubscription($profile_uri) + { + $this->log("Checking if $this->username has a subscription to $profile_uri"); + + $me = $this->getProfileUri(); + return $this->checkSubscription($me, $profile_uri); + } + + /** + * Check that this account is subscribed to by the given profile. + * @param string $profile_uri URI for the profile to check for + * @return boolean + */ + function hasSubscriber($profile_uri) + { + $this->log("Checking if $this->username is subscribed to by $profile_uri"); + + $me = $this->getProfileUri(); + return $this->checkSubscription($profile_uri, $me); + } + + protected function checkSubscription($subscriber, $subscribed) + { + // Using FOAF as the API methods for checking the social graph + // currently are unfriendly to remote profiles + $ns_foaf = 'http://xmlns.com/foaf/0.1/'; + $ns_sioc = 'http://rdfs.org/sioc/ns#'; + $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + + $dom = $this->xml($this->username . '/foaf'); + $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent'); + foreach ($agents as $agent) { + $agent_uri = $agent->getAttributeNS($ns_rdf, 'about'); + if ($agent_uri == $subscriber) { + $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows'); + foreach ($follows as $follow) { + $target = $follow->getAttributeNS($ns_rdf, 'resource'); + if ($target == ($subscribed . '#acct')) { + $this->log(" confirmed $subscriber subscribed to $subscribed"); + return true; + } + } + $this->log(" we found $subscriber but they don't follow $subscribed"); + return false; + } + } + $this->log(" can't find $subscriber in {$this->username}'s social graph."); + return false; + } + +} + +$args = array_slice($_SERVER['argv'], 1); +if (count($args) < 2) { + print <<<END_HELP +remote-tests.php <url1> <url2> + url1: base URL of a StatusNet instance + url2: base URL of another StatusNet instance + +This will register user accounts on the two given StatusNet instances +and run some tests to confirm that OStatus subscription and posting +between the two sites works correctly. + +END_HELP; +exit(1); +} + +$a = $args[0]; +$b = $args[1]; + +$tester = new OStatusTester($a, $b); +$tester->run(); + diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php index 6b35ec3e1..270e2c624 100644 --- a/plugins/OpenID/OpenIDPlugin.php +++ b/plugins/OpenID/OpenIDPlugin.php @@ -45,20 +45,15 @@ if (!defined('STATUSNET')) { class OpenIDPlugin extends Plugin { - /** - * Initializer for the plugin. - */ - - function __construct() - { - parent::__construct(); - } + public $openidOnly = false; /** * Add OpenID-related paths to the router table * * Hook for RouterInitialized event. * + * @param Net_URL_Mapper $m URL mapper + * * @return boolean hook return */ @@ -67,66 +62,255 @@ class OpenIDPlugin extends Plugin $m->connect('main/openid', array('action' => 'openidlogin')); $m->connect('main/openidtrust', array('action' => 'openidtrust')); $m->connect('settings/openid', array('action' => 'openidsettings')); - $m->connect('index.php?action=finishopenidlogin', array('action' => 'finishopenidlogin')); - $m->connect('index.php?action=finishaddopenid', array('action' => 'finishaddopenid')); + $m->connect('index.php?action=finishopenidlogin', + array('action' => 'finishopenidlogin')); + $m->connect('index.php?action=finishaddopenid', + array('action' => 'finishaddopenid')); $m->connect('main/openidserver', array('action' => 'openidserver')); return true; } + /** + * In OpenID-only mode, disable paths for password stuff + * + * @param string $path path to connect + * @param array $defaults path defaults + * @param array $rules path rules + * @param array $result unused + * + * @return boolean hook return + */ + + function onStartConnectPath(&$path, &$defaults, &$rules, &$result) + { + if ($this->openidOnly) { + static $block = array('main/login', + 'main/register', + 'main/recoverpassword', + 'settings/password'); + + if (in_array($path, $block)) { + return false; + } + } + + return true; + } + + /** + * If we've been hit with password-login args, redirect + * + * @param array $args args (URL, Get, post) + * + * @return boolean hook return + */ + + function onArgsInitialize($args) + { + if ($this->openidOnly) { + if (array_key_exists('action', $args)) { + $action = trim($args['action']); + if (in_array($action, array('login', 'register'))) { + common_redirect(common_local_url('openidlogin')); + exit(0); + } else if ($action == 'passwordsettings') { + common_redirect(common_local_url('openidsettings')); + exit(0); + } else if ($action == 'recoverpassword') { + throw new ClientException('Unavailable action'); + } + } + } + return true; + } + + /** + * Public XRDS output hook + * + * Puts the bits of code needed by some OpenID providers to show + * we're good citizens. + * + * @param Action $action Action being executed + * @param XMLOutputter &$xrdsOutputter Output channel + * + * @return boolean hook return + */ + function onEndPublicXRDS($action, &$xrdsOutputter) { $xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', - 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', - 'version' => '2.0')); + 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', + 'version' => '2.0')); $xrdsOutputter->element('Type', null, 'xri://$xrds*simple'); //consumer foreach (array('finishopenidlogin', 'finishaddopenid') as $finish) { $xrdsOutputter->showXrdsService(Auth_OpenID_RP_RETURN_TO_URL_TYPE, - common_local_url($finish)); + common_local_url($finish)); } //provider $xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/server', - common_local_url('openidserver'), - null, - null, - 'http://specs.openid.net/auth/2.0/identifier_select'); + common_local_url('openidserver'), + null, + null, + 'http://specs.openid.net/auth/2.0/identifier_select'); $xrdsOutputter->elementEnd('XRD'); } + /** + * User XRDS output hook + * + * Puts the bits of code needed to discover OpenID endpoints. + * + * @param Action $action Action being executed + * @param XMLOutputter &$xrdsOutputter Output channel + * + * @return boolean hook return + */ + function onEndUserXRDS($action, &$xrdsOutputter) { $xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', - 'xml:id' => 'openid', - 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', - 'version' => '2.0')); + 'xml:id' => 'openid', + 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', + 'version' => '2.0')); $xrdsOutputter->element('Type', null, 'xri://$xrds*simple'); //consumer $xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/return_to', - common_local_url('finishopenidlogin')); + common_local_url('finishopenidlogin')); //provider $xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/signon', - common_local_url('openidserver'), - null, - null, - common_profile_url($action->user->nickname)); + common_local_url('openidserver'), + null, + null, + common_profile_url($action->user->nickname)); $xrdsOutputter->elementEnd('XRD'); } + /** + * If we're in OpenID-only mode, hide all the main menu except OpenID login. + * + * @param Action $action Action being run + * + * @return boolean hook return + */ + + function onStartPrimaryNav($action) + { + if ($this->openidOnly && !common_logged_in()) { + // TRANS: Tooltip for main menu option "Login" + $tooltip = _m('TOOLTIP', 'Login to the site'); + // TRANS: Main menu option when not logged in to log in + $action->menuItem(common_local_url('openidlogin'), + _m('MENU', 'Login'), + $tooltip, + false, + 'nav_login'); + // TRANS: Tooltip for main menu option "Help" + $tooltip = _m('TOOLTIP', 'Help me!'); + // TRANS: Main menu option for help on the StatusNet site + $action->menuItem(common_local_url('doc', array('title' => 'help')), + _m('MENU', 'Help'), + $tooltip, + false, + 'nav_help'); + if (!common_config('site', 'private')) { + // TRANS: Tooltip for main menu option "Search" + $tooltip = _m('TOOLTIP', 'Search for people or text'); + // TRANS: Main menu option when logged in or when the StatusNet instance is not private + $action->menuItem(common_local_url('peoplesearch'), + _m('MENU', 'Search'), $tooltip, false, 'nav_search'); + } + Event::handle('EndPrimaryNav', array($action)); + return false; + } + return true; + } + + /** + * Menu for login + * + * If we're in openidOnly mode, we disable the menu for all other login. + * + * @param Action &$action Action being executed + * + * @return boolean hook return + */ + + function onStartLoginGroupNav(&$action) + { + if ($this->openidOnly) { + $this->showOpenIDLoginTab($action); + // Even though we replace this code, we + // DON'T run the End* hook, to keep others from + // adding tabs. Not nice, but. + return false; + } + + return true; + } + + /** + * Menu item for login + * + * @param Action &$action Action being executed + * + * @return boolean hook return + */ + function onEndLoginGroupNav(&$action) { + $this->showOpenIDLoginTab($action); + + return true; + } + + /** + * Show menu item for login + * + * @param Action $action Action being executed + * + * @return void + */ + + function showOpenIDLoginTab($action) + { $action_name = $action->trimmed('action'); $action->menuItem(common_local_url('openidlogin'), _m('OpenID'), _m('Login or register with OpenID'), $action_name === 'openidlogin'); + } + + /** + * Show menu item for password + * + * We hide it in openID-only mode + * + * @param Action $menu Widget for menu + * @param void &$unused Unused value + * + * @return void + */ + function onStartAccountSettingsPasswordMenuItem($menu, &$unused) { + if ($this->openidOnly) { + return false; + } return true; } + /** + * Menu item for OpenID settings + * + * @param Action &$action Action being executed + * + * @return boolean hook return + */ + function onEndAccountSettingsNav(&$action) { $action_name = $action->trimmed('action'); @@ -139,68 +323,102 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Autoloader + * + * Loads our classes if they're requested. + * + * @param string $cls Class requested + * + * @return boolean hook return + */ + function onAutoload($cls) { switch ($cls) { - case 'OpenidloginAction': - case 'FinishopenidloginAction': - case 'FinishaddopenidAction': - case 'XrdsAction': - case 'PublicxrdsAction': - case 'OpenidsettingsAction': - case 'OpenidserverAction': - case 'OpenidtrustAction': - require_once(INSTALLDIR.'/plugins/OpenID/' . strtolower(mb_substr($cls, 0, -6)) . '.php'); + case 'OpenidloginAction': + case 'FinishopenidloginAction': + case 'FinishaddopenidAction': + case 'XrdsAction': + case 'PublicxrdsAction': + case 'OpenidsettingsAction': + case 'OpenidserverAction': + case 'OpenidtrustAction': + require_once INSTALLDIR.'/plugins/OpenID/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; - case 'User_openid': - require_once(INSTALLDIR.'/plugins/OpenID/User_openid.php'); + case 'User_openid': + require_once INSTALLDIR.'/plugins/OpenID/User_openid.php'; return false; - case 'User_openid_trustroot': - require_once(INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php'); + case 'User_openid_trustroot': + require_once INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php'; return false; - default: + default: return true; } } + /** + * Sensitive actions + * + * These actions should use https when SSL support is 'sometimes' + * + * @param Action $action Action to form an URL for + * @param boolean &$ssl Whether to mark it for SSL + * + * @return boolean hook return + */ + function onSensitiveAction($action, &$ssl) { switch ($action) { - case 'finishopenidlogin': - case 'finishaddopenid': + case 'finishopenidlogin': + case 'finishaddopenid': $ssl = true; return false; - default: + default: return true; } } + /** + * Login actions + * + * These actions should be visible even when the site is marked private + * + * @param Action $action Action to show + * @param boolean &$login Whether it's a login action + * + * @return boolean hook return + */ + function onLoginAction($action, &$login) { switch ($action) { - case 'openidlogin': - case 'finishopenidlogin': - case 'openidserver': + case 'openidlogin': + case 'finishopenidlogin': + case 'openidserver': $login = true; return false; - default: + default: return true; } } /** - * We include a <meta> element linking to the publicxrds page, for OpenID + * We include a <meta> element linking to the userxrds page, for OpenID * client-side authentication. * + * @param Action $action Action being shown + * * @return void */ function onEndShowHeadElements($action) { - if($action instanceof ShowstreamAction){ + if ($action instanceof ShowstreamAction) { $action->element('link', array('rel' => 'openid2.provider', 'href' => common_local_url('openidserver'))); $action->element('link', array('rel' => 'openid2.local_id', @@ -216,25 +434,36 @@ class OpenIDPlugin extends Plugin /** * Redirect to OpenID login if they have an OpenID * + * @param Action $action Action being executed + * @param User $user User doing the action + * * @return boolean whether to continue */ function onRedirectToLogin($action, $user) { - if (!empty($user) && User_openid::hasOpenID($user->id)) { + if ($this->openidOnly || (!empty($user) && User_openid::hasOpenID($user->id))) { common_redirect(common_local_url('openidlogin'), 303); return false; } return true; } + /** + * Show some extra instructions for using OpenID + * + * @param Action $action Action being executed + * + * @return boolean hook value + */ + function onEndShowPageNotice($action) { $name = $action->trimmed('action'); switch ($name) { - case 'register': + case 'register': if (common_logged_in()) { $instr = '(Have an [OpenID](http://openid.net/)? ' . '[Add an OpenID to your account](%%action.openidsettings%%)!'; @@ -244,12 +473,12 @@ class OpenIDPlugin extends Plugin '(%%action.openidlogin%%)!)'; } break; - case 'login': + case 'login': $instr = '(Have an [OpenID](http://openid.net/)? ' . 'Try our [OpenID login]'. '(%%action.openidlogin%%)!)'; break; - default: + default: return true; } @@ -258,13 +487,21 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Load our document if requested + * + * @param string &$title Title to fetch + * @param string &$output HTML to output + * + * @return boolean hook value + */ + function onStartLoadDoc(&$title, &$output) { - if ($title == 'openid') - { + if ($title == 'openid') { $filename = INSTALLDIR.'/plugins/OpenID/doc-src/openid'; - $c = file_get_contents($filename); + $c = file_get_contents($filename); $output = common_markup_to_html($c); return false; // success! } @@ -272,10 +509,18 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Add our document to the global menu + * + * @param string $title Title being fetched + * @param string &$output HTML being output + * + * @return boolean hook value + */ + function onEndLoadDoc($title, &$output) { - if ($title == 'help') - { + if ($title == 'help') { $menuitem = '* [OpenID](%%doc.openid%%) - what OpenID is and how to use it with this service'; $output .= common_markup_to_html($menuitem); @@ -284,7 +529,16 @@ class OpenIDPlugin extends Plugin return true; } - function onCheckSchema() { + /** + * Data definitions + * + * Assure that our data objects are available in the DB + * + * @return boolean hook value + */ + + function onCheckSchema() + { $schema = Schema::get(); $schema->ensureTable('user_openid', array(new ColumnDef('canonical', 'varchar', @@ -307,6 +561,15 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Add our tables to be deleted when a user is deleted + * + * @param User $user User being deleted + * @param array &$tables Array of table names + * + * @return boolean hook value + */ + function onUserDeleteRelated($user, &$tables) { $tables[] = 'User_openid'; @@ -314,6 +577,14 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Add our version information to output + * + * @param array &$versions Array of version-data arrays + * + * @return boolean hook value + */ + function onPluginVersion(&$versions) { $versions[] = array('name' => 'OpenID', diff --git a/scripts/fixup_deletions.php b/scripts/fixup_deletions.php new file mode 100755 index 000000000..07ada7f9d --- /dev/null +++ b/scripts/fixup_deletions.php @@ -0,0 +1,166 @@ +#!/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('dry-run', 'start=', 'end='); + +$helptext = <<<END_OF_USERROLE_HELP +fixup_deletions.php [options] +Finds notices posted by deleted users and cleans them up. +Stray incompletely deleted items cause various fun problems! + + --dry-run look but don't touch + --start=N start looking at profile_id N instead of 1 + --end=N end looking at profile_id N instead of the max + +END_OF_USERROLE_HELP; + +require_once INSTALLDIR.'/scripts/commandline.inc'; + +/** + * Find the highest profile_id currently listed in the notice table; + * this field is indexed and should return very quickly. + * + * We check notice.profile_id rather than profile.id because we're + * looking for notices left behind after deletion; if the most recent + * accounts were deleted, we wouldn't have them from profile. + * + * @return int + * @access private + */ +function get_max_profile_id() +{ + $query = 'SELECT MAX(profile_id) AS id FROM notice'; + + $profile = new Profile(); + $profile->query($query); + + if ($profile->fetch()) { + return intval($profile->id); + } else { + die("Something went awry; could not look up max used profile_id."); + } +} + +/** + * Check for profiles in the given id range that are missing, presumed deleted. + * + * @param int $start beginning profile.id, inclusive + * @param int $end final profile.id, inclusive + * @return array of integer profile.ids + * @access private + */ +function get_missing_profiles($start, $end) +{ + $query = sprintf("SELECT id FROM profile WHERE id BETWEEN %d AND %d", + $start, $end); + + $profile = new Profile(); + $profile->query($query); + + $all = range($start, $end); + $known = array(); + while ($row = $profile->fetch()) { + $known[] = intval($profile->id); + } + unset($profile); + + $missing = array_diff($all, $known); + return $missing; +} + +/** + * Look for stray notices from this profile and, if present, kill them. + * + * @param int $profile_id + * @param bool $dry if true, we won't delete anything + */ +function cleanup_missing_profile($profile_id, $dry) +{ + $notice = new Notice(); + $notice->profile_id = $profile_id; + $notice->find(); + if ($notice->N == 0) { + return; + } + + $s = ($notice->N == 1) ? '' : 's'; + print "Deleted profile $profile_id has $notice->N stray notice$s:\n"; + + while ($notice->fetch()) { + print " notice $notice->id"; + if ($dry) { + print " (skipped; dry run)\n"; + } else { + $victim = clone($notice); + try { + $victim->delete(); + print " (deleted)\n"; + } catch (Exception $e) { + print " FAILED: "; + print $e->getMessage(); + print "\n"; + } + } + } +} + +$dry = have_option('dry-run'); + +$max_profile_id = get_max_profile_id(); +$chunk = 1000; + +if (have_option('start')) { + $begin = intval(get_option_value('start')); +} else { + $begin = 1; +} +if (have_option('end')) { + $final = min($max_profile_id, intval(get_option_value('end'))); +} else { + $final = $max_profile_id; +} + +if ($begin < 1) { + die("Silly human, you can't begin before profile number 1!\n"); +} +if ($final < $begin) { + die("Silly human, you can't end at $final if it's before $begin!\n"); +} + +// Identify missing profiles... +for ($start = $begin; $start <= $final; $start += $chunk) { + $end = min($start + $chunk - 1, $final); + + print "Checking for missing profiles between id $start and $end"; + if ($dry) { + print " (dry run)"; + } + print "...\n"; + $missing = get_missing_profiles($start, $end); + + foreach ($missing as $profile_id) { + cleanup_missing_profile($profile_id, $dry); + } +} + +echo "done.\n"; + diff --git a/scripts/importtwitteratom.php b/scripts/importtwitteratom.php index 7316f2108..c12e3b91a 100644 --- a/scripts/importtwitteratom.php +++ b/scripts/importtwitteratom.php @@ -102,7 +102,7 @@ function importActivityStream($user, $doc) for ($i = $entries->length - 1; $i >= 0; $i--) { $entry = $entries->item($i); $activity = new Activity($entry, $feed); - $object = $activity->object; + $object = $activity->objects[0]; if (!have_option('q', 'quiet')) { print $activity->content . "\n"; } diff --git a/tests/ActivityParseTests.php b/tests/ActivityParseTests.php index 9d8fd47af..fec8829eb 100644 --- a/tests/ActivityParseTests.php +++ b/tests/ActivityParseTests.php @@ -25,11 +25,11 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase $this->assertEquals($act->time, 1243860840); $this->assertEquals($act->verb, ActivityVerb::POST); - $this->assertFalse(empty($act->object)); - $this->assertEquals($act->object->title, 'Punctuation Changeset'); - $this->assertEquals($act->object->type, 'http://versioncentral.example.org/activity/changeset'); - $this->assertEquals($act->object->summary, 'Fixing punctuation because it makes it more readable.'); - $this->assertEquals($act->object->id, 'tag:versioncentral.example.org,2009:/change/1643245'); + $this->assertFalse(empty($act->objects[0])); + $this->assertEquals($act->objects[0]->title, 'Punctuation Changeset'); + $this->assertEquals($act->objects[0]->type, 'http://versioncentral.example.org/activity/changeset'); + $this->assertEquals($act->objects[0]->summary, 'Fixing punctuation because it makes it more readable.'); + $this->assertEquals($act->objects[0]->id, 'tag:versioncentral.example.org,2009:/change/1643245'); } public function testExample3() @@ -56,12 +56,12 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase $this->assertEquals($act->actor->title, 'John Doe'); $this->assertEquals($act->actor->id, 'mailto:johndoe@example.com'); - $this->assertFalse(empty($act->object)); - $this->assertEquals($act->object->type, ActivityObject::NOTE); - $this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a'); - $this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok'); - $this->assertEquals($act->object->summary, 'Some text.'); - $this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html'); + $this->assertFalse(empty($act->objects[0])); + $this->assertEquals($act->objects[0]->type, ActivityObject::NOTE); + $this->assertEquals($act->objects[0]->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a'); + $this->assertEquals($act->objects[0]->title, 'Atom-Powered Robots Run Amok'); + $this->assertEquals($act->objects[0]->summary, 'Some text.'); + $this->assertEquals($act->objects[0]->link, 'http://example.org/2003/12/13/atom03.html'); $this->assertFalse(empty($act->context)); @@ -90,8 +90,8 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase $this->assertEquals('http://example.net/conversation/11', $act->context->conversation); $this->assertEquals(array('http://example.net/user/1'), $act->context->attention); - $this->assertFalse(empty($act->object)); - $this->assertEquals($act->object->content, + $this->assertFalse(empty($act->objects[0])); + $this->assertEquals($act->objects[0]->content, '@<span class="vcard"><a href="http://example.net/user/1" class="url"><span class="fn nickname">evan</span></a></span> now is the time for all good men to come to the aid of their country. #<span class="tag"><a href="http://example.net/tag/thetime" rel="tag">thetime</a></span>'); $this->assertFalse(empty($act->actor)); @@ -207,7 +207,7 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase $this->assertTrue(is_array($actor->avatarLinks)); $this->assertEquals(1, count($actor->avatarLinks)); $this->assertEquals('http://files.posterous.com/user_profile_pics/480326/2009-08-05-142447.jpg', - $actor->avatarLinks[0]); + $actor->avatarLinks[0]->url); $this->assertNotNull($actor->poco); $this->assertEquals('evanpro', $actor->poco->preferredUsername); $this->assertEquals('Evan Prodromou', $actor->poco->displayName); @@ -215,6 +215,96 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase $this->assertNull($actor->poco->address); $this->assertEquals(0, count($actor->poco->urls)); } + + // Media test - cliqset + public function testExample8() + { + global $_example8; + $dom = DOMDocument::loadXML($_example8); + + $feed = $dom->documentElement; + + $entries = $feed->getElementsByTagName('entry'); + + $entry = $entries->item(0); + + $act = new Activity($entry, $feed); + + $this->assertFalse(empty($act)); + $this->assertEquals($act->time, 1269221753); + $this->assertEquals($act->verb, ActivityVerb::POST); + $this->assertEquals($act->summary, 'zcopley posted 5 photos on Flickr'); + + $this->assertFalse(empty($act->objects)); + $this->assertEquals(sizeof($act->objects), 5); + + $this->assertEquals($act->objects[0]->type, ActivityObject::PHOTO); + $this->assertEquals($act->objects[0]->title, 'IMG_1368'); + $this->assertNull($act->objects[0]->description); + $this->assertEquals( + $act->objects[0]->thumbnail, + 'http://media.cliqset.com/6f6fbee9d7dfbffc73b6ef626275eb5f_thumb.jpg' + ); + $this->assertEquals( + $act->objects[0]->link, + 'http://www.flickr.com/photos/zcopley/4452933806/' + ); + + $this->assertEquals($act->objects[1]->type, ActivityObject::PHOTO); + $this->assertEquals($act->objects[1]->title, 'IMG_1365'); + $this->assertNull($act->objects[1]->description); + $this->assertEquals( + $act->objects[1]->thumbnail, + 'http://media.cliqset.com/b8f3932cd0bba1b27f7c8b3ef986915e_thumb.jpg' + ); + $this->assertEquals( + $act->objects[1]->link, + 'http://www.flickr.com/photos/zcopley/4442630390/' + ); + + $this->assertEquals($act->objects[2]->type, ActivityObject::PHOTO); + $this->assertEquals($act->objects[2]->title, 'Classic'); + $this->assertEquals( + $act->objects[2]->description, + '-Powered by pikchur.com/n0u' + ); + $this->assertEquals( + $act->objects[2]->thumbnail, + 'http://media.cliqset.com/fc54c15f850b7a9a8efa644087a48c91_thumb.jpg' + ); + $this->assertEquals( + $act->objects[2]->link, + 'http://www.flickr.com/photos/zcopley/4430754103/' + ); + + $this->assertEquals($act->objects[3]->type, ActivityObject::PHOTO); + $this->assertEquals($act->objects[3]->title, 'IMG_1363'); + $this->assertNull($act->objects[3]->description); + + $this->assertEquals( + $act->objects[3]->thumbnail, + 'http://media.cliqset.com/4b1d307c9217e2114391a8b229d612cb_thumb.jpg' + ); + $this->assertEquals( + $act->objects[3]->link, + 'http://www.flickr.com/photos/zcopley/4416969717/' + ); + + $this->assertEquals($act->objects[4]->type, ActivityObject::PHOTO); + $this->assertEquals($act->objects[4]->title, 'IMG_1361'); + $this->assertNull($act->objects[4]->description); + + $this->assertEquals( + $act->objects[4]->thumbnail, + 'http://media.cliqset.com/23d9b4b96b286e0347d36052f22f6e60_thumb.jpg' + ); + $this->assertEquals( + $act->objects[4]->link, + 'http://www.flickr.com/photos/zcopley/4417734232/' + ); + + } + } $_example1 = <<<EXAMPLE1 @@ -508,3 +598,120 @@ $_example7 = <<<EXAMPLE7 </channel> </rss> EXAMPLE7; + +$_example8 = <<<EXAMPLE8 +<?xml version="1.0"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + <link href="http://pubsubhubbub.appspot.com/" rel="hub"/> + <title type="text">Activity Stream for: zcopley</title> + <id>http://cliqset.com/feed/atom?uid=zcopley</id> + <entry xmlns:service="http://activitystrea.ms/service-provider" xmlns:activity="http://activitystrea.ms/spec/1.0/"> + <thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <published>2010-03-22T01:35:53.000Z</published> + <service:provider> + <name>flickr</name> + <uri>http://flickr.com</uri> + <icon>http://cliqset-services.s3.amazonaws.com/flickr.png</icon> + </service:provider> + <activity:object> + <activity:object-type>http://activitystrea.ms/schema/1.0/photo</activity:object-type> + <title type="text">IMG_1368</title> + <link type="image/jpeg" rel="preview" href="http://media.cliqset.com/6f6fbee9d7dfbffc73b6ef626275eb5f_thumb.jpg"/> + <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/zcopley/4452933806/"/> + </activity:object> + <activity:object> + <activity:object-type>http://activitystrea.ms/schema/1.0/photo</activity:object-type> + <title type="text">IMG_1365</title> + <link type="image/jpeg" rel="preview" href="http://media.cliqset.com/b8f3932cd0bba1b27f7c8b3ef986915e_thumb.jpg"/> + <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/zcopley/4442630390/"/> + </activity:object> + <activity:object xmlns:media="http://purl.org/syndication/atommedia"> + <activity:object-type>http://activitystrea.ms/schema/1.0/photo</activity:object-type> + <title type="text">Classic</title> + <link type="image/jpeg" rel="preview" href="http://media.cliqset.com/fc54c15f850b7a9a8efa644087a48c91_thumb.jpg"/> + <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/zcopley/4430754103/"/> + <media:description type="text">-Powered by pikchur.com/n0u</media:description> + </activity:object> + <activity:object> + <activity:object-type>http://activitystrea.ms/schema/1.0/photo</activity:object-type> + <title type="text">IMG_1363</title> + <link type="image/jpeg" rel="preview" href="http://media.cliqset.com/4b1d307c9217e2114391a8b229d612cb_thumb.jpg"/> + <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/zcopley/4416969717/"/> + </activity:object> + <activity:object> + <activity:object-type>http://activitystrea.ms/schema/1.0/photo</activity:object-type> + <title type="text">IMG_1361</title> + <link type="image/jpeg" rel="preview" href="http://media.cliqset.com/23d9b4b96b286e0347d36052f22f6e60_thumb.jpg"/> + <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/zcopley/4417734232/"/> + </activity:object> + <title type="text">zcopley posted some photos on Flickr</title> + <summary type="text">zcopley posted 5 photos on Flickr</summary> + <category scheme="http://schemas.cliqset.com/activity/categories/1.0" term="PhotoPosted" label="Photo Posted"/> + <updated>2010-03-22T20:46:42.778Z</updated> + <id>tag:cliqset.com,2010-03-22:/user/zcopley/SVgAZubGhtAnSAee</id> + <link href="http://cliqset.com/user/zcopley/SVgAZubGhtAnSAee" type="text/xhtml" rel="alternate" title="zcopley posted some photos on Flickr"/> + <author> + <name>zcopley</name> + <uri>http://cliqset.com/user/zcopley</uri> + </author> + <activity:actor xmlns:poco="http://portablecontacts.net/spec/1.0"> + <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type> + <id>zcopley</id> + <poco:name> + <poco:givenName>Zach</poco:givenName> + <poco:familyName>Copley</poco:familyName> + </poco:name> + <link xmlns:media="http://purl.org/syndication/atommedia" type="image/png" rel="avatar" href="http://dynamic.cliqset.com/avatar/zcopley?s=80" media:height="80" media:width="80"/> + <link xmlns:media="http://purl.org/syndication/atommedia" type="image/png" rel="avatar" href="http://dynamic.cliqset.com/avatar/zcopley?s=120" media:height="120" media:width="120"/> + <link xmlns:media="http://purl.org/syndication/atommedia" type="image/png" rel="avatar" href="http://dynamic.cliqset.com/avatar/zcopley?s=200" media:height="200" media:width="200"/> + </activity:actor> + </entry> +</feed> +EXAMPLE8; + +$_example9 = <<<EXAMPLE9 +<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:media="http://search.yahoo.com/mrss" xmlns:activity="http://activitystrea.ms/spec/1.0/"> + <link rel="self" type="application/atom+xml" href="http://buzz.googleapis.com/feeds/117848251937215158042/public/posted"/> + <link rel="hub" href="http://pubsubhubbub.appspot.com/"/> + <title type="text">Google Buzz</title> + <updated>2010-03-22T01:55:53.596Z</updated> + <id>tag:google.com,2009:buzz-feed/public/posted/117848251937215158042</id> + <generator>Google - Google Buzz</generator> + <entry> + <title type="html">Buzz by Zach Copley from Flickr</title> + <summary type="text">IMG_1366</summary> + <published>2010-03-18T04:29:23.000Z</published> + <updated>2010-03-18T05:14:03.325Z</updated> + <id>tag:google.com,2009:buzz/z12zwdhxowq2d13q204cjr04kzu0cns5gh0</id> + <link rel="alternate" type="text/html" href="http://www.google.com/buzz/117848251937215158042/ZU7b6mHJEmC/IMG-1366"/> + <author> + <name>Zach Copley</name> + <uri>http://www.google.com/profiles/zcopley</uri> + </author> + <content type="html"><div>IMG_1366</div></content> + <link rel="enclosure" href="http://www.flickr.com/photos/22823034@N00/4442630700" type="image/jpeg" title="IMG_1366"/> + <media:content url="http://www.flickr.com/photos/22823034@N00/4442630700" type="image/jpeg" medium="image"> + <media:title>IMG_1366</media:title> + <media:player url="http://farm5.static.flickr.com/4053/4442630700_980b19a1a6_o.jpg" height="1600" width="1200"/> + </media:content> + <link rel="enclosure" href="http://www.flickr.com/photos/22823034@N00/4442630390" type="image/jpeg" title="IMG_1365"/> + <media:content url="http://www.flickr.com/photos/22823034@N00/4442630390" type="image/jpeg" medium="image"> + <media:title>IMG_1365</media:title> + <media:player url="http://farm5.static.flickr.com/4043/4442630390_62da5560ae_o.jpg" height="1200" width="1600"/> + </media:content> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <activity:object> + <activity:object-type>http://activitystrea.ms/schema/1.0/photo</activity:object-type> + <id>tag:google.com,2009:buzz/z12zwdhxowq2d13q204cjr04kzu0cns5gh0</id> + <title>Buzz by Zach Copley from Flickr</title> + <content type="html"><div>IMG_1366</div></content> + <link rel="enclosure" href="http://www.flickr.com/photos/22823034@N00/4442630700" type="image/jpeg" title="IMG_1366"/> + <link rel="enclosure" href="http://www.flickr.com/photos/22823034@N00/4442630390" type="image/jpeg" title="IMG_1365"/> + </activity:object> + <link rel="replies" type="application/atom+xml" href="http://buzz.googleapis.com/feeds/117848251937215158042/comments/z12zwdhxowq2d13q204cjr04kzu0cns5gh0" thr:count="0"/> + <thr:total>0</thr:total> + </entry> +</feed> +EXAMPLE9; diff --git a/tests/UserFeedParseTest.php b/tests/UserFeedParseTest.php index b3f9a6417..208e71be6 100644 --- a/tests/UserFeedParseTest.php +++ b/tests/UserFeedParseTest.php @@ -66,11 +66,11 @@ class UserFeedParseTests extends PHPUnit_Framework_TestCase // test the post //var_export($act1); - $this->assertEquals($act1->object->type, 'http://activitystrea.ms/schema/1.0/note'); - $this->assertEquals($act1->object->title, 'And now for something completely insane...'); + $this->assertEquals($act1->objects[0]->type, 'http://activitystrea.ms/schema/1.0/note'); + $this->assertEquals($act1->objects[0]->title, 'And now for something completely insane...'); - $this->assertEquals($act1->object->content, 'And now for something completely insane...'); - $this->assertEquals($act1->object->id, 'http://localhost/statusnet/notice/3'); + $this->assertEquals($act1->objects[0]->content, 'And now for something completely insane...'); + $this->assertEquals($act1->objects[0]->id, 'http://localhost/statusnet/notice/3'); } |