code:
<?php
/*
 * GNU Social - a federating social network
 * Copyright (C) 2014, Free Software Foundation, 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/>.
 */

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

/**
 * Superclass for plugins which add Activity types and such
 *
 * @category  Activity
 * @package   GNUsocial
 * @author    Mikael Nordfeldth <mmn@hethane.se>
 * @copyright 2014 Free Software Foundation, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://gnu.io/social
 */
abstract class ActivityHandlerPlugin extends Plugin
{
    /**
     * Returns a key string which represents this activity in HTML classes,
     * ids etc, as when offering selection of what type of post to make.
     * In MicroAppPlugin, this is paired with the user-visible localizable appTitle().
     *
     * @return string (compatible with HTML classes)
     */
    abstract function tag();

    /**
     * Return a list of ActivityStreams object type IRIs
     * which this micro-app handles. Default implementations
     * of the base class will use this list to check if a
     * given ActivityStreams object belongs to us, via
     * $this->isMyNotice() or $this->isMyActivity.
     *
     * An empty list means any type is ok. (Favorite verb etc.)
     *
     * @return array of strings
     */
    abstract function types();

    /**
     * Return a list of ActivityStreams verb IRIs which
     * this micro-app handles. Default implementations
     * of the base class will use this list to check if a
     * given ActivityStreams verb belongs to us, via
     * $this->isMyNotice() or $this->isMyActivity.
     *
     * All micro-app classes must override this method.
     *
     * @return array of strings
     */
    public function verbs() {
        return array(ActivityVerb::POST);
    }

    /**
     * Check if a given ActivityStreams activity should be handled by this
     * micro-app plugin.
     *
     * The default implementation checks against the activity type list
     * returned by $this->types(), and requires that exactly one matching
     * object be present. You can override this method to expand
     * your checks or to compare the activity's verb, etc.
     *
     * @param Activity $activity
     * @return boolean
     */
    function isMyActivity(Activity $act) {
        return (count($act->objects) == 1
            && ($act->objects[0] instanceof ActivityObject)
            && $this->isMyVerb($act->verb)
            && $this->isMyType($act->objects[0]->type));
    }

    /**
     * Check if a given notice object should be handled by this micro-app
     * plugin.
     *
     * The default implementation checks against the activity type list
     * returned by $this->types(). You can override this method to expand
     * your checks, but follow the execution chain to get it right.
     *
     * @param Notice $notice
     * @return boolean
     */
    function isMyNotice(Notice $notice) {
        return $this->isMyVerb($notice->verb) && $this->isMyType($notice->object_type);
    }

    function isMyVerb($verb) {
        $verb = $verb ?: ActivityVerb::POST;    // post is the default verb
        return ActivityUtils::compareVerbs($verb, $this->verbs());
    }

    function isMyType($type) {
        // Third argument to compareTypes is true, to allow for notices with empty object_type for example (verb-only)
        return count($this->types())===0 || ActivityUtils::compareTypes($type, $this->types());
    }

    /**
     * Given a parsed ActivityStreams activity, your plugin
     * gets to figure out how to actually save it into a notice
     * and any additional data structures you require.
     *
     * This function is deprecated and in the future, Notice::saveActivity
     * should be called from onStartHandleFeedEntryWithProfile in this class
     * (which instead turns to saveObjectFromActivity).
     *
     * @param Activity $activity
     * @param Profile $actor
     * @param array $options=array()
     *
     * @return Notice the resulting notice
     */
    public function saveNoticeFromActivity(Activity $activity, Profile $actor, array $options=array())
    {
        // Any plugin which has not implemented saveObjectFromActivity _must_
        // override this function until they are migrated (this function will
        // be deleted when all plugins are migrated to saveObjectFromActivity).

        if (isset($this->oldSaveNew)) {
            throw new ServerException('A function has been called for new saveActivity functionality, but is still set with an oldSaveNew configuration');
        }

        return Notice::saveActivity($activity, $actor, $options);
    }

    /**
    * Given a parsed ActivityStreams activity, your plugin gets
    * to figure out itself how to store the additional data into
    * the database, besides the base data stored by the core.
    *
    * This will handle just about all events where an activity
    * object gets saved, whether it is via AtomPub, OStatus
    * (WebSub and Salmon transports), or ActivityStreams-based
    * backup/restore of account data.
    *
    * You should be able to accept as input the output from an
    * asActivity() call on the stored object. Where applicable,
    * try to use existing ActivityStreams structures and object
    * types, and be liberal in accepting input from what might
    * be other compatible apps.
    *
    * All micro-app classes must override this method.
    *
    * @fixme are there any standard options?
    *
    * @param Activity $activity
    * @param Notice   $stored       The notice in our database for this certain object
    * @param array $options=array()
    *
    * @return object    If the verb handling plugin creates an object, it can be returned here (otherwise true)
    * @throws exception On any error.
    */
    protected function saveObjectFromActivity(Activity $activity, Notice $stored, array $options=array())
    {
        throw new ServerException('This function should be abstract when all plugins have migrated to saveObjectFromActivity');
    }

    /*
     * This usually gets called from Notice::saveActivity after a Notice object has been created,
     * so it contains a proper id and a uri for the object to be saved.
     */
    public function onStoreActivityObject(Activity $act, Notice $stored, array $options, &$object) {
        // $this->oldSaveNew is there during a migration period of plugins, to start using
        // Notice::saveActivity instead of Notice::saveNew
        if (!$this->isMyActivity($act) || isset($this->oldSaveNew)) {
            return true;
        }
        $object = $this->saveObjectFromActivity($act, $stored, $options);
        return false;
    }

    /**
     * Given an existing Notice object, your plugin gets to
     * figure out how to arrange it into an ActivityStreams
     * object.
     *
     * This will be how your specialized notice gets output in
     * Atom feeds and JSON-based ActivityStreams output, including
     * account backup/restore and OStatus (WebSub and Salmon transports).
     *
     * You should be able to round-trip data from this format back
     * through $this->saveNoticeFromActivity(). Where applicable, try
     * to use existing ActivityStreams structures and object types,
     * and consider interop with other compatible apps.
     *
     * All micro-app classes must override this method.
     *
     * @fixme this outputs an ActivityObject, not an Activity. Any compat issues?
     *
     * @param Notice $notice
     *
     * @return ActivityObject
     */
    abstract function activityObjectFromNotice(Notice $notice);

    /**
     * When a notice is deleted, you'll be called here for a chance
     * to clean up any related resources.
     *
     * All micro-app classes must override this method.
     *
     * @param Notice $notice
     */
    abstract function deleteRelated(Notice $notice);

    protected function notifyMentioned(Notice $stored, array &$mentioned_ids)
    {
        // pass through silently by default

        // If we want to stop any other plugin from notifying based on this activity, return false instead.
        return true;
    }

    /**
     * Called when generating Atom XML ActivityStreams output from an
     * ActivityObject belonging to this plugin. Gives the plugin
     * a chance to add custom output.
     *
     * Note that you can only add output of additional XML elements,
     * not change existing stuff here.
     *
     * If output is already handled by the base Activity classes,
     * you can leave this base implementation as a no-op.
     *
     * @param ActivityObject $obj
     * @param XMLOutputter $out to add elements at end of object
     */
    function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
    {
        // default is a no-op
    }

    /**
     * Called when generating JSON ActivityStreams output from an
     * ActivityObject belonging to this plugin. Gives the plugin
     * a chance to add custom output.
     *
     * Modify the array contents to your heart's content, and it'll
     * all get serialized out as JSON.
     *
     * If output is already handled by the base Activity classes,
     * you can leave this base implementation as a no-op.
     *
     * @param ActivityObject $obj
     * @param array &$out JSON-targeted array which can be modified
     */
    public function activityObjectOutputJson(ActivityObject $obj, array &$out)
    {
        // default is a no-op
    }

    /**
     * When a notice is deleted, delete the related objects
     * by calling the overridable $this->deleteRelated().
     *
     * @param Notice $notice Notice being deleted
     *
     * @return boolean hook value
     */
    public function onNoticeDeleteRelated(Notice $notice)
    {
        if ($this->isMyNotice($notice)) {
            try {
                $this->deleteRelated($notice);
            } catch (NoProfileException $e) {
                // we failed because of database lookup failure, Notice has no recognized profile as creator
                // so we skip this. If we want to remove missing notices we should do a SQL constraints check
                // in the affected plugin.
            } catch (AlreadyFulfilledException $e) {
                // Nothing to see here, it's obviously already gone...
            }
        }

        // Always continue this event in our activity handling plugins.
        return true;
    }

    /**
     * @param Notice $stored            The notice being distributed
     * @param array  &$mentioned_ids    List of profiles (from $stored->getReplies())
     */
    public function onStartNotifyMentioned(Notice $stored, array &$mentioned_ids)
    {
        if (!$this->isMyNotice($stored)) {
            return true;
        }

        return $this->notifyMentioned($stored, $mentioned_ids);
    }

    /**
     * Render a notice as one of our objects
     *
     * @param Notice         $notice  Notice to render
     * @param ActivityObject &$object Empty object to fill
     *
     * @return boolean hook value
     */
    function onStartActivityObjectFromNotice(Notice $notice, &$object)
    {
        if (!$this->isMyNotice($notice)) {
            return true;
        }

        $object = $this->activityObjectFromNotice($notice);
        return false;
    }

    /**
     * Handle a posted object from WebSub
     *
     * @param Activity        $activity activity to handle
     * @param Profile         $actor Profile for the feed
     *
     * @return boolean hook value
     */
    function onStartHandleFeedEntryWithProfile(Activity $activity, Profile $profile, &$notice)
    {
        if (!$this->isMyActivity($activity)) {
            return true;
        }

        // We are guaranteed to get a Profile back from checkAuthorship (or it throws an exception)
        $profile = ActivityUtils::checkAuthorship($activity, $profile);

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

        $options = array('uri' => $object->id,
                         'url' => $object->link,
                         'self' => $object->selfLink,
                         'is_local' => Notice::REMOTE,
                         'source' => 'ostatus');

        if (!isset($this->oldSaveNew)) {
            $notice = Notice::saveActivity($activity, $profile, $options);
        } else {
            $notice = $this->saveNoticeFromActivity($activity, $profile, $options);
        }

        return false;
    }

    /**
     * Handle a posted object from Salmon
     *
     * @param Activity $activity activity to handle
     * @param mixed    $target   user or group targeted
     *
     * @return boolean hook value
     */

    function onStartHandleSalmonTarget(Activity $activity, $target)
    {
        if (!$this->isMyActivity($activity)) {
            return true;
        }
        if (!isset($this->oldSaveNew)) {
            // Handle saveActivity in OStatus class for incoming salmon, remove this event
            // handler when all plugins have gotten rid of "oldSaveNew".
            return true;
        }

        $this->log(LOG_INFO, get_called_class()." checking {$activity->id} as a valid Salmon slap.");

        if ($target instanceof User_group || $target->isGroup()) {
            $uri = $target->getUri();
            if (!array_key_exists($uri, $activity->context->attention)) {
                // @todo FIXME: please document (i18n).
                // TRANS: Client exception thrown when ...
                throw new ClientException(_('Object not posted to this group.'));
            }
        } elseif ($target instanceof Profile && $target->isLocal()) {
            $original = null;
            // FIXME: Shouldn't favorites show up with a 'target' activityobject?
            if (!ActivityUtils::compareVerbs($activity->verb, array(ActivityVerb::POST)) && isset($activity->objects[0])) {
                // If this is not a post, it's a verb targeted at something (such as a Favorite attached to a note)
                if (!empty($activity->objects[0]->id)) {
                    $activity->context->replyToID = $activity->objects[0]->id;
                }
            }
            if (!empty($activity->context->replyToID)) {
                $original = Notice::getKV('uri', $activity->context->replyToID);
            }
            if ((!$original instanceof Notice || $original->profile_id != $target->id)
                    && !array_key_exists($target->getUri(), $activity->context->attention)) {
                // @todo FIXME: Please document (i18n).
                // TRANS: Client exception when ...
                throw new ClientException(_('Object not posted to this user.'));
            }
        } else {
            // TRANS: Server exception thrown when a micro app plugin uses a target that cannot be handled.
            throw new ServerException(_('Do not know how to handle this kind of target.'));
        }

        $oactor = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
        $actor = $oactor->localProfile();

        // FIXME: will this work in all cases? I made it work for Favorite...
        if (ActivityUtils::compareVerbs($activity->verb, array(ActivityVerb::POST))) {
            $object = $activity->objects[0];
        } else {
            $object = $activity;
        }

        $options = array('uri' => $object->id,
                         'url' => $object->link,
                         'self' => $object->selfLink,
                         'is_local' => Notice::REMOTE,
                         'source' => 'ostatus');

        $notice = $this->saveNoticeFromActivity($activity, $actor, $options);

        return false;
    }

    /**
     * Handle object posted via AtomPub
     *
     * @param Activity  $activity Activity that was posted
     * @param Profile   $scoped   Profile of user posting
     * @param Notice   &$notice   Resulting notice
     *
     * @return boolean hook value
     */
    public function onStartAtomPubNewActivity(Activity $activity, Profile $scoped, Notice &$notice=null)
    {
        if (!$this->isMyActivity($activity)) {
            return true;
        }

        $options = array('source' => 'atompub');

        $notice = $this->saveNoticeFromActivity($activity, $scoped, $options);

        return false;
    }

    /**
     * Handle object imported from a backup file
     *
     * @param User           $user     User to import for
     * @param ActivityObject $author   Original author per import file
     * @param Activity       $activity Activity to import
     * @param boolean        $trusted  Is this a trusted user?
     * @param boolean        &$done    Is this done (success or unrecoverable error)
     *
     * @return boolean hook value
     */
    function onStartImportActivity($user, $author, Activity $activity, $trusted, &$done)
    {
        if (!$this->isMyActivity($activity)) {
            return true;
        }

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

        $options = array('uri' => $object->id,
                         'url' => $object->link,
                         'self' => $object->selfLink,
                         'source' => 'restore');

        // $user->getProfile() is a Profile
        $saved = $this->saveNoticeFromActivity($activity,
                                               $user->getProfile(),
                                               $options);

        if (!empty($saved)) {
            $done = true;
        }

        return false;
    }

    /**
     * Event handler gives the plugin a chance to add custom
     * Atom XML ActivityStreams output from a previously filled-out
     * ActivityObject.
     *
     * The atomOutput method is called if it's one of
     * our matching types.
     *
     * @param ActivityObject $obj
     * @param XMLOutputter $out to add elements at end of object
     * @return boolean hook return value
     */
    function onEndActivityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
    {
        if (in_array($obj->type, $this->types())) {
            $this->activityObjectOutputAtom($obj, $out);
        }
        return true;
    }

    /**
     * Event handler gives the plugin a chance to add custom
     * JSON ActivityStreams output from a previously filled-out
     * ActivityObject.
     *
     * The activityObjectOutputJson method is called if it's one of
     * our matching types.
     *
     * @param ActivityObject $obj
     * @param array &$out JSON-targeted array which can be modified
     * @return boolean hook return value
     */
    function onEndActivityObjectOutputJson(ActivityObject $obj, array &$out)
    {
        if (in_array($obj->type, $this->types())) {
            $this->activityObjectOutputJson($obj, $out);
        }
        return true;
    }

    public function onStartOpenNoticeListItemElement(NoticeListItem $nli)
    {
        if (!$this->isMyNotice($nli->notice)) {
            return true;
        }

        $this->openNoticeListItemElement($nli);

        Event::handle('EndOpenNoticeListItemElement', array($nli));
        return false;
    }

    public function onStartCloseNoticeListItemElement(NoticeListItem $nli)
    {
        if (!$this->isMyNotice($nli->notice)) {
            return true;
        }

        $this->closeNoticeListItemElement($nli);

        Event::handle('EndCloseNoticeListItemElement', array($nli));
        return false;
    }

    protected function openNoticeListItemElement(NoticeListItem $nli)
    {
        $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
        $class = 'h-entry notice ' . $this->tag();
        if ($nli->notice->scope != 0 && $nli->notice->scope != 1) {
            $class .= ' limited-scope';
        }
        try {
            $class .= ' notice-source-'.common_to_alphanumeric($nli->notice->source);
        } catch (Exception $e) {
            // either source or what we filtered out was a zero-length string
        }
        $nli->out->elementStart('li', array('class' => $class,
                                            'id' => 'notice-' . $id));
    }

    protected function closeNoticeListItemElement(NoticeListItem $nli)
    {
        $nli->out->elementEnd('li');
    }

    // FIXME: This is overriden in MicroAppPlugin but shouldn't have to be
    public function onStartShowNoticeItem(NoticeListItem $nli)
    {
        if (!$this->isMyNotice($nli->notice)) {
            return true;
        }

        try {
            $this->showNoticeListItem($nli);
        } catch (Exception $e) {
            common_log(LOG_ERR, 'Error showing notice '.$nli->getNotice()->getID().': ' . $e->getMessage());
            $nli->out->element('p', 'error', sprintf(_('Error showing notice: %s'), $e->getMessage()));
        }

        Event::handle('EndShowNoticeItem', array($nli));
        return false;
    }

    protected function showNoticeListItem(NoticeListItem $nli)
    {
        $nli->showNoticeHeaders();
        $nli->showContent();
        $nli->showNoticeFooter();
    }

    public function onStartShowNoticeItemNotice(NoticeListItem $nli)
    {
        if (!$this->isMyNotice($nli->notice)) {
            return true;
        }

        $this->showNoticeItemNotice($nli);

        Event::handle('EndShowNoticeItemNotice', array($nli));
        return false;
    }

    protected function showNoticeItemNotice(NoticeListItem $nli)
    {
        $nli->showNoticeTitle();
        $nli->showAuthor();
        $nli->showAddressees();
        $nli->showContent();
    }

    public function onStartShowNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped=null)
    {
        if (!$this->isMyNotice($stored)) {
            return true;
        }

        try {
            $this->showNoticeContent($stored, $out, $scoped);
        } catch (Exception $e) {
            $out->element('div', 'error', $e->getMessage());
        }
        return false;
    }

    protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped=null)
    {
        $out->text($stored->getContent());
    }
}


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

repo_name: andremichelle/gnu

license: mit


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


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/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
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * A remote, atompub-receiving service
 *
 * 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  AtomPub
 * @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);
}

/**
 * A remote service that supports AtomPub
 *
 * @category  AtomPub
 * @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 ActivitySink
{
    protected $svcDocUrl   = null;
    protected $username    = null;
    protected $password    = null;
    protected $collections = array();

    function __construct($svcDocUrl, $username, $password)
    {
        $this->svcDocUrl = $svcDocUrl;
        $this->username  = $username;
        $this->password  = $password;

        $this->_parseSvcDoc();
    }

    private function _parseSvcDoc()
    {
        $client   = new HTTPClient();
        $response = $client->get($this->svcDocUrl);

        if ($response->getStatus() != 200) {
            throw new Exception("Can't get {$this->svcDocUrl}; response status " . $response->getStatus());
        }

        $xml = $response->getBody();

        $dom = new DOMDocument();

        // We don't want to bother with white spaces
        $dom->preserveWhiteSpace = false;

        // Don't spew XML warnings to output
        $old = error_reporting();
        error_reporting($old & ~E_WARNING);
        $ok = $dom->loadXML($xml);
        error_reporting($old);

        $path = new DOMXPath($dom);

        $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
        $path->registerNamespace('app', 'http://www.w3.org/2007/app');
        $path->registerNamespace('activity', 'http://activitystrea.ms/spec/1.0/');

        $collections = $path->query('//app:collection');

        for ($i = 0; $i < $collections->length; $i++) {
            $collection = $collections->item($i);
            $url = $collection->getAttribute('href');
            $takesEntries = false;
            $accepts = $path->query('app:accept', $collection);
            for ($j = 0; $j < $accepts->length; $j++) {
                $accept = $accepts->item($j);
                $acceptValue = $accept->nodeValue;
                if (preg_match('#application/atom\+xml(;\s*type=entry)?#', $acceptValue)) {
                    $takesEntries = true;
                    break;
                }
            }

            if (!$takesEntries) {
                continue;
            }
            $verbs = $path->query('activity:verb', $collection);
            if ($verbs->length == 0) {
                $this->_addCollection(ActivityVerb::POST, $url);
            } else {
                for ($k = 0; $k < $verbs->length; $k++) {
                    $verb = $verbs->item($k);
                    $this->_addCollection($verb->nodeValue, $url);
                }
            }
        }
    }

    private function _addCollection($verb, $url)
    {
        if (array_key_exists($verb, $this->collections)) {
            $this->collections[$verb][] = $url;
        } else {
            $this->collections[$verb] = array($url);
        }
        return;
    }

    function postActivity($activity)
    {
        if (!array_key_exists($activity->verb, $this->collections)) {
            throw new Exception("No collection for verb {$activity->verb}");
        } else {
            if (count($this->collections[$activity->verb]) > 1) {
                common_log(LOG_NOTICE, "More than one collection for verb {$activity->verb}");
            }
            $this->postToCollection($this->collections[$activity->verb][0], $activity);
        }
    }

    function postToCollection($url, $activity)
    {
        $client = new HTTPClient($url);

        $client->setMethod('POST');
        $client->setAuth($this->username, $this->password);
        $client->setHeader('Content-Type', 'application/atom+xml;type=entry');
        $client->setBody($activity->asString(true, true, true));

        $response = $client->send();

        $status = $response->getStatus();
        $reason = $response->getReasonPhrase();

        if ($status >= 200 && $status < 300) {
            return true;
        } else if ($status >= 400 && $status < 500) {
            // TRANS: Client exception thrown when post to collection fails with a 400 status.
            // TRANS: %1$s is a URL, %2$s is the status, %s$s is the fail reason.
            throw new ClientException(sprintf(_m('URLSTATUSREASON','%1$s %2$s %3$s'), $url, $status, $reason));
        } else if ($status >= 500 && $status < 600) {
            // TRANS: Server exception thrown when post to collection fails with a 500 status.
            // TRANS: %1$s is a URL, %2$s is the status, %s$s is the fail reason.
            throw new ServerException(sprintf(_m('URLSTATUSREASON','%1$s %2$s %3$s'), $url, $status, $reason));
        } else {
            // That's unexpected.
            // TRANS: Exception thrown when post to collection fails with a status that is not handled.
            // TRANS: %1$s is a URL, %2$s is the status, %s$s is the fail reason.
            throw new Exception(sprintf(_m('URLSTATUSREASON','%1$s %2$s %3$s'), $url, $status, $reason));
        }
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Activity source, to save in <atom:source>
 *
 * 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);
}

/**
 * Feed data to store in the <atom:source> element
 *
 * I wanted to use Atom10Feed but it seems more heavyweight than what's
 * needed here.
 *
 * @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 ActivitySource
{
    public $id;
    public $title;
    public $icon;
    public $updated;
    public $links;
}


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