]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'testing' into 0.9.x
authorEvan Prodromou <evan@status.net>
Sat, 20 Mar 2010 21:11:42 +0000 (16:11 -0500)
committerEvan Prodromou <evan@status.net>
Sat, 20 Mar 2010 21:11:42 +0000 (16:11 -0500)
Conflicts:
lib/activity.php

lib/activity.php
lib/activitycontext.php [new file with mode: 0644]
lib/activityobject.php [new file with mode: 0644]
lib/activityutils.php [new file with mode: 0644]
lib/activityverb.php [new file with mode: 0644]
lib/avatarlink.php [new file with mode: 0644]
lib/common.php
lib/poco.php [new file with mode: 0644]
lib/pocoaddress.php [new file with mode: 0644]
lib/pocourl.php [new file with mode: 0644]
plugins/OStatus/classes/Ostatus_profile.php

index 9ffd822db994c597a904c0f5609d21fe933e9801..b1744e68f585c681d76cab8097ee7c0b58d558b9 100644 (file)
@@ -32,1113 +32,6 @@ if (!defined('STATUSNET')) {
     exit(1);
 }
 
-class PoCoURL
-{
-    const URLS      = 'urls';
-    const TYPE      = 'type';
-    const VALUE     = 'value';
-    const PRIMARY   = 'primary';
-
-    public $type;
-    public $value;
-    public $primary;
-
-    function __construct($type, $value, $primary = false)
-    {
-        $this->type    = $type;
-        $this->value   = $value;
-        $this->primary = $primary;
-    }
-
-    function asString()
-    {
-        $xs = new XMLStringer(true);
-        $xs->elementStart('poco:urls');
-        $xs->element('poco:type', null, $this->type);
-        $xs->element('poco:value', null, $this->value);
-        if (!empty($this->primary)) {
-            $xs->element('poco:primary', null, 'true');
-        }
-        $xs->elementEnd('poco:urls');
-        return $xs->getString();
-    }
-}
-
-class PoCoAddress
-{
-    const ADDRESS   = 'address';
-    const FORMATTED = 'formatted';
-
-    public $formatted;
-
-    // @todo Other address fields
-
-    function asString()
-    {
-        if (!empty($this->formatted)) {
-            $xs = new XMLStringer(true);
-            $xs->elementStart('poco:address');
-            $xs->element('poco:formatted', null, common_xml_safe_str($this->formatted));
-            $xs->elementEnd('poco:address');
-            return $xs->getString();
-        }
-
-        return null;
-    }
-}
-
-class PoCo
-{
-    const NS = 'http://portablecontacts.net/spec/1.0';
-
-    const USERNAME     = 'preferredUsername';
-    const DISPLAYNAME  = 'displayName';
-    const NOTE         = 'note';
-
-    public $preferredUsername;
-    public $displayName;
-    public $note;
-    public $address;
-    public $urls = array();
-
-    function __construct($element = null)
-    {
-        if (empty($element)) {
-            return;
-        }
-
-        $this->preferredUsername = ActivityUtils::childContent(
-            $element,
-            self::USERNAME,
-            self::NS
-        );
-
-        $this->displayName = ActivityUtils::childContent(
-            $element,
-            self::DISPLAYNAME,
-            self::NS
-        );
-
-        $this->note = ActivityUtils::childContent(
-            $element,
-            self::NOTE,
-            self::NS
-        );
-
-        $this->address = $this->_getAddress($element);
-        $this->urls = $this->_getURLs($element);
-    }
-
-    private function _getURLs($element)
-    {
-        $urlEls = $element->getElementsByTagnameNS(self::NS, PoCoURL::URLS);
-        $urls = array();
-
-        foreach ($urlEls as $urlEl) {
-
-            $type = ActivityUtils::childContent(
-                $urlEl,
-                PoCoURL::TYPE,
-                PoCo::NS
-            );
-
-            $value = ActivityUtils::childContent(
-                $urlEl,
-                PoCoURL::VALUE,
-                PoCo::NS
-            );
-
-            $primary = ActivityUtils::childContent(
-                $urlEl,
-                PoCoURL::PRIMARY,
-                PoCo::NS
-            );
-
-            $isPrimary = false;
-
-            if (isset($primary) && $primary == 'true') {
-                $isPrimary = true;
-            }
-
-            // @todo check to make sure a primary hasn't already been added
-
-            array_push($urls, new PoCoURL($type, $value, $isPrimary));
-        }
-        return $urls;
-    }
-
-    private function _getAddress($element)
-    {
-        $addressEl = ActivityUtils::child(
-            $element,
-            PoCoAddress::ADDRESS,
-            PoCo::NS
-        );
-
-        if (!empty($addressEl)) {
-            $formatted = ActivityUtils::childContent(
-                $addressEl,
-                PoCoAddress::FORMATTED,
-                self::NS
-            );
-
-            if (!empty($formatted)) {
-                $address = new PoCoAddress();
-                $address->formatted = $formatted;
-                return $address;
-            }
-        }
-
-        return null;
-    }
-
-    function fromProfile($profile)
-    {
-        if (empty($profile)) {
-            return null;
-        }
-
-        $poco = new PoCo();
-
-        $poco->preferredUsername = $profile->nickname;
-        $poco->displayName       = $profile->getBestName();
-
-        $poco->note = $profile->bio;
-
-        $paddy = new PoCoAddress();
-        $paddy->formatted = $profile->location;
-        $poco->address = $paddy;
-
-        if (!empty($profile->homepage)) {
-            array_push(
-                $poco->urls,
-                new PoCoURL(
-                    'homepage',
-                    $profile->homepage,
-                    true
-                )
-            );
-        }
-
-        return $poco;
-    }
-
-    function fromGroup($group)
-    {
-        if (empty($group)) {
-            return null;
-        }
-
-        $poco = new PoCo();
-
-        $poco->preferredUsername = $group->nickname;
-        $poco->displayName       = $group->getBestName();
-
-        $poco->note = $group->description;
-
-        $paddy = new PoCoAddress();
-        $paddy->formatted = $group->location;
-        $poco->address = $paddy;
-
-        if (!empty($group->homepage)) {
-            array_push(
-                $poco->urls,
-                new PoCoURL(
-                    'homepage',
-                    $group->homepage,
-                    true
-                )
-            );
-        }
-
-        return $poco;
-    }
-
-    function getPrimaryURL()
-    {
-        foreach ($this->urls as $url) {
-            if ($url->primary) {
-                return $url;
-            }
-        }
-    }
-
-    function asString()
-    {
-        $xs = new XMLStringer(true);
-        $xs->element(
-            'poco:preferredUsername',
-            null,
-            $this->preferredUsername
-        );
-
-        $xs->element(
-            'poco:displayName',
-            null,
-            $this->displayName
-        );
-
-        if (!empty($this->note)) {
-            $xs->element('poco:note', null, common_xml_safe_str($this->note));
-        }
-
-        if (!empty($this->address)) {
-            $xs->raw($this->address->asString());
-        }
-
-        foreach ($this->urls as $url) {
-            $xs->raw($url->asString());
-        }
-
-        return $xs->getString();
-    }
-}
-
-/**
- * Utilities for turning DOMish things into Activityish things
- *
- * Some common functions that I didn't have the bandwidth to try to factor
- * into some kind of reasonable superclass, so just dumped here. Might
- * be useful to have an ActivityObject parent class or something.
- *
- * @category  OStatus
- * @package   StatusNet
- * @author    Evan Prodromou <evan@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
- * @link      http://status.net/
- */
-
-class ActivityUtils
-{
-    const ATOM = 'http://www.w3.org/2005/Atom';
-
-    const LINK = 'link';
-    const REL  = 'rel';
-    const TYPE = 'type';
-    const HREF = 'href';
-
-    const CONTENT = 'content';
-    const SRC     = 'src';
-
-    /**
-     * Get the permalink for an Activity object
-     *
-     * @param DOMElement $element A DOM element
-     *
-     * @return string related link, if any
-     */
-
-    static function getPermalink($element)
-    {
-        return self::getLink($element, 'alternate', 'text/html');
-    }
-
-    /**
-     * Get the permalink for an Activity object
-     *
-     * @param DOMElement $element A DOM element
-     *
-     * @return string related link, if any
-     */
-
-    static function getLink(DOMNode $element, $rel, $type=null)
-    {
-        $els = $element->childNodes;
-
-        foreach ($els as $link) {
-
-            if (!($link instanceof DOMElement)) {
-                continue;
-            }
-
-            if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
-
-                $linkRel = $link->getAttribute(self::REL);
-                $linkType = $link->getAttribute(self::TYPE);
-
-                if ($linkRel == $rel &&
-                    (is_null($type) || $linkType == $type)) {
-                    return $link->getAttribute(self::HREF);
-                }
-            }
-        }
-
-        return null;
-    }
-
-    static function getLinks(DOMNode $element, $rel, $type=null)
-    {
-        $els = $element->childNodes;
-        $out = array();
-
-        foreach ($els as $link) {
-            if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
-
-                $linkRel = $link->getAttribute(self::REL);
-                $linkType = $link->getAttribute(self::TYPE);
-
-                if ($linkRel == $rel &&
-                    (is_null($type) || $linkType == $type)) {
-                    $out[] = $link;
-                }
-            }
-        }
-
-        return $out;
-    }
-
-    /**
-     * Gets the first child element with the given tag
-     *
-     * @param DOMElement $element   element to pick at
-     * @param string     $tag       tag to look for
-     * @param string     $namespace Namespace to look under
-     *
-     * @return DOMElement found element or null
-     */
-
-    static function child(DOMNode $element, $tag, $namespace=self::ATOM)
-    {
-        $els = $element->childNodes;
-        if (empty($els) || $els->length == 0) {
-            return null;
-        } else {
-            for ($i = 0; $i < $els->length; $i++) {
-                $el = $els->item($i);
-                if ($el->localName == $tag && $el->namespaceURI == $namespace) {
-                    return $el;
-                }
-            }
-        }
-    }
-
-    /**
-     * Grab the text content of a DOM element child of the current element
-     *
-     * @param DOMElement $element   Element whose children we examine
-     * @param string     $tag       Tag to look up
-     * @param string     $namespace Namespace to use, defaults to Atom
-     *
-     * @return string content of the child
-     */
-
-    static function childContent(DOMNode $element, $tag, $namespace=self::ATOM)
-    {
-        $el = self::child($element, $tag, $namespace);
-
-        if (empty($el)) {
-            return null;
-        } else {
-            return $el->textContent;
-        }
-    }
-
-    /**
-     * Get the content of an atom:entry-like object
-     *
-     * @param DOMElement $element The element to examine.
-     *
-     * @return string unencoded HTML content of the element, like "This -&lt; is <b>HTML</b>."
-     *
-     * @todo handle remote content
-     * @todo handle embedded XML mime types
-     * @todo handle base64-encoded non-XML and non-text mime types
-     */
-
-    static function getContent($element)
-    {
-        $contentEl = ActivityUtils::child($element, self::CONTENT);
-
-        if (!empty($contentEl)) {
-
-            $src  = $contentEl->getAttribute(self::SRC);
-
-            if (!empty($src)) {
-                throw new ClientException(_("Can't handle remote content yet."));
-            }
-
-            $type = $contentEl->getAttribute(self::TYPE);
-
-            // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
-
-            if (empty($type) || $type == 'text') {
-                // Plain text source -- let's turn it into HTML!
-                return htmlspecialchars($contentEl->textContent);
-            } else if ($type == 'html') {
-                // The XML text decoding gives us an HTML string ready to roll.
-                return $contentEl->textContent;
-            } else if ($type == 'xhtml') {
-                // Embedded XHTML; we have to pull it out of the document tree,
-                // then serialize it back out to an HTML fragment string.
-                $divEl = ActivityUtils::child($contentEl, 'div', 'http://www.w3.org/1999/xhtml');
-                if (empty($divEl)) {
-                    return null;
-                }
-                $doc = $divEl->ownerDocument;
-                $text = '';
-                $children = $divEl->childNodes;
-
-                for ($i = 0; $i < $children->length; $i++) {
-                    $child = $children->item($i);
-                    $text .= $doc->saveXML($child);
-                }
-                return trim($text);
-            } else if (in_array($type, array('text/xml', 'application/xml')) ||
-                       preg_match('#(+|/)xml$#', $type)) {
-                throw new ClientException(_("Can't handle embedded XML content yet."));
-            } else if (strncasecmp($type, 'text/', 5)) {
-                return $contentEl->textContent;
-            } else {
-                throw new ClientException(_("Can't handle embedded Base64 content yet."));
-            }
-        }
-    }
-}
-
-// XXX: Arg! This wouldn't be necessary if we used Avatars conistently
-class AvatarLink
-{
-    public $url;
-    public $type;
-    public $size;
-    public $width;
-    public $height;
-
-    function __construct($element=null)
-    {
-        if ($element) {
-            // @fixme use correct namespaces
-            $this->url = $element->getAttribute('href');
-            $this->type = $element->getAttribute('type');
-            $width = $element->getAttribute('media:width');
-            if ($width != null) {
-                $this->width = intval($width);
-            }
-            $height = $element->getAttribute('media:height');
-            if ($height != null) {
-                $this->height = intval($height);
-            }
-        }
-    }
-
-    static function fromAvatar($avatar)
-    {
-        if (empty($avatar)) {
-            return null;
-        }
-        $alink = new AvatarLink();
-        $alink->type   = $avatar->mediatype;
-        $alink->height = $avatar->height;
-        $alink->width  = $avatar->width;
-        $alink->url    = $avatar->displayUrl();
-        return $alink;
-    }
-
-    static function fromFilename($filename, $size)
-    {
-        $alink = new AvatarLink();
-        $alink->url    = $filename;
-        $alink->height = $size;
-        if (!empty($filename)) {
-            $alink->width  = $size;
-            $alink->type   = self::mediatype($filename);
-        } else {
-            $alink->url    = User_group::defaultLogo($size);
-            $alink->type   = 'image/png';
-        }
-        return $alink;
-    }
-
-    // yuck!
-    static function mediatype($filename) {
-        $ext = strtolower(end(explode('.', $filename)));
-        if ($ext == 'jpeg') {
-            $ext = 'jpg';
-        }
-        // hope we don't support any others
-        $types = array('png', 'gif', 'jpg', 'jpeg');
-        if (in_array($ext, $types)) {
-            return 'image/' . $ext;
-        }
-        return null;
-    }
-}
-
-/**
- * A noun-ish thing in the activity universe
- *
- * The activity streams spec talks about activity objects, while also having
- * a tag activity:object, which is in fact an activity object. Aaaaaah!
- *
- * This is just a thing in the activity universe. Can be the subject, object,
- * or indirect object (target!) of an activity verb. Rotten name, and I'm
- * propagating it. *sigh*
- *
- * @category  OStatus
- * @package   StatusNet
- * @author    Evan Prodromou <evan@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
- * @link      http://status.net/
- */
-
-class ActivityObject
-{
-    const ARTICLE   = 'http://activitystrea.ms/schema/1.0/article';
-    const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
-    const NOTE      = 'http://activitystrea.ms/schema/1.0/note';
-    const STATUS    = 'http://activitystrea.ms/schema/1.0/status';
-    const FILE      = 'http://activitystrea.ms/schema/1.0/file';
-    const PHOTO     = 'http://activitystrea.ms/schema/1.0/photo';
-    const ALBUM     = 'http://activitystrea.ms/schema/1.0/photo-album';
-    const PLAYLIST  = 'http://activitystrea.ms/schema/1.0/playlist';
-    const VIDEO     = 'http://activitystrea.ms/schema/1.0/video';
-    const AUDIO     = 'http://activitystrea.ms/schema/1.0/audio';
-    const BOOKMARK  = 'http://activitystrea.ms/schema/1.0/bookmark';
-    const PERSON    = 'http://activitystrea.ms/schema/1.0/person';
-    const GROUP     = 'http://activitystrea.ms/schema/1.0/group';
-    const PLACE     = 'http://activitystrea.ms/schema/1.0/place';
-    const COMMENT   = 'http://activitystrea.ms/schema/1.0/comment';
-    // ^^^^^^^^^^ tea!
-
-    // Atom elements we snarf
-
-    const TITLE   = 'title';
-    const SUMMARY = 'summary';
-    const ID      = 'id';
-    const SOURCE  = 'source';
-
-    const NAME  = 'name';
-    const URI   = 'uri';
-    const EMAIL = 'email';
-
-    public $element;
-    public $type;
-    public $id;
-    public $title;
-    public $summary;
-    public $content;
-    public $link;
-    public $source;
-    public $avatarLinks = array();
-    public $geopoint;
-    public $poco;
-    public $displayName;
-
-    /**
-     * Constructor
-     *
-     * This probably needs to be refactored
-     * to generate a local class (ActivityPerson, ActivityFile, ...)
-     * based on the object type.
-     *
-     * @param DOMElement $element DOM thing to turn into an Activity thing
-     */
-
-    function __construct($element = null)
-    {
-        if (empty($element)) {
-            return;
-        }
-
-        $this->element = $element;
-
-        $this->geopoint = $this->_childContent(
-            $element,
-            ActivityContext::POINT,
-            ActivityContext::GEORSS
-        );
-
-        if ($element->tagName == 'author') {
-            $this->_fromAuthor($element);
-        } else if ($element->tagName == 'item') {
-            $this->_fromRssItem($element);
-        } else {
-            $this->_fromAtomEntry($element);
-        }
-
-        // Some per-type attributes...
-        if ($this->type == self::PERSON || $this->type == self::GROUP) {
-            $this->displayName = $this->title;
-
-            $photos = ActivityUtils::getLinks($element, 'photo');
-            if (count($photos)) {
-                foreach ($photos as $link) {
-                    $this->avatarLinks[] = new AvatarLink($link);
-                }
-            } else {
-                $avatars = ActivityUtils::getLinks($element, 'avatar');
-                foreach ($avatars as $link) {
-                    $this->avatarLinks[] = new AvatarLink($link);
-                }
-            }
-
-            $this->poco = new PoCo($element);
-        }
-    }
-
-    private function _fromAuthor($element)
-    {
-        $this->type  = self::PERSON; // XXX: is this fair?
-        $this->title = $this->_childContent($element, self::NAME);
-        $this->id    = $this->_childContent($element, self::URI);
-
-        if (empty($this->id)) {
-            $email = $this->_childContent($element, self::EMAIL);
-            if (!empty($email)) {
-                // XXX: acct: ?
-                $this->id = 'mailto:'.$email;
-            }
-        }
-    }
-
-    private function _fromAtomEntry($element)
-    {
-        $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
-                                           Activity::SPEC);
-
-        if (empty($this->type)) {
-            $this->type = ActivityObject::NOTE;
-        }
-
-        $this->id      = $this->_childContent($element, self::ID);
-        $this->title   = $this->_childContent($element, self::TITLE);
-        $this->summary = $this->_childContent($element, self::SUMMARY);
-
-        $this->source  = $this->_getSource($element);
-
-        $this->content = ActivityUtils::getContent($element);
-
-        $this->link = ActivityUtils::getPermalink($element);
-    }
-
-    // @fixme rationalize with Activity::_fromRssItem()
-
-    private function _fromRssItem($item)
-    {
-        $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, Activity::RSS);
-
-        $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, Activity::CONTENTNS);
-
-        if (!empty($contentEl)) {
-            $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES);
-        } else {
-            $descriptionEl = ActivityUtils::child($item, Activity::DESCRIPTION, Activity::RSS);
-            if (!empty($descriptionEl)) {
-                $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES);
-            }
-        }
-
-        $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, Activity::RSS);
-
-        $guidEl = ActivityUtils::child($item, Activity::GUID, Activity::RSS);
-
-        if (!empty($guidEl)) {
-            $this->id = $guidEl->textContent;
-
-            if ($guidEl->hasAttribute('isPermaLink')) {
-                // overwrites <link>
-                $this->link = $this->id;
-            }
-        }
-    }
-
-    public static function fromRssAuthor($el)
-    {
-        $text = $el->textContent;
-
-        if (preg_match('/^(.*?) \((.*)\)$/', $text, $match)) {
-            $email = $match[1];
-            $name = $match[2];
-        } else if (preg_match('/^(.*?) <(.*)>$/', $text, $match)) {
-            $name = $match[1];
-            $email = $match[2];
-        } else if (preg_match('/.*@.*/', $text)) {
-            $email = $text;
-            $name = null;
-        } else {
-            $name = $text;
-            $email = null;
-        }
-
-        // Not really enough info
-
-        $obj = new ActivityObject();
-
-        $obj->element = $el;
-
-        $obj->type  = ActivityObject::PERSON;
-        $obj->title = $name;
-
-        if (!empty($email)) {
-            $obj->id = 'mailto:'.$email;
-        }
-
-        return $obj;
-    }
-
-    public static function fromDcCreator($el)
-    {
-        // Not really enough info
-
-        $text = $el->textContent;
-
-        $obj = new ActivityObject();
-
-        $obj->element = $el;
-
-        $obj->title = $text;
-        $obj->type  = ActivityObject::PERSON;
-
-        return $obj;
-    }
-
-    public static function fromRssChannel($el)
-    {
-        $obj = new ActivityObject();
-
-        $obj->element = $el;
-
-        $obj->type = ActivityObject::PERSON; // @fixme guess better
-
-        $obj->title = ActivityUtils::childContent($el, ActivityObject::TITLE, self::RSS);
-        $obj->link  = ActivityUtils::childContent($el, ActivityUtils::LINK, self::RSS);
-        $obj->id    = ActivityUtils::getLink($el, self::SELF);
-
-        $desc = ActivityUtils::childContent($el, self::DESCRIPTION, self::RSS);
-
-        if (!empty($desc)) {
-            $obj->content = htmlspecialchars_decode($desc, ENT_QUOTES);
-        }
-
-        $imageEl = ActivityUtils::child($el, self::IMAGE, self::RSS);
-
-        if (!empty($imageEl)) {
-            $obj->avatarLinks[] = ActivityUtils::childContent($imageEl, self::URL, self::RSS);
-        }
-
-        return $obj;
-    }
-
-    private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
-    {
-        return ActivityUtils::childContent($element, $tag, $namespace);
-    }
-
-    // Try to get a unique id for the source feed
-
-    private function _getSource($element)
-    {
-        $sourceEl = ActivityUtils::child($element, 'source');
-
-        if (empty($sourceEl)) {
-            return null;
-        } else {
-            $href = ActivityUtils::getLink($sourceEl, 'self');
-            if (!empty($href)) {
-                return $href;
-            } else {
-                return ActivityUtils::childContent($sourceEl, 'id');
-            }
-        }
-    }
-
-    static function fromNotice(Notice $notice)
-    {
-        $object = new ActivityObject();
-
-        $object->type    = ActivityObject::NOTE;
-
-        $object->id      = $notice->uri;
-        $object->title   = $notice->content;
-        $object->content = $notice->rendered;
-        $object->link    = $notice->bestUrl();
-
-        return $object;
-    }
-
-    static function fromProfile(Profile $profile)
-    {
-        $object = new ActivityObject();
-
-        $object->type   = ActivityObject::PERSON;
-        $object->id     = $profile->getUri();
-        $object->title  = $profile->getBestName();
-        $object->link   = $profile->profileurl;
-
-        $orig = $profile->getOriginalAvatar();
-
-        if (!empty($orig)) {
-            $object->avatarLinks[] = AvatarLink::fromAvatar($orig);
-        }
-
-        $sizes = array(
-            AVATAR_PROFILE_SIZE,
-            AVATAR_STREAM_SIZE,
-            AVATAR_MINI_SIZE
-        );
-
-        foreach ($sizes as $size) {
-
-            $alink  = null;
-            $avatar = $profile->getAvatar($size);
-
-            if (!empty($avatar)) {
-                $alink = AvatarLink::fromAvatar($avatar);
-            } else {
-                $alink = new AvatarLink();
-                $alink->type   = 'image/png';
-                $alink->height = $size;
-                $alink->width  = $size;
-                $alink->url    = Avatar::defaultImage($size);
-            }
-
-            $object->avatarLinks[] = $alink;
-        }
-
-        if (isset($profile->lat) && isset($profile->lon)) {
-            $object->geopoint = (float)$profile->lat
-                . ' ' . (float)$profile->lon;
-        }
-
-        $object->poco = PoCo::fromProfile($profile);
-
-        return $object;
-    }
-
-    static function fromGroup($group)
-    {
-        $object = new ActivityObject();
-
-        $object->type   = ActivityObject::GROUP;
-        $object->id     = $group->getUri();
-        $object->title  = $group->getBestName();
-        $object->link   = $group->getUri();
-
-        $object->avatarLinks[] = AvatarLink::fromFilename(
-            $group->homepage_logo,
-            AVATAR_PROFILE_SIZE
-        );
-
-        $object->avatarLinks[] = AvatarLink::fromFilename(
-            $group->stream_logo,
-            AVATAR_STREAM_SIZE
-        );
-
-        $object->avatarLinks[] = AvatarLink::fromFilename(
-            $group->mini_logo,
-            AVATAR_MINI_SIZE
-        );
-
-        $object->poco = PoCo::fromGroup($group);
-
-        return $object;
-    }
-
-    function asString($tag='activity:object')
-    {
-        $xs = new XMLStringer(true);
-
-        $xs->elementStart($tag);
-
-        $xs->element('activity:object-type', null, $this->type);
-
-        $xs->element(self::ID, null, $this->id);
-
-        if (!empty($this->title)) {
-            $xs->element(
-                self::TITLE,
-                null,
-                common_xml_safe_str($this->title)
-            );
-        }
-
-        if (!empty($this->summary)) {
-            $xs->element(
-                self::SUMMARY,
-                null,
-                common_xml_safe_str($this->summary)
-            );
-        }
-
-        if (!empty($this->content)) {
-            // XXX: assuming HTML content here
-            $xs->element(
-                ActivityUtils::CONTENT,
-                array('type' => 'html'),
-                common_xml_safe_str($this->content)
-            );
-        }
-
-        if (!empty($this->link)) {
-            $xs->element(
-                'link',
-                array(
-                    'rel' => 'alternate',
-                    'type' => 'text/html',
-                    'href' => $this->link
-                ),
-                null
-            );
-        }
-
-        if ($this->type == ActivityObject::PERSON
-            || $this->type == ActivityObject::GROUP) {
-
-            foreach ($this->avatarLinks as $avatar) {
-                $xs->element(
-                    'link', array(
-                        'rel'  => 'avatar',
-                        'type'         => $avatar->type,
-                        'media:width'  => $avatar->width,
-                        'media:height' => $avatar->height,
-                        'href' => $avatar->url
-                    ),
-                    null
-                );
-            }
-        }
-
-        if (!empty($this->geopoint)) {
-            $xs->element(
-                'georss:point',
-                null,
-                $this->geopoint
-            );
-        }
-
-        if (!empty($this->poco)) {
-            $xs->raw($this->poco->asString());
-        }
-
-        $xs->elementEnd($tag);
-
-        return $xs->getString();
-    }
-}
-
-/**
- * Utility class to hold a bunch of constant defining default verb types
- *
- * @category  OStatus
- * @package   StatusNet
- * @author    Evan Prodromou <evan@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
- * @link      http://status.net/
- */
-
-class ActivityVerb
-{
-    const POST     = 'http://activitystrea.ms/schema/1.0/post';
-    const SHARE    = 'http://activitystrea.ms/schema/1.0/share';
-    const SAVE     = 'http://activitystrea.ms/schema/1.0/save';
-    const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite';
-    const PLAY     = 'http://activitystrea.ms/schema/1.0/play';
-    const FOLLOW   = 'http://activitystrea.ms/schema/1.0/follow';
-    const FRIEND   = 'http://activitystrea.ms/schema/1.0/make-friend';
-    const JOIN     = 'http://activitystrea.ms/schema/1.0/join';
-    const TAG      = 'http://activitystrea.ms/schema/1.0/tag';
-
-    // Custom OStatus verbs for the flipside until they're standardized
-    const DELETE     = 'http://ostatus.org/schema/1.0/unfollow';
-    const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite';
-    const UNFOLLOW   = 'http://ostatus.org/schema/1.0/unfollow';
-    const LEAVE      = 'http://ostatus.org/schema/1.0/leave';
-
-    // For simple profile-update pings; no content to share.
-    const UPDATE_PROFILE = 'http://ostatus.org/schema/1.0/update-profile';
-}
-
-class ActivityContext
-{
-    public $replyToID;
-    public $replyToUrl;
-    public $location;
-    public $attention = array();
-    public $conversation;
-
-    const THR     = 'http://purl.org/syndication/thread/1.0';
-    const GEORSS  = 'http://www.georss.org/georss';
-    const OSTATUS = 'http://ostatus.org/schema/1.0';
-
-    const INREPLYTO = 'in-reply-to';
-    const REF       = 'ref';
-    const HREF      = 'href';
-
-    const POINT     = 'point';
-
-    const ATTENTION    = 'ostatus:attention';
-    const CONVERSATION = 'ostatus:conversation';
-
-    function __construct($element)
-    {
-        $replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR);
-
-        if (!empty($replyToEl)) {
-            $this->replyToID  = $replyToEl->getAttribute(self::REF);
-            $this->replyToUrl = $replyToEl->getAttribute(self::HREF);
-        }
-
-        $this->location = $this->getLocation($element);
-
-        $this->conversation = ActivityUtils::getLink($element, self::CONVERSATION);
-
-        // Multiple attention links allowed
-
-        $links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK);
-
-        for ($i = 0; $i < $links->length; $i++) {
-
-            $link = $links->item($i);
-
-            $linkRel = $link->getAttribute(ActivityUtils::REL);
-
-            if ($linkRel == self::ATTENTION) {
-                $this->attention[] = $link->getAttribute(self::HREF);
-            }
-        }
-    }
-
-    /**
-     * Parse location given as a GeoRSS-simple point, if provided.
-     * http://www.georss.org/simple
-     *
-     * @param feed item $entry
-     * @return mixed Location or false
-     */
-    function getLocation($dom)
-    {
-        $points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT);
-
-        for ($i = 0; $i < $points->length; $i++) {
-            $point = $points->item($i)->textContent;
-            return self::locationFromPoint($point);
-        }
-
-        return null;
-    }
-
-    // XXX: Move to ActivityUtils or Location?
-    static function locationFromPoint($point)
-    {
-        $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
-        $point = preg_replace('/\s+/', ' ', $point);
-        $point = trim($point);
-        $coords = explode(' ', $point);
-        if (count($coords) == 2) {
-            list($lat, $lon) = $coords;
-            if (is_numeric($lat) && is_numeric($lon)) {
-                common_log(LOG_INFO, "Looking up location for $lat $lon from georss point");
-                return Location::fromLatLon($lat, $lon);
-            }
-        }
-        common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
-        return null;
-    }
-}
-
 /**
  * An activity in the ActivityStrea.ms world
  *
@@ -1328,7 +221,7 @@ class Activity
         }
     }
 
-    function _fromRssItem($item, $rss)
+    function _fromRssItem($item, $channel)
     {
         $verbEl = $this->_child($item, self::VERB);
 
@@ -1353,8 +246,8 @@ class Activity
             $dcCreatorEl = $this->_child($item, self::CREATOR, self::DC);
             if (!empty($dcCreatorEl)) {
                 $this->actor = ActivityObject::fromDcCreator($dcCreatorEl);
-            } else if (!empty($rss)) {
-                $this->actor = ActivityObject::fromRssChannel($rss);
+            } else if (!empty($channel)) {
+                $this->actor = ActivityObject::fromRssChannel($channel);
             }
         }
 
diff --git a/lib/activitycontext.php b/lib/activitycontext.php
new file mode 100644 (file)
index 0000000..2df7613
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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 ActivityContext
+{
+    public $replyToID;
+    public $replyToUrl;
+    public $location;
+    public $attention = array();
+    public $conversation;
+
+    const THR     = 'http://purl.org/syndication/thread/1.0';
+    const GEORSS  = 'http://www.georss.org/georss';
+    const OSTATUS = 'http://ostatus.org/schema/1.0';
+
+    const INREPLYTO = 'in-reply-to';
+    const REF       = 'ref';
+    const HREF      = 'href';
+
+    const POINT     = 'point';
+
+    const ATTENTION    = 'ostatus:attention';
+    const CONVERSATION = 'ostatus:conversation';
+
+    function __construct($element)
+    {
+        $replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR);
+
+        if (!empty($replyToEl)) {
+            $this->replyToID  = $replyToEl->getAttribute(self::REF);
+            $this->replyToUrl = $replyToEl->getAttribute(self::HREF);
+        }
+
+        $this->location = $this->getLocation($element);
+
+        $this->conversation = ActivityUtils::getLink($element, self::CONVERSATION);
+
+        // Multiple attention links allowed
+
+        $links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK);
+
+        for ($i = 0; $i < $links->length; $i++) {
+
+            $link = $links->item($i);
+
+            $linkRel = $link->getAttribute(ActivityUtils::REL);
+
+            if ($linkRel == self::ATTENTION) {
+                $this->attention[] = $link->getAttribute(self::HREF);
+            }
+        }
+    }
+
+    /**
+     * Parse location given as a GeoRSS-simple point, if provided.
+     * http://www.georss.org/simple
+     *
+     * @param feed item $entry
+     * @return mixed Location or false
+     */
+    function getLocation($dom)
+    {
+        $points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT);
+
+        for ($i = 0; $i < $points->length; $i++) {
+            $point = $points->item($i)->textContent;
+            return self::locationFromPoint($point);
+        }
+
+        return null;
+    }
+
+    // XXX: Move to ActivityUtils or Location?
+    static function locationFromPoint($point)
+    {
+        $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
+        $point = preg_replace('/\s+/', ' ', $point);
+        $point = trim($point);
+        $coords = explode(' ', $point);
+        if (count($coords) == 2) {
+            list($lat, $lon) = $coords;
+            if (is_numeric($lat) && is_numeric($lon)) {
+                common_log(LOG_INFO, "Looking up location for $lat $lon from georss point");
+                return Location::fromLatLon($lat, $lon);
+            }
+        }
+        common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
+        return null;
+    }
+}
diff --git a/lib/activityobject.php b/lib/activityobject.php
new file mode 100644 (file)
index 0000000..b1e9071
--- /dev/null
@@ -0,0 +1,494 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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);
+}
+
+/**
+ * A noun-ish thing in the activity universe
+ *
+ * The activity streams spec talks about activity objects, while also having
+ * a tag activity:object, which is in fact an activity object. Aaaaaah!
+ *
+ * This is just a thing in the activity universe. Can be the subject, object,
+ * or indirect object (target!) of an activity verb. Rotten name, and I'm
+ * propagating it. *sigh*
+ *
+ * @category  OStatus
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link      http://status.net/
+ */
+
+class ActivityObject
+{
+    const ARTICLE   = 'http://activitystrea.ms/schema/1.0/article';
+    const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
+    const NOTE      = 'http://activitystrea.ms/schema/1.0/note';
+    const STATUS    = 'http://activitystrea.ms/schema/1.0/status';
+    const FILE      = 'http://activitystrea.ms/schema/1.0/file';
+    const PHOTO     = 'http://activitystrea.ms/schema/1.0/photo';
+    const ALBUM     = 'http://activitystrea.ms/schema/1.0/photo-album';
+    const PLAYLIST  = 'http://activitystrea.ms/schema/1.0/playlist';
+    const VIDEO     = 'http://activitystrea.ms/schema/1.0/video';
+    const AUDIO     = 'http://activitystrea.ms/schema/1.0/audio';
+    const BOOKMARK  = 'http://activitystrea.ms/schema/1.0/bookmark';
+    const PERSON    = 'http://activitystrea.ms/schema/1.0/person';
+    const GROUP     = 'http://activitystrea.ms/schema/1.0/group';
+    const PLACE     = 'http://activitystrea.ms/schema/1.0/place';
+    const COMMENT   = 'http://activitystrea.ms/schema/1.0/comment';
+    // ^^^^^^^^^^ tea!
+
+    // Atom elements we snarf
+
+    const TITLE   = 'title';
+    const SUMMARY = 'summary';
+    const ID      = 'id';
+    const SOURCE  = 'source';
+
+    const NAME  = 'name';
+    const URI   = 'uri';
+    const EMAIL = 'email';
+
+    public $element;
+    public $type;
+    public $id;
+    public $title;
+    public $summary;
+    public $content;
+    public $link;
+    public $source;
+    public $avatarLinks = array();
+    public $geopoint;
+    public $poco;
+    public $displayName;
+
+    /**
+     * Constructor
+     *
+     * This probably needs to be refactored
+     * to generate a local class (ActivityPerson, ActivityFile, ...)
+     * based on the object type.
+     *
+     * @param DOMElement $element DOM thing to turn into an Activity thing
+     */
+
+    function __construct($element = null)
+    {
+        if (empty($element)) {
+            return;
+        }
+
+        $this->element = $element;
+
+        $this->geopoint = $this->_childContent(
+            $element,
+            ActivityContext::POINT,
+            ActivityContext::GEORSS
+        );
+
+        if ($element->tagName == 'author') {
+            $this->_fromAuthor($element);
+        } else if ($element->tagName == 'item') {
+            $this->_fromRssItem($element);
+        } else {
+            $this->_fromAtomEntry($element);
+        }
+
+        // Some per-type attributes...
+        if ($this->type == self::PERSON || $this->type == self::GROUP) {
+            $this->displayName = $this->title;
+
+            $photos = ActivityUtils::getLinks($element, 'photo');
+            if (count($photos)) {
+                foreach ($photos as $link) {
+                    $this->avatarLinks[] = new AvatarLink($link);
+                }
+            } else {
+                $avatars = ActivityUtils::getLinks($element, 'avatar');
+                foreach ($avatars as $link) {
+                    $this->avatarLinks[] = new AvatarLink($link);
+                }
+            }
+
+            $this->poco = new PoCo($element);
+        }
+    }
+
+    private function _fromAuthor($element)
+    {
+        $this->type  = self::PERSON; // XXX: is this fair?
+        $this->title = $this->_childContent($element, self::NAME);
+        $this->id    = $this->_childContent($element, self::URI);
+
+        if (empty($this->id)) {
+            $email = $this->_childContent($element, self::EMAIL);
+            if (!empty($email)) {
+                // XXX: acct: ?
+                $this->id = 'mailto:'.$email;
+            }
+        }
+    }
+
+    private function _fromAtomEntry($element)
+    {
+        $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
+                                           Activity::SPEC);
+
+        if (empty($this->type)) {
+            $this->type = ActivityObject::NOTE;
+        }
+
+        $this->id      = $this->_childContent($element, self::ID);
+        $this->summary = ActivityUtils::childHtmlContent($element, self::SUMMARY);
+        $this->content = ActivityUtils::getContent($element);
+
+        // We don't like HTML in our titles, although it's technically allowed
+
+        $title = ActivityUtils::childHtmlContent($element, self::TITLE);
+
+        $this->title = html_entity_decode(strip_tags($title));
+
+        $this->source  = $this->_getSource($element);
+
+        $this->link = ActivityUtils::getPermalink($element);
+    }
+
+    // @fixme rationalize with Activity::_fromRssItem()
+
+    private function _fromRssItem($item)
+    {
+        $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, Activity::RSS);
+
+        $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, Activity::CONTENTNS);
+
+        if (!empty($contentEl)) {
+            $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES);
+        } else {
+            $descriptionEl = ActivityUtils::child($item, Activity::DESCRIPTION, Activity::RSS);
+            if (!empty($descriptionEl)) {
+                $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES);
+            }
+        }
+
+        $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, Activity::RSS);
+
+        $guidEl = ActivityUtils::child($item, Activity::GUID, Activity::RSS);
+
+        if (!empty($guidEl)) {
+            $this->id = $guidEl->textContent;
+
+            if ($guidEl->hasAttribute('isPermaLink')) {
+                // overwrites <link>
+                $this->link = $this->id;
+            }
+        }
+    }
+
+    public static function fromRssAuthor($el)
+    {
+        $text = $el->textContent;
+
+        if (preg_match('/^(.*?) \((.*)\)$/', $text, $match)) {
+            $email = $match[1];
+            $name = $match[2];
+        } else if (preg_match('/^(.*?) <(.*)>$/', $text, $match)) {
+            $name = $match[1];
+            $email = $match[2];
+        } else if (preg_match('/.*@.*/', $text)) {
+            $email = $text;
+            $name = null;
+        } else {
+            $name = $text;
+            $email = null;
+        }
+
+        // Not really enough info
+
+        $obj = new ActivityObject();
+
+        $obj->element = $el;
+
+        $obj->type  = ActivityObject::PERSON;
+        $obj->title = $name;
+
+        if (!empty($email)) {
+            $obj->id = 'mailto:'.$email;
+        }
+
+        return $obj;
+    }
+
+    public static function fromDcCreator($el)
+    {
+        // Not really enough info
+
+        $text = $el->textContent;
+
+        $obj = new ActivityObject();
+
+        $obj->element = $el;
+
+        $obj->title = $text;
+        $obj->type  = ActivityObject::PERSON;
+
+        return $obj;
+    }
+
+    public static function fromRssChannel($el)
+    {
+        $obj = new ActivityObject();
+
+        $obj->element = $el;
+
+        $obj->type = ActivityObject::PERSON; // @fixme guess better
+
+        $obj->title = ActivityUtils::childContent($el, ActivityObject::TITLE, Activity::RSS);
+        $obj->link  = ActivityUtils::childContent($el, ActivityUtils::LINK, Activity::RSS);
+        $obj->id    = ActivityUtils::getLink($el, Activity::SELF);
+
+        if (empty($obj->id)) {
+            $obj->id = $obj->link;
+        }
+
+        $desc = ActivityUtils::childContent($el, Activity::DESCRIPTION, Activity::RSS);
+
+        if (!empty($desc)) {
+            $obj->content = htmlspecialchars_decode($desc, ENT_QUOTES);
+        }
+
+        $imageEl = ActivityUtils::child($el, Activity::IMAGE, Activity::RSS);
+
+        if (!empty($imageEl)) {
+            $obj->avatarLinks[] = ActivityUtils::childContent($imageEl, Activity::URL, Activity::RSS);
+        }
+
+        return $obj;
+    }
+
+    private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
+    {
+        return ActivityUtils::childContent($element, $tag, $namespace);
+    }
+
+    // Try to get a unique id for the source feed
+
+    private function _getSource($element)
+    {
+        $sourceEl = ActivityUtils::child($element, 'source');
+
+        if (empty($sourceEl)) {
+            return null;
+        } else {
+            $href = ActivityUtils::getLink($sourceEl, 'self');
+            if (!empty($href)) {
+                return $href;
+            } else {
+                return ActivityUtils::childContent($sourceEl, 'id');
+            }
+        }
+    }
+
+    static function fromNotice(Notice $notice)
+    {
+        $object = new ActivityObject();
+
+        $object->type    = ActivityObject::NOTE;
+
+        $object->id      = $notice->uri;
+        $object->title   = $notice->content;
+        $object->content = $notice->rendered;
+        $object->link    = $notice->bestUrl();
+
+        return $object;
+    }
+
+    static function fromProfile(Profile $profile)
+    {
+        $object = new ActivityObject();
+
+        $object->type   = ActivityObject::PERSON;
+        $object->id     = $profile->getUri();
+        $object->title  = $profile->getBestName();
+        $object->link   = $profile->profileurl;
+
+        $orig = $profile->getOriginalAvatar();
+
+        if (!empty($orig)) {
+            $object->avatarLinks[] = AvatarLink::fromAvatar($orig);
+        }
+
+        $sizes = array(
+            AVATAR_PROFILE_SIZE,
+            AVATAR_STREAM_SIZE,
+            AVATAR_MINI_SIZE
+        );
+
+        foreach ($sizes as $size) {
+
+            $alink  = null;
+            $avatar = $profile->getAvatar($size);
+
+            if (!empty($avatar)) {
+                $alink = AvatarLink::fromAvatar($avatar);
+            } else {
+                $alink = new AvatarLink();
+                $alink->type   = 'image/png';
+                $alink->height = $size;
+                $alink->width  = $size;
+                $alink->url    = Avatar::defaultImage($size);
+            }
+
+            $object->avatarLinks[] = $alink;
+        }
+
+        if (isset($profile->lat) && isset($profile->lon)) {
+            $object->geopoint = (float)$profile->lat
+                . ' ' . (float)$profile->lon;
+        }
+
+        $object->poco = PoCo::fromProfile($profile);
+
+        return $object;
+    }
+
+    static function fromGroup($group)
+    {
+        $object = new ActivityObject();
+
+        $object->type   = ActivityObject::GROUP;
+        $object->id     = $group->getUri();
+        $object->title  = $group->getBestName();
+        $object->link   = $group->getUri();
+
+        $object->avatarLinks[] = AvatarLink::fromFilename(
+            $group->homepage_logo,
+            AVATAR_PROFILE_SIZE
+        );
+
+        $object->avatarLinks[] = AvatarLink::fromFilename(
+            $group->stream_logo,
+            AVATAR_STREAM_SIZE
+        );
+
+        $object->avatarLinks[] = AvatarLink::fromFilename(
+            $group->mini_logo,
+            AVATAR_MINI_SIZE
+        );
+
+        $object->poco = PoCo::fromGroup($group);
+
+        return $object;
+    }
+
+    function asString($tag='activity:object')
+    {
+        $xs = new XMLStringer(true);
+
+        $xs->elementStart($tag);
+
+        $xs->element('activity:object-type', null, $this->type);
+
+        $xs->element(self::ID, null, $this->id);
+
+        if (!empty($this->title)) {
+            $xs->element(
+                self::TITLE,
+                null,
+                common_xml_safe_str($this->title)
+            );
+        }
+
+        if (!empty($this->summary)) {
+            $xs->element(
+                self::SUMMARY,
+                null,
+                common_xml_safe_str($this->summary)
+            );
+        }
+
+        if (!empty($this->content)) {
+            // XXX: assuming HTML content here
+            $xs->element(
+                ActivityUtils::CONTENT,
+                array('type' => 'html'),
+                common_xml_safe_str($this->content)
+            );
+        }
+
+        if (!empty($this->link)) {
+            $xs->element(
+                'link',
+                array(
+                    'rel' => 'alternate',
+                    'type' => 'text/html',
+                    'href' => $this->link
+                ),
+                null
+            );
+        }
+
+        if ($this->type == ActivityObject::PERSON
+            || $this->type == ActivityObject::GROUP) {
+
+            foreach ($this->avatarLinks as $avatar) {
+                $xs->element(
+                    'link', array(
+                        'rel'  => 'avatar',
+                        'type'         => $avatar->type,
+                        'media:width'  => $avatar->width,
+                        'media:height' => $avatar->height,
+                        'href' => $avatar->url
+                    ),
+                    null
+                );
+            }
+        }
+
+        if (!empty($this->geopoint)) {
+            $xs->element(
+                'georss:point',
+                null,
+                $this->geopoint
+            );
+        }
+
+        if (!empty($this->poco)) {
+            $xs->raw($this->poco->asString());
+        }
+
+        $xs->elementEnd($tag);
+
+        return $xs->getString();
+    }
+}
diff --git a/lib/activityutils.php b/lib/activityutils.php
new file mode 100644 (file)
index 0000000..c85a3db
--- /dev/null
@@ -0,0 +1,243 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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);
+}
+
+/**
+ * Utilities for turning DOMish things into Activityish things
+ *
+ * Some common functions that I didn't have the bandwidth to try to factor
+ * into some kind of reasonable superclass, so just dumped here. Might
+ * be useful to have an ActivityObject parent class or something.
+ *
+ * @category  OStatus
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link      http://status.net/
+ */
+
+class ActivityUtils
+{
+    const ATOM = 'http://www.w3.org/2005/Atom';
+
+    const LINK = 'link';
+    const REL  = 'rel';
+    const TYPE = 'type';
+    const HREF = 'href';
+
+    const CONTENT = 'content';
+    const SRC     = 'src';
+
+    /**
+     * Get the permalink for an Activity object
+     *
+     * @param DOMElement $element A DOM element
+     *
+     * @return string related link, if any
+     */
+
+    static function getPermalink($element)
+    {
+        return self::getLink($element, 'alternate', 'text/html');
+    }
+
+    /**
+     * Get the permalink for an Activity object
+     *
+     * @param DOMElement $element A DOM element
+     *
+     * @return string related link, if any
+     */
+
+    static function getLink(DOMNode $element, $rel, $type=null)
+    {
+        $els = $element->childNodes;
+
+        foreach ($els as $link) {
+
+            if (!($link instanceof DOMElement)) {
+                continue;
+            }
+
+            if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
+
+                $linkRel = $link->getAttribute(self::REL);
+                $linkType = $link->getAttribute(self::TYPE);
+
+                if ($linkRel == $rel &&
+                    (is_null($type) || $linkType == $type)) {
+                    return $link->getAttribute(self::HREF);
+                }
+            }
+        }
+
+        return null;
+    }
+
+    static function getLinks(DOMNode $element, $rel, $type=null)
+    {
+        $els = $element->childNodes;
+        $out = array();
+
+        foreach ($els as $link) {
+            if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
+
+                $linkRel = $link->getAttribute(self::REL);
+                $linkType = $link->getAttribute(self::TYPE);
+
+                if ($linkRel == $rel &&
+                    (is_null($type) || $linkType == $type)) {
+                    $out[] = $link;
+                }
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * Gets the first child element with the given tag
+     *
+     * @param DOMElement $element   element to pick at
+     * @param string     $tag       tag to look for
+     * @param string     $namespace Namespace to look under
+     *
+     * @return DOMElement found element or null
+     */
+
+    static function child(DOMNode $element, $tag, $namespace=self::ATOM)
+    {
+        $els = $element->childNodes;
+        if (empty($els) || $els->length == 0) {
+            return null;
+        } else {
+            for ($i = 0; $i < $els->length; $i++) {
+                $el = $els->item($i);
+                if ($el->localName == $tag && $el->namespaceURI == $namespace) {
+                    return $el;
+                }
+            }
+        }
+    }
+
+    /**
+     * Grab the text content of a DOM element child of the current element
+     *
+     * @param DOMElement $element   Element whose children we examine
+     * @param string     $tag       Tag to look up
+     * @param string     $namespace Namespace to use, defaults to Atom
+     *
+     * @return string content of the child
+     */
+
+    static function childContent(DOMNode $element, $tag, $namespace=self::ATOM)
+    {
+        $el = self::child($element, $tag, $namespace);
+
+        if (empty($el)) {
+            return null;
+        } else {
+            return $el->textContent;
+        }
+    }
+
+    static function childHtmlContent(DOMNode $element, $tag, $namespace=self::ATOM)
+    {
+        $el = self::child($element, $tag, $namespace);
+
+        if (empty($el)) {
+            return null;
+        } else {
+            return self::textConstruct($el);
+        }
+    }
+
+    /**
+     * Get the content of an atom:entry-like object
+     *
+     * @param DOMElement $element The element to examine.
+     *
+     * @return string unencoded HTML content of the element, like "This -&lt; is <b>HTML</b>."
+     *
+     * @todo handle remote content
+     * @todo handle embedded XML mime types
+     * @todo handle base64-encoded non-XML and non-text mime types
+     */
+
+    static function getContent($element)
+    {
+        return self::childHtmlContent($element, self::CONTENT, self::ATOM);
+    }
+
+    static function textConstruct($el)
+    {
+        $src  = $el->getAttribute(self::SRC);
+
+        if (!empty($src)) {
+            throw new ClientException(_("Can't handle remote content yet."));
+        }
+
+        $type = $el->getAttribute(self::TYPE);
+
+        // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
+
+        if (empty($type) || $type == 'text') {
+            return $el->textContent;
+        } else if ($type == 'html') {
+            $text = $el->textContent;
+            return htmlspecialchars_decode($text, ENT_QUOTES);
+        } else if ($type == 'xhtml') {
+            $divEl = ActivityUtils::child($el, 'div', 'http://www.w3.org/1999/xhtml');
+            if (empty($divEl)) {
+                return null;
+            }
+            $doc = $divEl->ownerDocument;
+            $text = '';
+            $children = $divEl->childNodes;
+
+            for ($i = 0; $i < $children->length; $i++) {
+                $child = $children->item($i);
+                $text .= $doc->saveXML($child);
+            }
+            return trim($text);
+        } else if (in_array($type, array('text/xml', 'application/xml')) ||
+                   preg_match('#(+|/)xml$#', $type)) {
+            throw new ClientException(_("Can't handle embedded XML content yet."));
+        } else if (strncasecmp($type, 'text/', 5)) {
+            return $el->textContent;
+        } else {
+            throw new ClientException(_("Can't handle embedded Base64 content yet."));
+        }
+    }
+}
diff --git a/lib/activityverb.php b/lib/activityverb.php
new file mode 100644 (file)
index 0000000..76f2b84
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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);
+}
+
+/**
+ * Utility class to hold a bunch of constant defining default verb types
+ *
+ * @category  OStatus
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link      http://status.net/
+ */
+
+class ActivityVerb
+{
+    const POST     = 'http://activitystrea.ms/schema/1.0/post';
+    const SHARE    = 'http://activitystrea.ms/schema/1.0/share';
+    const SAVE     = 'http://activitystrea.ms/schema/1.0/save';
+    const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite';
+    const PLAY     = 'http://activitystrea.ms/schema/1.0/play';
+    const FOLLOW   = 'http://activitystrea.ms/schema/1.0/follow';
+    const FRIEND   = 'http://activitystrea.ms/schema/1.0/make-friend';
+    const JOIN     = 'http://activitystrea.ms/schema/1.0/join';
+    const TAG      = 'http://activitystrea.ms/schema/1.0/tag';
+
+    // Custom OStatus verbs for the flipside until they're standardized
+    const DELETE     = 'http://ostatus.org/schema/1.0/unfollow';
+    const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite';
+    const UNFOLLOW   = 'http://ostatus.org/schema/1.0/unfollow';
+    const LEAVE      = 'http://ostatus.org/schema/1.0/leave';
+
+    // For simple profile-update pings; no content to share.
+    const UPDATE_PROFILE = 'http://ostatus.org/schema/1.0/update-profile';
+}
diff --git a/lib/avatarlink.php b/lib/avatarlink.php
new file mode 100644 (file)
index 0000000..e67799e
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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);
+}
+
+// XXX: Arg! This wouldn't be necessary if we used Avatars conistently
+class AvatarLink
+{
+    public $url;
+    public $type;
+    public $size;
+    public $width;
+    public $height;
+
+    function __construct($element=null)
+    {
+        if ($element) {
+            // @fixme use correct namespaces
+            $this->url = $element->getAttribute('href');
+            $this->type = $element->getAttribute('type');
+            $width = $element->getAttribute('media:width');
+            if ($width != null) {
+                $this->width = intval($width);
+            }
+            $height = $element->getAttribute('media:height');
+            if ($height != null) {
+                $this->height = intval($height);
+            }
+        }
+    }
+
+    static function fromAvatar($avatar)
+    {
+        if (empty($avatar)) {
+            return null;
+        }
+        $alink = new AvatarLink();
+        $alink->type   = $avatar->mediatype;
+        $alink->height = $avatar->height;
+        $alink->width  = $avatar->width;
+        $alink->url    = $avatar->displayUrl();
+        return $alink;
+    }
+
+    static function fromFilename($filename, $size)
+    {
+        $alink = new AvatarLink();
+        $alink->url    = $filename;
+        $alink->height = $size;
+        if (!empty($filename)) {
+            $alink->width  = $size;
+            $alink->type   = self::mediatype($filename);
+        } else {
+            $alink->url    = User_group::defaultLogo($size);
+            $alink->type   = 'image/png';
+        }
+        return $alink;
+    }
+
+    // yuck!
+    static function mediatype($filename) {
+        $ext = strtolower(end(explode('.', $filename)));
+        if ($ext == 'jpeg') {
+            $ext = 'jpg';
+        }
+        // hope we don't support any others
+        $types = array('png', 'gif', 'jpg', 'jpeg');
+        if (in_array($ext, $types)) {
+            return 'image/' . $ext;
+        }
+        return null;
+    }
+}
index 5d53270e30b85ac97682d1e0a5438ca43891ecf9..334a88ffd560f87ca6d2978e07ef06209068f8d9 100644 (file)
@@ -123,7 +123,6 @@ require_once INSTALLDIR.'/lib/util.php';
 require_once INSTALLDIR.'/lib/action.php';
 require_once INSTALLDIR.'/lib/mail.php';
 require_once INSTALLDIR.'/lib/subs.php';
-require_once INSTALLDIR.'/lib/activity.php';
 
 require_once INSTALLDIR.'/lib/clientexception.php';
 require_once INSTALLDIR.'/lib/serverexception.php';
diff --git a/lib/poco.php b/lib/poco.php
new file mode 100644 (file)
index 0000000..2157062
--- /dev/null
@@ -0,0 +1,240 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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 PoCo
+{
+    const NS = 'http://portablecontacts.net/spec/1.0';
+
+    const USERNAME     = 'preferredUsername';
+    const DISPLAYNAME  = 'displayName';
+    const NOTE         = 'note';
+
+    public $preferredUsername;
+    public $displayName;
+    public $note;
+    public $address;
+    public $urls = array();
+
+    function __construct($element = null)
+    {
+        if (empty($element)) {
+            return;
+        }
+
+        $this->preferredUsername = ActivityUtils::childContent(
+            $element,
+            self::USERNAME,
+            self::NS
+        );
+
+        $this->displayName = ActivityUtils::childContent(
+            $element,
+            self::DISPLAYNAME,
+            self::NS
+        );
+
+        $this->note = ActivityUtils::childContent(
+            $element,
+            self::NOTE,
+            self::NS
+        );
+
+        $this->address = $this->_getAddress($element);
+        $this->urls = $this->_getURLs($element);
+    }
+
+    private function _getURLs($element)
+    {
+        $urlEls = $element->getElementsByTagnameNS(self::NS, PoCoURL::URLS);
+        $urls = array();
+
+        foreach ($urlEls as $urlEl) {
+
+            $type = ActivityUtils::childContent(
+                $urlEl,
+                PoCoURL::TYPE,
+                PoCo::NS
+            );
+
+            $value = ActivityUtils::childContent(
+                $urlEl,
+                PoCoURL::VALUE,
+                PoCo::NS
+            );
+
+            $primary = ActivityUtils::childContent(
+                $urlEl,
+                PoCoURL::PRIMARY,
+                PoCo::NS
+            );
+
+            $isPrimary = false;
+
+            if (isset($primary) && $primary == 'true') {
+                $isPrimary = true;
+            }
+
+            // @todo check to make sure a primary hasn't already been added
+
+            array_push($urls, new PoCoURL($type, $value, $isPrimary));
+        }
+        return $urls;
+    }
+
+    private function _getAddress($element)
+    {
+        $addressEl = ActivityUtils::child(
+            $element,
+            PoCoAddress::ADDRESS,
+            PoCo::NS
+        );
+
+        if (!empty($addressEl)) {
+            $formatted = ActivityUtils::childContent(
+                $addressEl,
+                PoCoAddress::FORMATTED,
+                self::NS
+            );
+
+            if (!empty($formatted)) {
+                $address = new PoCoAddress();
+                $address->formatted = $formatted;
+                return $address;
+            }
+        }
+
+        return null;
+    }
+
+    function fromProfile($profile)
+    {
+        if (empty($profile)) {
+            return null;
+        }
+
+        $poco = new PoCo();
+
+        $poco->preferredUsername = $profile->nickname;
+        $poco->displayName       = $profile->getBestName();
+
+        $poco->note = $profile->bio;
+
+        $paddy = new PoCoAddress();
+        $paddy->formatted = $profile->location;
+        $poco->address = $paddy;
+
+        if (!empty($profile->homepage)) {
+            array_push(
+                $poco->urls,
+                new PoCoURL(
+                    'homepage',
+                    $profile->homepage,
+                    true
+                )
+            );
+        }
+
+        return $poco;
+    }
+
+    function fromGroup($group)
+    {
+        if (empty($group)) {
+            return null;
+        }
+
+        $poco = new PoCo();
+
+        $poco->preferredUsername = $group->nickname;
+        $poco->displayName       = $group->getBestName();
+
+        $poco->note = $group->description;
+
+        $paddy = new PoCoAddress();
+        $paddy->formatted = $group->location;
+        $poco->address = $paddy;
+
+        if (!empty($group->homepage)) {
+            array_push(
+                $poco->urls,
+                new PoCoURL(
+                    'homepage',
+                    $group->homepage,
+                    true
+                )
+            );
+        }
+
+        return $poco;
+    }
+
+    function getPrimaryURL()
+    {
+        foreach ($this->urls as $url) {
+            if ($url->primary) {
+                return $url;
+            }
+        }
+    }
+
+    function asString()
+    {
+        $xs = new XMLStringer(true);
+        $xs->element(
+            'poco:preferredUsername',
+            null,
+            $this->preferredUsername
+        );
+
+        $xs->element(
+            'poco:displayName',
+            null,
+            $this->displayName
+        );
+
+        if (!empty($this->note)) {
+            $xs->element('poco:note', null, common_xml_safe_str($this->note));
+        }
+
+        if (!empty($this->address)) {
+            $xs->raw($this->address->asString());
+        }
+
+        foreach ($this->urls as $url) {
+            $xs->raw($url->asString());
+        }
+
+        return $xs->getString();
+    }
+}
diff --git a/lib/pocoaddress.php b/lib/pocoaddress.php
new file mode 100644 (file)
index 0000000..60873bd
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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 PoCoAddress
+{
+    const ADDRESS   = 'address';
+    const FORMATTED = 'formatted';
+
+    public $formatted;
+
+    // @todo Other address fields
+
+    function asString()
+    {
+        if (!empty($this->formatted)) {
+            $xs = new XMLStringer(true);
+            $xs->elementStart('poco:address');
+            $xs->element('poco:formatted', null, common_xml_safe_str($this->formatted));
+            $xs->elementEnd('poco:address');
+            return $xs->getString();
+        }
+
+        return null;
+    }
+}
diff --git a/lib/pocourl.php b/lib/pocourl.php
new file mode 100644 (file)
index 0000000..803484d
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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 PoCoURL
+{
+    const URLS      = 'urls';
+    const TYPE      = 'type';
+    const VALUE     = 'value';
+    const PRIMARY   = 'primary';
+
+    public $type;
+    public $value;
+    public $primary;
+
+    function __construct($type, $value, $primary = false)
+    {
+        $this->type    = $type;
+        $this->value   = $value;
+        $this->primary = $primary;
+    }
+
+    function asString()
+    {
+        $xs = new XMLStringer(true);
+        $xs->elementStart('poco:urls');
+        $xs->element('poco:type', null, $this->type);
+        $xs->element('poco:value', null, $this->value);
+        if (!empty($this->primary)) {
+            $xs->element('poco:primary', null, 'true');
+        }
+        $xs->elementEnd('poco:urls');
+        return $xs->getString();
+    }
+}
index 562ab3bde51f13927e44d1f9be9c72ee60049917..d2e046a602579ab18534d446198ceb746fa0c53f 100644 (file)
@@ -388,11 +388,17 @@ class Ostatus_profile extends Memcached_DataObject
     {
         $feed = $doc->documentElement;
 
-        if ($feed->localName != 'feed' || $feed->namespaceURI != Activity::ATOM) {
-            common_log(LOG_ERR, __METHOD__ . ": not an Atom feed, ignoring");
-            return;
+        if ($feed->localName == 'feed' && $feed->namespaceURI == Activity::ATOM) {
+            $this->processAtomFeed($feed, $source);
+        } else if ($feed->localName == 'rss') { // @fixme check namespace
+            $this->processRssFeed($feed, $source);
+        } else {
+            throw new Exception("Unknown feed format.");
         }
+    }
 
+    public function processAtomFeed(DOMElement $feed, $source)
+    {
         $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
         if ($entries->length == 0) {
             common_log(LOG_ERR, __METHOD__ . ": no entries in feed update, ignoring");
@@ -405,6 +411,26 @@ class Ostatus_profile extends Memcached_DataObject
         }
     }
 
+    public function processRssFeed(DOMElement $rss, $source)
+    {
+        $channels = $rss->getElementsByTagName('channel');
+
+        if ($channels->length == 0) {
+            throw new Exception("RSS feed without a channel.");
+        } else if ($channels->length > 1) {
+            common_log(LOG_WARNING, __METHOD__ . ": more than one channel in an RSS feed");
+        }
+
+        $channel = $channels->item(0);
+
+        $items = $channel->getElementsByTagName('item');
+
+        for ($i = 0; $i < $items->length; $i++) {
+            $item = $items->item($i);
+            $this->processEntry($item, $channel, $source);
+        }
+    }
+
     /**
      * Process a posted entry from this feed source.
      *
@@ -442,24 +468,27 @@ class Ostatus_profile extends Memcached_DataObject
                 return false;
             }
         } else {
-            // Individual user feeds may contain only posts from themselves.
-            // Authorship is validated against the profile URI on upper layers,
-            // through PuSH setup or Salmon signature checks.
-            $actorUri = self::getActorProfileURI($activity);
-            if ($actorUri == $this->uri) {
-                // Check if profile info has changed and update it
-                $this->updateFromActivityObject($activity->actor);
+            $actor = $activity->actor;
+
+            if (empty($actor)) {
+                // OK here! assume the default
+            } else if ($actor->id == $this->uri || $actor->link == $this->uri) {
+                $this->updateFromActivityObject($actor);
             } else {
-                common_log(LOG_WARNING, "OStatus: skipping post with bad author: got $actorUri expected $this->uri");
-                return false;
+                throw new Exception("Got an actor '{$actor->title}' ({$actor->id}) on single-user feed for {$this->uri}");
             }
+
             $oprofile = $this;
         }
 
+        // It's not always an ActivityObject::NOTE, but... let's just say it is.
+
+        $note = $activity->object;
+
         // 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;
         // tag: URIs for instance are found in Google Buzz feeds.
-        $sourceUri = $activity->object->id;
+        $sourceUri = $note->id;
         $dupe = Notice::staticGet('uri', $sourceUri);
         if ($dupe) {
             common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri");
@@ -468,16 +497,30 @@ class Ostatus_profile extends Memcached_DataObject
 
         // We'll also want to save a web link to the original notice, if provided.
         $sourceUrl = null;
-        if ($activity->object->link) {
-            $sourceUrl = $activity->object->link;
+        if ($note->link) {
+            $sourceUrl = $note->link;
         } else if ($activity->link) {
             $sourceUrl = $activity->link;
-        } else if (preg_match('!^https?://!', $activity->object->id)) {
-            $sourceUrl = $activity->object->id;
+        } else if (preg_match('!^https?://!', $note->id)) {
+            $sourceUrl = $note->id;
+        }
+
+        // Use summary as fallback for content
+
+        if (!empty($note->content)) {
+            $sourceContent = $note->content;
+        } else if (!empty($note->summary)) {
+            $sourceContent = $note->summary;
+        } else if (!empty($note->title)) {
+            $sourceContent = $note->title;
+        } else {
+            // @fixme fetch from $sourceUrl?
+            throw new ClientException("No content for notice {$sourceUri}");
         }
 
         // Get (safe!) HTML and text versions of the content
-        $rendered = $this->purify($activity->object->content);
+
+        $rendered = $this->purify($sourceContent);
         $content = html_entity_decode(strip_tags($rendered));
 
         $shortened = common_shorten_links($content);
@@ -488,8 +531,8 @@ class Ostatus_profile extends Memcached_DataObject
         $attachment = null;
 
         if (Notice::contentTooLong($shortened)) {
-            $attachment = $this->saveHTMLFile($activity->object->title, $rendered);
-            $summary = $activity->object->summary;
+            $attachment = $this->saveHTMLFile($note->title, $rendered);
+            $summary = html_entity_decode(strip_tags($note->summary));
             if (empty($summary)) {
                 $summary = $content;
             }
@@ -795,9 +838,20 @@ class Ostatus_profile extends Memcached_DataObject
             throw new FeedSubNoHubException();
         }
 
-        // Try to get a profile from the feed activity:subject
+        $feedEl = $discover->root;
+
+        if ($feedEl->tagName == 'feed') {
+            return self::ensureAtomFeed($feedEl, $hints);
+        } else if ($feedEl->tagName == 'channel') {
+            return self::ensureRssChannel($feedEl, $hints);
+        } else {
+            throw new FeedSubBadXmlException($feeduri);
+        }
+    }
 
-        $feedEl = $discover->feed->documentElement;
+    public static function ensureAtomFeed($feedEl, $hints)
+    {
+        // Try to get a profile from the feed activity:subject
 
         $subject = ActivityUtils::child($feedEl, Activity::SUBJECT, Activity::SPEC);
 
@@ -818,7 +872,7 @@ class Ostatus_profile extends Memcached_DataObject
         // Sheesh. Not a very nice feed! Let's try fingerpoken in the
         // entries.
 
-        $entries = $discover->feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
+        $entries = $feedEl->getElementsByTagNameNS(Activity::ATOM, 'entry');
 
         if (!empty($entries) && $entries->length > 0) {
 
@@ -845,6 +899,17 @@ class Ostatus_profile extends Memcached_DataObject
         throw new FeedSubException("Can't find enough profile information to make a feed.");
     }
 
+    public static function ensureRssChannel($feedEl, $hints)
+    {
+        // @fixme we should check whether this feed has elements
+        // with different <author> or <dc:creator> elements, and... I dunno.
+        // Do something about that.
+
+        $obj = ActivityObject::fromRssChannel($feedEl);
+
+        return self::ensureActivityObjectProfile($obj, $hints);
+    }
+
     /**
      * Download and update given avatar image
      *
@@ -1307,9 +1372,19 @@ class Ostatus_profile extends Memcached_DataObject
             return $hints['nickname'];
         }
 
-        // Try the definitive ID
+        // Try the profile url (like foo.example.com or example.com/user/foo)
+
+        $profileUrl = ($object->link) ? $object->link : $hints['profileurl'];
+
+        if (!empty($profileUrl)) {
+            $nickname = self::nicknameFromURI($profileUrl);
+        }
+
+        // Try the URI (may be a tag:, http:, acct:, ...
 
-        $nickname = self::nicknameFromURI($object->id);
+        if (empty($nickname)) {
+            $nickname = self::nicknameFromURI($object->id);
+        }
 
         // Try a Webfinger if one was passed (way) down