We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

activityobject.php 30.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * An activity
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Feed
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @author    Zach Copley <zach@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
 * @link      http://status.net/
 */

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

/**
 * 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';
67
    const _LIST     = 'http://activitystrea.ms/schema/1.0/list'; // LIST is reserved
68 69 70
    const PLACE     = 'http://activitystrea.ms/schema/1.0/place';
    const COMMENT   = 'http://activitystrea.ms/schema/1.0/comment';
    // ^^^^^^^^^^ tea!
71
    const ACTIVITY = 'http://activitystrea.ms/schema/1.0/activity';
72
    const SERVICE   = 'http://activitystrea.ms/schema/1.0/service';
73
    const IMAGE     = 'http://activitystrea.ms/schema/1.0/image';
74
    const COLLECTION = 'http://activitystrea.ms/schema/1.0/collection';
Evan Prodromou's avatar
Evan Prodromou committed
75
    const APPLICATION = 'http://activitystrea.ms/schema/1.0/application';
76 77 78 79 80 81 82 83 84 85 86 87

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

88 89 90 91 92 93 94
    const POSTEROUS   = 'http://posterous.com/help/rss/1.0';
    const AUTHOR      = 'author';
    const USERIMAGE   = 'userImage';
    const PROFILEURL  = 'profileUrl';
    const NICKNAME    = 'nickName';
    const DISPLAYNAME = 'displayName';

95 96 97 98 99 100
    public $element;
    public $type;
    public $id;
    public $title;
    public $summary;
    public $content;
101
    public $owner;
102 103 104 105 106 107 108
    public $link;
    public $source;
    public $avatarLinks = array();
    public $geopoint;
    public $poco;
    public $displayName;

Zach Copley's avatar
Zach Copley committed
109 110 111 112 113 114
    // @todo move this stuff to it's own PHOTO activity object
    const MEDIA_DESCRIPTION = 'description';

    public $thumbnail;
    public $largerImage;
    public $description;
115
    public $extra = array();
Zach Copley's avatar
Zach Copley committed
116

117 118
    public $stream;

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
    /**
     * Constructor
     *
     * This probably needs to be refactored
     * to generate a local class (ActivityPerson, ActivityFile, ...)
     * based on the object type.
     *
     * @param DOMElement $element DOM thing to turn into an Activity thing
     */
    function __construct($element = null)
    {
        if (empty($element)) {
            return;
        }

        $this->element = $element;

        $this->geopoint = $this->_childContent(
            $element,
            ActivityContext::POINT,
            ActivityContext::GEORSS
        );

        if ($element->tagName == 'author') {
            $this->_fromAuthor($element);
        } else if ($element->tagName == 'item') {
            $this->_fromRssItem($element);
        } else {
            $this->_fromAtomEntry($element);
        }

        // Some per-type attributes...
        if ($this->type == self::PERSON || $this->type == self::GROUP) {
            $this->displayName = $this->title;

            $photos = ActivityUtils::getLinks($element, 'photo');
            if (count($photos)) {
                foreach ($photos as $link) {
                    $this->avatarLinks[] = new AvatarLink($link);
                }
            } else {
                $avatars = ActivityUtils::getLinks($element, 'avatar');
                foreach ($avatars as $link) {
                    $this->avatarLinks[] = new AvatarLink($link);
                }
            }

            $this->poco = new PoCo($element);
        }
Zach Copley's avatar
Zach Copley committed
168 169 170 171 172 173 174 175 176 177 178 179

        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
            );
        }
180 181 182 183
        if ($this->type == self::_LIST) {
            $owner = ActivityUtils::child($this->element, Activity::AUTHOR, Activity::SPEC);
            $this->owner = new ActivityObject($owner);
        }
184 185 186 187
    }

    private function _fromAuthor($element)
    {
188 189 190 191 192 193 194
        $this->type = $this->_childContent($element,
                                           Activity::OBJECTTYPE,
                                           Activity::SPEC);

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

196 197 198 199 200 201 202 203 204 205 206 207 208
        // start with <atom:title>

        $title = ActivityUtils::childHtmlContent($element, self::TITLE);

        if (!empty($title)) {
            $this->title = html_entity_decode(strip_tags($title), ENT_QUOTES, 'UTF-8');
        }

        // fall back to <atom:name>

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

210 211 212 213 214 215 216 217 218 219 220
        // 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>
221 222 223 224 225 226 227 228

        if (empty($this->id)) {
            $email = $this->_childContent($element, self::EMAIL);
            if (!empty($email)) {
                // XXX: acct: ?
                $this->id = 'mailto:'.$email;
            }
        }
229 230 231 232 233 234 235 236

        $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;
        }
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
    }

    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

        $title = ActivityUtils::childHtmlContent($element, self::TITLE);

255
        $this->title = html_entity_decode(strip_tags($title), ENT_QUOTES, 'UTF-8');
256 257 258 259

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

        $this->link = ActivityUtils::getPermalink($element);
260 261 262 263 264 265

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

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

268
    // @todo FIXME: rationalize with Activity::_fromRssItem()
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
    private function _fromRssItem($item)
    {
        $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, Activity::RSS);

        $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, Activity::CONTENTNS);

        if (!empty($contentEl)) {
            $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES);
        } else {
            $descriptionEl = ActivityUtils::child($item, Activity::DESCRIPTION, Activity::RSS);
            if (!empty($descriptionEl)) {
                $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES);
            }
        }

        $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, Activity::RSS);

        $guidEl = ActivityUtils::child($item, Activity::GUID, Activity::RSS);

        if (!empty($guidEl)) {
            $this->id = $guidEl->textContent;

            if ($guidEl->hasAttribute('isPermaLink')) {
                // overwrites <link>
                $this->link = $this->id;
            }
        }
    }

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

        if (preg_match('/^(.*?) \((.*)\)$/', $text, $match)) {
            $email = $match[1];
            $name = $match[2];
        } else if (preg_match('/^(.*?) <(.*)>$/', $text, $match)) {
            $name = $match[1];
            $email = $match[2];
        } else if (preg_match('/.*@.*/', $text)) {
            $email = $text;
            $name = null;
        } else {
            $name = $text;
            $email = null;
        }

        // Not really enough info

        $obj = new ActivityObject();

        $obj->element = $el;

        $obj->type  = ActivityObject::PERSON;
        $obj->title = $name;

        if (!empty($email)) {
            $obj->id = 'mailto:'.$email;
        }

        return $obj;
    }

    public static function fromDcCreator($el)
    {
        // Not really enough info

        $text = $el->textContent;

        $obj = new ActivityObject();

        $obj->element = $el;

        $obj->title = $text;
        $obj->type  = ActivityObject::PERSON;

        return $obj;
    }

    public static function fromRssChannel($el)
    {
        $obj = new ActivityObject();

        $obj->element = $el;

        $obj->type = ActivityObject::PERSON; // @fixme guess better

        $obj->title = ActivityUtils::childContent($el, ActivityObject::TITLE, Activity::RSS);
        $obj->link  = ActivityUtils::childContent($el, ActivityUtils::LINK, Activity::RSS);
        $obj->id    = ActivityUtils::getLink($el, Activity::SELF);

        if (empty($obj->id)) {
            $obj->id = $obj->link;
        }

        $desc = ActivityUtils::childContent($el, Activity::DESCRIPTION, Activity::RSS);

        if (!empty($desc)) {
            $obj->content = htmlspecialchars_decode($desc, ENT_QUOTES);
        }

        $imageEl = ActivityUtils::child($el, Activity::IMAGE, Activity::RSS);

        if (!empty($imageEl)) {
373 374 375 376
            $url = ActivityUtils::childContent($imageEl, Activity::URL, Activity::RSS);
            $al = new AvatarLink();
            $al->url = $url;
            $obj->avatarLinks[] = $al;
377 378 379 380 381
        }

        return $obj;
    }

382 383 384 385 386 387 388 389 390
    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)) {
391 392 393
            $al = new AvatarLink();
            $al->url = $userImage;
            $obj->avatarLinks[] = $al;
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
        }

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

409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
    private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
    {
        return ActivityUtils::childContent($element, $tag, $namespace);
    }

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

    private function _getSource($element)
    {
        $sourceEl = ActivityUtils::child($element, 'source');

        if (empty($sourceEl)) {
            return null;
        } else {
            $href = ActivityUtils::getLink($sourceEl, 'self');
            if (!empty($href)) {
                return $href;
            } else {
                return ActivityUtils::childContent($sourceEl, 'id');
            }
        }
    }

    static function fromNotice(Notice $notice)
    {
        $object = new ActivityObject();
435

Siebrand Mazeland's avatar
Siebrand Mazeland committed
436
        if (Event::handle('StartActivityObjectFromNotice', array($notice, &$object))) {
437

Siebrand Mazeland's avatar
Siebrand Mazeland committed
438
            $object->type    = (empty($notice->object_type)) ? ActivityObject::NOTE : $notice->object_type;
439

Siebrand Mazeland's avatar
Siebrand Mazeland committed
440 441 442
            $object->id      = $notice->uri;
            $object->content = $notice->rendered;
            $object->link    = $notice->bestUrl();
443

Siebrand Mazeland's avatar
Siebrand Mazeland committed
444 445
            Event::handle('EndActivityObjectFromNotice', array($notice, &$object));
        }
446 447 448 449 450 451 452 453

        return $object;
    }

    static function fromProfile(Profile $profile)
    {
        $object = new ActivityObject();

Siebrand Mazeland's avatar
Siebrand Mazeland committed
454 455 456 457 458
        if (Event::handle('StartActivityObjectFromProfile', array($profile, &$object))) {
            $object->type   = ActivityObject::PERSON;
            $object->id     = $profile->getUri();
            $object->title  = $profile->getBestName();
            $object->link   = $profile->profileurl;
459

Siebrand Mazeland's avatar
Siebrand Mazeland committed
460
            $orig = $profile->getOriginalAvatar();
461

Siebrand Mazeland's avatar
Siebrand Mazeland committed
462 463 464
            if (!empty($orig)) {
                $object->avatarLinks[] = AvatarLink::fromAvatar($orig);
            }
465

Siebrand Mazeland's avatar
Siebrand Mazeland committed
466
            $sizes = array(
467 468 469 470
                AVATAR_PROFILE_SIZE,
                AVATAR_STREAM_SIZE,
                AVATAR_MINI_SIZE
            );
471

Siebrand Mazeland's avatar
Siebrand Mazeland committed
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
            foreach ($sizes as $size) {
                $alink  = null;
                $avatar = $profile->getAvatar($size);

                if (!empty($avatar)) {
                    $alink = AvatarLink::fromAvatar($avatar);
                } else {
                    $alink = new AvatarLink();
                    $alink->type   = 'image/png';
                    $alink->height = $size;
                    $alink->width  = $size;
                    $alink->url    = Avatar::defaultImage($size);

                    if ($size == AVATAR_PROFILE_SIZE) {
                        // Hack for Twitter import: we don't have a 96x96 image,
                        // but we do have a 73x73 image. For now, fake it with that.
                        $avatar = $profile->getAvatar(73);
                        if ($avatar) {
                            $alink = AvatarLink::fromAvatar($avatar);
                            $alink->height= $size;
                            $alink->width = $size;
                        }
                    }
                }

                $object->avatarLinks[] = $alink;
            }

            if (isset($profile->lat) && isset($profile->lon)) {
                $object->geopoint = (float)$profile->lat
                    . ' ' . (float)$profile->lon;
            }

            $object->poco = PoCo::fromProfile($profile);

507 508 509 510
            if ($profile->getUser()) {
                $object->extra[] = array('followers', array('url' => common_local_url('subscribers', array('nickname' => $profile->nickname))));
            }

Siebrand Mazeland's avatar
Siebrand Mazeland committed
511 512
            Event::handle('EndActivityObjectFromProfile', array($profile, &$object));
        }
513 514 515 516 517 518 519 520

        return $object;
    }

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

Siebrand Mazeland's avatar
Siebrand Mazeland committed
521
        if (Event::handle('StartActivityObjectFromGroup', array($group, &$object))) {
522

Siebrand Mazeland's avatar
Siebrand Mazeland committed
523 524 525 526
            $object->type   = ActivityObject::GROUP;
            $object->id     = $group->getUri();
            $object->title  = $group->getBestName();
            $object->link   = $group->getUri();
527

Siebrand Mazeland's avatar
Siebrand Mazeland committed
528 529
            $object->avatarLinks[] = AvatarLink::fromFilename($group->homepage_logo,
                                                              AVATAR_PROFILE_SIZE);
530

Siebrand Mazeland's avatar
Siebrand Mazeland committed
531 532
            $object->avatarLinks[] = AvatarLink::fromFilename($group->stream_logo,
                                                              AVATAR_STREAM_SIZE);
533

Siebrand Mazeland's avatar
Siebrand Mazeland committed
534 535
            $object->avatarLinks[] = AvatarLink::fromFilename($group->mini_logo,
                                                              AVATAR_MINI_SIZE);
536

Siebrand Mazeland's avatar
Siebrand Mazeland committed
537 538 539
            $object->poco = PoCo::fromGroup($group);
            Event::handle('EndActivityObjectFromGroup', array($group, &$object));
        }
540 541 542

        return $object;
    }
543

544 545 546 547 548 549 550 551 552 553 554 555
    static function fromPeopletag($ptag)
    {
        $object = new ActivityObject();
        if (Event::handle('StartActivityObjectFromPeopletag', array($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::staticGet('id', $ptag->tagger);
            $object->poco    = PoCo::fromProfile($object->owner);
Siebrand Mazeland's avatar
Siebrand Mazeland committed
556
            Event::handle('EndActivityObjectFromPeopletag', array($ptag, &$object));
557 558 559 560
        }
        return $object;
    }

561 562 563 564 565 566 567 568
    static function fromFile(File $file)
    {
        $object = new ActivityObject();

        if (Event::handle('StartActivityObjectFromFile', array($file, &$object))) {

            $object->type = self::mimeTypeToObjectType($file->mimetype);
            $object->id   = TagURI::mint(sprintf("file:%d", $file->id));
569
            $object->link = common_local_url('attachment', array('attachment' => $file->id));
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584

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

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

            $thumbnail = $file->getThumbnail();

            if (!empty($thumbnail)) {
                $object->thumbnail = $thumbnail;
            }

Evan Prodromou's avatar
Evan Prodromou committed
585 586
            switch (ActivityObject::canonicalType($object->type)) {
            case 'image':
587 588
                $object->largerImage = $file->url;
                break;
Evan Prodromou's avatar
Evan Prodromou committed
589 590
            case 'video':
            case 'audio':
591 592 593 594 595 596 597 598 599 600
                $object->stream = $file->url;
                break;
            }

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

        return $object;
    }

Evan Prodromou's avatar
Evan Prodromou committed
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
    static function fromNoticeSource(Notice_source $source)
    {
        $object = new ActivityObject();

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

            if (in_array($source->code, array('web', 'xmpp', 'mail', 'omb', 'system', 'api'))) {
                // We use one ID for all well-known StatusNet sources
                $object->id = "tag:status.net,2009:notice-source:".$source->code;
            } else if ($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->extras[] = array('status_net', array('source_code' => $source->code));

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

        return $object;
    }

Siebrand Mazeland's avatar
Siebrand Mazeland committed
641 642 643 644 645
    function outputTo($xo, $tag='activity:object')
    {
        if (!empty($tag)) {
            $xo->elementStart($tag);
        }
646

647 648
        if (Event::handle('StartActivityObjectOutputAtom', array($this, $xo))) {
            $xo->element('activity:object-type', null, $this->type);
649

650
            // <author> uses URI
651 652

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

658 659 660 661 662 663 664 665 666 667 668
            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);
                }
            }
669

670 671 672 673 674 675 676
            if (!empty($this->summary)) {
                $xo->element(
                    self::SUMMARY,
                    null,
                    common_xml_safe_str($this->summary)
                );
            }
677

678 679 680 681 682 683 684 685
            if (!empty($this->content)) {
                // XXX: assuming HTML content here
                $xo->element(
                    ActivityUtils::CONTENT,
                    array('type' => 'html'),
                    common_xml_safe_str($this->content)
                );
            }
686

687
            if (!empty($this->link)) {
688
                $xo->element(
689 690 691 692 693
                    'link',
                    array(
                        'rel' => 'alternate',
                        'type' => 'text/html',
                        'href' => $this->link
694 695 696 697 698
                    ),
                    null
                );
            }

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

704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
            if ($this->type == ActivityObject::PERSON
                || $this->type == ActivityObject::GROUP) {

                foreach ($this->avatarLinks as $avatar) {
                    $xo->element(
                        'link', array(
                            'rel'  => 'avatar',
                            'type'         => $avatar->type,
                            'media:width'  => $avatar->width,
                            'media:height' => $avatar->height,
                            'href' => $avatar->url
                        ),
                        null
                    );
                }
            }
720

721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738
            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) = $el;
                $xo->element($extraTag, $attrs, $content);
            }
739

740
            Event::handle('EndActivityObjectOutputAtom', array($this, $xo));
741 742
        }

Siebrand Mazeland's avatar
Siebrand Mazeland committed
743 744 745
        if (!empty($tag)) {
            $xo->elementEnd($tag);
        }
746 747

        return;
Siebrand Mazeland's avatar
Siebrand Mazeland committed
748
    }
749 750 751 752 753

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

Siebrand Mazeland's avatar
Siebrand Mazeland committed
754
        $this->outputTo($xs, $tag);
755 756 757

        return $xs->getString();
    }
758 759 760 761 762 763 764 765 766 767 768 769

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

    function asArray()
    {
        $object = array();

770
        if (Event::handle('StartActivityObjectOutputJson', array($this, &$object))) {
771 772 773 774 775
            // XXX: attachments are added by Activity

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

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

777
            // displayName
778 779 780 781

            if ($this->title) {
                $object['displayName'] = $this->title;
            }
782

783
            // downstreamDuplicates
784

785
            // id
786 787 788 789 790 791

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

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

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

799
                $imgLink          = null;
800
                $avatarMediaLinks = array();
801

802
                foreach ($this->avatarLinks as $a) {
Zach Copley's avatar
Zach Copley committed
803

804 805 806 807 808 809 810 811
                    // Make a MediaLink for every other Avatar
                    $avatar = new ActivityStreamsMediaLink(
                        $a->url,
                        $a->width,
                        $a->height,
                        $a->type,
                        'avatar'
                    );
Zach Copley's avatar
Zach Copley committed
812

813 814 815 816
                    // Find the big avatar to use as the "image"
                    if ($a->height == AVATAR_PROFILE_SIZE) {
                        $imgLink = $avatar;
                    }
Zach Copley's avatar
Zach Copley committed
817

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

821 822 823 824 825
                if (!array_key_exists('status_net', $object)) {
                    $object['status_net'] = array();
                }

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

827
                // image
828 829 830
                if (!empty($imgLink)) {
                    $object['image']  = $imgLink->asArray();
                }
831
            }
832

833 834 835 836
            // objectType
            //
            // We can probably use the whole schema URL here but probably the
            // relative simple name is easier to parse
837 838

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

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

Evan Prodromou's avatar
Evan Prodromou committed
843
            // content
844 845 846 847 848
            $object['content'] = $this->content;

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

            // updated (probably don't need this)
849

850
            // TODO: upstreamDuplicates
851

Evan Prodromou's avatar
Evan Prodromou committed
852 853 854
            if ($this->link) {
                $object['url'] = $this->link;
            }
Zach Copley's avatar
Zach Copley committed
855

856 857 858 859
            /* 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
860

861 862
            foreach ($this->extra as $e) {
                list($objectName, $props, $txt) = $e;
863 864 865 866 867 868 869 870 871 872 873
                if (!empty($objectName)) {
                    $parts = explode(":", $objectName);
                    if (count($parts) == 2 && $parts[0] == "statusnet") {
                        if (!array_key_exists('status_net', $object)) {
                            $object['status_net'] = array();
                        }
                        $object['status_net'][$parts[1]] = $props;
                    } else {
                        $object[$objectName] = $props;
                    }
                }
874
            }
875

876
            if (!empty($this->geopoint)) {
877

Evan Prodromou's avatar
Evan Prodromou committed
878
                list($lat, $lon) = explode(' ', $this->geopoint);
879

Evan Prodromou's avatar
Evan Prodromou committed
880 881 882 883 884
                $object['location'] = array(
                    'objectType' => 'place',
                    'position' => sprintf("%+02.5F%+03.5F/", $lat, $lon),
                    'lat' => $lat,
                    'lon' => $lon
885
                );
Evan Prodromou's avatar
Evan Prodromou committed
886 887 888 889 890 891 892 893 894 895 896 897 898 899 900

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

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

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

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

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

907 908 909 910 911 912 913 914 915 916 917 918 919 920
            if (!empty($this->thumbnail)) {
                if (is_string($this->thumbnail)) {
                    $object['image'] = array('url' => $this->thumbnail);
                } else {
                    $object['image'] = array('url' => $this->thumbnail->url);
                    if ($this->thumbnail->width) {
                        $object['image']['width'] = $this->thumbnail->width;
                    }
                    if ($this->thumbnail->height) {
                        $object['image']['height'] = $this->thumbnail->height;
                    }
                }
            }

Evan Prodromou's avatar
Evan Prodromou committed
921
            switch (ActivityObject::canonicalType($this->type)) {
Evan Prodromou's avatar
Evan Prodromou committed
922
            case 'image':
923 924 925 926
                if (!empty($this->largerImage)) {
                    $object['fullImage'] = array('url' => $this->largerImage);
                }
                break;
Evan Prodromou's avatar
Evan Prodromou committed
927 928
            case 'audio':
            case 'video':
929 930 931 932 933 934
                if (!empty($this->stream)) {
                    $object['stream'] = array('url' => $this->stream);
                }
                break;
            }

935
            Event::handle('EndActivityObjectOutputJson', array($this, &$object));
936
        }
937
        return array_filter($object);
938
    }
939 940 941 942 943 944 945 946 947

    static function canonicalType($type) {
        $ns = 'http://activitystrea.ms/schema/1.0/';
        if (substr($type, 0, mb_strlen($ns)) == $ns) {
            return substr($type, mb_strlen($ns));
        } else {
            return $type;
        }
    }
948 949 950

    static function mimeTypeToObjectType($mimeType) {
        $ot = null;
951 952 953 954 955 956 957

        // Default

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

958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975
        $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;
    }
976
}