code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * class to import activities as part of a user's timeline
 *
 * PHP version 5
 *
 * 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  Cache
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Class comment
 *
 * @category  General
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
class ActivityImporter extends QueueHandler
{
    private $trusted = false;

    /**
     * Function comment
     *
     * @param
     *
     * @return
     */
    function handle($data) : bool
    {
        list($user, $author, $activity, $trusted) = $data;

        $this->trusted = $trusted;

        $done = null;

        try {
            if (Event::handle('StartImportActivity',
                              array($user, $author, $activity, $trusted, &$done))) {
                switch ($activity->verb) {
                case ActivityVerb::FOLLOW:
                    $this->subscribeProfile($user, $author, $activity);
                    break;
                case ActivityVerb::JOIN:
                    $this->joinGroup($user, $activity);
                    break;
                case ActivityVerb::POST:
                    $this->postNote($user, $author, $activity);
                    break;
                default:
                    // TRANS: Client exception thrown when using an unknown verb for the activity importer.
                    throw new ClientException(sprintf(_("Unknown verb: \"%s\"."),$activity->verb));
                }
                Event::handle('EndImportActivity',
                              array($user, $author, $activity, $trusted));
                $done = true;
            }
        } catch (Exception $e) {
            common_log(LOG_ERR, $e->getMessage());
            $done = true;
        }
        return $done;
    }

    function subscribeProfile($user, $author, $activity)
    {
        $profile = $user->getProfile();

        if ($activity->objects[0]->id == $author->id) {
            if (!$this->trusted) {
                // TRANS: Client exception thrown when trying to force a subscription for an untrusted user.
                throw new ClientException(_('Cannot force subscription for untrusted user.'));
            }

            $other = $activity->actor;
            $otherUser = User::getKV('uri', $other->id);

            if (!$otherUser instanceof User) {
                // TRANS: Client exception thrown when trying to force a remote user to subscribe.
                throw new Exception(_('Cannot force remote user to subscribe.'));
            }

            $otherProfile = $otherUser->getProfile();

            // XXX: don't do this for untrusted input!

            Subscription::ensureStart($otherProfile, $profile);
        } else if (empty($activity->actor)
                   || $activity->actor->id == $author->id) {

            $other = $activity->objects[0];

            try {
                $otherProfile = Profile::fromUri($other->id);
                // TRANS: Client exception thrown when trying to subscribe to an unknown profile.
            } catch (UnknownUriException $e) {
                // Let's convert it to a client exception instead of server.
                throw new ClientException(_('Unknown profile.'));
            }

            Subscription::ensureStart($profile, $otherProfile);
        } else {
            // TRANS: Client exception thrown when trying to import an event not related to the importing user.
            throw new Exception(_('This activity seems unrelated to our user.'));
        }
    }

    function joinGroup($user, $activity)
    {
        // XXX: check that actor == subject

        $uri = $activity->objects[0]->id;

        $group = User_group::getKV('uri', $uri);

        if (!$group instanceof User_group) {
            $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
            if (!$oprofile->isGroup()) {
                // TRANS: Client exception thrown when trying to join a remote group that is not a group.
                throw new ClientException(_('Remote profile is not a group!'));
            }
            $group = $oprofile->localGroup();
        }

        assert(!empty($group));

        if ($user->isMember($group)) {
            // TRANS: Client exception thrown when trying to join a group the importing user is already a member of.
            throw new ClientException(_("User is already a member of this group."));
        }

        $user->joinGroup($group);
    }

    // XXX: largely cadged from Ostatus_profile::processNote()

    function postNote($user, $author, $activity)
    {
        $note = $activity->objects[0];

        $sourceUri = $note->id;

        $notice = Notice::getKV('uri', $sourceUri);

        if ($notice instanceof Notice) {

            common_log(LOG_INFO, "Notice {$sourceUri} already exists.");

            if ($this->trusted) {

                $profile = $notice->getProfile();

                $uri = $profile->getUri();

                if ($uri === $author->id) {
                    common_log(LOG_INFO, sprintf('Updating notice author from %s to %s', $author->id, $user->getUri()));
                    $orig = clone($notice);
                    $notice->profile_id = $user->id;
                    $notice->update($orig);
                    return;
                } else {
                    // TRANS: Client exception thrown when trying to import a notice by another user.
                    // TRANS: %1$s is the source URI of the notice, %2$s is the URI of the author.
                    throw new ClientException(sprintf(_('Already know about notice %1$s and '.
                                                        ' it has a different author %2$s.'),
                                                      $sourceUri, $uri));
                }
            } else {
                // TRANS: Client exception thrown when trying to overwrite the author information for a non-trusted user during import.
                throw new ClientException(_('Not overwriting author info for non-trusted user.'));
            }
        }

        // 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?
            // TRANS: Client exception thrown when trying to import a notice without content.
            // TRANS: %s is the notice URI.
            throw new ClientException(sprintf(_('No content for notice %s.'),$sourceUri));
        }

        // Get (safe!) HTML and text versions of the content

        $rendered = common_purify($sourceContent);
        $content = common_strip_html($rendered);

        $shortened = $user->shortenLinks($content);

        $options = array('is_local' => Notice::LOCAL_PUBLIC,
                         'uri' => $sourceUri,
                         'rendered' => $rendered,
                         'replies' => array(),
                         'groups' => array(),
                         'tags' => array(),
                         'urls' => array(),
                         'distribute' => false);

        // Check for optional attributes...

        if (!empty($activity->time)) {
            $options['created'] = common_sql_date($activity->time);
        }

        if ($activity->context) {
            // Any individual or group attn: targets?

            list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);

            // Maintain direct reply associations
            // @fixme what about conversation ID?
            if (!empty($activity->context->replyToID)) {
                $orig = Notice::getKV('uri', $activity->context->replyToID);
                if ($orig instanceof Notice) {
                    $options['reply_to'] = $orig->id;
                }
            }

            $location = $activity->context->location;

            if ($location) {
                $options['lat'] = $location->lat;
                $options['lon'] = $location->lon;
                if ($location->location_id) {
                    $options['location_ns'] = $location->location_ns;
                    $options['location_id'] = $location->location_id;
                }
            }
        }

        // Atom categories <-> hashtags

        foreach ($activity->categories as $cat) {
            if ($cat->term) {
                $term = common_canonical_tag($cat->term);
                if ($term) {
                    $options['tags'][] = $term;
                }
            }
        }

        // Atom enclosures -> attachment URLs
        foreach ($activity->enclosures as $href) {
            // @fixme save these locally or....?
            $options['urls'][] = $href;
        }

        common_log(LOG_INFO, "Saving notice {$options['uri']}");

        $saved = Notice::saveNew($user->id,
                                 $content,
                                 'restore', // TODO: restore the actual source
                                 $options);

        return $saved;
    }

    protected function filterAttention(array $attn)
    {
        $groups = array();  // TODO: context->attention
        $replies = array(); // TODO: context->attention

        foreach ($attn as $recipient=>$type) {

            // Is the recipient a local user?

            $user = User::getKV('uri', $recipient);

            if ($user instanceof User) {
                // TODO: @fixme sender verification, spam etc?
                $replies[] = $recipient;
                continue;
            }

            // Is the recipient a remote group?
            $oprofile = Ostatus_profile::ensureProfileURI($recipient);

            if ($oprofile) {
                if (!$oprofile->isGroup()) {
                    // may be canonicalized or something
                    $replies[] = $oprofile->uri;
                }
                continue;
            }

            // Is the recipient a local group?
            // TODO: @fixme uri on user_group isn't reliable yet
            // $group = User_group::getKV('uri', $recipient);
            $id = OStatusPlugin::localGroupFromUrl($recipient);

            if ($id) {
                $group = User_group::getKV('id', $id);
                if ($group) {
                    // Deliver to all members of this local group if allowed.
                    $profile = $sender->localProfile();
                    if ($profile->isMember($group)) {
                        $groups[] = $group->id;
                    } else {
                        common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
                    }
                    continue;
                } else {
                    common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
                }
            }
        }

        return array($groups, $replies);
    }
}


code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * Title of module
 *
 * PHP version 5
 *
 * 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  Cache
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    // This check helps protect against security problems;
    // your code file can't be executed directly from the web.
    exit(1);
}

/**
 * Class comment
 *
 * @category  General
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
class ActivityMover extends QueueHandler
{
    function transport()
    {
        return 'actmove';
    }

    function handle($data) : bool
    {
        list ($act, $sink, $userURI, $remoteURI) = $data;

        $user   = User::getKV('uri', $userURI);
        try {
            $remote = Profile::fromUri($remoteURI);
        } catch (UnknownUriException $e) {
            // Don't retry. It's hard to tell whether it's because of
            // lookup failures or because the URI is permanently gone.
            // If we knew it was temporary, we'd return false here.
            return true;
        }

        try {
            $this->moveActivity($act, $sink, $user, $remote);
        } catch (ClientException $cex) {
            $this->log(LOG_WARNING,
                       $cex->getMessage());
            // "don't retry me"
            return true;
        } catch (ServerException $sex) {
            $this->log(LOG_WARNING,
                       $sex->getMessage());
            // "retry me" (because we think the server might handle it next time)
            return false;
        } catch (Exception $ex) {
            $this->log(LOG_WARNING,
                       $ex->getMessage());
            // "don't retry me"
            return true;
        }
    }

    function moveActivity($act, $sink, $user, $remote)
    {
        if (empty($user)) {
            // TRANS: Exception thrown if a non-existing user is provided. %s is a user ID.
            throw new Exception(sprintf(_('No such user "%s".'),$act->actor->id));
        }

        switch ($act->verb) {
/*        case ActivityVerb::FAVORITE:
            $this->log(LOG_INFO,
                       "Moving favorite of {$act->objects[0]->id} by ".
                       "{$act->actor->id} to {$remote->nickname}.");
            // push it, then delete local
            $sink->postActivity($act);
            $notice = Notice::getKV('uri', $act->objects[0]->id);
            if (!empty($notice)) {
                $fave = Fave::pkeyGet(array('user_id' => $user->id,
                                            'notice_id' => $notice->id));
                $fave->delete();
            }
            break;*/
        case ActivityVerb::POST:
            $this->log(LOG_INFO,
                       "Moving notice {$act->objects[0]->id} by ".
                       "{$act->actor->id} to {$remote->nickname}.");
            // XXX: send a reshare, not a post
            $sink->postActivity($act);
            $notice = Notice::getKV('uri', $act->objects[0]->id);
            if (!empty($notice)) {
                $notice->deleteAs($user->getProfile(), false);
            }
            break;
        case ActivityVerb::JOIN:
            $this->log(LOG_INFO,
                       "Moving group join of {$act->objects[0]->id} by ".
                       "{$act->actor->id} to {$remote->nickname}.");
            $sink->postActivity($act);
            $group = User_group::getKV('uri', $act->objects[0]->id);
            if (!empty($group)) {
                $user->leaveGroup($group);
            }
            break;
        case ActivityVerb::FOLLOW:
            if ($act->actor->id === $user->getUri()) {
                $this->log(LOG_INFO,
                           "Moving subscription to {$act->objects[0]->id} by ".
                           "{$act->actor->id} to {$remote->nickname}.");
                $sink->postActivity($act);
                try {
                    $other = Profile::fromUri($act->objects[0]->id);
                    Subscription::cancel($user->getProfile(), $other);
                } catch (UnknownUriException $e) {
                    // Can't cancel subscription if we don't know who to alert
                }
            } else {
                $otherUser = User::getKV('uri', $act->actor->id);
                if (!empty($otherUser)) {
                    $this->log(LOG_INFO,
                               "Changing sub to {$act->objects[0]->id}".
                               "by {$act->actor->id} to {$remote->nickname}.");
                    $otherProfile = $otherUser->getProfile();
                    Subscription::ensureStart($otherProfile, $remote);
                    Subscription::cancel($otherProfile, $user->getProfile());
                } else {
                    $this->log(LOG_NOTICE,
                               "Not changing sub to {$act->objects[0]->id}".
                               "by remote {$act->actor->id} ".
                               "to {$remote->nickname}.");
                }
            }
            break;
        }
    }

    /**
     * Log some data
     *
     * Add a header for our class so we know who did it.
     *
     * @param int    $level   Log level, like LOG_ERR or LOG_INFO
     * @param string $message Message to log
     *
     * @return void
     */
    protected function log($level, $message)
    {
        common_log($level, "ActivityMover: " . $message);
    }
}

repo_name: suraj/sloth

license: mit


code:
<?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('GNUSOCIAL')) {
    exit(1);
}

require_once INSTALLDIR . '/lib/activitystreams/activitystreamjsondocument.php';

/**
 * 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 _LIST       = 'http://activitystrea.ms/schema/1.0/list'; // LIST is reserved
    const PLACE       = 'http://activitystrea.ms/schema/1.0/place';
    const COMMENT     = 'http://activitystrea.ms/schema/1.0/comment';
    // ^^^^^^^^^^ tea!
    const ACTIVITY    = 'http://activitystrea.ms/schema/1.0/activity';
    const SERVICE     = 'http://activitystrea.ms/schema/1.0/service';
    const IMAGE       = 'http://activitystrea.ms/schema/1.0/image';
    const COLLECTION  = 'http://activitystrea.ms/schema/1.0/collection';
    const APPLICATION = 'http://activitystrea.ms/schema/1.0/application';

    // 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';

    const POSTEROUS = 'http://posterous.com/help/rss/1.0';
    const AUTHOR = 'author';
    const USERIMAGE = 'userImage';
    const PROFILEURL = 'profileUrl';
    const NICKNAME = 'nickName';
    const DISPLAYNAME = 'displayName';
    const MEDIA_DESCRIPTION = 'description';
    public $element;
    public $type;
    public $id;
    public $title;
    public $summary;
    public $content;
    public $owner;
    public $link;   // think APP (Atom Publishing Protocol)
    public $selfLink;
    public $source;
    public $avatarLinks = [];
    public $geopoint;
    public $poco;

    // @todo move this stuff to it's own PHOTO activity object
    public $displayName;
    public $thumbnail;
    public $largerImage;
    public $description;
    public $extra = [];

    public $stream;

    /**
     * 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
     */
    public 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);
        } elseif ($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);
        }

        if ($this->type == self::PHOTO) {
            $this->thumbnail = ActivityUtils::getLink($element, 'preview');
            $this->largerImage = ActivityUtils::getLink($element, 'enclosure');

            $this->description = ActivityUtils::childContent(
                $element,
                ActivityObject::MEDIA_DESCRIPTION,
                Activity::MEDIA
            );
        }
        if ($this->type == self::_LIST) {
            $owner = ActivityUtils::child($this->element, Activity::AUTHOR, Activity::SPEC);
            $this->owner = new ActivityObject($owner);
        }
    }

    private function _childContent($element, $tag, $namespace = ActivityUtils::ATOM)
    {
        return ActivityUtils::childContent($element, $tag, $namespace);
    }

    private function _fromAuthor($element)
    {
        $this->type = $this->_childContent(
            $element,
            Activity::OBJECTTYPE,
            Activity::SPEC
        );

        if (empty($this->type)) {
            $this->type = self::PERSON; // XXX: is this fair?
        }

        // Start with <poco::displayName>

        $this->title = ActivityUtils::childContent($element, PoCo::DISPLAYNAME, PoCo::NS);

        // try falling back to <atom:title>

        if (empty($this->title)) {
            $title = ActivityUtils::childHtmlContent($element, self::TITLE);

            if (!empty($title)) {
                $this->title = common_strip_html($title);
            }
        }

        // fall back to <atom:name> as a last resort

        if (empty($this->title)) {
            $this->title = $this->_childContent($element, self::NAME);
        }

        // start with <atom:id>

        $this->id = $this->_childContent($element, self::ID);

        // fall back to <atom:uri>

        if (empty($this->id)) {
            $this->id = $this->_childContent($element, self::URI);
        }

        // fall further back to <atom:email>

        if (empty($this->id)) {
            $email = $this->_childContent($element, self::EMAIL);
            if (!empty($email)) {
                // XXX: acct: ?
                $this->id = 'mailto:' . $email;
            }
        }

        $this->link = ActivityUtils::getPermalink($element);

        // fall finally back to <link rel=alternate>

        if (empty($this->id) && !empty($this->link)) { // fallback if there's no ID
            $this->id = $this->link;
        }
    }

    // @todo FIXME: rationalize with Activity::_fromRssItem()

    private function _fromRssItem($item)
    {
        if (empty($this->type)) {
            $this->type = ActivityObject::NOTE;
        }

        $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') && $guidEl->getAttribute('isPermaLink') != 'false') {
                // overwrites <link>
                $this->link = $this->id;
            }
        }
    }

    private function _fromAtomEntry($element)
    {
        $this->type = $this->_childContent(
            $element,
            Activity::OBJECTTYPE,
            Activity::SPEC
        );

        if (empty($this->type)) {
            $this->type = ActivityObject::NOTE;
        }

        $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
        $this->title = common_strip_html(ActivityUtils::childHtmlContent($element, self::TITLE));

        $this->source = $this->_getSource($element);

        $this->link = ActivityUtils::getPermalink($element);
        $this->selfLink = ActivityUtils::getSelfLink($element);

        $this->id = $this->_childContent($element, self::ID);

        if (empty($this->id) && !empty($this->link)) { // fallback if there's no ID
            $this->id = $this->link;
        }

        $els = $element->childNodes;

        for ($i = 0; $i < $els->length; $i++) {
            $link = $els->item($i);
            if ($link->localName == ActivityUtils::LINK && $link->namespaceURI == ActivityUtils::ATOM) {
                $attrs = [];
                foreach ($link->attributes as $attrName => $attrNode) {
                    $attrs[$attrName] = $attrNode->nodeValue;
                }
                $this->extra[] = [$link->localName,
                    $attrs,
                    $link->nodeValue];
            }
        }
    }

    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');
            }
        }
    }

    public static function fromRssAuthor($el)
    {
        $text = $el->textContent;

        if (preg_match('/^(.*?) \((.*)\)$/', $text, $match)) {
            $email = $match[1];
            $name = $match[2];
        } elseif (preg_match('/^(.*?) <(.*)>$/', $text, $match)) {
            $name = $match[1];
            $email = $match[2];
        } elseif (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)) {
            $url = ActivityUtils::childContent($imageEl, Activity::URL, Activity::RSS);
            $al = new AvatarLink();
            $al->url = $url;
            $obj->avatarLinks[] = $al;
        }

        return $obj;
    }

    // Try to get a unique id for the source feed

    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;
    }

    public static function fromGroup(User_group $group)
    {
        $object = new ActivityObject();

        if (Event::handle('StartActivityObjectFromGroup', [$group, &$object])) {
            $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);
            Event::handle('EndActivityObjectFromGroup', [$group, &$object]);
        }

        return $object;
    }

    public static function fromPeopletag($ptag)
    {
        $object = new ActivityObject();
        if (Event::handle('StartActivityObjectFromPeopletag', [$ptag, &$object])) {
            $object->type = ActivityObject::_LIST;

            $object->id = $ptag->getUri();
            $object->title = $ptag->tag;
            $object->summary = $ptag->description;
            $object->link = $ptag->homeUrl();
            $object->owner = Profile::getKV('id', $ptag->tagger);
            $object->poco = PoCo::fromProfile($object->owner);
            Event::handle('EndActivityObjectFromPeopletag', [$ptag, &$object]);
        }
        return $object;
    }

    public static function fromFile(File $file)
    {
        $object = new ActivityObject();

        if (Event::handle('StartActivityObjectFromFile', [$file, &$object])) {
            $object->type = self::mimeTypeToObjectType($file->mimetype);
            $object->id = TagURI::mint(sprintf("file:%d", $file->id));
            $object->link = $file->getAttachmentUrl();

            if ($file->title) {
                $object->title = $file->title;
            }

            if ($file->date) {
                $object->date = $file->date;
            }

            try {
                $thumbnail = $file->getThumbnail();
                $object->thumbnail = $thumbnail;
            } catch (UseFileAsThumbnailException $e) {
                $object->thumbnail = null;
            } catch (UnsupportedMediaException $e) {
                $object->thumbnail = null;
            }

            switch (self::canonicalType($object->type)) {
                case 'image':
                    $object->largerImage = $file->getUrl();
                    break;
                case 'video':
                case 'audio':
                    $object->stream = $file->getUrl();
                    break;
            }

            Event::handle('EndActivityObjectFromFile', [$file, &$object]);
        }

        return $object;
    }

    public static function mimeTypeToObjectType($mimeType)
    {
        $ot = null;

        // Default

        if (empty($mimeType)) {
            return self::FILE;
        }

        $parts = explode('/', $mimeType);

        switch ($parts[0]) {
            case 'image':
                $ot = self::IMAGE;
                break;
            case 'audio':
                $ot = self::AUDIO;
                break;
            case 'video':
                $ot = self::VIDEO;
                break;
            default:
                $ot = self::FILE;
        }

        return $ot;
    }

    public static function canonicalType($type)
    {
        return ActivityUtils::resolveUri($type, true);
    }

    public static function fromNoticeSource(Notice_source $source)
    {
        $object = new ActivityObject();
        $wellKnown = ['web', 'xmpp', 'mail', 'omb', 'system', 'api', 'ostatus',
            'activity', 'feed', 'mirror', 'twitter', 'facebook'];

        if (Event::handle('StartActivityObjectFromNoticeSource', [$source, &$object])) {
            $object->type = ActivityObject::APPLICATION;

            if (in_array($source->code, $wellKnown)) {
                // We use one ID for all well-known StatusNet sources
                $object->id = "tag:status.net,2009:notice-source:" . $source->code;
            } elseif ($source->url) {
                // They registered with an URL
                $object->id = $source->url;
            } else {
                // Locally-registered, no URL
                $object->id = TagURI::mint("notice-source:" . $source->code);
            }

            if ($source->url) {
                $object->link = $source->url;
            }

            if ($source->name) {
                $object->title = $source->name;
            } else {
                $object->title = $source->code;
            }

            if ($source->created) {
                $object->date = $source->created;
            }

            $object->extra[] = ['status_net', ['source_code' => $source->code]];

            Event::handle('EndActivityObjectFromNoticeSource', [$source, &$object]);
        }

        return $object;
    }

    public static function fromMessage(Message $message)
    {
        $object = new ActivityObject();

        if (Event::handle('StartActivityObjectFromMessage', [$message, &$object])) {
            $object->type = ActivityObject::NOTE;
            $object->id = ($message->uri) ? $message->uri : (($message->url) ? $message->url : TagURI::mint(sprintf("message:%d", $message->id)));
            $object->content = $message->rendered;
            $object->date = $message->created;

            if ($message->url) {
                $object->link = $message->url;
            } else {
                $object->link = common_local_url('showmessage', ['message' => $message->id]);
            }

            $object->extra[] = ['status_net', ['message_id' => $message->id]];

            Event::handle('EndActivityObjectFromMessage', [$message, &$object]);
        }

        return $object;
    }

    /*
     * Returns an array based on this Activity Object suitable for
     * encoding as JSON.
     *
     * @return array $object the activity object array
     */

    public function asString($tag = 'activity:object')
    {
        $xs = new XMLStringer(true);

        $this->outputTo($xs, $tag);

        return $xs->getString();
    }

    public function outputTo($xo, $tag = 'activity:object')
    {
        if (!empty($tag)) {
            $xo->elementStart($tag);
        }

        if (Event::handle('StartActivityObjectOutputAtom', [$this, $xo])) {
            $xo->element('activity:object-type', null, $this->type);

            // <author> uses URI

            if ($tag == 'author') {
                $xo->element(self::URI, null, $this->id);
            } else {
                $xo->element(self::ID, null, $this->id);
            }

            if (!empty($this->title)) {
                $name = common_xml_safe_str($this->title);
                if ($tag == 'author') {
                    // XXX: Backward compatibility hack -- atom:name should contain
                    // full name here, instead of nickname, i.e.: $name. Change
                    // this in the next version.
                    $xo->element(self::NAME, null, $this->poco->preferredUsername);
                } else {
                    $xo->element(self::TITLE, null, $name);
                }
            }

            if (!empty($this->summary)) {
                $xo->element(
                    self::SUMMARY,
                    null,
                    common_xml_safe_str($this->summary)
                );
            }

            if (!empty($this->content)) {
                // XXX: assuming HTML content here
                $xo->element(
                    ActivityUtils::CONTENT,
                    ['type' => 'html'],
                    common_xml_safe_str($this->content)
                );
            }

            if (!empty($this->link)) {
                $xo->element(
                    'link',
                    [
                        'rel' => 'alternate',
                        'type' => 'text/html',
                        'href' => $this->link
                    ]
                );
            }

            if (!empty($this->selfLink)) {
                $xo->element(
                    'link',
                    [
                        'rel' => 'self',
                        'type' => 'application/atom+xml',
                        'href' => $this->selfLink
                    ]
                );
            }

            if (!empty($this->owner)) {
                $owner = $this->owner->asActivityNoun(self::AUTHOR);
                $xo->raw($owner);
            }

            if ($this->type == ActivityObject::PERSON
                || $this->type == ActivityObject::GROUP) {
                foreach ($this->avatarLinks as $alink) {
                    $xo->element(
                        'link',
                        [
                            'rel' => 'avatar',
                            'type' => $alink->type,
                            'media:width' => $alink->width,
                            'media:height' => $alink->height,
                            'href' => $alink->url,
                        ]
                    );
                }
            }

            if (!empty($this->geopoint)) {
                $xo->element(
                    'georss:point',
                    null,
                    $this->geopoint
                );
            }

            if (!empty($this->poco)) {
                $this->poco->outputTo($xo);
            }

            // @fixme there's no way here to make a tree; elements can only contain plaintext
            // @fixme these may collide with JSON extensions
            foreach ($this->extra as $el) {
                list($extraTag, $attrs, $content) = array_pad($el, 3, null);
                $xo->element($extraTag, $attrs, $content);
            }

            Event::handle('EndActivityObjectOutputAtom', [$this, $xo]);
        }

        if (!empty($tag)) {
            $xo->elementEnd($tag);
        }

        return;
    }

    public function asArray()
    {
        $object = [];

        if (Event::handle('StartActivityObjectOutputJson', [$this, &$object])) {
            // XXX: attachments are added by Activity

            // author (Add object for author? Could be useful for repeats.)

            // content (Add rendered version of the notice?)

            // downstreamDuplicates

            // id

            if ($this->id) {
                $object['id'] = $this->id;
            } elseif ($this->link) {
                $object['id'] = $this->link;
            }

            if ($this->type == ActivityObject::PERSON
                || $this->type == ActivityObject::GROUP) {

                // displayName
                $object['displayName'] = $this->title;

                // XXX: Not sure what the best avatar is to use for the
                // author's "image". For now, I'm using the large size.

                $imgLink = null;
                $avatarMediaLinks = [];

                foreach ($this->avatarLinks as $a) {

                    // Make a MediaLink for every other Avatar
                    $avatar = new ActivityStreamsMediaLink(
                        $a->url,
                        $a->width,
                        $a->height,
                        $a->type,
                        'avatar'
                    );

                    // Find the big avatar to use as the "image"
                    if ($a->height == AVATAR_PROFILE_SIZE) {
                        $imgLink = $avatar;
                    }

                    $avatarMediaLinks[] = $avatar->asArray();
                }

                if (!array_key_exists('status_net', $object)) {
                    $object['status_net'] = [];
                }

                $object['status_net']['avatarLinks'] = $avatarMediaLinks; // extension

                // image
                if (!empty($imgLink)) {
                    $object['image'] = $imgLink->asArray();
                }
            }

            // objectType
            //
            // We can probably use the whole schema URL here but probably the
            // relative simple name is easier to parse

            $object['objectType'] = self::canonicalType($this->type);

            // summary
            $object['summary'] = $this->summary;

            // content, usually rendered HTML
            $object['content'] = $this->content;

            // published (probably don't need. Might be useful for repeats.)

            // updated (probably don't need this)

            // TODO: upstreamDuplicates

            if ($this->link) {
                $object['url'] = $this->link;
            }

            /* Extensions */
            // @fixme these may collide with XML extensions
            // @fixme multiple tags of same name will overwrite each other
            // @fixme text content from XML extensions will be lost

            foreach ($this->extra as $e) {
                list($objectName, $props, $txt) = array_pad($e, 3, null);
                if (!empty($objectName)) {
                    $parts = explode(":", $objectName);
                    if (count($parts) == 2 && $parts[0] == "statusnet") {
                        if (!array_key_exists('status_net', $object)) {
                            $object['status_net'] = [];
                        }
                        $object['status_net'][$parts[1]] = $props;
                    } else {
                        $object[$objectName] = $props;
                    }
                }
            }

            if (!empty($this->geopoint)) {
                list($lat, $lon) = explode(' ', $this->geopoint);

                if (!empty($lat) && !empty($lon)) {
                    $object['location'] = [
                        'objectType' => 'place',
                        'position' => sprintf("%+02.5F%+03.5F/", $lat, $lon),
                        'lat' => $lat,
                        'lon' => $lon
                    ];

                    $loc = Location::fromLatLon((float)$lat, (float)$lon);

                    if ($loc) {
                        $name = $loc->getName();

                        if ($name) {
                            $object['location']['displayName'] = $name;
                        }
                        $url = $loc->getURL();

                        if ($url) {
                            $object['location']['url'] = $url;
                        }
                    }
                }
            }

            if (!empty($this->poco)) {
                $object['portablecontacts_net'] = array_filter($this->poco->asArray());
            }

            if (!empty($this->thumbnail)) {
                if (is_string($this->thumbnail)) {
                    $object['image'] = ['url' => $this->thumbnail];
                } else {
                    $object['image'] = ['url' => $this->thumbnail->getUrl()];
                    if ($this->thumbnail->width) {
                        $object['image']['width'] = $this->thumbnail->width;
                    }
                    if ($this->thumbnail->height) {
                        $object['image']['height'] = $this->thumbnail->height;
                    }
                }
            }

            switch (self::canonicalType($this->type)) {
                case 'image':
                    if (!empty($this->largerImage)) {
                        $object['fullImage'] = ['url' => $this->largerImage];
                    }
                    break;
                case 'audio':
                case 'video':
                    if (!empty($this->stream)) {
                        $object['stream'] = ['url' => $this->stream];
                    }
                    break;
            }

            Event::handle('EndActivityObjectOutputJson', [$this, &$object]);
        }
        return array_filter($object);
    }

    public function getIdentifiers()
    {
        $ids = [];
        foreach (['id', 'link', 'url'] as $id) {
            if (isset($this->$id)) {
                $ids[] = $this->$id;
            }
        }
        return array_unique($ids);
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Class for serializing Activity Streams in JSON
 *
 * 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    Zach Copley <zach@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('GNUSOCIAL')) {
    exit(1);
}

/**
 * A class for generating JSON documents that represent an Activity Streams
 *
 * @category Feed
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */
class ActivityStreamJSONDocument extends JSONActivityCollection
{
    // Note: Lot of AS folks think the content type should be:
    // 'application/stream+json; charset=utf-8', but this is more
    // useful at the moment, because some programs actually understand
    // it.
    const CONTENT_TYPE = 'application/json; charset=utf-8';

    /* Top level array representing the document */
    protected $doc = [];

    /* The current authenticated user */
    protected $cur;
    protected $scoped = null;

    /* Title of the document */
    protected $title;

    /* Links associated with this document */
    protected $links;

    /* Count of items in this document */
    // XXX This is cryptically referred to in the spec: "The Stream serialization MAY contain a count property."
    protected $count;

    /**
     * Constructor
     *
     * @param User $cur the current authenticated user
     * @throws UserNoProfileException
     */

    public function __construct($cur = null, $title = null, array $items = [], $links = null, $url = null)
    {
        parent::__construct($items, $url);

        $this->cur = $cur ?: common_current_user();
        $this->scoped = !is_null($this->cur) ? $this->cur->getProfile() : null;

        /* Title of the JSON document */
        $this->title = $title;

        if (!empty($items)) {
            $this->count = count($this->items);
        }

        /* Array of links associated with the document */
        $this->links = empty($links) ? [] : $items;

        /* URL of a document, this document? containing a list of all the items in the stream */
        if (!empty($url)) {
            $this->url = $url;
        }
    }

    /**
     * Set the title of the document
     *
     * @param string $title the title
     */

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function setUrl($url)
    {
        $this->url = $url;
    }

    /**
     * Add more than one Item to the document
     *
     * @param mixed $notices an array of Notice objects or handle
     * @throws ClientException
     * @throws ServerException
     */

    public function addItemsFromNotices($notices)
    {
        if (is_array($notices)) {
            foreach ($notices as $notice) {
                $this->addItemFromNotice($notice);
            }
        } else {
            while ($notices->fetch()) {
                $this->addItemFromNotice($notices);
            }
        }
    }

    /**
     * Add a single Notice to the document
     *
     * @param Notice $notice a Notice to add
     */

    public function addItemFromNotice($notice)
    {
        try {
            $act = $notice->asActivity($this->scoped);
        } catch (Exception $e) {
            // We know exceptions like
            // "No result found on Fave lookup."
            // may happen because of deleted notices etc.
            // These are irrelevant for the feed purposes.
            return;
        }
        $act->extra[] = $notice->noticeInfo($this->scoped);
        array_push($this->items, $act->asArray());
        $this->count++;
    }

    /**
     * Add a link to the JSON document
     *
     * @param string $url the URL for the link
     * @param string $rel the link relationship
     * @throws Exception
     */
    public function addLink($url = null, $rel = null, $mediaType = null)
    {
        $link = new ActivityStreamsLink($url, $rel, $mediaType);
        array_push($this->links, $link->asArray());
    }

    /*
     * Return the entire document as a big string of JSON
     *
     * @return string encoded JSON output
     */
    public function asString()
    {
        $this->doc['generator'] = 'GNU social ' . GNUSOCIAL_VERSION; // extension
        $this->doc['title'] = $this->title;
        $this->doc['url'] = $this->url;
        $this->doc['totalItems'] = $this->count;
        $this->doc['items'] = $this->items;
        $this->doc['links'] = $this->links; // extension
        return json_encode(array_filter($this->doc)); // filter out empty elements
    }
}


code:
<?php

/**
 * A class for representing links in JSON Activities
 *
 * @category Feed
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */

class ActivityStreamsLink
{
    private $linkDict;

    function __construct($url = null, $rel = null, $mediaType = null)
    {
        // links MUST have a URL
        if (empty($url)) {
            throw new Exception('Links must have a URL.');
        }

        $this->linkDict = array(
            'url'   => $url,
            'rel'   => $rel,      // extension
            'type'  => $mediaType // extension
        );
    }

    function asArray()
    {
        return array_filter($this->linkDict);
    }
}


code:
<?php

/**
 * A class for representing MediaLinks in JSON Activities
 *
 * @category Feed
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */

class ActivityStreamsMediaLink extends ActivityStreamsLink
{
    private $linkDict;

    function __construct(
        $url       = null,
        $width     = null,
        $height    = null,
        $mediaType = null, // extension
        $rel       = null, // extension
        $duration  = null
    )
    {
        parent::__construct($url, $rel, $mediaType);
        $this->linkDict = array(
            'width'      => intval($width),
            'height'     => intval($height),
            'duration'   => intval($duration)
        );
    }

    function asArray()
    {
        return array_merge(
            parent::asArray(),
            array_filter($this->linkDict)
        );
    }
}


code:
<?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(DOMNode $element)
    {
        return self::getLink($element, 'alternate', 'text/html');
    }

    static function getSelfLink(DOMNode $element)
    {
        return self::getLink($element, 'self', 'application/atom+xml');
    }

    /**
     * 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);

                // XXX: Am I allowed to do this according to specs? (matching using common_bare_mime)
                if ($linkRel == $rel &&
                    (is_null($type) || common_bare_mime($linkType) == common_bare_mime($type))) {
                    return $link->getAttribute(self::HREF);
                }
            }
        }

        return null;
    }

    static function getLinks(DOMNode $element, $rel, $type=null)
    {
        $els = $element->childNodes;
        $out = array();

        for ($i = 0; $i < $els->length; $i++) {
            $link = $els->item($i);
            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;
                }
            }
        }
    }

    /**
     * Gets all immediate child elements 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 array found element or null
     */

    static function children(DOMNode $element, $tag, $namespace=self::ATOM)
    {
        $results = array();

        $els = $element->childNodes;

        if (!empty($els) && $els->length > 0) {
            for ($i = 0; $i < $els->length; $i++) {
                $el = $els->item($i);
                if ($el->localName == $tag && $el->namespaceURI == $namespace) {
                    $results[] = $el;
                }
            }
        }

        return $results;
    }

    /**
     * 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 -< 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)) {
            // TRANS: Client exception thrown when there is no source attribute.
            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') {
            // We have plaintext saved as the XML text content.
            // Since we want HTML, we need to escape any special chars.
            return htmlspecialchars($el->textContent);
        } else if ($type == 'html') {
            // We have HTML saved as the XML text content.
            // No additional processing required once we've got it.
            $text = $el->textContent;
            return $text;
        } else if ($type == 'xhtml') {
            // Per spec, the <content type="xhtml"> contains a single
            // HTML <div> with XHTML namespace on it as a child node.
            // We need to pull all of that <div>'s child nodes and
            // serialize them back to an (X)HTML source fragment.
            $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)) {
            // TRANS: Client exception thrown when there embedded XML content is found that cannot be processed yet.
            throw new ClientException(_("Can't handle embedded XML content yet."));
        } else if (strncasecmp($type, 'text/', 5)) {
            return $el->textContent;
        } else {
            // TRANS: Client exception thrown when base64 encoded content is found that cannot be processed 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)
    {
        // Check mailto: URIs first
        $validate = new Validate();

        if (preg_match('/^mailto:(.*)$/', $uri, $match)) {
            return $validate->email($match[1], common_config('email', 'check_domain'));
        }

        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_schemes' => array('tag')))) {
            return true;
        }

        return false;
    }

    static function getFeedAuthor(DOMElement $feedEl)
    {
        // Try old and deprecated activity:subject

        $subject = ActivityUtils::child($feedEl, Activity::SUBJECT, Activity::SPEC);

        if (!empty($subject)) {
            return new ActivityObject($subject);
        }

        // Try the feed author

        $author = ActivityUtils::child($feedEl, Activity::AUTHOR, Activity::ATOM);

        if (!empty($author)) {
            return new ActivityObject($author);
        }

        // Sheesh. Not a very nice feed! Let's try fingerpoken in the
        // entries.

        $entries = $feedEl->getElementsByTagNameNS(Activity::ATOM, 'entry');

        if (!empty($entries) && $entries->length > 0) {

            $entry = $entries->item(0);

            // Try the (deprecated) activity:actor

            $actor = ActivityUtils::child($entry, Activity::ACTOR, Activity::SPEC);

            if (!empty($actor)) {
                return new ActivityObject($actor);
            }

            // Try the author

            $author = ActivityUtils::child($entry, Activity::AUTHOR, Activity::ATOM);

            if (!empty($author)) {
                return new ActivityObject($author);
            }
        }

        return null;
    }

    static function compareTypes($type, $objects)
    {
        $type = self::resolveUri($type, false);
        foreach ((array)$objects as $object) {
            if ($type === self::resolveUri($object)) {
                return true;
            }
        }
        return false;
    }

    static function compareVerbs($type, $objects)
    {
        return self::compareTypes($type, $objects);
    }

    static function resolveUri($uri, $make_relative=false)
    {
        if (empty($uri)) {
            throw new ServerException('No URI to resolve in ActivityUtils::resolveUri');
        }

        if (!$make_relative && parse_url($uri, PHP_URL_SCHEME) == '') { // relative -> absolute
            $uri = Activity::SCHEMA . $uri;
        } elseif ($make_relative) { // absolute -> relative
            $uri = basename($uri); //preg_replace('/^http:\/\/activitystrea\.ms\/schema\/1\.0\//', '', $uri);
        } // absolute schemas pass through unharmed

        return $uri;
    }

    static function findLocalObject(array $uris, $type=ActivityObject::NOTE) {
        $obj_class = null;
        // TODO: Extend this in plugins etc. and describe in EVENTS.txt
        if (Event::handle('StartFindLocalActivityObject', array($uris, $type, &$obj_class))) {
            switch (self::resolveUri($type)) {
            case ActivityObject::PERSON:
                // GROUP will also be here in due time...
                $obj_class = 'Profile';
                break;
            default:
                $obj_class = 'Notice';
            }
        }
        $object = null;
        $uris = array_unique($uris);
        foreach ($uris as $uri) {
            try {
                // the exception thrown will cancel before reaching $object
                $object = call_user_func("{$obj_class}::fromUri", $uri);
                break;
            } catch (UnknownUriException $e) {
                common_debug('Could not find local activity object from uri: '.$e->object_uri);
            }
        }
        if (!$object instanceof Managed_DataObject) {
            throw new ServerException('Could not find any activityobject stored locally with given URIs: '.var_export($uris,true));
        }
        Event::handle('EndFindLocalActivityObject', array($object->getUri(), $object->getObjectType(), $object));
        return $object;
    }

    // Check authorship by supplying a Profile as a default and letting plugins
    // set it to something else if the activity's author is actually someone
    // else (like with a group or peopletag feed as handled in OStatus).
    //
    // NOTE: Returned is not necessarily the supplied profile! For example,
    // the "feed author" may be a group, but the "activity author" is a person!
    static function checkAuthorship(Activity $activity, Profile $profile)
    {
        if (Event::handle('CheckActivityAuthorship', array($activity, &$profile))) {
            // if (empty($activity->actor)), then we generated this Activity ourselves and can trust $profile

            $actor_uri = $profile->getUri();

            if (!in_array($actor_uri, array($activity->actor->id, $activity->actor->link))) {
                // A mismatch between our locally stored URI and the supplied author?
                // Probably not more than a blog feed or something (with multiple authors or so)
                // but log it for future inspection.
                common_log(LOG_WARNING, "Got an actor '{$activity->actor->title}' ({$activity->actor->id}) on single-user feed for " . $actor_uri);
            } elseif (empty($activity->actor->id)) {
                // Plain <author> without ActivityStreams actor info.
                // We'll just ignore this info for now and save the update under the feed's identity.
            }
        }

        if (!$profile instanceof Profile) {
            throw new ServerException('Could not get an author Profile for activity');
        }

        return $profile;
    }

    static public function typeToTitle($type)
    {
        return ucfirst(self::resolveUri($type, true));
    }

    static public function verbToTitle($verb)
    {
        return ucfirst(self::resolveUri($verb, true));
    }
}


code:
<?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 LIKE     = 'http://activitystrea.ms/schema/1.0/like'; // This is a synonym of 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';
    const DELETE   = 'http://activitystrea.ms/schema/1.0/delete';
    const UPDATE   = 'http://activitystrea.ms/schema/1.0/update';

    // Custom OStatus verbs for the flipside until they're standardized
    const UNFAVORITE = 'http://activitystrea.ms/schema/1.0/unfavorite';
    const UNLIKE     = 'http://activitystrea.ms/schema/1.0/unlike'; // This is a synonym of unfavorite
    const UNFOLLOW   = 'http://ostatus.org/schema/1.0/unfollow';
    const LEAVE      = 'http://ostatus.org/schema/1.0/leave';
    const UNTAG      = 'http://ostatus.org/schema/1.0/untag';

    // For simple profile-update pings; no content to share.
    const UPDATE_PROFILE = 'http://ostatus.org/schema/1.0/update-profile';

    static function canonical($verb) {
        $ns = 'http://activitystrea.ms/schema/1.0/';
        if (substr($verb, 0, mb_strlen($ns)) == $ns) {
            return substr($verb, mb_strlen($ns));
        } else {
            return $verb;
        }
    }
}


code:
<?php
if (!defined('GNUSOCIAL')) { exit(1); }

/*
 * Collection primarily as the root of an Activity Streams doc but can be used as the value
 * of extension properties in a variety of situations.
 *
 * A valid Collection object serialization MUST contain at least the url or items properties.
 */
class JSONActivityCollection {

    /* Non-negative integer specifying the total number of activities within the stream */
    protected $totalItems;

    /* An array containing a listing of Objects of any object type */
    protected $items;

    /* IRI referencing a JSON document containing the full listing of objects in the collection */
    protected $url;

    /**
     * Constructor
     *
     * @param array  $items       array of activity items
     * @param string $url         url of a doc list all the objs in the collection
     * @param int    $totalItems  total number of items in the collection
     */
    function __construct(array $items=[], $url = null)
    {
        $this->items      = empty($items) ? array() : $items;
        $this->totalItems = count($items);
        $this->url        = $url;
    }

    /**
     * Get the total number of items in the collection
     *
     * @return int total the total
     */
    public function getTotalItems()
    {
        $this->totalItems = count($items);
        return $this->totalItems;
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010 StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Class for activity streams
 *
 * Includes objects like notices, subscriptions and from plugins.
 *
 * We extend atomusernoticefeed since it does some nice setup for us.
 *
 */
class UserActivityStream extends AtomUserNoticeFeed
{
    public $activities = array();
    public $after = null;

    const OUTPUT_STRING = 1;
    const OUTPUT_RAW = 2;
    public $outputMode = self::OUTPUT_STRING;

    /**
     *
     * @param User $user
     * @param boolean $indent
     * @param boolean $outputMode: UserActivityStream::OUTPUT_STRING to return a string,
     *                           or UserActivityStream::OUTPUT_RAW to go to raw output.
     *                           Raw output mode will attempt to stream, keeping less
     *                           data in memory but will leave $this->activities incomplete.
     */
    function __construct($user, $indent = true, $outputMode = UserActivityStream::OUTPUT_STRING, $after = null)
    {
        parent::__construct($user, null, $indent);

        $this->outputMode = $outputMode;

        if ($this->outputMode == self::OUTPUT_STRING) {
            // String buffering? Grab all the notices now.
            $notices = $this->getNotices();
        } elseif ($this->outputMode == self::OUTPUT_RAW) {
            // Raw output... need to restructure from the stringer init.
            $this->xw = new XMLWriter();
            $this->xw->openURI('php://output');
            if(is_null($indent)) {
                $indent = common_config('site', 'indent');
            }
            $this->xw->setIndent($indent);

            // We'll fetch notices later.
            $notices = array();
        } else {
            throw new Exception('Invalid outputMode provided to ' . __METHOD__);
        }

        $this->after = $after;

        // Assume that everything but notices is feasible
        // to pull at once and work with in memory...

        $subscriptions = $this->getSubscriptions();
        $subscribers   = $this->getSubscribers();
        $groups        = $this->getGroups();

        $objs = array_merge($subscriptions, $subscribers, $groups, $notices);

        Event::handle('AppendUserActivityStreamObjects', array($this, &$objs));

        $subscriptions = null;
        $subscribers   = null;
        $groups        = null;

        unset($subscriptions);
        unset($subscribers);
        unset($groups);

        // Sort by create date

        usort($objs, 'UserActivityStream::compareObject');

        // We'll keep these around for later, and interleave them into
        // the output stream with the user's notices.

        $this->objs = $objs;
    }

    /**
     * Interleave the pre-sorted objects with the user's
     * notices, all in reverse chron order.
     */
    function renderEntries($format=Feed::ATOM, $handle=null)
    {
        $haveOne = false;

        $end = time() + 1;
        foreach ($this->objs as $obj) {
            set_time_limit(10);

            try {
                $act = $obj->asActivity();
            } catch (Exception $e) {
                common_log(LOG_ERR, $e->getMessage());
                continue;
            }

            $start = $act->time;

            if ($this->outputMode == self::OUTPUT_RAW && $start != $end) {
                // In raw mode, we haven't pre-fetched notices.
                // Grab the chunks of notices between other activities.
                try {
                    $notices = $this->getNoticesBetween($start, $end);
                    foreach ($notices as $noticeAct) {
                        try {
                            $nact = $noticeAct->asActivity($this->user->getProfile());
                            if ($format == Feed::ATOM) {
                                $nact->outputTo($this, false, false);
                            } else {
                                if ($haveOne) {
                                    fwrite($handle, ",");
                                }
                                fwrite($handle, json_encode($nact->asArray()));
                                $haveOne = true;
                            }
                        } catch (Exception $e) {
                            common_log(LOG_ERR, $e->getMessage());
                            continue;
                        }
                        $nact = null;
                        unset($nact);
                    }
                } catch (Exception $e) {
                    common_log(LOG_ERR, $e->getMessage());
                }
            }

            $notices = null;
            unset($notices);

            try {
                if ($format == Feed::ATOM) {
                    // Only show the author sub-element if it's different from default user
                    $act->outputTo($this, false, ($act->actor->id != $this->user->getUri()));
                } else {
                    if ($haveOne) {
                        fwrite($handle, ",");
                    }
                    fwrite($handle, json_encode($act->asArray()));
                    $haveOne = true;
                }
            } catch (Exception $e) {
                common_log(LOG_ERR, $e->getMessage());
            }

            $act = null;
            unset($act);

            $end = $start;
        }

        if ($this->outputMode == self::OUTPUT_RAW) {
            // Grab anything after the last pre-sorted activity.
            try {
                if (!empty($this->after)) {
                    $notices = $this->getNoticesBetween($this->after, $end);
                } else {
                    $notices = $this->getNoticesBetween(0, $end);
                }
                foreach ($notices as $noticeAct) {
                    try {
                        $nact = $noticeAct->asActivity($this->user->getProfile());
                        if ($format == Feed::ATOM) {
                            $nact->outputTo($this, false, false);
                        } else {
                            if ($haveOne) {
                                fwrite($handle, ",");
                            }
                            fwrite($handle, json_encode($nact->asArray()));
                            $haveOne = true;
                        }
                    } catch (Exception $e) {
                        common_log(LOG_ERR, $e->getMessage());
                        continue;
                    }
                }
            } catch (Exception $e) {
                common_log(LOG_ERR, $e->getMessage());
            }
        }

        if (empty($this->after) || strtotime($this->user->created) > $this->after) {
            // We always add the registration activity at the end, even if
            // they have older activities (from restored backups) in their stream.

            try {
                $ract = $this->user->registrationActivity();
                if ($format == Feed::ATOM) {
                    $ract->outputTo($this, false, false);
                } else {
                    if ($haveOne) {
                        fwrite($handle, ",");
                    }
                    fwrite($handle, json_encode($ract->asArray()));
                    $haveOne = true;
                }
            } catch (Exception $e) {
                common_log(LOG_ERR, $e->getMessage());
            }
        }
    }

    function compareObject($a, $b)
    {
        $ac = strtotime((empty($a->created)) ? $a->modified : $a->created);
        $bc = strtotime((empty($b->created)) ? $b->modified : $b->created);

        return (($ac == $bc) ? 0 : (($ac < $bc) ? 1 : -1));
    }

    function getSubscriptions()
    {
        $subs = array();

        $sub = new Subscription();

        $sub->subscriber = $this->user->id;

        if (!empty($this->after)) {
            $sub->whereAdd("created > '" . common_sql_date($this->after) . "'");
        }

        if ($sub->find()) {
            while ($sub->fetch()) {
                if ($sub->subscribed != $this->user->id) {
                    $subs[] = clone($sub);
                }
            }
        }

        return $subs;
    }

    function getSubscribers()
    {
        $subs = array();

        $sub = new Subscription();

        $sub->subscribed = $this->user->id;

        if (!empty($this->after)) {
            $sub->whereAdd("created > '" . common_sql_date($this->after) . "'");
        }

        if ($sub->find()) {
            while ($sub->fetch()) {
                if ($sub->subscriber != $this->user->id) {
                    $subs[] = clone($sub);
                }
            }
        }

        return $subs;
    }

    /**
     *
     * @param int $start unix timestamp for earliest
     * @param int $end unix timestamp for latest
     * @return array of Notice objects
     */
    function getNoticesBetween($start=0, $end=0)
    {
        $notices = array();

        $notice = new Notice();

        $notice->profile_id = $this->user->id;

        // Only stuff after $this->after

        if (!empty($this->after)) {
            if ($start) {
                $start = max($start, $this->after);
            }
            if ($end) {
                $end = max($end, $this->after);
            }
        }

        if ($start) {
            $tsstart = common_sql_date($start);
            $notice->whereAdd("created >= '$tsstart'");
        }
        if ($end) {
            $tsend = common_sql_date($end);
            $notice->whereAdd("created < '$tsend'");
        }

        $notice->orderBy('created DESC');

        if ($notice->find()) {
            while ($notice->fetch()) {
                $notices[] = clone($notice);
            }
        }

        return $notices;
    }

    function getNotices()
    {
        if (!empty($this->after)) {
            return $this->getNoticesBetween($this->after);
        } else {
            return $this->getNoticesBetween();
        }
    }

    function getGroups()
    {
        $groups = array();

        $gm = new Group_member();

        $gm->profile_id = $this->user->id;

        if (!empty($this->after)) {
            $gm->whereAdd("created > '" . common_sql_date($this->after) . "'");
        }

        if ($gm->find()) {
            while ($gm->fetch()) {
                $groups[] = clone($gm);
            }
        }

        return $groups;
    }

    function createdAfter($item) {
        $created = strtotime((empty($item->created)) ? $item->modified : $item->created);
        return ($created >= $this->after);
    }

    function writeJSON($handle)
    {
        require_once INSTALLDIR . '/lib/activitystreams/activitystreamjsondocument.php';
        fwrite($handle, '{"items": [');
        $this->renderEntries(Feed::JSON, $handle);
        fwrite($handle, ']}');
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Base class for administrative forms
 *
 * 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  Widget
 * @package   StatusNet
 * @author    Zach Copley <zach@status.net>
 * @copyright 2009 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET') && !defined('LACONICA')) {
    exit(1);
}

/**
 * Base class for Administrative forms
 *
 * Just a place holder for some utility methods to simply some
 * repetitive form building code
 *
 * @category Widget
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 *
 * @see      Form
 */
class AdminForm extends Form
{
    /**
     * Utility to simplify some of the duplicated code around
     * params and settings.
     *
     * @param string $setting      Name of the setting
     * @param string $title        Title to use for the input
     * @param string $instructions Instructions for this field
     * @param string $section      config section, default = 'site'
     *
     * @return void
     */
    function input($setting, $title, $instructions, $section='site')
    {
        $this->out->input($setting, $title, $this->value($setting, $section), $instructions);
    }

    /**
     * Utility to simplify getting the posted-or-stored setting value
     *
     * @param string $setting Name of the setting
     * @param string $main    configuration section, default = 'site'
     *
     * @return string param value if posted, or current config value
     */
    function value($setting, $main='site')
    {
        $value = $this->out->trimmed($setting);
        if (empty($value)) {
            $value = common_config($main, $setting);
        }
        return $value;
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Superclass for admin panel actions
 *
 * 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  UI
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2009 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * superclass for admin panel actions
 *
 * Common code for all admin panel actions.
 *
 * @category UI
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 *
 * @todo Find some commonalities with SettingsAction and combine
 */
class AdminPanelAction extends Action
{
    var $success = true;
    var $msg     = null;

    /**
     * Prepare for the action
     *
     * We check to see that the user is logged in, has
     * authenticated in this session, and has the right
     * to configure the site.
     *
     * @param array $args Array of arguments from Web driver
     *
     * @return boolean success flag
     */
    function prepare(array $args = array())
    {
        parent::prepare($args);

        // User must be logged in.

        if (!common_logged_in()) {
            // TRANS: Error message displayed when trying to perform an action that requires a logged in user.
            $this->clientError(_('Not logged in.'));
        }

        $user = common_current_user();

        // ...because they're logged in

        assert(!empty($user));

        // It must be a "real" login, not saved cookie login

        if (!common_is_real_login()) {
            // Cookie theft is too easy; we require automatic
            // logins to re-authenticate before admining the site
            common_set_returnto($this->selfUrl());
            if (Event::handle('RedirectToLogin', array($this, $user))) {
                common_redirect(common_local_url('login'), 303);
            }
        }

        // User must have the right to change admin settings

        if (!$user->hasRight(Right::CONFIGURESITE)) {
            // TRANS: Client error message thrown when a user tries to change admin settings but has no access rights.
            $this->clientError(_('You cannot make changes to this site.'));
        }

        // This panel must be enabled

        $name = $this->trimmed('action');

        $name = mb_substr($name, 0, -10);

        if (!self::canAdmin($name)) {
            // TRANS: Client error message throw when a certain panel's settings cannot be changed.
            $this->clientError(_('Changes to that panel are not allowed.'), 403);
        }

        return true;
    }

    /**
     * handle the action
     *
     * Check session token and try to save the settings if this is a
     * POST. Otherwise, show the form.
     *
     * @param array $args unused.
     *
     * @return void
     */
    function handle()
    {
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
            $this->checkSessionToken();
            try {
                $this->saveSettings();

                // Reload settings

                Config::loadSettings();

                $this->success = true;
                // TRANS: Message after successful saving of administrative settings.
                $this->msg     = _('Settings saved.');
            } catch (Exception $e) {
                $this->success = false;
                $this->msg     = $e->getMessage();
            }
        }
        $this->showPage();
    }

    /**
     * Show tabset for this page
     *
     * Uses the AdminPanelNav widget
     *
     * @return void
     * @see AdminPanelNav
     */
    function showLocalNav()
    {
        $nav = new AdminPanelNav($this);
        $nav->show();
    }

    /**
     * Show the content section of the page
     *
     * Here, we show the admin panel's form.
     *
     * @return void.
     */
    function showContent()
    {
        $this->showForm();
    }

    /**
     * Show content block. Overrided just to add a special class
     * to the content div to allow styling.
     *
     * @return nothing
     */
    function showContentBlock()
    {
        $this->elementStart('div', array('id' => 'content', 'class' => 'admin'));
        $this->showPageTitle();
        $this->showPageNoticeBlock();
        $this->elementStart('div', array('id' => 'content_inner'));
        // show the actual content (forms, lists, whatever)
        $this->showContent();
        $this->elementEnd('div');
        $this->elementEnd('div');
    }

    /**
     * show human-readable instructions for the page, or
     * a success/failure on save.
     *
     * @return void
     */
    function showPageNotice()
    {
        if ($this->msg) {
            $this->element('div', ($this->success) ? 'success' : 'error',
                           $this->msg);
        } else {
            $inst   = $this->getInstructions();
            $output = common_markup_to_html($inst);

            $this->elementStart('div', 'instructions');
            $this->raw($output);
            $this->elementEnd('div');
        }
    }

    /**
     * Show the admin panel form
     *
     * Sub-classes should overload this.
     *
     * @return void
     */
    function showForm()
    {
        // TRANS: Client error message.
        $this->clientError(_('showForm() not implemented.'));
    }

    /**
     * Instructions for using this form.
     *
     * String with instructions for using the form.
     *
     * Subclasses should overload this.
     *
     * @return void
     */
    function getInstructions()
    {
        return '';
    }

    /**
     * Save settings from the form
     *
     * Validate and save the settings from the user.
     *
     * @return void
     */
    function saveSettings()
    {
        // TRANS: Client error message
        $this->clientError(_('saveSettings() not implemented.'));
    }

    static function canAdmin($name)
    {
        $isOK = false;

        if (Event::handle('AdminPanelCheck', array($name, &$isOK))) {
            $isOK = in_array($name, common_config('admin', 'panels'));
        }

        return $isOK;
    }

    function showProfileBlock()
    {
    }
}