]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/activity.php
5cbab8d5f353296101c953a0baf4d14740b9b341
[quix0rs-gnu-social.git] / lib / activity.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * An activity
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  Feed
23  * @package   StatusNet
24  * @author    Evan Prodromou <evan@status.net>
25  * @author    Zach Copley <zach@status.net>
26  * @copyright 2010 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET')) {
32     exit(1);
33 }
34
35 class PoCoURL
36 {
37     const URLS      = 'urls';
38     const TYPE      = 'type';
39     const VALUE     = 'value';
40     const PRIMARY   = 'primary';
41
42     public $type;
43     public $value;
44     public $primary;
45
46     function __construct($type, $value, $primary = false)
47     {
48         $this->type    = $type;
49         $this->value   = $value;
50         $this->primary = $primary;
51     }
52
53     function asString()
54     {
55         $xs = new XMLStringer(true);
56         $xs->elementStart('poco:urls');
57         $xs->element('poco:type', null, $this->type);
58         $xs->element('poco:value', null, $this->value);
59         if (!empty($this->primary)) {
60             $xs->element('poco:primary', null, 'true');
61         }
62         $xs->elementEnd('poco:urls');
63         return $xs->getString();
64     }
65 }
66
67 class PoCoAddress
68 {
69     const ADDRESS   = 'address';
70     const FORMATTED = 'formatted';
71
72     public $formatted;
73
74     // @todo Other address fields
75
76     function asString()
77     {
78         if (!empty($this->formatted)) {
79             $xs = new XMLStringer(true);
80             $xs->elementStart('poco:address');
81             $xs->element('poco:formatted', null, $this->formatted);
82             $xs->elementEnd('poco:address');
83             return $xs->getString();
84         }
85
86         return null;
87     }
88 }
89
90 class PoCo
91 {
92     const NS = 'http://portablecontacts.net/spec/1.0';
93
94     const USERNAME     = 'preferredUsername';
95     const DISPLAYNAME  = 'displayName';
96     const NOTE         = 'note';
97
98     public $preferredUsername;
99     public $displayName;
100     public $note;
101     public $address;
102     public $urls = array();
103
104     function __construct($element = null)
105     {
106         if (empty($element)) {
107             return;
108         }
109
110         $this->preferredUsername = ActivityUtils::childContent(
111             $element,
112             self::USERNAME,
113             self::NS
114         );
115
116         $this->displayName = ActivityUtils::childContent(
117             $element,
118             self::DISPLAYNAME,
119             self::NS
120         );
121
122         $this->note = ActivityUtils::childContent(
123             $element,
124             self::NOTE,
125             self::NS
126         );
127
128         $this->address = $this->_getAddress($element);
129         $this->urls = $this->_getURLs($element);
130     }
131
132     private function _getURLs($element)
133     {
134         $urlEls = $element->getElementsByTagnameNS(self::NS, PoCoURL::URLS);
135         $urls = array();
136
137         foreach ($urlEls as $urlEl) {
138
139             $type = ActivityUtils::childContent(
140                 $urlEl,
141                 PoCoURL::TYPE,
142                 PoCo::NS
143             );
144
145             $value = ActivityUtils::childContent(
146                 $urlEl,
147                 PoCoURL::VALUE,
148                 PoCo::NS
149             );
150
151             $primary = ActivityUtils::childContent(
152                 $urlEl,
153                 PoCoURL::PRIMARY,
154                 PoCo::NS
155             );
156
157             array_push($urls, new PoCoURL($type, $value, $primary));
158         }
159         return $urls;
160     }
161
162     private function _getAddress($element)
163     {
164         $addressEl = ActivityUtils::child(
165             $element,
166             PoCoAddress::ADDRESS,
167             PoCo::NS
168         );
169
170         $formatted = ActivityUtils::childContent(
171             $addressEl,
172             PoCoAddress::FORMATTED,
173             self::NS
174         );
175
176         if (!empty($formatted)) {
177             $address = new PoCoAddress();
178             $address->formatted = $formatted;
179             return $address;
180         }
181
182         return null;
183     }
184
185     function fromProfile($profile)
186     {
187         if (empty($profile)) {
188             return null;
189         }
190
191         $poco = new PoCo();
192
193         $poco->preferredUsername = $profile->nickname;
194         $poco->displayName       = $profile->getBestName();
195
196         $poco->note = $profile->bio;
197
198         $paddy = new PoCoAddress();
199         $paddy->formatted = $profile->location;
200         $poco->address = $paddy;
201
202         if (!empty($profile->homepage)) {
203             array_push(
204                 $poco->urls,
205                 new PoCoURL(
206                     'homepage',
207                     $profile->homepage,
208                     true
209                 )
210             );
211         }
212
213         return $poco;
214     }
215
216     function asString()
217     {
218         $xs = new XMLStringer(true);
219         $xs->element(
220             'poco:preferredUsername',
221             null,
222             $this->preferredUsername
223         );
224
225         $xs->element(
226             'poco:displayName',
227             null,
228             $this->displayName
229         );
230
231         if (!empty($this->note)) {
232             $xs->element('poco:note', null, $this->note);
233         }
234
235         if (!empty($this->address)) {
236             $xs->raw($this->address->asString());
237         }
238
239         foreach ($this->urls as $url) {
240             $xs->raw($url->asString());
241         }
242
243         return $xs->getString();
244     }
245 }
246
247 /**
248  * Utilities for turning DOMish things into Activityish things
249  *
250  * Some common functions that I didn't have the bandwidth to try to factor
251  * into some kind of reasonable superclass, so just dumped here. Might
252  * be useful to have an ActivityObject parent class or something.
253  *
254  * @category  OStatus
255  * @package   StatusNet
256  * @author    Evan Prodromou <evan@status.net>
257  * @copyright 2010 StatusNet, Inc.
258  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
259  * @link      http://status.net/
260  */
261
262 class ActivityUtils
263 {
264     const ATOM = 'http://www.w3.org/2005/Atom';
265
266     const LINK = 'link';
267     const REL  = 'rel';
268     const TYPE = 'type';
269     const HREF = 'href';
270
271     const CONTENT = 'content';
272     const SRC     = 'src';
273
274     /**
275      * Get the permalink for an Activity object
276      *
277      * @param DOMElement $element A DOM element
278      *
279      * @return string related link, if any
280      */
281
282     static function getPermalink($element)
283     {
284         return self::getLink($element, 'alternate', 'text/html');
285     }
286
287     /**
288      * Get the permalink for an Activity object
289      *
290      * @param DOMElement $element A DOM element
291      *
292      * @return string related link, if any
293      */
294
295     static function getLink($element, $rel, $type=null)
296     {
297         $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
298
299         foreach ($links as $link) {
300
301             $linkRel = $link->getAttribute(self::REL);
302             $linkType = $link->getAttribute(self::TYPE);
303
304             if ($linkRel == $rel &&
305                 (is_null($type) || $linkType == $type)) {
306                 return $link->getAttribute(self::HREF);
307             }
308         }
309
310         return null;
311     }
312
313     /**
314      * Gets the first child element with the given tag
315      *
316      * @param DOMElement $element   element to pick at
317      * @param string     $tag       tag to look for
318      * @param string     $namespace Namespace to look under
319      *
320      * @return DOMElement found element or null
321      */
322
323     static function child($element, $tag, $namespace=self::ATOM)
324     {
325         $els = $element->childNodes;
326         if (empty($els) || $els->length == 0) {
327             return null;
328         } else {
329             for ($i = 0; $i < $els->length; $i++) {
330                 $el = $els->item($i);
331                 if ($el->localName == $tag && $el->namespaceURI == $namespace) {
332                     return $el;
333                 }
334             }
335         }
336     }
337
338     /**
339      * Grab the text content of a DOM element child of the current element
340      *
341      * @param DOMElement $element   Element whose children we examine
342      * @param string     $tag       Tag to look up
343      * @param string     $namespace Namespace to use, defaults to Atom
344      *
345      * @return string content of the child
346      */
347
348     static function childContent($element, $tag, $namespace=self::ATOM)
349     {
350         $el = self::child($element, $tag, $namespace);
351
352         if (empty($el)) {
353             return null;
354         } else {
355             return $el->textContent;
356         }
357     }
358
359     /**
360      * Get the content of an atom:entry-like object
361      *
362      * @param DOMElement $element The element to examine.
363      *
364      * @return string unencoded HTML content of the element, like "This -&lt; is <b>HTML</b>."
365      *
366      * @todo handle remote content
367      * @todo handle embedded XML mime types
368      * @todo handle base64-encoded non-XML and non-text mime types
369      */
370
371     static function getContent($element)
372     {
373         $contentEl = ActivityUtils::child($element, self::CONTENT);
374
375         if (!empty($contentEl)) {
376
377             $src  = $contentEl->getAttribute(self::SRC);
378
379             if (!empty($src)) {
380                 throw new ClientException(_("Can't handle remote content yet."));
381             }
382
383             $type = $contentEl->getAttribute(self::TYPE);
384
385             // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
386
387             if ($type == 'text') {
388                 return $contentEl->textContent;
389             } else if ($type == 'html') {
390                 $text = $contentEl->textContent;
391                 return htmlspecialchars_decode($text, ENT_QUOTES);
392             } else if ($type == 'xhtml') {
393                 $divEl = ActivityUtils::child($contentEl, 'div');
394                 if (empty($divEl)) {
395                     return null;
396                 }
397                 $doc = $divEl->ownerDocument;
398                 $text = '';
399                 $children = $divEl->childNodes;
400
401                 for ($i = 0; $i < $children->length; $i++) {
402                     $child = $children->item($i);
403                     $text .= $doc->saveXML($child);
404                 }
405                 return trim($text);
406             } else if (in_array(array('text/xml', 'application/xml'), $type) ||
407                        preg_match('#(+|/)xml$#', $type)) {
408                 throw new ClientException(_("Can't handle embedded XML content yet."));
409             } else if (strncasecmp($type, 'text/', 5)) {
410                 return $contentEl->textContent;
411             } else {
412                 throw new ClientException(_("Can't handle embedded Base64 content yet."));
413             }
414         }
415     }
416 }
417
418 /**
419  * A noun-ish thing in the activity universe
420  *
421  * The activity streams spec talks about activity objects, while also having
422  * a tag activity:object, which is in fact an activity object. Aaaaaah!
423  *
424  * This is just a thing in the activity universe. Can be the subject, object,
425  * or indirect object (target!) of an activity verb. Rotten name, and I'm
426  * propagating it. *sigh*
427  *
428  * @category  OStatus
429  * @package   StatusNet
430  * @author    Evan Prodromou <evan@status.net>
431  * @copyright 2010 StatusNet, Inc.
432  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
433  * @link      http://status.net/
434  */
435
436 class ActivityObject
437 {
438     const ARTICLE   = 'http://activitystrea.ms/schema/1.0/article';
439     const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
440     const NOTE      = 'http://activitystrea.ms/schema/1.0/note';
441     const STATUS    = 'http://activitystrea.ms/schema/1.0/status';
442     const FILE      = 'http://activitystrea.ms/schema/1.0/file';
443     const PHOTO     = 'http://activitystrea.ms/schema/1.0/photo';
444     const ALBUM     = 'http://activitystrea.ms/schema/1.0/photo-album';
445     const PLAYLIST  = 'http://activitystrea.ms/schema/1.0/playlist';
446     const VIDEO     = 'http://activitystrea.ms/schema/1.0/video';
447     const AUDIO     = 'http://activitystrea.ms/schema/1.0/audio';
448     const BOOKMARK  = 'http://activitystrea.ms/schema/1.0/bookmark';
449     const PERSON    = 'http://activitystrea.ms/schema/1.0/person';
450     const GROUP     = 'http://activitystrea.ms/schema/1.0/group';
451     const PLACE     = 'http://activitystrea.ms/schema/1.0/place';
452     const COMMENT   = 'http://activitystrea.ms/schema/1.0/comment';
453     // ^^^^^^^^^^ tea!
454
455     // Atom elements we snarf
456
457     const TITLE   = 'title';
458     const SUMMARY = 'summary';
459     const ID      = 'id';
460     const SOURCE  = 'source';
461
462     const NAME  = 'name';
463     const URI   = 'uri';
464     const EMAIL = 'email';
465
466     public $element;
467     public $type;
468     public $id;
469     public $title;
470     public $summary;
471     public $content;
472     public $link;
473     public $source;
474     public $avatar;
475     public $geopoint;
476     public $poco;
477     public $displayName;
478
479     /**
480      * Constructor
481      *
482      * This probably needs to be refactored
483      * to generate a local class (ActivityPerson, ActivityFile, ...)
484      * based on the object type.
485      *
486      * @param DOMElement $element DOM thing to turn into an Activity thing
487      */
488
489     function __construct($element = null)
490     {
491         if (empty($element)) {
492             return;
493         }
494
495         $this->element = $element;
496
497         if ($element->tagName == 'author') {
498
499             $this->type  = self::PERSON; // XXX: is this fair?
500             $this->title = $this->_childContent($element, self::NAME);
501             $this->id    = $this->_childContent($element, self::URI);
502
503             if (empty($this->id)) {
504                 $email = $this->_childContent($element, self::EMAIL);
505                 if (!empty($email)) {
506                     // XXX: acct: ?
507                     $this->id = 'mailto:'.$email;
508                 }
509             }
510
511         } else {
512
513             $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
514                                                Activity::SPEC);
515
516             if (empty($this->type)) {
517                 $this->type = ActivityObject::NOTE;
518             }
519
520             $this->id      = $this->_childContent($element, self::ID);
521             $this->title   = $this->_childContent($element, self::TITLE);
522             $this->summary = $this->_childContent($element, self::SUMMARY);
523
524             $this->source  = $this->_getSource($element);
525
526             $this->content = ActivityUtils::getContent($element);
527
528             $this->link = ActivityUtils::getPermalink($element);
529
530         }
531
532         // Some per-type attributes...
533         if ($this->type == self::PERSON || $this->type == self::GROUP) {
534             $this->displayName = $this->title;
535
536             // @fixme we may have multiple avatars with different resolutions specified
537             $this->avatar = ActivityUtils::getLink($element, 'avatar');
538
539             $this->poco = new PoCo($element);
540         }
541     }
542
543     private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
544     {
545         return ActivityUtils::childContent($element, $tag, $namespace);
546     }
547
548     // Try to get a unique id for the source feed
549
550     private function _getSource($element)
551     {
552         $sourceEl = ActivityUtils::child($element, 'source');
553
554         if (empty($sourceEl)) {
555             return null;
556         } else {
557             $href = ActivityUtils::getLink($sourceEl, 'self');
558             if (!empty($href)) {
559                 return $href;
560             } else {
561                 return ActivityUtils::childContent($sourceEl, 'id');
562             }
563         }
564     }
565
566     static function fromNotice($notice)
567     {
568         $object = new ActivityObject();
569
570         $object->type    = ActivityObject::NOTE;
571
572         $object->id      = $notice->uri;
573         $object->title   = $notice->content;
574         $object->content = $notice->rendered;
575         $object->link    = $notice->bestUrl();
576
577         return $object;
578     }
579
580     static function fromProfile($profile)
581     {
582         $object = new ActivityObject();
583
584         $object->type   = ActivityObject::PERSON;
585         $object->id     = $profile->getUri();
586         $object->title  = $profile->getBestName();
587         $object->link   = $profile->profileurl;
588         $object->avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
589
590         if (isset($profile->lat) && isset($profile->lon)) {
591             $object->geopoint = (float)$profile->lat . ' ' . (float)$profile->lon;
592         }
593
594         $object->poco = PoCo::fromProfile($profile);
595
596         return $object;
597     }
598
599     function asString($tag='activity:object')
600     {
601         $xs = new XMLStringer(true);
602
603         $xs->elementStart($tag);
604
605         $xs->element('activity:object-type', null, $this->type);
606
607         $xs->element(self::ID, null, $this->id);
608
609         if (!empty($this->title)) {
610             $xs->element(self::TITLE, null, $this->title);
611         }
612
613         if (!empty($this->summary)) {
614             $xs->element(self::SUMMARY, null, $this->summary);
615         }
616
617         if (!empty($this->content)) {
618             // XXX: assuming HTML content here
619             $xs->element(ActivityUtils::CONTENT, array('type' => 'html'), $this->content);
620         }
621
622         if (!empty($this->link)) {
623             $xs->element(
624                 'link',
625                 array(
626                     'rel' => 'alternate',
627                     'type' => 'text/html',
628                     'href' => $this->link
629                 ),
630                 null
631             );
632         }
633
634         if ($this->type == ActivityObject::PERSON
635             || $this->type == ActivityObject::GROUP) {
636             $xs->element(
637                 'link', array(
638                     'type' => empty($this->avatar) ? 'image/png' : $this->avatar->mediatype,
639                     'rel'  => 'avatar',
640                     'href' => empty($this->avatar)
641                     ? Avatar::defaultImage(AVATAR_PROFILE_SIZE)
642                     : $this->avatar->displayUrl()
643                 ),
644                 null
645             );
646         }
647
648         if (!empty($this->geopoint)) {
649             $xs->element(
650                 'georss:point',
651                 null,
652                 $this->geopoint
653             );
654         }
655
656         if (!empty($this->poco)) {
657             $xs->raw($this->poco->asString());
658         }
659
660         $xs->elementEnd($tag);
661
662         return $xs->getString();
663     }
664 }
665
666 /**
667  * Utility class to hold a bunch of constant defining default verb types
668  *
669  * @category  OStatus
670  * @package   StatusNet
671  * @author    Evan Prodromou <evan@status.net>
672  * @copyright 2010 StatusNet, Inc.
673  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
674  * @link      http://status.net/
675  */
676
677 class ActivityVerb
678 {
679     const POST     = 'http://activitystrea.ms/schema/1.0/post';
680     const SHARE    = 'http://activitystrea.ms/schema/1.0/share';
681     const SAVE     = 'http://activitystrea.ms/schema/1.0/save';
682     const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite';
683     const PLAY     = 'http://activitystrea.ms/schema/1.0/play';
684     const FOLLOW   = 'http://activitystrea.ms/schema/1.0/follow';
685     const FRIEND   = 'http://activitystrea.ms/schema/1.0/make-friend';
686     const JOIN     = 'http://activitystrea.ms/schema/1.0/join';
687     const TAG      = 'http://activitystrea.ms/schema/1.0/tag';
688
689     // Custom OStatus verbs for the flipside until they're standardized
690     const DELETE     = 'http://ostatus.org/schema/1.0/unfollow';
691     const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite';
692     const UNFOLLOW   = 'http://ostatus.org/schema/1.0/unfollow';
693     const LEAVE      = 'http://ostatus.org/schema/1.0/leave';
694 }
695
696 class ActivityContext
697 {
698     public $replyToID;
699     public $replyToUrl;
700     public $location;
701     public $attention = array();
702     public $conversation;
703
704     const THR     = 'http://purl.org/syndication/thread/1.0';
705     const GEORSS  = 'http://www.georss.org/georss';
706     const OSTATUS = 'http://ostatus.org/schema/1.0';
707
708     const INREPLYTO = 'in-reply-to';
709     const REF       = 'ref';
710     const HREF      = 'href';
711
712     const POINT     = 'point';
713
714     const ATTENTION    = 'ostatus:attention';
715     const CONVERSATION = 'ostatus:conversation';
716
717     function __construct($element)
718     {
719         $replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR);
720
721         if (!empty($replyToEl)) {
722             $this->replyToID  = $replyToEl->getAttribute(self::REF);
723             $this->replyToUrl = $replyToEl->getAttribute(self::HREF);
724         }
725
726         $this->location = $this->getLocation($element);
727
728         $this->conversation = ActivityUtils::getLink($element, self::CONVERSATION);
729
730         // Multiple attention links allowed
731
732         $links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK);
733
734         for ($i = 0; $i < $links->length; $i++) {
735
736             $link = $links->item($i);
737
738             $linkRel = $link->getAttribute(ActivityUtils::REL);
739
740             if ($linkRel == self::ATTENTION) {
741                 $this->attention[] = $link->getAttribute(self::HREF);
742             }
743         }
744     }
745
746     /**
747      * Parse location given as a GeoRSS-simple point, if provided.
748      * http://www.georss.org/simple
749      *
750      * @param feed item $entry
751      * @return mixed Location or false
752      */
753     function getLocation($dom)
754     {
755         $points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT);
756
757         for ($i = 0; $i < $points->length; $i++) {
758             $point = $points->item($i)->textContent;
759             $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
760             $point = preg_replace('/\s+/', ' ', $point);
761             $point = trim($point);
762             $coords = explode(' ', $point);
763             if (count($coords) == 2) {
764                 list($lat, $lon) = $coords;
765                 if (is_numeric($lat) && is_numeric($lon)) {
766                     common_log(LOG_INFO, "Looking up location for $lat $lon from georss");
767                     return Location::fromLatLon($lat, $lon);
768                 }
769             }
770             common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
771         }
772
773         return null;
774     }
775 }
776
777 /**
778  * An activity in the ActivityStrea.ms world
779  *
780  * An activity is kind of like a sentence: someone did something
781  * to something else.
782  *
783  * 'someone' is the 'actor'; 'did something' is the verb;
784  * 'something else' is the object.
785  *
786  * @category  OStatus
787  * @package   StatusNet
788  * @author    Evan Prodromou <evan@status.net>
789  * @copyright 2010 StatusNet, Inc.
790  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
791  * @link      http://status.net/
792  */
793
794 class Activity
795 {
796     const SPEC   = 'http://activitystrea.ms/spec/1.0/';
797     const SCHEMA = 'http://activitystrea.ms/schema/1.0/';
798
799     const VERB       = 'verb';
800     const OBJECT     = 'object';
801     const ACTOR      = 'actor';
802     const SUBJECT    = 'subject';
803     const OBJECTTYPE = 'object-type';
804     const CONTEXT    = 'context';
805     const TARGET     = 'target';
806
807     const ATOM = 'http://www.w3.org/2005/Atom';
808
809     const AUTHOR    = 'author';
810     const PUBLISHED = 'published';
811     const UPDATED   = 'updated';
812
813     public $actor;   // an ActivityObject
814     public $verb;    // a string (the URL)
815     public $object;  // an ActivityObject
816     public $target;  // an ActivityObject
817     public $context; // an ActivityObject
818     public $time;    // Time of the activity
819     public $link;    // an ActivityObject
820     public $entry;   // the source entry
821     public $feed;    // the source feed
822
823     public $summary; // summary of activity
824     public $content; // HTML content of activity
825     public $id;      // ID of the activity
826     public $title;   // title of the activity
827
828     /**
829      * Turns a regular old Atom <entry> into a magical activity
830      *
831      * @param DOMElement $entry Atom entry to poke at
832      * @param DOMElement $feed  Atom feed, for context
833      */
834
835     function __construct($entry = null, $feed = null)
836     {
837         if (is_null($entry)) {
838             return;
839         }
840
841         $this->entry = $entry;
842         $this->feed  = $feed;
843
844         $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
845
846         if (!empty($pubEl)) {
847             $this->time = strtotime($pubEl->textContent);
848         } else {
849             // XXX technically an error; being liberal. Good idea...?
850             $updateEl = $this->_child($entry, self::UPDATED, self::ATOM);
851             if (!empty($updateEl)) {
852                 $this->time = strtotime($updateEl->textContent);
853             } else {
854                 $this->time = null;
855             }
856         }
857
858         $this->link = ActivityUtils::getPermalink($entry);
859
860         $verbEl = $this->_child($entry, self::VERB);
861
862         if (!empty($verbEl)) {
863             $this->verb = trim($verbEl->textContent);
864         } else {
865             $this->verb = ActivityVerb::POST;
866             // XXX: do other implied stuff here
867         }
868
869         $objectEl = $this->_child($entry, self::OBJECT);
870
871         if (!empty($objectEl)) {
872             $this->object = new ActivityObject($objectEl);
873         } else {
874             $this->object = new ActivityObject($entry);
875         }
876
877         $actorEl = $this->_child($entry, self::ACTOR);
878
879         if (!empty($actorEl)) {
880
881             $this->actor = new ActivityObject($actorEl);
882
883         } else if (!empty($feed) &&
884                    $subjectEl = $this->_child($feed, self::SUBJECT)) {
885
886             $this->actor = new ActivityObject($subjectEl);
887
888         } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
889
890             $this->actor = new ActivityObject($authorEl);
891
892         } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
893                                                               self::ATOM)) {
894
895             $this->actor = new ActivityObject($authorEl);
896         }
897
898         $contextEl = $this->_child($entry, self::CONTEXT);
899
900         if (!empty($contextEl)) {
901             $this->context = new ActivityContext($contextEl);
902         } else {
903             $this->context = new ActivityContext($entry);
904         }
905
906         $targetEl = $this->_child($entry, self::TARGET);
907
908         if (!empty($targetEl)) {
909             $this->target = new ActivityObject($targetEl);
910         }
911
912         $this->summary = ActivityUtils::childContent($entry, 'summary');
913         $this->id      = ActivityUtils::childContent($entry, 'id');
914         $this->content = ActivityUtils::getContent($entry);
915     }
916
917     /**
918      * Returns an Atom <entry> based on this activity
919      *
920      * @return DOMElement Atom entry
921      */
922
923     function toAtomEntry()
924     {
925         return null;
926     }
927
928     function asString($namespace=false)
929     {
930         $xs = new XMLStringer(true);
931
932         if ($namespace) {
933             $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
934                            'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
935                            'xmlns:georss' => 'http://www.georss.org/georss',
936                            'xmlns:ostatus' => 'http://ostatus.org/schema/1.0',
937                            'xmlns:poco' => 'http://portablecontacts.net/spec/1.0');
938         } else {
939             $attrs = array();
940         }
941
942         $xs->elementStart('entry', $attrs);
943
944         $xs->element('id', null, $this->id);
945         $xs->element('title', null, $this->title);
946         $xs->element('published', null, common_date_iso8601($this->time));
947         $xs->element('content', array('type' => 'html'), $this->content);
948
949         if (!empty($this->summary)) {
950             $xs->element('summary', null, $this->summary);
951         }
952
953         if (!empty($this->link)) {
954             $xs->element('link', array('rel' => 'alternate',
955                                        'type' => 'text/html'),
956                          $this->link);
957         }
958
959         // XXX: add context
960         // XXX: add target
961
962         $xs->raw($this->actor->asString('activity:actor'));
963         $xs->element('activity:verb', null, $this->verb);
964         $xs->raw($this->object->asString());
965
966         $xs->elementEnd('entry');
967
968         return $xs->getString();
969     }
970
971     private function _child($element, $tag, $namespace=self::SPEC)
972     {
973         return ActivityUtils::child($element, $tag, $namespace);
974     }
975 }