summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrion Vibber <brion@pobox.com>2010-03-21 16:28:56 -0700
committerBrion Vibber <brion@pobox.com>2010-03-21 16:28:56 -0700
commitbe7efe750469312ad57815d20692bb5f5832ae94 (patch)
treee347dd8c0b5d74cbdff419a348eb303369280a93
parente458e9fe6352df6063079979b20a6924f97198f4 (diff)
parentfcb614d0eb1f98bf8704654ed06e1f9d9733d359 (diff)
Merge branch 'testing' into 0.9.x
-rw-r--r--lib/activity.php63
-rw-r--r--lib/activityobject.php60
-rw-r--r--lib/activityutils.php22
-rw-r--r--lib/atomcategory.php77
-rw-r--r--plugins/OStatus/OStatusPlugin.php19
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php128
-rw-r--r--tests/ActivityParseTests.php85
7 files changed, 363 insertions, 91 deletions
diff --git a/lib/activity.php b/lib/activity.php
index b1744e68f..bd1d5d56c 100644
--- a/lib/activity.php
+++ b/lib/activity.php
@@ -238,17 +238,17 @@ class Activity
$this->time = strtotime($pubDateEl->textContent);
}
- $authorEl = $this->_child($item, self::AUTHOR, self::RSS);
-
- if (!empty($authorEl)) {
+ if ($authorEl = $this->_child($item, self::AUTHOR, self::RSS)) {
$this->actor = ActivityObject::fromRssAuthor($authorEl);
+ } else if ($dcCreatorEl = $this->_child($item, self::CREATOR, self::DC)) {
+ $this->actor = ActivityObject::fromDcCreator($dcCreatorEl);
+ } else if ($posterousEl = $this->_child($item, ActivityObject::AUTHOR, ActivityObject::POSTEROUS)) {
+ // Special case for Posterous.com
+ $this->actor = ActivityObject::fromPosterousAuthor($posterousEl);
+ } else if (!empty($channel)) {
+ $this->actor = ActivityObject::fromRssChannel($channel);
} else {
- $dcCreatorEl = $this->_child($item, self::CREATOR, self::DC);
- if (!empty($dcCreatorEl)) {
- $this->actor = ActivityObject::fromDcCreator($dcCreatorEl);
- } else if (!empty($channel)) {
- $this->actor = ActivityObject::fromRssChannel($channel);
- }
+ // No actor!
}
$this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, self::RSS);
@@ -362,48 +362,3 @@ class Activity
}
}
-class AtomCategory
-{
- public $term;
- public $scheme;
- public $label;
-
- function __construct($element=null)
- {
- if ($element && $element->attributes) {
- $this->term = $this->extract($element, 'term');
- $this->scheme = $this->extract($element, 'scheme');
- $this->label = $this->extract($element, 'label');
- }
- }
-
- protected function extract($element, $attrib)
- {
- $node = $element->attributes->getNamedItemNS(Activity::ATOM, $attrib);
- if ($node) {
- return trim($node->textContent);
- }
- $node = $element->attributes->getNamedItem($attrib);
- if ($node) {
- return trim($node->textContent);
- }
- return null;
- }
-
- function asString()
- {
- $attribs = array();
- if ($this->term !== null) {
- $attribs['term'] = $this->term;
- }
- if ($this->scheme !== null) {
- $attribs['scheme'] = $this->scheme;
- }
- if ($this->label !== null) {
- $attribs['label'] = $this->label;
- }
- $xs = new XMLStringer();
- $xs->element('category', $attribs);
- return $xs->asString();
- }
-}
diff --git a/lib/activityobject.php b/lib/activityobject.php
index b1e9071ed..0a358ccab 100644
--- a/lib/activityobject.php
+++ b/lib/activityobject.php
@@ -80,6 +80,13 @@ class ActivityObject
const URI = 'uri';
const EMAIL = 'email';
+ const POSTEROUS = 'http://posterous.com/help/rss/1.0';
+ const AUTHOR = 'author';
+ const USERIMAGE = 'userImage';
+ const PROFILEURL = 'profileUrl';
+ const NICKNAME = 'nickName';
+ const DISPLAYNAME = 'displayName';
+
public $element;
public $type;
public $id;
@@ -149,7 +156,11 @@ class ActivityObject
{
$this->type = self::PERSON; // XXX: is this fair?
$this->title = $this->_childContent($element, self::NAME);
- $this->id = $this->_childContent($element, self::URI);
+
+ $id = $this->_childContent($element, self::URI);
+ if (ActivityUtils::validateUri($id)) {
+ $this->id = $id;
+ }
if (empty($this->id)) {
$email = $this->_childContent($element, self::EMAIL);
@@ -162,6 +173,15 @@ class ActivityObject
private function _fromAtomEntry($element)
{
+ if ($element->localName == 'actor') {
+ // Old-fashioned <activity:actor>...
+ // First pull anything from <author>, then we'll add on top.
+ $author = ActivityUtils::child($element->parentNode, 'author');
+ if ($author) {
+ $this->_fromAuthor($author);
+ }
+ }
+
$this->type = $this->_childContent($element, Activity::OBJECTTYPE,
Activity::SPEC);
@@ -169,7 +189,11 @@ class ActivityObject
$this->type = ActivityObject::NOTE;
}
- $this->id = $this->_childContent($element, self::ID);
+ $id = $this->_childContent($element, self::ID);
+ if (ActivityUtils::validateUri($id)) {
+ $this->id = $id;
+ }
+
$this->summary = ActivityUtils::childHtmlContent($element, self::SUMMARY);
$this->content = ActivityUtils::getContent($element);
@@ -290,9 +314,39 @@ class ActivityObject
$imageEl = ActivityUtils::child($el, Activity::IMAGE, Activity::RSS);
if (!empty($imageEl)) {
- $obj->avatarLinks[] = ActivityUtils::childContent($imageEl, Activity::URL, Activity::RSS);
+ $url = ActivityUtils::childContent($imageEl, Activity::URL, Activity::RSS);
+ $al = new AvatarLink();
+ $al->url = $url;
+ $obj->avatarLinks[] = $al;
+ }
+
+ return $obj;
+ }
+
+ public static function fromPosterousAuthor($el)
+ {
+ $obj = new ActivityObject();
+
+ $obj->type = ActivityObject::PERSON; // @fixme any others...?
+
+ $userImage = ActivityUtils::childContent($el, self::USERIMAGE, self::POSTEROUS);
+
+ if (!empty($userImage)) {
+ $al = new AvatarLink();
+ $al->url = $userImage;
+ $obj->avatarLinks[] = $al;
}
+ $obj->link = ActivityUtils::childContent($el, self::PROFILEURL, self::POSTEROUS);
+ $obj->id = $obj->link;
+
+ $obj->poco = new PoCo();
+
+ $obj->poco->preferredUsername = ActivityUtils::childContent($el, self::NICKNAME, self::POSTEROUS);
+ $obj->poco->displayName = ActivityUtils::childContent($el, self::DISPLAYNAME, self::POSTEROUS);
+
+ $obj->title = $obj->poco->displayName;
+
return $obj;
}
diff --git a/lib/activityutils.php b/lib/activityutils.php
index c85a3db55..a7e99fb11 100644
--- a/lib/activityutils.php
+++ b/lib/activityutils.php
@@ -240,4 +240,26 @@ class ActivityUtils
throw new ClientException(_("Can't handle embedded Base64 content yet."));
}
}
+
+ /**
+ * Is this a valid URI for remote profile/notice identification?
+ * Does not have to be a resolvable URL.
+ * @param string $uri
+ * @return boolean
+ */
+ static function validateUri($uri)
+ {
+ if (Validate::uri($uri)) {
+ return true;
+ }
+
+ // Possibly an upstream bug; tag: URIs aren't validated properly
+ // unless you explicitly ask for them. All other schemes are accepted
+ // for basic URI validation without asking.
+ if (Validate::uri($uri, array('allowed_scheme' => array('tag')))) {
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/lib/atomcategory.php b/lib/atomcategory.php
new file mode 100644
index 000000000..4cc3b4f4d
--- /dev/null
+++ b/lib/atomcategory.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class AtomCategory
+{
+ public $term;
+ public $scheme;
+ public $label;
+
+ function __construct($element=null)
+ {
+ if ($element && $element->attributes) {
+ $this->term = $this->extract($element, 'term');
+ $this->scheme = $this->extract($element, 'scheme');
+ $this->label = $this->extract($element, 'label');
+ }
+ }
+
+ protected function extract($element, $attrib)
+ {
+ $node = $element->attributes->getNamedItemNS(Activity::ATOM, $attrib);
+ if ($node) {
+ return trim($node->textContent);
+ }
+ $node = $element->attributes->getNamedItem($attrib);
+ if ($node) {
+ return trim($node->textContent);
+ }
+ return null;
+ }
+
+ function asString()
+ {
+ $attribs = array();
+ if ($this->term !== null) {
+ $attribs['term'] = $this->term;
+ }
+ if ($this->scheme !== null) {
+ $attribs['scheme'] = $this->scheme;
+ }
+ if ($this->label !== null) {
+ $attribs['label'] = $this->label;
+ }
+ $xs = new XMLStringer();
+ $xs->element('category', $attribs);
+ return $xs->asString();
+ }
+}
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php
index 58f373e45..c985fb4bc 100644
--- a/plugins/OStatus/OStatusPlugin.php
+++ b/plugins/OStatus/OStatusPlugin.php
@@ -947,23 +947,4 @@ class OStatusPlugin extends Plugin
}
return false;
}
-
- /**
- * Utility function to check if the given URL is a canonical user profile
- * page, and if so return the ID number.
- *
- * @param string $url
- * @return mixed int or false
- */
- public static function localProfileFromUrl($url)
- {
- $template = common_local_url('userbyid', array('id' => '31337'));
- $template = preg_quote($template, '/');
- $template = str_replace('31337', '(\d+)', $template);
- if (preg_match("/$template/", $url, $matches)) {
- return intval($matches[1]);
- }
- return false;
- }
-
}
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
index d2e046a60..de5175427 100644
--- a/plugins/OStatus/classes/Ostatus_profile.php
+++ b/plugins/OStatus/classes/Ostatus_profile.php
@@ -708,9 +708,13 @@ class Ostatus_profile extends Memcached_DataObject
}
/**
+ * Look up and if necessary create an Ostatus_profile for the remote entity
+ * with the given profile page URL. This should never return null -- you
+ * will either get an object or an exception will be thrown.
+ *
* @param string $profile_url
* @return Ostatus_profile
- * @throws FeedSubException
+ * @throws Exception
*/
public static function ensureProfileURL($profile_url, $hints=array())
@@ -731,7 +735,7 @@ class Ostatus_profile extends Memcached_DataObject
$response = $client->get($profile_url);
if (!$response->isOk()) {
- return null;
+ throw new Exception("Could not reach profile page: " . $profile_url);
}
// Check if we have a non-canonical URL
@@ -785,11 +789,20 @@ class Ostatus_profile extends Memcached_DataObject
if (!empty($feedurl)) {
$hints['feedurl'] = $feedurl;
-
return self::ensureFeedURL($feedurl, $hints);
}
+
+ throw new Exception("Could not find a feed URL for profile page " . $finalUrl);
}
+ /**
+ * Look up the Ostatus_profile, if present, for a remote entity with the
+ * given profile page URL. Will return null for both unknown and invalid
+ * remote profiles.
+ *
+ * @return mixed Ostatus_profile or null
+ * @throws Exception for local profiles
+ */
static function getFromProfileURL($profile_url)
{
$profile = Profile::staticGet('profileurl', $profile_url);
@@ -821,6 +834,14 @@ class Ostatus_profile extends Memcached_DataObject
return null;
}
+ /**
+ * Look up and if necessary create an Ostatus_profile for remote entity
+ * with the given update feed. This should never return null -- you will
+ * either get an object or an exception will be thrown.
+ *
+ * @return Ostatus_profile
+ * @throws Exception
+ */
public static function ensureFeedURL($feed_url, $hints=array())
{
$discover = new FeedDiscovery();
@@ -849,6 +870,18 @@ class Ostatus_profile extends Memcached_DataObject
}
}
+ /**
+ * Look up and, if necessary, create an Ostatus_profile for the remote
+ * profile with the given Atom feed - actually loaded from the feed.
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
+ *
+ * @param DOMElement $feedEl root element of a loaded Atom feed
+ * @param array $hints additional discovery information passed from higher levels
+ * @fixme should this be marked public?
+ * @return Ostatus_profile
+ * @throws Exception
+ */
public static function ensureAtomFeed($feedEl, $hints)
{
// Try to get a profile from the feed activity:subject
@@ -899,8 +932,40 @@ class Ostatus_profile extends Memcached_DataObject
throw new FeedSubException("Can't find enough profile information to make a feed.");
}
+ /**
+ * Look up and, if necessary, create an Ostatus_profile for the remote
+ * profile with the given RSS feed - actually loaded from the feed.
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
+ *
+ * @param DOMElement $feedEl root element of a loaded RSS feed
+ * @param array $hints additional discovery information passed from higher levels
+ * @fixme should this be marked public?
+ * @return Ostatus_profile
+ * @throws Exception
+ */
public static function ensureRssChannel($feedEl, $hints)
{
+ // Special-case for Posterous. They have some nice metadata in their
+ // posterous:author elements. We should use them instead of the channel.
+
+ $items = $feedEl->getElementsByTagName('item');
+
+ if ($items->length > 0) {
+ $item = $items->item(0);
+ $authorEl = ActivityUtils::child($item, ActivityObject::AUTHOR, ActivityObject::POSTEROUS);
+ if (!empty($authorEl)) {
+ $obj = ActivityObject::fromPosterousAuthor($authorEl);
+ // Posterous has multiple authors per feed, and multiple feeds
+ // per author. We check if this is the "main" feed for this author.
+ if (array_key_exists('profileurl', $hints) &&
+ !empty($obj->poco) &&
+ common_url_to_nickname($hints['profileurl']) == $obj->poco->preferredUsername) {
+ return self::ensureActivityObjectProfile($obj, $hints);
+ }
+ }
+ }
+
// @fixme we should check whether this feed has elements
// with different <author> or <dc:creator> elements, and... I dunno.
// Do something about that.
@@ -1042,11 +1107,14 @@ class Ostatus_profile extends Memcached_DataObject
/**
* Fetch, or build if necessary, an Ostatus_profile for the actor
* in a given Activity Streams activity.
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
*
* @param Activity $activity
* @param string $feeduri if we already know the canonical feed URI!
* @param string $salmonuri if we already know the salmon return channel URI
* @return Ostatus_profile
+ * @throws Exception
*/
public static function ensureActorProfile($activity, $hints=array())
@@ -1054,6 +1122,18 @@ class Ostatus_profile extends Memcached_DataObject
return self::ensureActivityObjectProfile($activity->actor, $hints);
}
+ /**
+ * Fetch, or build if necessary, an Ostatus_profile for the profile
+ * in a given Activity Streams object (can be subject, actor, or object).
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
+ *
+ * @param ActivityObject $object
+ * @param array $hints additional discovery information passed from higher levels
+ * @return Ostatus_profile
+ * @throws Exception
+ */
+
public static function ensureActivityObjectProfile($object, $hints=array())
{
$profile = self::getActivityObjectProfile($object);
@@ -1068,35 +1148,45 @@ class Ostatus_profile extends Memcached_DataObject
/**
* @param Activity $activity
* @return mixed matching Ostatus_profile or false if none known
+ * @throws ServerException if feed info invalid
*/
public static function getActorProfile($activity)
{
return self::getActivityObjectProfile($activity->actor);
}
+ /**
+ * @param ActivityObject $activity
+ * @return mixed matching Ostatus_profile or false if none known
+ * @throws ServerException if feed info invalid
+ */
protected static function getActivityObjectProfile($object)
{
$uri = self::getActivityObjectProfileURI($object);
return Ostatus_profile::staticGet('uri', $uri);
}
- protected static function getActorProfileURI($activity)
- {
- return self::getActivityObjectProfileURI($activity->actor);
- }
-
/**
- * @param Activity $activity
+ * Get the identifier URI for the remote entity described
+ * by this ActivityObject. This URI is *not* guaranteed to be
+ * a resolvable HTTP/HTTPS URL.
+ *
+ * @param ActivityObject $object
* @return string
- * @throws ServerException
+ * @throws ServerException if feed info invalid
*/
protected static function getActivityObjectProfileURI($object)
{
- $opts = array('allowed_schemes' => array('http', 'https'));
- if ($object->id && Validate::uri($object->id, $opts)) {
- return $object->id;
+ if ($object->id) {
+ if (ActivityUtils::validateUri($object->id)) {
+ return $object->id;
+ }
}
- if ($object->link && Validate::uri($object->link, $opts)) {
+
+ // If the id is missing or invalid (we've seen feeds mistakenly listing
+ // things like local usernames in that field) then we'll use the profile
+ // page link, if valid.
+ if ($object->link && common_valid_http_url($object->link)) {
return $object->link;
}
throw new ServerException("No author ID URI found");
@@ -1109,6 +1199,8 @@ class Ostatus_profile extends Memcached_DataObject
/**
* Create local ostatus_profile and profile/user_group entries for
* the provided remote user or group.
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
*
* @param ActivityObject $object
* @param array $hints
@@ -1125,7 +1217,8 @@ class Ostatus_profile extends Memcached_DataObject
throw new Exception("No profile URI");
}
- if (OStatusPlugin::localProfileFromUrl($homeuri)) {
+ $user = User::staticGet('uri', $homeuri);
+ if ($user) {
throw new Exception("Local user can't be referenced as remote.");
}
@@ -1425,6 +1518,11 @@ class Ostatus_profile extends Memcached_DataObject
}
/**
+ * Look up, and if necessary create, an Ostatus_profile for the remote
+ * entity with the given webfinger address.
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
+ *
* @param string $addr webfinger address
* @return Ostatus_profile
* @throws Exception on error conditions
diff --git a/tests/ActivityParseTests.php b/tests/ActivityParseTests.php
index b6980a6bb..9d8fd47af 100644
--- a/tests/ActivityParseTests.php
+++ b/tests/ActivityParseTests.php
@@ -170,6 +170,51 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase
$this->assertFalse(empty($actor));
$this->assertEquals($actor->title, "Joseph Scott");
}
+
+ public function testExample7()
+ {
+ global $_example7;
+
+ $dom = DOMDocument::loadXML($_example7);
+
+ $rss = $dom->documentElement;
+
+ $channels = $dom->getElementsByTagName('channel');
+
+ $channel = $channels->item(0);
+
+ $items = $channel->getElementsByTagName('item');
+
+ $item = $items->item(0);
+
+ $act = new Activity($item, $channel);
+
+ $this->assertEquals(ActivityVerb::POST, $act->verb);
+ $this->assertEquals('http://evanpro.posterous.com/checking-out-captain-bones', $act->link);
+ $this->assertEquals('http://evanpro.posterous.com/checking-out-captain-bones', $act->id);
+ $this->assertEquals('Checking out captain bones', $act->title);
+ $this->assertEquals(1269095551, $act->time);
+
+ $actor = $act->actor;
+
+ $this->assertEquals(ActivityObject::PERSON, $actor->type);
+ $this->assertEquals('http://posterous.com/people/3sDslhaepotz', $actor->id);
+ $this->assertEquals('Evan Prodromou', $actor->title);
+ $this->assertNull($actor->summary);
+ $this->assertNull($actor->content);
+ $this->assertEquals('http://posterous.com/people/3sDslhaepotz', $actor->link);
+ $this->assertNull($actor->source);
+ $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]);
+ $this->assertNotNull($actor->poco);
+ $this->assertEquals('evanpro', $actor->poco->preferredUsername);
+ $this->assertEquals('Evan Prodromou', $actor->poco->displayName);
+ $this->assertNull($actor->poco->note);
+ $this->assertNull($actor->poco->address);
+ $this->assertEquals(0, count($actor->poco->urls));
+ }
}
$_example1 = <<<EXAMPLE1
@@ -423,3 +468,43 @@ $_example6 = <<<EXAMPLE6
</rss>
EXAMPLE6;
+$_example7 = <<<EXAMPLE7
+<?xml version="1.0" encoding="UTF-8"?>
+ <rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:posterous="http://posterous.com/help/rss/1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
+ <channel>
+ <title>evanpro's posterous</title>
+ <link>http://evanpro.posterous.com</link>
+ <description>Most recent posts at evanpro's posterous</description>
+ <generator>posterous.com</generator>
+ <link type="application/json" xmlns="http://www.w3.org/2005/Atom" rel="http://api.friendfeed.com/2008/03#sup" href="http://posterous.com/api/sup_update#56bcc5eb7"/>
+ <atom:link rel="self" href="http://evanpro.posterous.com/rss.xml"/>
+ <atom:link rel="hub" href="http://posterous.superfeedr.com"/>
+ <item>
+ <pubDate>Sat, 20 Mar 2010 07:32:31 -0700</pubDate>
+ <title>Checking out captain bones</title>
+ <link>http://evanpro.posterous.com/checking-out-captain-bones</link>
+ <guid>http://evanpro.posterous.com/checking-out-captain-bones</guid>
+ <description>
+ <![CDATA[<p>
+ <p>Bones!</p>
+
+ </p>
+
+ <p><a href="http://evanpro.posterous.com/checking-out-captain-bones">Permalink</a>
+
+ | <a href="http://evanpro.posterous.com/checking-out-captain-bones#comment">Leave a comment&nbsp;&nbsp;&raquo;</a>
+
+ </p>]]>
+ </description>
+ <posterous:author>
+ <posterous:userImage>http://files.posterous.com/user_profile_pics/480326/2009-08-05-142447.jpg</posterous:userImage>
+ <posterous:profileUrl>http://posterous.com/people/3sDslhaepotz</posterous:profileUrl>
+ <posterous:firstName>Evan</posterous:firstName>
+ <posterous:lastnNme>Prodromou</posterous:lastnNme>
+ <posterous:nickName>evanpro</posterous:nickName>
+ <posterous:displayName>Evan Prodromou</posterous:displayName>
+ </posterous:author>
+ </item>
+ </channel>
+</rss>
+EXAMPLE7;