Merge branch 'testing' into 0.9.x

This commit is contained in:
Brion Vibber 2010-03-21 16:28:56 -07:00
commit be7efe7504
7 changed files with 363 additions and 91 deletions

View File

@ -238,17 +238,17 @@ class Activity
$this->time = strtotime($pubDateEl->textContent); $this->time = strtotime($pubDateEl->textContent);
} }
$authorEl = $this->_child($item, self::AUTHOR, self::RSS); if ($authorEl = $this->_child($item, self::AUTHOR, self::RSS)) {
if (!empty($authorEl)) {
$this->actor = ActivityObject::fromRssAuthor($authorEl); $this->actor = ActivityObject::fromRssAuthor($authorEl);
} else { } else if ($dcCreatorEl = $this->_child($item, self::CREATOR, self::DC)) {
$dcCreatorEl = $this->_child($item, self::CREATOR, self::DC);
if (!empty($dcCreatorEl)) {
$this->actor = ActivityObject::fromDcCreator($dcCreatorEl); $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)) { } else if (!empty($channel)) {
$this->actor = ActivityObject::fromRssChannel($channel); $this->actor = ActivityObject::fromRssChannel($channel);
} } else {
// No actor!
} }
$this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, self::RSS); $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();
}
}

View File

@ -80,6 +80,13 @@ class ActivityObject
const URI = 'uri'; const URI = 'uri';
const EMAIL = 'email'; 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 $element;
public $type; public $type;
public $id; public $id;
@ -149,7 +156,11 @@ class ActivityObject
{ {
$this->type = self::PERSON; // XXX: is this fair? $this->type = self::PERSON; // XXX: is this fair?
$this->title = $this->_childContent($element, self::NAME); $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)) { if (empty($this->id)) {
$email = $this->_childContent($element, self::EMAIL); $email = $this->_childContent($element, self::EMAIL);
@ -162,6 +173,15 @@ class ActivityObject
private function _fromAtomEntry($element) 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, $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
Activity::SPEC); Activity::SPEC);
@ -169,7 +189,11 @@ class ActivityObject
$this->type = ActivityObject::NOTE; $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->summary = ActivityUtils::childHtmlContent($element, self::SUMMARY);
$this->content = ActivityUtils::getContent($element); $this->content = ActivityUtils::getContent($element);
@ -290,12 +314,42 @@ class ActivityObject
$imageEl = ActivityUtils::child($el, Activity::IMAGE, Activity::RSS); $imageEl = ActivityUtils::child($el, Activity::IMAGE, Activity::RSS);
if (!empty($imageEl)) { 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; 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;
}
private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM) private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
{ {
return ActivityUtils::childContent($element, $tag, $namespace); return ActivityUtils::childContent($element, $tag, $namespace);

View File

@ -240,4 +240,26 @@ class ActivityUtils
throw new ClientException(_("Can't handle embedded Base64 content yet.")); 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;
}
} }

77
lib/atomcategory.php Normal file
View File

@ -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();
}
}

View File

@ -947,23 +947,4 @@ class OStatusPlugin extends Plugin
} }
return false; 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;
}
} }

View File

@ -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 * @param string $profile_url
* @return Ostatus_profile * @return Ostatus_profile
* @throws FeedSubException * @throws Exception
*/ */
public static function ensureProfileURL($profile_url, $hints=array()) public static function ensureProfileURL($profile_url, $hints=array())
@ -731,7 +735,7 @@ class Ostatus_profile extends Memcached_DataObject
$response = $client->get($profile_url); $response = $client->get($profile_url);
if (!$response->isOk()) { if (!$response->isOk()) {
return null; throw new Exception("Could not reach profile page: " . $profile_url);
} }
// Check if we have a non-canonical URL // Check if we have a non-canonical URL
@ -785,11 +789,20 @@ class Ostatus_profile extends Memcached_DataObject
if (!empty($feedurl)) { if (!empty($feedurl)) {
$hints['feedurl'] = $feedurl; $hints['feedurl'] = $feedurl;
return self::ensureFeedURL($feedurl, $hints); 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) static function getFromProfileURL($profile_url)
{ {
$profile = Profile::staticGet('profileurl', $profile_url); $profile = Profile::staticGet('profileurl', $profile_url);
@ -821,6 +834,14 @@ class Ostatus_profile extends Memcached_DataObject
return null; 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()) public static function ensureFeedURL($feed_url, $hints=array())
{ {
$discover = new FeedDiscovery(); $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) public static function ensureAtomFeed($feedEl, $hints)
{ {
// Try to get a profile from the feed activity:subject // 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."); 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) 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 // @fixme we should check whether this feed has elements
// with different <author> or <dc:creator> elements, and... I dunno. // with different <author> or <dc:creator> elements, and... I dunno.
// Do something about that. // 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 * Fetch, or build if necessary, an Ostatus_profile for the actor
* in a given Activity Streams activity. * 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 Activity $activity
* @param string $feeduri if we already know the canonical feed URI! * @param string $feeduri if we already know the canonical feed URI!
* @param string $salmonuri if we already know the salmon return channel URI * @param string $salmonuri if we already know the salmon return channel URI
* @return Ostatus_profile * @return Ostatus_profile
* @throws Exception
*/ */
public static function ensureActorProfile($activity, $hints=array()) public static function ensureActorProfile($activity, $hints=array())
@ -1054,6 +1122,18 @@ class Ostatus_profile extends Memcached_DataObject
return self::ensureActivityObjectProfile($activity->actor, $hints); 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()) public static function ensureActivityObjectProfile($object, $hints=array())
{ {
$profile = self::getActivityObjectProfile($object); $profile = self::getActivityObjectProfile($object);
@ -1068,35 +1148,45 @@ class Ostatus_profile extends Memcached_DataObject
/** /**
* @param Activity $activity * @param Activity $activity
* @return mixed matching Ostatus_profile or false if none known * @return mixed matching Ostatus_profile or false if none known
* @throws ServerException if feed info invalid
*/ */
public static function getActorProfile($activity) public static function getActorProfile($activity)
{ {
return self::getActivityObjectProfile($activity->actor); 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) protected static function getActivityObjectProfile($object)
{ {
$uri = self::getActivityObjectProfileURI($object); $uri = self::getActivityObjectProfileURI($object);
return Ostatus_profile::staticGet('uri', $uri); 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 * @return string
* @throws ServerException * @throws ServerException if feed info invalid
*/ */
protected static function getActivityObjectProfileURI($object) protected static function getActivityObjectProfileURI($object)
{ {
$opts = array('allowed_schemes' => array('http', 'https')); if ($object->id) {
if ($object->id && Validate::uri($object->id, $opts)) { if (ActivityUtils::validateUri($object->id)) {
return $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; return $object->link;
} }
throw new ServerException("No author ID URI found"); 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 * Create local ostatus_profile and profile/user_group entries for
* the provided remote user or group. * 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 ActivityObject $object
* @param array $hints * @param array $hints
@ -1125,7 +1217,8 @@ class Ostatus_profile extends Memcached_DataObject
throw new Exception("No profile URI"); 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."); 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 * @param string $addr webfinger address
* @return Ostatus_profile * @return Ostatus_profile
* @throws Exception on error conditions * @throws Exception on error conditions

View File

@ -170,6 +170,51 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase
$this->assertFalse(empty($actor)); $this->assertFalse(empty($actor));
$this->assertEquals($actor->title, "Joseph Scott"); $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 $_example1 = <<<EXAMPLE1
@ -423,3 +468,43 @@ $_example6 = <<<EXAMPLE6
</rss> </rss>
EXAMPLE6; 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;