]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/activityutils.php
Merge branch 'nightly' into 'nightly'
[quix0rs-gnu-social.git] / lib / activityutils.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 /**
36  * Utilities for turning DOMish things into Activityish things
37  *
38  * Some common functions that I didn't have the bandwidth to try to factor
39  * into some kind of reasonable superclass, so just dumped here. Might
40  * be useful to have an ActivityObject parent class or something.
41  *
42  * @category  OStatus
43  * @package   StatusNet
44  * @author    Evan Prodromou <evan@status.net>
45  * @copyright 2010 StatusNet, Inc.
46  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
47  * @link      http://status.net/
48  */
49 class ActivityUtils
50 {
51     const ATOM = 'http://www.w3.org/2005/Atom';
52
53     const LINK = 'link';
54     const REL  = 'rel';
55     const TYPE = 'type';
56     const HREF = 'href';
57
58     const CONTENT = 'content';
59     const SRC     = 'src';
60
61     /**
62      * Get the permalink for an Activity object
63      *
64      * @param DOMElement $element A DOM element
65      *
66      * @return string related link, if any
67      */
68     static function getPermalink(DOMNode $element)
69     {
70         return self::getLink($element, 'alternate', 'text/html');
71     }
72
73     static function getSelfLink(DOMNode $element)
74     {
75         return self::getLink($element, 'self', 'application/atom+xml');
76     }
77
78     /**
79      * Get the permalink for an Activity object
80      *
81      * @param DOMElement $element A DOM element
82      *
83      * @return string related link, if any
84      */
85     static function getLink(DOMNode $element, $rel, $type=null)
86     {
87         $els = $element->childNodes;
88
89         foreach ($els as $link) {
90             if (!($link instanceof DOMElement)) {
91                 continue;
92             }
93
94             if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
95                 $linkRel = $link->getAttribute(self::REL);
96                 $linkType = $link->getAttribute(self::TYPE);
97
98                 // XXX: Am I allowed to do this according to specs? (matching using common_bare_mime)
99                 if ($linkRel == $rel &&
100                     (is_null($type) || common_bare_mime($linkType) == common_bare_mime($type))) {
101                     return $link->getAttribute(self::HREF);
102                 }
103             }
104         }
105
106         return null;
107     }
108
109     static function getLinks(DOMNode $element, $rel, $type=null)
110     {
111         $els = $element->childNodes;
112         $out = array();
113         
114         for ($i = 0; $i < $els->length; $i++) {
115             $link = $els->item($i);
116             if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
117                 $linkRel = $link->getAttribute(self::REL);
118                 $linkType = $link->getAttribute(self::TYPE);
119
120                 if ($linkRel == $rel &&
121                     (is_null($type) || $linkType == $type)) {
122                     $out[] = $link;
123                 }
124             }
125         }
126
127         return $out;
128     }
129
130     /**
131      * Gets the first child element with the given tag
132      *
133      * @param DOMElement $element   element to pick at
134      * @param string     $tag       tag to look for
135      * @param string     $namespace Namespace to look under
136      *
137      * @return DOMElement found element or null
138      */
139     static function child(DOMNode $element, $tag, $namespace=self::ATOM)
140     {
141         $els = $element->childNodes;
142         if (empty($els) || $els->length == 0) {
143             return null;
144         } else {
145             for ($i = 0; $i < $els->length; $i++) {
146                 $el = $els->item($i);
147                 if ($el->localName == $tag && $el->namespaceURI == $namespace) {
148                     return $el;
149                 }
150             }
151         }
152     }
153
154     /**
155      * Gets all immediate child elements with the given tag
156      *
157      * @param DOMElement $element   element to pick at
158      * @param string     $tag       tag to look for
159      * @param string     $namespace Namespace to look under
160      *
161      * @return array found element or null
162      */
163
164     static function children(DOMNode $element, $tag, $namespace=self::ATOM)
165     {
166         $results = array();
167
168         $els = $element->childNodes;
169
170         if (!empty($els) && $els->length > 0) {
171             for ($i = 0; $i < $els->length; $i++) {
172                 $el = $els->item($i);
173                 if ($el->localName == $tag && $el->namespaceURI == $namespace) {
174                     $results[] = $el;
175                 }
176             }
177         }
178
179         return $results;
180     }
181
182     /**
183      * Grab the text content of a DOM element child of the current element
184      *
185      * @param DOMElement $element   Element whose children we examine
186      * @param string     $tag       Tag to look up
187      * @param string     $namespace Namespace to use, defaults to Atom
188      *
189      * @return string content of the child
190      */
191     static function childContent(DOMNode $element, $tag, $namespace=self::ATOM)
192     {
193         $el = self::child($element, $tag, $namespace);
194
195         if (empty($el)) {
196             return null;
197         } else {
198             return $el->textContent;
199         }
200     }
201
202     static function childHtmlContent(DOMNode $element, $tag, $namespace=self::ATOM)
203     {
204         $el = self::child($element, $tag, $namespace);
205
206         if (empty($el)) {
207             return null;
208         } else {
209             return self::textConstruct($el);
210         }
211     }
212
213     /**
214      * Get the content of an atom:entry-like object
215      *
216      * @param DOMElement $element The element to examine.
217      *
218      * @return string unencoded HTML content of the element, like "This -&lt; is <b>HTML</b>."
219      *
220      * @todo handle remote content
221      * @todo handle embedded XML mime types
222      * @todo handle base64-encoded non-XML and non-text mime types
223      */
224     static function getContent($element)
225     {
226         return self::childHtmlContent($element, self::CONTENT, self::ATOM);
227     }
228
229     static function textConstruct($el)
230     {
231         $src  = $el->getAttribute(self::SRC);
232
233         if (!empty($src)) {
234             // TRANS: Client exception thrown when there is no source attribute.
235             throw new ClientException(_("Can't handle remote content yet."));
236         }
237
238         $type = $el->getAttribute(self::TYPE);
239
240         // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
241
242         if (empty($type) || $type == 'text') {
243             // We have plaintext saved as the XML text content.
244             // Since we want HTML, we need to escape any special chars.
245             return htmlspecialchars($el->textContent);
246         } else if ($type == 'html') {
247             // We have HTML saved as the XML text content.
248             // No additional processing required once we've got it.
249             $text = $el->textContent;
250             return $text;
251         } else if ($type == 'xhtml') {
252             // Per spec, the <content type="xhtml"> contains a single
253             // HTML <div> with XHTML namespace on it as a child node.
254             // We need to pull all of that <div>'s child nodes and
255             // serialize them back to an (X)HTML source fragment.
256             $divEl = ActivityUtils::child($el, 'div', 'http://www.w3.org/1999/xhtml');
257             if (empty($divEl)) {
258                 return null;
259             }
260             $doc = $divEl->ownerDocument;
261             $text = '';
262             $children = $divEl->childNodes;
263
264             for ($i = 0; $i < $children->length; $i++) {
265                 $child = $children->item($i);
266                 $text .= $doc->saveXML($child);
267             }
268             return trim($text);
269         } else if (in_array($type, array('text/xml', 'application/xml')) ||
270                    preg_match('#(+|/)xml$#', $type)) {
271             // TRANS: Client exception thrown when there embedded XML content is found that cannot be processed yet.
272             throw new ClientException(_("Can't handle embedded XML content yet."));
273         } else if (strncasecmp($type, 'text/', 5)) {
274             return $el->textContent;
275         } else {
276             // TRANS: Client exception thrown when base64 encoded content is found that cannot be processed yet.
277             throw new ClientException(_("Can't handle embedded Base64 content yet."));
278         }
279     }
280
281     /**
282      * Is this a valid URI for remote profile/notice identification?
283      * Does not have to be a resolvable URL.
284      * @param string $uri
285      * @return boolean
286      */
287     static function validateUri($uri)
288     {
289         // Check mailto: URIs first
290         $validate = new Validate();
291
292         if (preg_match('/^mailto:(.*)$/', $uri, $match)) {
293             return $validate->email($match[1], common_config('email', 'check_domain'));
294         }
295
296         if ($validate->uri($uri)) {
297             return true;
298         }
299
300         // Possibly an upstream bug; tag: URIs aren't validated properly
301         // unless you explicitly ask for them. All other schemes are accepted
302         // for basic URI validation without asking.
303         if ($validate->uri($uri, array('allowed_schemes' => array('tag')))) {
304             return true;
305         }
306
307         return false;
308     }
309
310     static function getFeedAuthor(DOMElement $feedEl)
311     {
312         // Try old and deprecated activity:subject
313
314         $subject = ActivityUtils::child($feedEl, Activity::SUBJECT, Activity::SPEC);
315
316         if (!empty($subject)) {
317             return new ActivityObject($subject);
318         }
319
320         // Try the feed author
321
322         $author = ActivityUtils::child($feedEl, Activity::AUTHOR, Activity::ATOM);
323
324         if (!empty($author)) {
325             return new ActivityObject($author);
326         }
327
328         // Sheesh. Not a very nice feed! Let's try fingerpoken in the
329         // entries.
330
331         $entries = $feedEl->getElementsByTagNameNS(Activity::ATOM, 'entry');
332
333         if (!empty($entries) && $entries->length > 0) {
334
335             $entry = $entries->item(0);
336
337             // Try the (deprecated) activity:actor
338
339             $actor = ActivityUtils::child($entry, Activity::ACTOR, Activity::SPEC);
340
341             if (!empty($actor)) {
342                 return new ActivityObject($actor);
343             }
344
345             // Try the author
346
347             $author = ActivityUtils::child($entry, Activity::AUTHOR, Activity::ATOM);
348
349             if (!empty($author)) {
350                 return new ActivityObject($author);
351             }
352         }
353
354         return null;
355     }
356
357     static function compareTypes($type, $objects)
358     {
359         $type = self::resolveUri($type, false);
360         foreach ((array)$objects as $object) {
361             if ($type === self::resolveUri($object)) {
362                 return true;
363             }
364         }
365         return false;
366     }
367
368     static function compareVerbs($type, $objects)
369     {
370         return self::compareTypes($type, $objects);
371     }
372
373     static function resolveUri($uri, $make_relative=false)
374     {
375         if (empty($uri)) {
376             throw new ServerException('No URI to resolve in ActivityUtils::resolveUri');
377         }
378
379         if (!$make_relative && parse_url($uri, PHP_URL_SCHEME) == '') { // relative -> absolute
380             $uri = Activity::SCHEMA . $uri;
381         } elseif ($make_relative) { // absolute -> relative
382             $uri = basename($uri); //preg_replace('/^http:\/\/activitystrea\.ms\/schema\/1\.0\//', '', $uri);
383         } // absolute schemas pass through unharmed
384
385         return $uri;
386     }
387
388     static function findLocalObject(array $uris, $type=ActivityObject::NOTE) {
389         $obj_class = null;
390         // TODO: Extend this in plugins etc. and describe in EVENTS.txt
391         if (Event::handle('StartFindLocalActivityObject', array($uris, $type, &$obj_class))) {
392             switch (self::resolveUri($type)) {
393             case ActivityObject::PERSON:
394                 // GROUP will also be here in due time...
395                 $obj_class = 'Profile';
396                 break;
397             default:
398                 $obj_class = 'Notice';
399             }
400         }
401         $object = null;
402         $uris = array_unique($uris);
403         foreach ($uris as $uri) {
404             try {
405                 // the exception thrown will cancel before reaching $object
406                 $object = call_user_func("{$obj_class}::fromUri", $uri);
407                 break;
408             } catch (UnknownUriException $e) {
409                 common_debug('Could not find local activity object from uri: '.$e->object_uri);
410             }
411         }
412         if (!$object instanceof Managed_DataObject) {
413             throw new ServerException('Could not find any activityobject stored locally with given URIs: '.var_export($uris,true));
414         }
415         Event::handle('EndFindLocalActivityObject', array($object->getUri(), $object->getObjectType(), $object));
416         return $object;
417     }
418
419     // Check authorship by supplying a Profile as a default and letting plugins
420     // set it to something else if the activity's author is actually someone
421     // else (like with a group or peopletag feed as handled in OStatus).
422     //
423     // NOTE: Returned is not necessarily the supplied profile! For example,
424     // the "feed author" may be a group, but the "activity author" is a person!
425     static function checkAuthorship(Activity $activity, Profile $profile)
426     {
427         if (Event::handle('CheckActivityAuthorship', array($activity, &$profile))) {
428             // if (empty($activity->actor)), then we generated this Activity ourselves and can trust $profile
429
430             $actor_uri = $profile->getUri();
431
432             if (!in_array($actor_uri, array($activity->actor->id, $activity->actor->link))) {
433                 // A mismatch between our locally stored URI and the supplied author?
434                 // Probably not more than a blog feed or something (with multiple authors or so)
435                 // but log it for future inspection.
436                 common_log(LOG_WARNING, "Got an actor '{$activity->actor->title}' ({$activity->actor->id}) on single-user feed for " . $actor_uri);
437             } elseif (empty($activity->actor->id)) {
438                 // Plain <author> without ActivityStreams actor info.
439                 // We'll just ignore this info for now and save the update under the feed's identity.
440             }
441         }
442
443         if (!$profile instanceof Profile) {
444             throw new ServerException('Could not get an author Profile for activity');
445         }
446
447         return $profile;
448     }
449
450     static public function typeToTitle($type)
451     {
452         return ucfirst(self::resolveUri($type, true));
453     }
454
455     static public function verbToTitle($verb)
456     {
457         return ucfirst(self::resolveUri($verb, true));
458     }
459 }