summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--actions/apifavoritecreate.php2
-rw-r--r--actions/foaf.php68
-rwxr-xr-xclasses/Conversation.php3
-rw-r--r--classes/Fave.php10
-rw-r--r--classes/Notice.php4
-rw-r--r--index.php4
-rw-r--r--lib/activity.php24
-rw-r--r--lib/activityobject.php20
-rw-r--r--lib/apiaction.php2
-rw-r--r--lib/attachmentlist.php20
-rw-r--r--lib/command.php2
-rw-r--r--lib/router.php29
-rw-r--r--lib/servererroraction.php9
-rw-r--r--lib/util.php3
-rw-r--r--plugins/OStatus/actions/groupsalmon.php3
-rw-r--r--plugins/OStatus/actions/usersalmon.php11
-rw-r--r--plugins/OStatus/actions/userxrd.php10
-rw-r--r--plugins/OStatus/classes/Magicsig.php10
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php5
-rw-r--r--plugins/OStatus/extlib/Math/BigInteger.php8
-rw-r--r--plugins/OStatus/lib/discovery.php2
-rw-r--r--plugins/OStatus/lib/linkheader.php2
-rw-r--r--plugins/OStatus/lib/magicenvelope.php17
-rw-r--r--plugins/OStatus/lib/xrd.php9
-rw-r--r--plugins/OStatus/tests/remote-tests.php555
-rw-r--r--plugins/OpenID/OpenIDPlugin.php387
-rwxr-xr-xscripts/fixup_deletions.php166
-rw-r--r--scripts/importtwitteratom.php2
-rw-r--r--tests/ActivityParseTests.php235
-rw-r--r--tests/UserFeedParseTest.php8
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();
+ }
}
/**
diff --git a/index.php b/index.php
index 36ba3a0d2..ea5c80277 100644
--- a/index.php
+++ b/index.php
@@ -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">&lt;div&gt;IMG_1366&lt;/div&gt;</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">&lt;div&gt;IMG_1366&lt;/div&gt;</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');
}