]> git.mxchange.org Git - friendica.git/blob - include/ostatus.php
9472c56e03b175973552ba838adaaa87a7032b8f
[friendica.git] / include / ostatus.php
1 <?php
2 /**
3  * @file include/ostatus.php
4  */
5
6 use Friendica\App;
7 use Friendica\Core\System;
8 use Friendica\Core\Config;
9 use Friendica\Network\Probe;
10 use Friendica\Util\Lock;
11
12 require_once 'include/Contact.php';
13 require_once 'include/threads.php';
14 require_once 'include/html2bbcode.php';
15 require_once 'include/bbcode.php';
16 require_once 'include/items.php';
17 require_once 'mod/share.php';
18 require_once 'include/enotify.php';
19 require_once 'include/socgraph.php';
20 require_once 'include/Photo.php';
21 require_once 'include/probe.php';
22 require_once 'include/follow.php';
23 require_once 'include/api.php';
24 require_once 'mod/proxy.php';
25 require_once 'include/xml.php';
26 require_once 'include/cache.php';
27
28 /**
29  * @brief This class contain functions for the OStatus protocol
30  *
31  */
32 class ostatus {
33
34         private static $itemlist;
35         private static $conv_list = array();
36
37         /**
38          * @brief Fetches author data
39          *
40          * @param object $xpath The xpath object
41          * @param object $context The xml context of the author details
42          * @param array $importer user record of the importing user
43          * @param array $contact Called by reference, will contain the fetched contact
44          * @param bool $onlyfetch Only fetch the header without updating the contact entries
45          *
46          * @return array Array of author related entries for the item
47          */
48         private static function fetchauthor($xpath, $context, $importer, &$contact, $onlyfetch) {
49
50                 $author = array();
51                 $author["author-link"] = $xpath->evaluate('atom:author/atom:uri/text()', $context)->item(0)->nodeValue;
52                 $author["author-name"] = $xpath->evaluate('atom:author/atom:name/text()', $context)->item(0)->nodeValue;
53                 $addr = $xpath->evaluate('atom:author/atom:email/text()', $context)->item(0)->nodeValue;
54
55                 $aliaslink = $author["author-link"];
56
57                 $alternate = $xpath->query("atom:author/atom:link[@rel='alternate']", $context)->item(0)->attributes;
58                 if (is_object($alternate)) {
59                         foreach ($alternate AS $attributes) {
60                                 if (($attributes->name == "href") && ($attributes->textContent != "")) {
61                                         $author["author-link"] = $attributes->textContent;
62                                 }
63                         }
64                 }
65
66                 $author["contact-id"] = $contact["id"];
67
68                 $found = false;
69
70                 if ($author["author-link"] != "") {
71                         if ($aliaslink == "") {
72                                 $aliaslink = $author["author-link"];
73                         }
74
75                         $condition = array("`uid` = ? AND `nurl` IN (?, ?) AND `network` != ?", $importer["uid"],
76                                         normalise_link($author["author-link"]), normalise_link($aliaslink), NETWORK_STATUSNET);
77                         $r = dba::select('contact', array(), $condition, array('limit' => 1));
78
79                         if (dbm::is_result($r)) {
80                                 $found = true;
81                                 $contact = $r;
82                                 $author["contact-id"] = $r["id"];
83                                 $author["author-link"] = $r["url"];
84                         }
85                 }
86
87                 if (!$found && ($addr != "")) {
88                         $condition = array("`uid` = ? AND `addr` = ? AND `network` != ?",
89                                         $importer["uid"], $addr, NETWORK_STATUSNET);
90                         $r = dba::select('contact', array(), $condition, array('limit' => 1));
91
92                         if (dbm::is_result($r)) {
93                                 $contact = $r;
94                                 $author["contact-id"] = $r["id"];
95                                 $author["author-link"] = $r["url"];
96                         }
97                 }
98
99                 $avatarlist = array();
100                 $avatars = $xpath->query("atom:author/atom:link[@rel='avatar']", $context);
101                 foreach ($avatars AS $avatar) {
102                         $href = "";
103                         $width = 0;
104                         foreach ($avatar->attributes AS $attributes) {
105                                 if ($attributes->name == "href") {
106                                         $href = $attributes->textContent;
107                                 }
108                                 if ($attributes->name == "width") {
109                                         $width = $attributes->textContent;
110                                 }
111                         }
112                         if ($href != "") {
113                                 $avatarlist[$width] = $href;
114                         }
115                 }
116                 if (count($avatarlist) > 0) {
117                         krsort($avatarlist);
118                         $author["author-avatar"] = Probe::fixAvatar(current($avatarlist), $author["author-link"]);
119                 }
120
121                 $displayname = $xpath->evaluate('atom:author/poco:displayName/text()', $context)->item(0)->nodeValue;
122                 if ($displayname != "") {
123                         $author["author-name"] = $displayname;
124                 }
125
126                 $author["owner-name"] = $author["author-name"];
127                 $author["owner-link"] = $author["author-link"];
128                 $author["owner-avatar"] = $author["author-avatar"];
129
130                 // Only update the contacts if it is an OStatus contact
131                 if ($r && !$onlyfetch && ($contact["network"] == NETWORK_OSTATUS)) {
132
133                         // Update contact data
134
135                         // This query doesn't seem to work
136                         // $value = $xpath->query("atom:link[@rel='salmon']", $context)->item(0)->nodeValue;
137                         // if ($value != "")
138                         //      $contact["notify"] = $value;
139
140                         // This query doesn't seem to work as well - I hate these queries
141                         // $value = $xpath->query("atom:link[@rel='self' and @type='application/atom+xml']", $context)->item(0)->nodeValue;
142                         // if ($value != "")
143                         //      $contact["poll"] = $value;
144
145                         $value = $xpath->evaluate('atom:author/atom:uri/text()', $context)->item(0)->nodeValue;
146                         if ($value != "")
147                                 $contact["alias"] = $value;
148
149                         $value = $xpath->evaluate('atom:author/poco:displayName/text()', $context)->item(0)->nodeValue;
150                         if ($value != "")
151                                 $contact["name"] = $value;
152
153                         $value = $xpath->evaluate('atom:author/poco:preferredUsername/text()', $context)->item(0)->nodeValue;
154                         if ($value != "")
155                                 $contact["nick"] = $value;
156
157                         $value = $xpath->evaluate('atom:author/poco:note/text()', $context)->item(0)->nodeValue;
158                         if ($value != "")
159                                 $contact["about"] = html2bbcode($value);
160
161                         $value = $xpath->evaluate('atom:author/poco:address/poco:formatted/text()', $context)->item(0)->nodeValue;
162                         if ($value != "")
163                                 $contact["location"] = $value;
164
165                         if (($contact["name"] != $r[0]["name"]) || ($contact["nick"] != $r[0]["nick"]) || ($contact["about"] != $r[0]["about"]) ||
166                                 ($contact["alias"] != $r[0]["alias"]) || ($contact["location"] != $r[0]["location"])) {
167
168                                 logger("Update contact data for contact ".$contact["id"], LOGGER_DEBUG);
169
170                                 q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `alias` = '%s', `about` = '%s', `location` = '%s', `name-date` = '%s' WHERE `id` = %d",
171                                         dbesc($contact["name"]), dbesc($contact["nick"]), dbesc($contact["alias"]),
172                                         dbesc($contact["about"]), dbesc($contact["location"]),
173                                         dbesc(datetime_convert()), intval($contact["id"]));
174                         }
175
176                         if (isset($author["author-avatar"]) && ($author["author-avatar"] != $r[0]['avatar'])) {
177                                 logger("Update profile picture for contact ".$contact["id"], LOGGER_DEBUG);
178
179                                 update_contact_avatar($author["author-avatar"], $importer["uid"], $contact["id"]);
180                         }
181
182                         // Ensure that we are having this contact (with uid=0)
183                         $cid = get_contact($author["author-link"], 0);
184
185                         if ($cid) {
186                                 $fields = array('url', 'name', 'nick', 'alias', 'about', 'location');
187                                 $old_contact = dba::select('contact', $fields, array('id' => $cid), array('limit' => 1));
188
189                                 // Update it with the current values
190                                 $fields = array('url' => $author["author-link"], 'name' => $contact["name"],
191                                                 'nick' => $contact["nick"], 'alias' => $contact["alias"],
192                                                 'about' => $contact["about"], 'location' => $contact["location"],
193                                                 'success_update' => datetime_convert(), 'last-update' => datetime_convert());
194
195                                 dba::update('contact', $fields, array('id' => $cid), $old_contact);
196
197                                 // Update the avatar
198                                 update_contact_avatar($author["author-avatar"], 0, $cid);
199                         }
200
201                         $contact["generation"] = 2;
202                         $contact["hide"] = false; // OStatus contacts are never hidden
203                         $contact["photo"] = $author["author-avatar"];
204                         $gcid = update_gcontact($contact);
205
206                         link_gcontact($gcid, $contact["uid"], $contact["id"]);
207                 }
208
209                 return $author;
210         }
211
212         /**
213          * @brief Fetches author data from a given XML string
214          *
215          * @param string $xml The XML
216          * @param array $importer user record of the importing user
217          *
218          * @return array Array of author related entries for the item
219          */
220         public static function salmon_author($xml, $importer) {
221
222                 if ($xml == "")
223                         return;
224
225                 $doc = new DOMDocument();
226                 @$doc->loadXML($xml);
227
228                 $xpath = new DomXPath($doc);
229                 $xpath->registerNamespace('atom', NAMESPACE_ATOM1);
230                 $xpath->registerNamespace('thr', NAMESPACE_THREAD);
231                 $xpath->registerNamespace('georss', NAMESPACE_GEORSS);
232                 $xpath->registerNamespace('activity', NAMESPACE_ACTIVITY);
233                 $xpath->registerNamespace('media', NAMESPACE_MEDIA);
234                 $xpath->registerNamespace('poco', NAMESPACE_POCO);
235                 $xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS);
236                 $xpath->registerNamespace('statusnet', NAMESPACE_STATUSNET);
237
238                 $entries = $xpath->query('/atom:entry');
239
240                 foreach ($entries AS $entry) {
241                         // fetch the author
242                         $author = self::fetchauthor($xpath, $entry, $importer, $contact, true);
243                         return $author;
244                 }
245         }
246
247         /**
248          * @brief Read attributes from element
249          *
250          * @param object $element Element object
251          *
252          * @return array attributes
253          */
254         private static function read_attributes($element) {
255                 $attribute = array();
256
257                 foreach ($element->attributes AS $attributes) {
258                         $attribute[$attributes->name] = $attributes->textContent;
259                 }
260
261                 return $attribute;
262         }
263
264         /**
265          * @brief Imports an XML string containing OStatus elements
266          *
267          * @param string $xml The XML
268          * @param array $importer user record of the importing user
269          * @param array $contact
270          * @param string $hub Called by reference, returns the fetched hub data
271          */
272         public static function import($xml, $importer, &$contact, &$hub) {
273                 self::process($xml, $importer, $contact, $hub);
274         }
275
276         /**
277          * @brief Internal feed processing
278          *
279          * @param string $xml The XML
280          * @param array $importer user record of the importing user
281          * @param array $contact
282          * @param string $hub Called by reference, returns the fetched hub data
283          * @param boolean $stored Is the post fresh imported or from the database?
284          * @param boolean $initialize Is it the leading post so that data has to be initialized?
285          *
286          * @return boolean Could the XML be processed?
287          */
288         private static function process($xml, $importer, &$contact, &$hub, $stored = false, $initialize = true) {
289                 if ($initialize) {
290                         self::$itemlist = array();
291                         self::$conv_list = array();
292                 }
293
294                 logger("Import OStatus message", LOGGER_DEBUG);
295
296                 if ($xml == "") {
297                         return false;
298                 }
299                 $doc = new DOMDocument();
300                 @$doc->loadXML($xml);
301
302                 $xpath = new DomXPath($doc);
303                 $xpath->registerNamespace('atom', NAMESPACE_ATOM1);
304                 $xpath->registerNamespace('thr', NAMESPACE_THREAD);
305                 $xpath->registerNamespace('georss', NAMESPACE_GEORSS);
306                 $xpath->registerNamespace('activity', NAMESPACE_ACTIVITY);
307                 $xpath->registerNamespace('media', NAMESPACE_MEDIA);
308                 $xpath->registerNamespace('poco', NAMESPACE_POCO);
309                 $xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS);
310                 $xpath->registerNamespace('statusnet', NAMESPACE_STATUSNET);
311
312                 $hub = "";
313                 $hub_attributes = $xpath->query("/atom:feed/atom:link[@rel='hub']")->item(0)->attributes;
314                 if (is_object($hub_attributes)) {
315                         foreach ($hub_attributes AS $hub_attribute) {
316                                 if ($hub_attribute->name == "href") {
317                                         $hub = $hub_attribute->textContent;
318                                         logger("Found hub ".$hub, LOGGER_DEBUG);
319                                 }
320                         }
321                 }
322
323                 $header = array();
324                 $header["uid"] = $importer["uid"];
325                 $header["network"] = NETWORK_OSTATUS;
326                 $header["type"] = "remote";
327                 $header["wall"] = 0;
328                 $header["origin"] = 0;
329                 $header["gravity"] = GRAVITY_PARENT;
330
331                 $first_child = $doc->firstChild->tagName;
332
333                 if ($first_child == "feed") {
334                         $entries = $xpath->query('/atom:feed/atom:entry');
335                 } else {
336                         $entries = $xpath->query('/atom:entry');
337                 }
338
339                 if ($entries->length == 1) {
340                         // We reformat the XML to make it better readable
341                         $doc2 = new DOMDocument();
342                         $doc2->loadXML($xml);
343                         $doc2->preserveWhiteSpace = false;
344                         $doc2->formatOutput = true;
345                         $xml2 = $doc2->saveXML();
346
347                         $header["protocol"] = PROTOCOL_OSTATUS_SALMON;
348                         $header["source"] = $xml2;
349                 } elseif (!$initialize) {
350                         return false;
351                 }
352
353                 // Fetch the first author
354                 $authordata = $xpath->query('//author')->item(0);
355                 $author = self::fetchauthor($xpath, $authordata, $importer, $contact, $stored);
356
357                 $entry = $xpath->query('/atom:entry');
358
359                 // Reverse the order of the entries
360                 $entrylist = array();
361
362                 foreach ($entries AS $entry) {
363                         $entrylist[] = $entry;
364                 }
365
366                 foreach (array_reverse($entrylist) AS $entry) {
367                         // fetch the author
368                         $authorelement = $xpath->query('/atom:entry/atom:author', $entry);
369                         if ($authorelement->length > 0) {
370                                 $author = self::fetchauthor($xpath, $entry, $importer, $contact, $stored);
371                         }
372
373                         $value = $xpath->evaluate('atom:author/poco:preferredUsername/text()', $entry)->item(0)->nodeValue;
374                         if ($value != "") {
375                                 $nickname = $value;
376                         } else {
377                                 $nickname = $author["author-name"];
378                         }
379
380                         $item = array_merge($header, $author);
381
382                         $item["uri"] = $xpath->query('atom:id/text()', $entry)->item(0)->nodeValue;
383
384                         $item["verb"] = $xpath->query('activity:verb/text()', $entry)->item(0)->nodeValue;
385
386                         // Delete a message
387                         if (in_array($item["verb"], array('qvitter-delete-notice', ACTIVITY_DELETE, 'delete'))) {
388                                 self::deleteNotice($item);
389                                 continue;
390                         }
391
392                         if (in_array($item["verb"], array(NAMESPACE_OSTATUS."/unfavorite", ACTIVITY_UNFAVORITE))) {
393                                 // Ignore "Unfavorite" message
394                                 logger("Ignore unfavorite message ".print_r($item, true), LOGGER_DEBUG);
395                                 continue;
396                         }
397
398                         // Deletions come with the same uri, so we check for duplicates after processing deletions
399                         if (dba::exists('item', array('uid' => $importer["uid"], 'uri' => $item["uri"]))) {
400                                 logger('Post with URI '.$item["uri"].' already existed for user '.$importer["uid"].'.', LOGGER_DEBUG);
401                                 continue;
402                         } else {
403                                 logger('Processing post with URI '.$item["uri"].' for user '.$importer["uid"].'.', LOGGER_DEBUG);
404                         }
405
406                         if ($item["verb"] == ACTIVITY_JOIN) {
407                                 // ignore "Join" messages
408                                 logger("Ignore join message ".print_r($item, true), LOGGER_DEBUG);
409                                 continue;
410                         }
411
412                         if ($item["verb"] == "http://mastodon.social/schema/1.0/block") {
413                                 // ignore mastodon "block" messages
414                                 logger("Ignore block message ".print_r($item, true), LOGGER_DEBUG);
415                                 continue;
416                         }
417
418                         if ($item["verb"] == ACTIVITY_FOLLOW) {
419                                 new_follower($importer, $contact, $item, $nickname);
420                                 continue;
421                         }
422
423                         if ($item["verb"] == NAMESPACE_OSTATUS."/unfollow") {
424                                 lose_follower($importer, $contact, $item, $dummy);
425                                 continue;
426                         }
427
428                         if ($item["verb"] == ACTIVITY_FAVORITE) {
429                                 $orig_uri = $xpath->query("activity:object/atom:id", $entry)->item(0)->nodeValue;
430                                 logger("Favorite ".$orig_uri." ".print_r($item, true));
431
432                                 $item["verb"] = ACTIVITY_LIKE;
433                                 $item["parent-uri"] = $orig_uri;
434                                 $item["gravity"] = GRAVITY_LIKE;
435                         }
436
437                         // http://activitystrea.ms/schema/1.0/rsvp-yes
438                         if (!in_array($item["verb"], array(ACTIVITY_POST, ACTIVITY_LIKE, ACTIVITY_SHARE))) {
439                                 logger("Unhandled verb ".$item["verb"]." ".print_r($item, true), LOGGER_DEBUG);
440                         }
441
442                         self::processPost($xpath, $entry, $item, $importer);
443
444                         if ($initialize && (count(self::$itemlist) > 0)) {
445                                 if (self::$itemlist[0]['uri'] == self::$itemlist[0]['parent-uri']) {
446                                         // We will import it everytime, when it is started by our contacts
447                                         $valid = !empty(self::$itemlist[0]['contact-id']);
448                                         if (!$valid) {
449                                                 // If not, then it depends on this setting
450                                                 $valid = !Config::get('system','ostatus_full_threads');
451                                         }
452                                         if ($valid) {
453                                                 // Never post a thread when the only interaction by our contact was a like
454                                                 $valid = false;
455                                                 $verbs = array(ACTIVITY_POST, ACTIVITY_SHARE);
456                                                 foreach (self::$itemlist AS $item) {
457                                                         if (!empty($item['contact-id']) && in_array($item['verb'], $verbs)) {
458                                                                 $valid = true;
459                                                         }
460                                                 }
461                                         }
462                                 } else {
463                                         // But we will only import complete threads
464                                         $valid = dba::exists('item', array('uid' => $importer["uid"], 'uri' => self::$itemlist[0]['parent-uri']));
465                                 }
466
467                                 if ($valid) {
468                                         $default_contact = 0;
469                                         $key = count(self::$itemlist);
470                                         for ($key = count(self::$itemlist) - 1; $key >= 0; $key--) {
471                                                 if (empty(self::$itemlist[$key]['contact-id'])) {
472                                                         self::$itemlist[$key]['contact-id'] = $default_contact;
473                                                 } else {
474                                                         $default_contact = $item['contact-id'];
475                                                 }
476                                         }
477                                         foreach (self::$itemlist AS $item) {
478                                                 $found = dba::exists('item', array('uid' => $importer["uid"], 'uri' => $item["uri"]));
479                                                 if ($found) {
480                                                         logger("Item with uri ".$item["uri"]." for user ".$importer["uid"]." already exists.", LOGGER_DEBUG);
481                                                 } else {
482                                                         // We are having duplicated entries. Hopefully this solves it.
483                                                         if (Lock::set('ostatus_process_item_store')) {
484                                                                 $ret = item_store($item);
485                                                                 Lock::remove('ostatus_process_item_store');
486                                                                 logger("Item with uri ".$item["uri"]." for user ".$importer["uid"].' stored. Return value: '.$ret);
487                                                         } else {
488                                                                 $ret = item_store($item);
489                                                                 logger("We couldn't lock - but tried to store the item anyway. Return value is ".$ret);
490                                                         }
491                                                 }
492                                         }
493                                 }
494                                 self::$itemlist = array();
495                         }
496                         logger('Processing done for post with URI '.$item["uri"].' for user '.$importer["uid"].'.', LOGGER_DEBUG);
497                 }
498                 return true;
499         }
500
501         private static function deleteNotice($item) {
502
503                 $condition = array('uid' => $item['uid'], 'author-link' => $item['author-link'], 'uri' => $item['uri']);
504                 $deleted = dba::select('item', array('id', 'parent-uri'), $condition, array('limit' => 1));
505                 if (!dbm::is_result($deleted)) {
506                         logger('Item from '.$item['author-link'].' with uri '.$item['uri'].' for user '.$item['uid']." wasn't found. We don't delete it. ");
507                         return;
508                 }
509
510                 // Currently we don't have a central deletion function that we could use in this case. The function "item_drop" doesn't work for that case
511                 dba::update('item', array('deleted' => true, 'title' => '', 'body' => '',
512                                         'edited' => datetime_convert(), 'changed' => datetime_convert()),
513                                 array('id' => $deleted["id"]));
514
515                 delete_thread($deleted["id"], $deleted["parent-uri"]);
516
517                 logger('Deleted item with uri '.$item['uri'].' for user '.$item['uid']);
518         }
519
520         /**
521          * @brief Processes the XML for a post
522          *
523          * @param object $xpath The xpath object
524          * @param object $entry The xml entry that is processed
525          * @param array $item The item array
526          * @param array $importer user record of the importing user
527          */
528         private static function processPost($xpath, $entry, &$item, $importer) {
529                 $item["body"] = html2bbcode($xpath->query('atom:content/text()', $entry)->item(0)->nodeValue);
530                 $item["object-type"] = $xpath->query('activity:object-type/text()', $entry)->item(0)->nodeValue;
531                 if (($item["object-type"] == ACTIVITY_OBJ_BOOKMARK) || ($item["object-type"] == ACTIVITY_OBJ_EVENT)) {
532                         $item["title"] = $xpath->query('atom:title/text()', $entry)->item(0)->nodeValue;
533                         $item["body"] = $xpath->query('atom:summary/text()', $entry)->item(0)->nodeValue;
534                 } elseif ($item["object-type"] == ACTIVITY_OBJ_QUESTION) {
535                         $item["title"] = $xpath->query('atom:title/text()', $entry)->item(0)->nodeValue;
536                 }
537
538                 $item["created"] = $xpath->query('atom:published/text()', $entry)->item(0)->nodeValue;
539                 $item["edited"] = $xpath->query('atom:updated/text()', $entry)->item(0)->nodeValue;
540                 $conversation = $xpath->query('ostatus:conversation/text()', $entry)->item(0)->nodeValue;
541                 $item['conversation-uri'] = $conversation;
542
543                 $conv = $xpath->query('ostatus:conversation', $entry);
544                 if (is_object($conv->item(0))) {
545                         foreach ($conv->item(0)->attributes AS $attributes) {
546                                 if ($attributes->name == "ref") {
547                                         $item['conversation-uri'] = $attributes->textContent;
548                                 }
549                                 if ($attributes->name == "href") {
550                                         $item['conversation-href'] = $attributes->textContent;
551                                 }
552                         }
553                 }
554
555                 $related = "";
556
557                 $inreplyto = $xpath->query('thr:in-reply-to', $entry);
558                 if (is_object($inreplyto->item(0))) {
559                         foreach ($inreplyto->item(0)->attributes AS $attributes) {
560                                 if ($attributes->name == "ref") {
561                                         $item["parent-uri"] = $attributes->textContent;
562                                 }
563                                 if ($attributes->name == "href") {
564                                         $related = $attributes->textContent;
565                                 }
566                         }
567                 }
568
569                 $georsspoint = $xpath->query('georss:point', $entry);
570                 if (!empty($georsspoint) && ($georsspoint->length > 0)) {
571                         $item["coord"] = $georsspoint->item(0)->nodeValue;
572                 }
573
574                 $categories = $xpath->query('atom:category', $entry);
575                 if ($categories) {
576                         foreach ($categories AS $category) {
577                                 foreach ($category->attributes AS $attributes) {
578                                         if ($attributes->name == "term") {
579                                                 $term = $attributes->textContent;
580                                                 if (strlen($item["tag"])) {
581                                                         $item["tag"] .= ',';
582                                                 }
583                                                 $item["tag"] .= "#[url=".System::baseUrl()."/search?tag=".$term."]".$term."[/url]";
584                                         }
585                                 }
586                         }
587                 }
588
589                 $self = '';
590                 $add_body = '';
591
592                 $links = $xpath->query('atom:link', $entry);
593                 if ($links) {
594                         $link_data = self::processLinks($links, $item);
595                         $self = $link_data['self'];
596                         $add_body = $link_data['add_body'];
597                 }
598
599                 $repeat_of = "";
600
601                 $notice_info = $xpath->query('statusnet:notice_info', $entry);
602                 if ($notice_info && ($notice_info->length > 0)) {
603                         foreach ($notice_info->item(0)->attributes AS $attributes) {
604                                 if ($attributes->name == "source") {
605                                         $item["app"] = strip_tags($attributes->textContent);
606                                 }
607                                 if ($attributes->name == "repeat_of") {
608                                         $repeat_of = $attributes->textContent;
609                                 }
610                         }
611                 }
612                 // Is it a repeated post?
613                 if (($repeat_of != "") || ($item["verb"] == ACTIVITY_SHARE)) {
614                         $link_data = self::processRepeatedItem($xpath, $entry, $item, $importer);
615                         if (!empty($link_data['add_body'])) {
616                                 $add_body .= $link_data['add_body'];
617                         }
618                 }
619
620                 $item["body"] .= $add_body;
621
622                 // Only add additional data when there is no picture in the post
623                 if (!strstr($item["body"],'[/img]')) {
624                         $item["body"] = add_page_info_to_body($item["body"]);
625                 }
626
627                 // Mastodon Content Warning
628                 if (($item["verb"] == ACTIVITY_POST) && $xpath->evaluate('boolean(atom:summary)', $entry)) {
629                         $clear_text = $xpath->query('atom:summary/text()', $entry)->item(0)->nodeValue;
630
631                         $item["body"] = html2bbcode($clear_text) . '[spoiler]' . $item["body"] . '[/spoiler]';
632                 }
633
634                 if (($self != '') && empty($item['protocol'])) {
635                         self::fetchSelf($self, $item);
636                 }
637
638                 if (!empty($item["conversation-href"])) {
639                         self::fetchConversation($item['conversation-href'], $item['conversation-uri']);
640                 }
641
642                 if (isset($item["parent-uri"]) && ($related != '')) {
643                         if (!dba::exists('item', array('uid' => $importer["uid"], 'uri' => $item['parent-uri']))) {
644                                 self::fetchRelated($related, $item["parent-uri"], $importer);
645                         } else {
646                                 logger('Reply with URI '.$item["uri"].' already existed for user '.$importer["uid"].'.', LOGGER_DEBUG);
647                         }
648
649                         $item["type"] = 'remote-comment';
650                         $item["gravity"] = GRAVITY_COMMENT;
651                 } else {
652                         $item["parent-uri"] = $item["uri"];
653                 }
654
655                 if (($item['author-link'] != '') && !empty($item['protocol'])) {
656                         $item = store_conversation($item);
657                 }
658
659                 self::$itemlist[] = $item;
660         }
661
662         /**
663          * @brief Fetch the conversation for posts
664          *
665          * @param string $conversation The link to the conversation
666          * @param string $conversation_uri The conversation in "uri" format
667          */
668         private static function fetchConversation($conversation, $conversation_uri) {
669
670                 // Ensure that we only store a conversation once in a process
671                 if (isset(self::$conv_list[$conversation])) {
672                         return;
673                 }
674
675                 self::$conv_list[$conversation] = true;
676
677                 $conversation_data = z_fetch_url($conversation);
678
679                 if (!$conversation_data['success']) {
680                         return;
681                 }
682
683                 $xml = '';
684
685                 if (stristr($conversation_data['header'], 'Content-Type: application/atom+xml')) {
686                         $xml = $conversation_data['body'];
687                 }
688
689                 if ($xml == '') {
690                         $doc = new DOMDocument();
691                         if (!@$doc->loadHTML($conversation_data['body'])) {
692                                 return;
693                         }
694                         $xpath = new DomXPath($doc);
695
696                         $links = $xpath->query('//link');
697                         if ($links) {
698                                 foreach ($links AS $link) {
699                                         $attribute = ostatus::read_attributes($link);
700                                         if (($attribute['rel'] == 'alternate') && ($attribute['type'] == 'application/atom+xml')) {
701                                                 $file = $attribute['href'];
702                                         }
703                                 }
704                                 if ($file != '') {
705                                         $conversation_atom = z_fetch_url($attribute['href']);
706
707                                         if ($conversation_atom['success']) {
708                                                 $xml = $conversation_atom['body'];
709                                         }
710                                 }
711                         }
712                 }
713
714                 if ($xml == '') {
715                         return;
716                 }
717
718                 self::storeConversation($xml, $conversation, $conversation_uri);
719         }
720
721         /**
722          * @brief Store a feed in several conversation entries
723          *
724          * @param string $xml The feed
725          */
726         private static function storeConversation($xml, $conversation = '', $conversation_uri = '') {
727                 $doc = new DOMDocument();
728                 @$doc->loadXML($xml);
729
730                 $xpath = new DomXPath($doc);
731                 $xpath->registerNamespace('atom', NAMESPACE_ATOM1);
732                 $xpath->registerNamespace('thr', NAMESPACE_THREAD);
733                 $xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS);
734
735                 $entries = $xpath->query('/atom:feed/atom:entry');
736
737                 // Now store the entries
738                 foreach ($entries AS $entry) {
739                         $doc2 = new DOMDocument();
740                         $doc2->preserveWhiteSpace = false;
741                         $doc2->formatOutput = true;
742
743                         $conv_data = array();
744
745                         $conv_data['protocol'] = PROTOCOL_SPLITTED_CONV;
746                         $conv_data['network'] = NETWORK_OSTATUS;
747                         $conv_data['uri'] = $xpath->query('atom:id/text()', $entry)->item(0)->nodeValue;
748
749                         $inreplyto = $xpath->query('thr:in-reply-to', $entry);
750                         if (is_object($inreplyto->item(0))) {
751                                 foreach ($inreplyto->item(0)->attributes AS $attributes) {
752                                         if ($attributes->name == "ref") {
753                                                 $conv_data['reply-to-uri'] = $attributes->textContent;
754                                         }
755                                 }
756                         }
757
758                         $conv = $xpath->query('ostatus:conversation/text()', $entry)->item(0)->nodeValue;
759                         $conv_data['conversation-uri'] = $conv;
760
761                         $conv = $xpath->query('ostatus:conversation', $entry);
762                         if (is_object($conv->item(0))) {
763                                 foreach ($conv->item(0)->attributes AS $attributes) {
764                                         if ($attributes->name == "ref") {
765                                                 $conv_data['conversation-uri'] = $attributes->textContent;
766                                         }
767                                         if ($attributes->name == "href") {
768                                                 $conv_data['conversation-href'] = $attributes->textContent;
769                                         }
770                                 }
771                         }
772
773                         if ($conversation != '') {
774                                 $conv_data['conversation-uri'] = $conversation;
775                         }
776
777                         if ($conversation_uri != '') {
778                                 $conv_data['conversation-uri'] = $conversation_uri;
779                         }
780
781                         $entry = $doc2->importNode($entry, true);
782
783                         $doc2->appendChild($entry);
784
785                         $conv_data['source'] = $doc2->saveXML();
786
787                         $condition = array('item-uri' => $conv_data['uri'],'protocol' => PROTOCOL_OSTATUS_FEED);
788                         if (dba::exists('conversation', $condition)) {
789                                 logger('Delete deprecated entry for URI '.$conv_data['uri'], LOGGER_DEBUG);
790                                 dba::delete('conversation', array('item-uri' => $conv_data['uri']));
791                         }
792
793                         logger('Store conversation data for uri '.$conv_data['uri'], LOGGER_DEBUG);
794                         store_conversation($conv_data);
795                 }
796         }
797
798         /**
799          * @brief Fetch the own post so that it can be stored later
800          * @param array $item The item array
801          *
802          * We want to store the original data for later processing.
803          * This function is meant for cases where we process a feed with multiple entries.
804          * In that case we need to fetch the single posts here.
805          *
806          * @param string $self The link to the self item
807          */
808         private static function fetchSelf($self, &$item) {
809                 $condition = array('`item-uri` = ? AND `protocol` IN (?, ?)', $self, PROTOCOL_DFRN, PROTOCOL_OSTATUS_SALMON);
810                 if (dba::exists('conversation', $condition)) {
811                         logger('Conversation '.$item['uri'].' is already stored.', LOGGER_DEBUG);
812                         return;
813                 }
814
815                 $self_data = z_fetch_url($self);
816
817                 if (!$self_data['success']) {
818                         return;
819                 }
820
821                 // We reformat the XML to make it better readable
822                 $doc = new DOMDocument();
823                 $doc->loadXML($self_data['body']);
824                 $doc->preserveWhiteSpace = false;
825                 $doc->formatOutput = true;
826                 $xml = $doc->saveXML();
827
828                 $item["protocol"] = PROTOCOL_OSTATUS_SALMON;
829                 $item["source"] = $xml;
830
831                 logger('Conversation '.$item['uri'].' is now fetched.', LOGGER_DEBUG);
832         }
833
834         /**
835          * @brief Fetch related posts and processes them
836          *
837          * @param string $related The link to the related item
838          * @param string $related_uri The related item in "uri" format
839          * @param array $importer user record of the importing user
840          */
841         private static function fetchRelated($related, $related_uri, $importer) {
842                 $condition = array('`item-uri` = ? AND `protocol` IN (?, ?)', $related_uri, PROTOCOL_DFRN, PROTOCOL_OSTATUS_SALMON);
843                 $conversation = dba::select('conversation', array('source', 'protocol'), $condition,  array('limit' => 1));
844                 if (dbm::is_result($conversation)) {
845                         $stored = true;
846                         $xml = $conversation['source'];
847                         if (self::process($xml, $importer, $contact, $hub, $stored, false)) {
848                                 logger('Got valid cached XML for URI '.$related_uri, LOGGER_DEBUG);
849                                 return;
850                         }
851                         if ($conversation['protocol'] == PROTOCOL_OSTATUS_SALMON) {
852                                 logger('Delete invalid cached XML for URI '.$related_uri, LOGGER_DEBUG);
853                                 dba::delete('conversation', array('item-uri' => $related_uri));
854                         }
855                 }
856
857                 $stored = false;
858                 $related_data = z_fetch_url($related);
859
860                 if (!$related_data['success']) {
861                         return;
862                 }
863
864                 $xml = '';
865
866                 if (stristr($related_data['header'], 'Content-Type: application/atom+xml')) {
867                         logger('Directly fetched XML for URI '.$related_uri, LOGGER_DEBUG);
868                         $xml = $related_data['body'];
869                 }
870
871                 if ($xml == '') {
872                         $doc = new DOMDocument();
873                         if (!@$doc->loadHTML($related_data['body'])) {
874                                 return;
875                         }
876                         $xpath = new DomXPath($doc);
877
878                         $atom_file = '';
879
880                         $links = $xpath->query('//link');
881                         if ($links) {
882                                 foreach ($links AS $link) {
883                                         $attribute = self::read_attributes($link);
884                                         if (($attribute['rel'] == 'alternate') && ($attribute['type'] == 'application/atom+xml')) {
885                                                 $atom_file = $attribute['href'];
886                                         }
887                                 }
888                                 if ($atom_file != '') {
889                                         $related_atom = z_fetch_url($atom_file);
890
891                                         if ($related_atom['success']) {
892                                                 logger('Fetched XML for URI '.$related_uri, LOGGER_DEBUG);
893                                                 $xml = $related_atom['body'];
894                                         }
895                                 }
896                         }
897                 }
898
899                 // Workaround for older GNU Social servers
900                 if (($xml == '') && strstr($related, '/notice/')) {
901                         $related_atom = z_fetch_url(str_replace('/notice/', '/api/statuses/show/', $related).'.atom');
902
903                         if ($related_atom['success']) {
904                                 logger('GNU Social workaround to fetch XML for URI '.$related_uri, LOGGER_DEBUG);
905                                 $xml = $related_atom['body'];
906                         }
907                 }
908
909                 // Even more worse workaround for GNU Social ;-)
910                 if ($xml == '') {
911                         $related_guess = ostatus::convert_href($related_uri);
912                         $related_atom = z_fetch_url(str_replace('/notice/', '/api/statuses/show/', $related_guess).'.atom');
913
914                         if ($related_atom['success']) {
915                                 logger('GNU Social workaround 2 to fetch XML for URI '.$related_uri, LOGGER_DEBUG);
916                                 $xml = $related_atom['body'];
917                         }
918                 }
919
920                 // Finally we take the data that we fetched from "ostatus:conversation"
921                 if ($xml == '') {
922                         $condition = array('item-uri' => $related_uri, 'protocol' => PROTOCOL_SPLITTED_CONV);
923                         $conversation = dba::select('conversation', array('source'), $condition,  array('limit' => 1));
924                         if (dbm::is_result($conversation)) {
925                                 $stored = true;
926                                 logger('Got cached XML from conversation for URI '.$related_uri, LOGGER_DEBUG);
927                                 $xml = $conversation['source'];
928                         }
929                 }
930
931                 if ($xml != '') {
932                         self::process($xml, $importer, $contact, $hub, $stored, false);
933                 } else {
934                         logger("XML couldn't be fetched for URI: ".$related_uri." - href: ".$related, LOGGER_DEBUG);
935                 }
936                 return;
937         }
938
939         /**
940          * @brief Processes the XML for a repeated post
941          *
942          * @param object $xpath The xpath object
943          * @param object $entry The xml entry that is processed
944          * @param array $item The item array
945          * @param array $importer user record of the importing user
946          *
947          * @return array with data from links
948          */
949         private static function processRepeatedItem($xpath, $entry, &$item, $importer) {
950                 $activityobjects = $xpath->query('activity:object', $entry)->item(0);
951
952                 if (!is_object($activityobjects)) {
953                         return array();
954                 }
955
956                 $link_data = array();
957
958                 $orig_uri = $xpath->query('atom:id/text()', $activityobjects)->item(0)->nodeValue;
959
960                 $links = $xpath->query("atom:link", $activityobjects);
961                 if ($links) {
962                         $link_data = self::processLinks($links, $item);
963                 }
964
965                 $orig_body = $xpath->query('atom:content/text()', $activityobjects)->item(0)->nodeValue;
966                 $orig_created = $xpath->query('atom:published/text()', $activityobjects)->item(0)->nodeValue;
967                 $orig_edited = $xpath->query('atom:updated/text()', $activityobjects)->item(0)->nodeValue;
968
969                 $orig_contact = $contact;
970                 $orig_author = self::fetchauthor($xpath, $activityobjects, $importer, $orig_contact, false);
971
972                 $item["author-name"] = $orig_author["author-name"];
973                 $item["author-link"] = $orig_author["author-link"];
974                 $item["author-avatar"] = $orig_author["author-avatar"];
975
976                 $item["body"] = html2bbcode($orig_body);
977                 $item["created"] = $orig_created;
978                 $item["edited"] = $orig_edited;
979
980                 $item["uri"] = $orig_uri;
981
982                 $item["verb"] = $xpath->query('activity:verb/text()', $activityobjects)->item(0)->nodeValue;
983
984                 $item["object-type"] = $xpath->query('activity:object-type/text()', $activityobjects)->item(0)->nodeValue;
985
986                 $inreplyto = $xpath->query('thr:in-reply-to', $activityobjects);
987                 if (is_object($inreplyto->item(0))) {
988                         foreach ($inreplyto->item(0)->attributes AS $attributes) {
989                                 if ($attributes->name == "ref") {
990                                         $item["parent-uri"] = $attributes->textContent;
991                                 }
992                         }
993                 }
994
995                 return $link_data;
996         }
997
998         /**
999          * @brief Processes links in the XML
1000          *
1001          * @param object $links The xml data that contain links
1002          * @param array $item The item array
1003          *
1004          * @return array with data from the links
1005          */
1006         private static function processLinks($links, &$item) {
1007                 $link_data = array('add_body' => '', 'self' => '');
1008
1009                 foreach ($links AS $link) {
1010                         $attribute = self::read_attributes($link);
1011
1012                         if (($attribute['rel'] != "") && ($attribute['href'] != "")) {
1013                                 switch ($attribute['rel']) {
1014                                         case "alternate":
1015                                                 $item["plink"] = $attribute['href'];
1016                                                 if (($item["object-type"] == ACTIVITY_OBJ_QUESTION) ||
1017                                                         ($item["object-type"] == ACTIVITY_OBJ_EVENT)) {
1018                                                         $item["body"] .= add_page_info($attribute['href']);
1019                                                 }
1020                                                 break;
1021                                         case "ostatus:conversation":
1022                                                 $link_data['conversation'] = $attribute['href'];
1023                                                 $item['conversation-href'] = $link_data['conversation'];
1024                                                 if (!isset($item['conversation-uri'])) {
1025                                                         $item['conversation-uri'] = $item['conversation-href'];
1026                                                 }
1027                                                 break;
1028                                         case "enclosure":
1029                                                 $filetype = strtolower(substr($attribute['type'], 0, strpos($attribute['type'],'/')));
1030                                                 if ($filetype == 'image') {
1031                                                         $link_data['add_body'] .= "\n[img]".$attribute['href'].'[/img]';
1032                                                 } else {
1033                                                         if (strlen($item["attach"])) {
1034                                                                 $item["attach"] .= ',';
1035                                                         }
1036                                                         if (!isset($attribute['length'])) {
1037                                                                 $attribute['length'] = "0";
1038                                                         }
1039                                                         $item["attach"] .= '[attach]href="'.$attribute['href'].'" length="'.$attribute['length'].'" type="'.$attribute['type'].'" title="'.$attribute['title'].'"[/attach]';
1040                                                 }
1041                                                 break;
1042                                         case "related":
1043                                                 if ($item["object-type"] != ACTIVITY_OBJ_BOOKMARK) {
1044                                                         if (!isset($item["parent-uri"])) {
1045                                                                 $item["parent-uri"] = $attribute['href'];
1046                                                         }
1047                                                         $link_data['related'] = $attribute['href'];
1048                                                 } else {
1049                                                         $item["body"] .= add_page_info($attribute['href']);
1050                                                 }
1051                                                 break;
1052                                         case "self":
1053                                                 if ($item["plink"] == '') {
1054                                                         $item["plink"] = $attribute['href'];
1055                                                 }
1056                                                 $link_data['self'] = $attribute['href'];
1057                                                 break;
1058                                 }
1059                         }
1060                 }
1061                 return $link_data;
1062         }
1063
1064 /**
1065          * @brief Create an url out of an uri
1066          *
1067          * @param string $href URI in the format "parameter1:parameter1:..."
1068          *
1069          * @return string URL in the format http(s)://....
1070          */
1071         public static function convert_href($href) {
1072                 $elements = explode(":",$href);
1073
1074                 if ((count($elements) <= 2) || ($elements[0] != "tag"))
1075                         return $href;
1076
1077                 $server = explode(",", $elements[1]);
1078                 $conversation = explode("=", $elements[2]);
1079
1080                 if ((count($elements) == 4) && ($elements[2] == "post"))
1081                         return "http://".$server[0]."/notice/".$elements[3];
1082
1083                 if ((count($conversation) != 2) || ($conversation[1] =="")) {
1084                         return $href;
1085                 }
1086                 if ($elements[3] == "objectType=thread") {
1087                         return "http://".$server[0]."/conversation/".$conversation[1];
1088                 } else {
1089                         return "http://".$server[0]."/notice/".$conversation[1];
1090                 }
1091                 return $href;
1092         }
1093
1094         /**
1095          * @brief Checks if the current post is a reshare
1096          *
1097          * @param array $item The item array of thw post
1098          *
1099          * @return string The guid if the post is a reshare
1100          */
1101         private static function get_reshared_guid($item) {
1102                 $body = trim($item["body"]);
1103
1104                 // Skip if it isn't a pure repeated messages
1105                 // Does it start with a share?
1106                 if (strpos($body, "[share") > 0)
1107                         return "";
1108
1109                 // Does it end with a share?
1110                 if (strlen($body) > (strrpos($body, "[/share]") + 8))
1111                         return "";
1112
1113                 $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body);
1114                 // Skip if there is no shared message in there
1115                 if ($body == $attributes)
1116                         return false;
1117
1118                 $guid = "";
1119                 preg_match("/guid='(.*?)'/ism", $attributes, $matches);
1120                 if ($matches[1] != "")
1121                         $guid = $matches[1];
1122
1123                 preg_match('/guid="(.*?)"/ism', $attributes, $matches);
1124                 if ($matches[1] != "")
1125                         $guid = $matches[1];
1126
1127                 return $guid;
1128         }
1129
1130         /**
1131          * @brief Cleans the body of a post if it contains picture links
1132          *
1133          * @param string $body The body
1134          *
1135          * @return string The cleaned body
1136          */
1137         private static function format_picture_post($body) {
1138                 $siteinfo = get_attached_data($body);
1139
1140                 if (($siteinfo["type"] == "photo")) {
1141                         if (isset($siteinfo["preview"]))
1142                                 $preview = $siteinfo["preview"];
1143                         else
1144                                 $preview = $siteinfo["image"];
1145
1146                         // Is it a remote picture? Then make a smaller preview here
1147                         $preview = proxy_url($preview, false, PROXY_SIZE_SMALL);
1148
1149                         // Is it a local picture? Then make it smaller here
1150                         $preview = str_replace(array("-0.jpg", "-0.png"), array("-2.jpg", "-2.png"), $preview);
1151                         $preview = str_replace(array("-1.jpg", "-1.png"), array("-2.jpg", "-2.png"), $preview);
1152
1153                         if (isset($siteinfo["url"]))
1154                                 $url = $siteinfo["url"];
1155                         else
1156                                 $url = $siteinfo["image"];
1157
1158                         $body = trim($siteinfo["text"])." [url]".$url."[/url]\n[img]".$preview."[/img]";
1159                 }
1160
1161                 return $body;
1162         }
1163
1164         /**
1165          * @brief Adds the header elements to the XML document
1166          *
1167          * @param object $doc XML document
1168          * @param array $owner Contact data of the poster
1169          *
1170          * @return object header root element
1171          */
1172         private static function add_header($doc, $owner) {
1173
1174                 $a = get_app();
1175
1176                 $root = $doc->createElementNS(NAMESPACE_ATOM1, 'feed');
1177                 $doc->appendChild($root);
1178
1179                 $root->setAttribute("xmlns:thr", NAMESPACE_THREAD);
1180                 $root->setAttribute("xmlns:georss", NAMESPACE_GEORSS);
1181                 $root->setAttribute("xmlns:activity", NAMESPACE_ACTIVITY);
1182                 $root->setAttribute("xmlns:media", NAMESPACE_MEDIA);
1183                 $root->setAttribute("xmlns:poco", NAMESPACE_POCO);
1184                 $root->setAttribute("xmlns:ostatus", NAMESPACE_OSTATUS);
1185                 $root->setAttribute("xmlns:statusnet", NAMESPACE_STATUSNET);
1186                 $root->setAttribute("xmlns:mastodon", NAMESPACE_MASTODON);
1187
1188                 $attributes = array("uri" => "https://friendi.ca", "version" => FRIENDICA_VERSION."-".DB_UPDATE_VERSION);
1189                 xml::add_element($doc, $root, "generator", FRIENDICA_PLATFORM, $attributes);
1190                 xml::add_element($doc, $root, "id", System::baseUrl()."/profile/".$owner["nick"]);
1191                 xml::add_element($doc, $root, "title", sprintf("%s timeline", $owner["name"]));
1192                 xml::add_element($doc, $root, "subtitle", sprintf("Updates from %s on %s", $owner["name"], $a->config["sitename"]));
1193                 xml::add_element($doc, $root, "logo", $owner["photo"]);
1194                 xml::add_element($doc, $root, "updated", datetime_convert("UTC", "UTC", "now", ATOM_TIME));
1195
1196                 $author = self::add_author($doc, $owner);
1197                 $root->appendChild($author);
1198
1199                 $attributes = array("href" => $owner["url"], "rel" => "alternate", "type" => "text/html");
1200                 xml::add_element($doc, $root, "link", "", $attributes);
1201
1202                 /// @TODO We have to find out what this is
1203                 /// $attributes = array("href" => System::baseUrl()."/sup",
1204                 ///             "rel" => "http://api.friendfeed.com/2008/03#sup",
1205                 ///             "type" => "application/json");
1206                 /// xml::add_element($doc, $root, "link", "", $attributes);
1207
1208                 self::hublinks($doc, $root, $owner["nick"]);
1209
1210                 $attributes = array("href" => System::baseUrl()."/salmon/".$owner["nick"], "rel" => "salmon");
1211                 xml::add_element($doc, $root, "link", "", $attributes);
1212
1213                 $attributes = array("href" => System::baseUrl()."/salmon/".$owner["nick"], "rel" => "http://salmon-protocol.org/ns/salmon-replies");
1214                 xml::add_element($doc, $root, "link", "", $attributes);
1215
1216                 $attributes = array("href" => System::baseUrl()."/salmon/".$owner["nick"], "rel" => "http://salmon-protocol.org/ns/salmon-mention");
1217                 xml::add_element($doc, $root, "link", "", $attributes);
1218
1219                 $attributes = array("href" => System::baseUrl()."/api/statuses/user_timeline/".$owner["nick"].".atom",
1220                                 "rel" => "self", "type" => "application/atom+xml");
1221                 xml::add_element($doc, $root, "link", "", $attributes);
1222
1223                 return $root;
1224         }
1225
1226         /**
1227          * @brief Add the link to the push hubs to the XML document
1228          *
1229          * @param object $doc XML document
1230          * @param object $root XML root element where the hub links are added
1231          */
1232         public static function hublinks($doc, $root, $nick) {
1233                 $h = System::baseUrl() . '/pubsubhubbub/'.$nick;
1234                 xml::add_element($doc, $root, "link", "", array("href" => $h, "rel" => "hub"));
1235         }
1236
1237         /**
1238          * @brief Adds attachement data to the XML document
1239          *
1240          * @param object $doc XML document
1241          * @param object $root XML root element where the hub links are added
1242          * @param array $item Data of the item that is to be posted
1243          */
1244         private static function get_attachment($doc, $root, $item) {
1245                 $o = "";
1246                 $siteinfo = get_attached_data($item["body"]);
1247
1248                 switch ($siteinfo["type"]) {
1249                         case 'photo':
1250                                 $imgdata = get_photo_info($siteinfo["image"]);
1251                                 $attributes = array("rel" => "enclosure",
1252                                                 "href" => $siteinfo["image"],
1253                                                 "type" => $imgdata["mime"],
1254                                                 "length" => intval($imgdata["size"]));
1255                                 xml::add_element($doc, $root, "link", "", $attributes);
1256                                 break;
1257                         case 'video':
1258                                 $attributes = array("rel" => "enclosure",
1259                                                 "href" => $siteinfo["url"],
1260                                                 "type" => "text/html; charset=UTF-8",
1261                                                 "length" => "",
1262                                                 "title" => $siteinfo["title"]);
1263                                 xml::add_element($doc, $root, "link", "", $attributes);
1264                                 break;
1265                         default:
1266                                 break;
1267                 }
1268
1269                 if (!Config::get('system', 'ostatus_not_attach_preview') && ($siteinfo["type"] != "photo") && isset($siteinfo["image"])) {
1270                         $imgdata = get_photo_info($siteinfo["image"]);
1271                         $attributes = array("rel" => "enclosure",
1272                                         "href" => $siteinfo["image"],
1273                                         "type" => $imgdata["mime"],
1274                                         "length" => intval($imgdata["size"]));
1275
1276                         xml::add_element($doc, $root, "link", "", $attributes);
1277                 }
1278
1279                 $arr = explode('[/attach],', $item['attach']);
1280                 if (count($arr)) {
1281                         foreach ($arr as $r) {
1282                                 $matches = false;
1283                                 $cnt = preg_match('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\" title=\"(.*?)\"|', $r, $matches);
1284                                 if ($cnt) {
1285                                         $attributes = array("rel" => "enclosure",
1286                                                         "href" => $matches[1],
1287                                                         "type" => $matches[3]);
1288
1289                                         if (intval($matches[2])) {
1290                                                 $attributes["length"] = intval($matches[2]);
1291                                         }
1292                                         if (trim($matches[4]) != "") {
1293                                                 $attributes["title"] = trim($matches[4]);
1294                                         }
1295                                         xml::add_element($doc, $root, "link", "", $attributes);
1296                                 }
1297                         }
1298                 }
1299         }
1300
1301         /**
1302          * @brief Adds the author element to the XML document
1303          *
1304          * @param object $doc XML document
1305          * @param array $owner Contact data of the poster
1306          *
1307          * @return object author element
1308          */
1309         private static function add_author($doc, $owner) {
1310
1311                 $r = q("SELECT `homepage`, `publish` FROM `profile` WHERE `uid` = %d AND `is-default` LIMIT 1", intval($owner["uid"]));
1312                 if (dbm::is_result($r)) {
1313                         $profile = $r[0];
1314                 }
1315                 $author = $doc->createElement("author");
1316                 xml::add_element($doc, $author, "id", $owner["url"]);
1317                 xml::add_element($doc, $author, "activity:object-type", ACTIVITY_OBJ_PERSON);
1318                 xml::add_element($doc, $author, "uri", $owner["url"]);
1319                 xml::add_element($doc, $author, "name", $owner["nick"]);
1320                 xml::add_element($doc, $author, "email", $owner["addr"]);
1321                 xml::add_element($doc, $author, "summary", bbcode($owner["about"], false, false, 7));
1322
1323                 $attributes = array("rel" => "alternate", "type" => "text/html", "href" => $owner["url"]);
1324                 xml::add_element($doc, $author, "link", "", $attributes);
1325
1326                 $attributes = array(
1327                                 "rel" => "avatar",
1328                                 "type" => "image/jpeg", // To-Do?
1329                                 "media:width" => 175,
1330                                 "media:height" => 175,
1331                                 "href" => $owner["photo"]);
1332                 xml::add_element($doc, $author, "link", "", $attributes);
1333
1334                 if (isset($owner["thumb"])) {
1335                         $attributes = array(
1336                                         "rel" => "avatar",
1337                                         "type" => "image/jpeg", // To-Do?
1338                                         "media:width" => 80,
1339                                         "media:height" => 80,
1340                                         "href" => $owner["thumb"]);
1341                         xml::add_element($doc, $author, "link", "", $attributes);
1342                 }
1343
1344                 xml::add_element($doc, $author, "poco:preferredUsername", $owner["nick"]);
1345                 xml::add_element($doc, $author, "poco:displayName", $owner["name"]);
1346                 xml::add_element($doc, $author, "poco:note", bbcode($owner["about"], false, false, 7));
1347
1348                 if (trim($owner["location"]) != "") {
1349                         $element = $doc->createElement("poco:address");
1350                         xml::add_element($doc, $element, "poco:formatted", $owner["location"]);
1351                         $author->appendChild($element);
1352                 }
1353
1354                 if (trim($profile["homepage"]) != "") {
1355                         $urls = $doc->createElement("poco:urls");
1356                         xml::add_element($doc, $urls, "poco:type", "homepage");
1357                         xml::add_element($doc, $urls, "poco:value", $profile["homepage"]);
1358                         xml::add_element($doc, $urls, "poco:primary", "true");
1359                         $author->appendChild($urls);
1360                 }
1361
1362                 if (count($profile)) {
1363                         xml::add_element($doc, $author, "followers", "", array("url" => System::baseUrl()."/viewcontacts/".$owner["nick"]));
1364                         xml::add_element($doc, $author, "statusnet:profile_info", "", array("local_id" => $owner["uid"]));
1365                 }
1366
1367                 if ($profile["publish"]) {
1368                         xml::add_element($doc, $author, "mastodon:scope", "public");
1369                 }
1370                 return $author;
1371         }
1372
1373         /**
1374          * @TODO Picture attachments should look like this:
1375          *      <a href="https://status.pirati.ca/attachment/572819" title="https://status.pirati.ca/file/heluecht-20151202T222602-rd3u49p.gif"
1376          *      class="attachment thumbnail" id="attachment-572819" rel="nofollow external">https://status.pirati.ca/attachment/572819</a>
1377          *
1378         */
1379
1380         /**
1381          * @brief Returns the given activity if present - otherwise returns the "post" activity
1382          *
1383          * @param array $item Data of the item that is to be posted
1384          *
1385          * @return string activity
1386          */
1387         private static function construct_verb($item) {
1388                 if ($item['verb'])
1389                         return $item['verb'];
1390                 return ACTIVITY_POST;
1391         }
1392
1393         /**
1394          * @brief Returns the given object type if present - otherwise returns the "note" object type
1395          *
1396          * @param array $item Data of the item that is to be posted
1397          *
1398          * @return string Object type
1399          */
1400         private static function construct_objecttype($item) {
1401                 if (in_array($item['object-type'], array(ACTIVITY_OBJ_NOTE, ACTIVITY_OBJ_COMMENT)))
1402                         return $item['object-type'];
1403                 return ACTIVITY_OBJ_NOTE;
1404         }
1405
1406         /**
1407          * @brief Adds an entry element to the XML document
1408          *
1409          * @param object $doc XML document
1410          * @param array $item Data of the item that is to be posted
1411          * @param array $owner Contact data of the poster
1412          * @param bool $toplevel
1413          *
1414          * @return object Entry element
1415          */
1416         private static function entry($doc, $item, $owner, $toplevel = false) {
1417                 $repeated_guid = self::get_reshared_guid($item);
1418                 if ($repeated_guid != "")
1419                         $xml = self::reshare_entry($doc, $item, $owner, $repeated_guid, $toplevel);
1420
1421                 if ($xml)
1422                         return $xml;
1423
1424                 if ($item["verb"] == ACTIVITY_LIKE) {
1425                         return self::like_entry($doc, $item, $owner, $toplevel);
1426                 } elseif (in_array($item["verb"], array(ACTIVITY_FOLLOW, NAMESPACE_OSTATUS."/unfollow"))) {
1427                         return self::follow_entry($doc, $item, $owner, $toplevel);
1428                 } else {
1429                         return self::note_entry($doc, $item, $owner, $toplevel);
1430                 }
1431         }
1432
1433         /**
1434          * @brief Adds a source entry to the XML document
1435          *
1436          * @param object $doc XML document
1437          * @param array $contact Array of the contact that is added
1438          *
1439          * @return object Source element
1440          */
1441         private static function source_entry($doc, $contact) {
1442                 $source = $doc->createElement("source");
1443                 xml::add_element($doc, $source, "id", $contact["poll"]);
1444                 xml::add_element($doc, $source, "title", $contact["name"]);
1445                 xml::add_element($doc, $source, "link", "", array("rel" => "alternate",
1446                                                                 "type" => "text/html",
1447                                                                 "href" => $contact["alias"]));
1448                 xml::add_element($doc, $source, "link", "", array("rel" => "self",
1449                                                                 "type" => "application/atom+xml",
1450                                                                 "href" => $contact["poll"]));
1451                 xml::add_element($doc, $source, "icon", $contact["photo"]);
1452                 xml::add_element($doc, $source, "updated", datetime_convert("UTC","UTC",$contact["success_update"]."+00:00",ATOM_TIME));
1453
1454                 return $source;
1455         }
1456
1457         /**
1458          * @brief Fetches contact data from the contact or the gcontact table
1459          *
1460          * @param string $url URL of the contact
1461          * @param array $owner Contact data of the poster
1462          *
1463          * @return array Contact array
1464          */
1465         private static function contact_entry($url, $owner) {
1466
1467                 $r = q("SELECT * FROM `contact` WHERE `nurl` = '%s' AND `uid` IN (0, %d) ORDER BY `uid` DESC LIMIT 1",
1468                         dbesc(normalise_link($url)), intval($owner["uid"]));
1469                 if (dbm::is_result($r)) {
1470                         $contact = $r[0];
1471                         $contact["uid"] = -1;
1472                 }
1473
1474                 if (!dbm::is_result($r)) {
1475                         $r = q("SELECT * FROM `gcontact` WHERE `nurl` = '%s' LIMIT 1",
1476                                 dbesc(normalise_link($url)));
1477                         if (dbm::is_result($r)) {
1478                                 $contact = $r[0];
1479                                 $contact["uid"] = -1;
1480                                 $contact["success_update"] = $contact["updated"];
1481                         }
1482                 }
1483
1484                 if (!dbm::is_result($r))
1485                         $contact = owner;
1486
1487                 if (!isset($contact["poll"])) {
1488                         $data = probe_url($url);
1489                         $contact["poll"] = $data["poll"];
1490
1491                         if (!$contact["alias"])
1492                                 $contact["alias"] = $data["alias"];
1493                 }
1494
1495                 if (!isset($contact["alias"]))
1496                         $contact["alias"] = $contact["url"];
1497
1498                 return $contact;
1499         }
1500
1501         /**
1502          * @brief Adds an entry element with reshared content
1503          *
1504          * @param object $doc XML document
1505          * @param array $item Data of the item that is to be posted
1506          * @param array $owner Contact data of the poster
1507          * @param $repeated_guid
1508          * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
1509          *
1510          * @return object Entry element
1511          */
1512         private static function reshare_entry($doc, $item, $owner, $repeated_guid, $toplevel) {
1513
1514                 if (($item["id"] != $item["parent"]) && (normalise_link($item["author-link"]) != normalise_link($owner["url"]))) {
1515                         logger("OStatus entry is from author ".$owner["url"]." - not from ".$item["author-link"].". Quitting.", LOGGER_DEBUG);
1516                 }
1517
1518                 $title = self::entry_header($doc, $entry, $owner, $toplevel);
1519
1520                 $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `guid` = '%s' AND NOT `private` AND `network` IN ('%s', '%s', '%s') LIMIT 1",
1521                         intval($owner["uid"]), dbesc($repeated_guid),
1522                         dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS));
1523                 if (dbm::is_result($r)) {
1524                         $repeated_item = $r[0];
1525                 } else {
1526                         return false;
1527                 }
1528                 $contact = self::contact_entry($repeated_item['author-link'], $owner);
1529
1530                 $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']);
1531
1532                 $title = $owner["nick"]." repeated a notice by ".$contact["nick"];
1533
1534                 self::entry_content($doc, $entry, $item, $owner, $title, ACTIVITY_SHARE, false);
1535
1536                 $as_object = $doc->createElement("activity:object");
1537
1538                 xml::add_element($doc, $as_object, "activity:object-type", NAMESPACE_ACTIVITY_SCHEMA."activity");
1539
1540                 self::entry_content($doc, $as_object, $repeated_item, $owner, "", "", false);
1541
1542                 $author = self::add_author($doc, $contact);
1543                 $as_object->appendChild($author);
1544
1545                 $as_object2 = $doc->createElement("activity:object");
1546
1547                 xml::add_element($doc, $as_object2, "activity:object-type", self::construct_objecttype($repeated_item));
1548
1549                 $title = sprintf("New comment by %s", $contact["nick"]);
1550
1551                 self::entry_content($doc, $as_object2, $repeated_item, $owner, $title);
1552
1553                 $as_object->appendChild($as_object2);
1554
1555                 self::entry_footer($doc, $as_object, $item, $owner, false);
1556
1557                 $source = self::source_entry($doc, $contact);
1558
1559                 $as_object->appendChild($source);
1560
1561                 $entry->appendChild($as_object);
1562
1563                 self::entry_footer($doc, $entry, $item, $owner);
1564
1565                 return $entry;
1566         }
1567
1568         /**
1569          * @brief Adds an entry element with a "like"
1570          *
1571          * @param object $doc XML document
1572          * @param array $item Data of the item that is to be posted
1573          * @param array $owner Contact data of the poster
1574          * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
1575          *
1576          * @return object Entry element with "like"
1577          */
1578         private static function like_entry($doc, $item, $owner, $toplevel) {
1579
1580                 if (($item["id"] != $item["parent"]) && (normalise_link($item["author-link"]) != normalise_link($owner["url"]))) {
1581                         logger("OStatus entry is from author ".$owner["url"]." - not from ".$item["author-link"].". Quitting.", LOGGER_DEBUG);
1582                 }
1583
1584                 $title = self::entry_header($doc, $entry, $owner, $toplevel);
1585
1586                 $verb = NAMESPACE_ACTIVITY_SCHEMA."favorite";
1587                 self::entry_content($doc, $entry, $item, $owner, "Favorite", $verb, false);
1588
1589                 $as_object = $doc->createElement("activity:object");
1590
1591                 $parent = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d",
1592                         dbesc($item["thr-parent"]), intval($item["uid"]));
1593                 $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']);
1594
1595                 xml::add_element($doc, $as_object, "activity:object-type", self::construct_objecttype($parent[0]));
1596
1597                 self::entry_content($doc, $as_object, $parent[0], $owner, "New entry");
1598
1599                 $entry->appendChild($as_object);
1600
1601                 self::entry_footer($doc, $entry, $item, $owner);
1602
1603                 return $entry;
1604         }
1605
1606         /**
1607          * @brief Adds the person object element to the XML document
1608          *
1609          * @param object $doc XML document
1610          * @param array $owner Contact data of the poster
1611          * @param array $contact Contact data of the target
1612          *
1613          * @return object author element
1614          */
1615         private static function add_person_object($doc, $owner, $contact) {
1616
1617                 $object = $doc->createElement("activity:object");
1618                 xml::add_element($doc, $object, "activity:object-type", ACTIVITY_OBJ_PERSON);
1619
1620                 if ($contact['network'] == NETWORK_PHANTOM) {
1621                         xml::add_element($doc, $object, "id", $contact['url']);
1622                         return $object;
1623                 }
1624
1625                 xml::add_element($doc, $object, "id", $contact["alias"]);
1626                 xml::add_element($doc, $object, "title", $contact["nick"]);
1627
1628                 $attributes = array("rel" => "alternate", "type" => "text/html", "href" => $contact["url"]);
1629                 xml::add_element($doc, $object, "link", "", $attributes);
1630
1631                 $attributes = array(
1632                                 "rel" => "avatar",
1633                                 "type" => "image/jpeg", // To-Do?
1634                                 "media:width" => 175,
1635                                 "media:height" => 175,
1636                                 "href" => $contact["photo"]);
1637                 xml::add_element($doc, $object, "link", "", $attributes);
1638
1639                 xml::add_element($doc, $object, "poco:preferredUsername", $contact["nick"]);
1640                 xml::add_element($doc, $object, "poco:displayName", $contact["name"]);
1641
1642                 if (trim($contact["location"]) != "") {
1643                         $element = $doc->createElement("poco:address");
1644                         xml::add_element($doc, $element, "poco:formatted", $contact["location"]);
1645                         $object->appendChild($element);
1646                 }
1647
1648                 return $object;
1649         }
1650
1651         /**
1652          * @brief Adds a follow/unfollow entry element
1653          *
1654          * @param object $doc XML document
1655          * @param array $item Data of the follow/unfollow message
1656          * @param array $owner Contact data of the poster
1657          * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
1658          *
1659          * @return object Entry element
1660          */
1661         private static function follow_entry($doc, $item, $owner, $toplevel) {
1662
1663                 $item["id"] = $item["parent"] = 0;
1664                 $item["created"] = $item["edited"] = date("c");
1665                 $item["private"] = true;
1666
1667                 $contact = Probe::uri($item['follow']);
1668
1669                 if ($contact['alias'] == '') {
1670                         $contact['alias'] = $contact["url"];
1671                 } else {
1672                         $item['follow'] = $contact['alias'];
1673                 }
1674
1675                 $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s'",
1676                         intval($owner['uid']), dbesc(normalise_link($contact["url"])));
1677
1678                 if (dbm::is_result($r)) {
1679                         $connect_id = $r[0]['id'];
1680                 } else {
1681                         $connect_id = 0;
1682                 }
1683
1684                 if ($item['verb'] == ACTIVITY_FOLLOW) {
1685                         $message = t('%s is now following %s.');
1686                         $title = t('following');
1687                         $action = "subscription";
1688                 } else {
1689                         $message = t('%s stopped following %s.');
1690                         $title = t('stopped following');
1691                         $action = "unfollow";
1692                 }
1693
1694                 $item["uri"] = $item['parent-uri'] = $item['thr-parent'] =
1695                                 'tag:'.get_app()->get_hostname().
1696                                 ','.date('Y-m-d').':'.$action.':'.$owner['uid'].
1697                                 ':person:'.$connect_id.':'.$item['created'];
1698
1699                 $item["body"] = sprintf($message, $owner["nick"], $contact["nick"]);
1700
1701                 self::entry_header($doc, $entry, $owner, $toplevel);
1702
1703                 self::entry_content($doc, $entry, $item, $owner, $title);
1704
1705                 $object = self::add_person_object($doc, $owner, $contact);
1706                 $entry->appendChild($object);
1707
1708                 self::entry_footer($doc, $entry, $item, $owner);
1709
1710                 return $entry;
1711         }
1712
1713         /**
1714          * @brief Adds a regular entry element
1715          *
1716          * @param object $doc XML document
1717          * @param array $item Data of the item that is to be posted
1718          * @param array $owner Contact data of the poster
1719          * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
1720          *
1721          * @return object Entry element
1722          */
1723         private static function note_entry($doc, $item, $owner, $toplevel) {
1724
1725                 if (($item["id"] != $item["parent"]) && (normalise_link($item["author-link"]) != normalise_link($owner["url"]))) {
1726                         logger("OStatus entry is from author ".$owner["url"]." - not from ".$item["author-link"].". Quitting.", LOGGER_DEBUG);
1727                 }
1728
1729                 $title = self::entry_header($doc, $entry, $owner, $toplevel);
1730
1731                 xml::add_element($doc, $entry, "activity:object-type", ACTIVITY_OBJ_NOTE);
1732
1733                 self::entry_content($doc, $entry, $item, $owner, $title);
1734
1735                 self::entry_footer($doc, $entry, $item, $owner);
1736
1737                 return $entry;
1738         }
1739
1740         /**
1741          * @brief Adds a header element to the XML document
1742          *
1743          * @param object $doc XML document
1744          * @param object $entry The entry element where the elements are added
1745          * @param array $owner Contact data of the poster
1746          * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
1747          *
1748          * @return string The title for the element
1749          */
1750         private static function entry_header($doc, &$entry, $owner, $toplevel) {
1751                 /// @todo Check if this title stuff is really needed (I guess not)
1752                 if (!$toplevel) {
1753                         $entry = $doc->createElement("entry");
1754                         $title = sprintf("New note by %s", $owner["nick"]);
1755                 } else {
1756                         $entry = $doc->createElementNS(NAMESPACE_ATOM1, "entry");
1757
1758                         $entry->setAttribute("xmlns:thr", NAMESPACE_THREAD);
1759                         $entry->setAttribute("xmlns:georss", NAMESPACE_GEORSS);
1760                         $entry->setAttribute("xmlns:activity", NAMESPACE_ACTIVITY);
1761                         $entry->setAttribute("xmlns:media", NAMESPACE_MEDIA);
1762                         $entry->setAttribute("xmlns:poco", NAMESPACE_POCO);
1763                         $entry->setAttribute("xmlns:ostatus", NAMESPACE_OSTATUS);
1764                         $entry->setAttribute("xmlns:statusnet", NAMESPACE_STATUSNET);
1765                         $entry->setAttribute("xmlns:mastodon", NAMESPACE_MASTODON);
1766
1767                         $author = self::add_author($doc, $owner);
1768                         $entry->appendChild($author);
1769
1770                         $title = sprintf("New comment by %s", $owner["nick"]);
1771                 }
1772                 return $title;
1773         }
1774
1775         /**
1776          * @brief Adds elements to the XML document
1777          *
1778          * @param object $doc XML document
1779          * @param object $entry Entry element where the content is added
1780          * @param array $item Data of the item that is to be posted
1781          * @param array $owner Contact data of the poster
1782          * @param string $title Title for the post
1783          * @param string $verb The activity verb
1784          * @param bool $complete Add the "status_net" element?
1785          */
1786         private static function entry_content($doc, $entry, $item, $owner, $title, $verb = "", $complete = true) {
1787
1788                 if ($verb == "")
1789                         $verb = self::construct_verb($item);
1790
1791                 xml::add_element($doc, $entry, "id", $item["uri"]);
1792                 xml::add_element($doc, $entry, "title", $title);
1793
1794                 $body = self::format_picture_post($item['body']);
1795
1796                 if ($item['title'] != "")
1797                         $body = "[b]".$item['title']."[/b]\n\n".$body;
1798
1799                 $body = bbcode($body, false, false, 7);
1800
1801                 xml::add_element($doc, $entry, "content", $body, array("type" => "html"));
1802
1803                 xml::add_element($doc, $entry, "link", "", array("rel" => "alternate", "type" => "text/html",
1804                                                                 "href" => System::baseUrl()."/display/".$item["guid"]));
1805
1806                 if ($complete && ($item["id"] > 0))
1807                         xml::add_element($doc, $entry, "status_net", "", array("notice_id" => $item["id"]));
1808
1809                 xml::add_element($doc, $entry, "activity:verb", $verb);
1810
1811                 xml::add_element($doc, $entry, "published", datetime_convert("UTC","UTC",$item["created"]."+00:00",ATOM_TIME));
1812                 xml::add_element($doc, $entry, "updated", datetime_convert("UTC","UTC",$item["edited"]."+00:00",ATOM_TIME));
1813         }
1814
1815         /**
1816          * @brief Adds the elements at the foot of an entry to the XML document
1817          *
1818          * @param object $doc XML document
1819          * @param object $entry The entry element where the elements are added
1820          * @param array $item Data of the item that is to be posted
1821          * @param array $owner Contact data of the poster
1822          * @param $complete
1823          */
1824         private static function entry_footer($doc, $entry, $item, $owner, $complete = true) {
1825
1826                 $mentioned = array();
1827
1828                 if (($item['parent'] != $item['id']) || ($item['parent-uri'] !== $item['uri']) || (($item['thr-parent'] !== '') && ($item['thr-parent'] !== $item['uri']))) {
1829                         $parent = q("SELECT `guid`, `author-link`, `owner-link` FROM `item` WHERE `id` = %d", intval($item["parent"]));
1830                         $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']);
1831
1832                         $thrparent = q("SELECT `guid`, `author-link`, `owner-link`, `plink` FROM `item` WHERE `uid` = %d AND `uri` = '%s'",
1833                                         intval($owner["uid"]),
1834                                         dbesc($parent_item));
1835                         if ($thrparent) {
1836                                 $mentioned[$thrparent[0]["author-link"]] = $thrparent[0]["author-link"];
1837                                 $mentioned[$thrparent[0]["owner-link"]] = $thrparent[0]["owner-link"];
1838                                 $parent_plink = $thrparent[0]["plink"];
1839                         } else {
1840                                 $mentioned[$parent[0]["author-link"]] = $parent[0]["author-link"];
1841                                 $mentioned[$parent[0]["owner-link"]] = $parent[0]["owner-link"];
1842                                 $parent_plink = System::baseUrl()."/display/".$parent[0]["guid"];
1843                         }
1844
1845                         $attributes = array(
1846                                         "ref" => $parent_item,
1847                                         "href" => $parent_plink);
1848                         xml::add_element($doc, $entry, "thr:in-reply-to", "", $attributes);
1849
1850                         $attributes = array(
1851                                         "rel" => "related",
1852                                         "href" => $parent_plink);
1853                         xml::add_element($doc, $entry, "link", "", $attributes);
1854                 }
1855
1856                 if (intval($item["parent"]) > 0) {
1857                         $conversation_href = System::baseUrl()."/display/".$owner["nick"]."/".$item["parent"];
1858                         $conversation_uri = $conversation_href;
1859
1860                         if (isset($parent_item)) {
1861                                 $r = dba::fetch_first("SELECT `conversation-uri`, `conversation-href` FROM `conversation` WHERE `item-uri` = ?", $parent_item);
1862                                 if (dbm::is_result($r)) {
1863                                         if ($r['conversation-uri'] != '') {
1864                                                 $conversation_uri = $r['conversation-uri'];
1865                                         }
1866                                         if ($r['conversation-href'] != '') {
1867                                                 $conversation_href = $r['conversation-href'];
1868                                         }
1869                                 }
1870                         }
1871
1872                         xml::add_element($doc, $entry, "link", "", array("rel" => "ostatus:conversation", "href" => $conversation_href));
1873
1874                         $attributes = array(
1875                                         "href" => $conversation_href,
1876                                         "local_id" => $item["parent"],
1877                                         "ref" => $conversation_uri);
1878
1879                         xml::add_element($doc, $entry, "ostatus:conversation", $conversation_uri, $attributes);
1880                 }
1881
1882                 $tags = item_getfeedtags($item);
1883
1884                 if (count($tags))
1885                         foreach ($tags as $t)
1886                                 if ($t[0] == "@")
1887                                         $mentioned[$t[1]] = $t[1];
1888
1889                 // Make sure that mentions are accepted (GNU Social has problems with mixing HTTP and HTTPS)
1890                 $newmentions = array();
1891                 foreach ($mentioned AS $mention) {
1892                         $newmentions[str_replace("http://", "https://", $mention)] = str_replace("http://", "https://", $mention);
1893                         $newmentions[str_replace("https://", "http://", $mention)] = str_replace("https://", "http://", $mention);
1894                 }
1895                 $mentioned = $newmentions;
1896
1897                 foreach ($mentioned AS $mention) {
1898                         $r = q("SELECT `forum`, `prv` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s'",
1899                                 intval($owner["uid"]),
1900                                 dbesc(normalise_link($mention)));
1901                         if ($r[0]["forum"] || $r[0]["prv"])
1902                                 xml::add_element($doc, $entry, "link", "", array("rel" => "mentioned",
1903                                                                                         "ostatus:object-type" => ACTIVITY_OBJ_GROUP,
1904                                                                                         "href" => $mention));
1905                         else
1906                                 xml::add_element($doc, $entry, "link", "", array("rel" => "mentioned",
1907                                                                                         "ostatus:object-type" => ACTIVITY_OBJ_PERSON,
1908                                                                                         "href" => $mention));
1909                 }
1910
1911                 if (!$item["private"]) {
1912                         xml::add_element($doc, $entry, "link", "", array("rel" => "ostatus:attention",
1913                                                                         "href" => "http://activityschema.org/collection/public"));
1914                         xml::add_element($doc, $entry, "link", "", array("rel" => "mentioned",
1915                                                                         "ostatus:object-type" => "http://activitystrea.ms/schema/1.0/collection",
1916                                                                         "href" => "http://activityschema.org/collection/public"));
1917                         xml::add_element($doc, $entry, "mastodon:scope", "public");
1918                 }
1919
1920                 if (count($tags))
1921                         foreach ($tags as $t)
1922                                 if ($t[0] != "@")
1923                                         xml::add_element($doc, $entry, "category", "", array("term" => $t[2]));
1924
1925                 self::get_attachment($doc, $entry, $item);
1926
1927                 if ($complete && ($item["id"] > 0)) {
1928                         $app = $item["app"];
1929                         if ($app == "")
1930                                 $app = "web";
1931
1932                         $attributes = array("local_id" => $item["id"], "source" => $app);
1933
1934                         if (isset($parent["id"]))
1935                                 $attributes["repeat_of"] = $parent["id"];
1936
1937                         if ($item["coord"] != "")
1938                                 xml::add_element($doc, $entry, "georss:point", $item["coord"]);
1939
1940                         xml::add_element($doc, $entry, "statusnet:notice_info", "", $attributes);
1941                 }
1942         }
1943
1944         /**
1945          * @brief Creates the XML feed for a given nickname
1946          *
1947          * @param App $a The application class
1948          * @param string $owner_nick Nickname of the feed owner
1949          * @param string $last_update Date of the last update
1950          * @param integer $max_items Number of maximum items to fetch
1951          *
1952          * @return string XML feed
1953          */
1954         public static function feed(App $a, $owner_nick, &$last_update, $max_items = 300) {
1955                 $stamp = microtime(true);
1956
1957                 $cachekey = "ostatus:feed:".$owner_nick.":".$last_update;
1958
1959                 $previous_created = $last_update;
1960
1961                 $result = Cache::get($cachekey);
1962                 if (!is_null($result)) {
1963                         logger('Feed duration: '.number_format(microtime(true) - $stamp, 3).' - '.$owner_nick.' - '.$previous_created.' (cached)', LOGGER_DEBUG);
1964                         $last_update = $result['last_update'];
1965                         return $result['feed'];
1966                 }
1967
1968                 $r = q("SELECT `contact`.*, `user`.`nickname`, `user`.`timezone`, `user`.`page-flags`
1969                                 FROM `contact` INNER JOIN `user` ON `user`.`uid` = `contact`.`uid`
1970                                 WHERE `contact`.`self` AND `user`.`nickname` = '%s' LIMIT 1",
1971                                 dbesc($owner_nick));
1972                 if (!dbm::is_result($r)) {
1973                         return;
1974                 }
1975
1976                 $owner = $r[0];
1977
1978                 if (!strlen($last_update)) {
1979                         $last_update = 'now -30 days';
1980                 }
1981
1982                 $check_date = datetime_convert('UTC','UTC',$last_update,'Y-m-d H:i:s');
1983                 $authorid = get_contact($owner["url"], 0);
1984
1985                 $items = q("SELECT `item`.*, `item`.`id` AS `item_id` FROM `item` USE INDEX (`uid_contactid_created`)
1986                                 STRAIGHT_JOIN `thread` ON `thread`.`iid` = `item`.`parent`
1987                                 WHERE `item`.`uid` = %d AND `item`.`contact-id` = %d AND
1988                                         `item`.`author-id` = %d AND `item`.`created` > '%s' AND
1989                                         NOT `item`.`deleted` AND NOT `item`.`private` AND
1990                                         `thread`.`network` IN ('%s', '%s')
1991                                 ORDER BY `item`.`created` DESC LIMIT %d",
1992                                 intval($owner["uid"]), intval($owner["id"]),
1993                                 intval($authorid), dbesc($check_date),
1994                                 dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN), intval($max_items));
1995
1996                 $doc = new DOMDocument('1.0', 'utf-8');
1997                 $doc->formatOutput = true;
1998
1999                 $root = self::add_header($doc, $owner);
2000
2001                 foreach ($items AS $item) {
2002                         if (Config::get('system', 'ostatus_debug')) {
2003                                 $item['body'] .= '🍼';
2004                         }
2005                         $entry = self::entry($doc, $item, $owner);
2006                         $root->appendChild($entry);
2007
2008                         if ($last_update < $item['created']) {
2009                                 $last_update = $item['created'];
2010                         }
2011                 }
2012
2013                 $feeddata = trim($doc->saveXML());
2014
2015                 $msg = array('feed' => $feeddata, 'last_update' => $last_update);
2016                 Cache::set($cachekey, $msg, CACHE_QUARTER_HOUR);
2017
2018                 logger('Feed duration: '.number_format(microtime(true) - $stamp, 3).' - '.$owner_nick.' - '.$previous_created, LOGGER_DEBUG);
2019
2020                 return $feeddata;
2021         }
2022
2023         /**
2024          * @brief Creates the XML for a salmon message
2025          *
2026          * @param array $item Data of the item that is to be posted
2027          * @param array $owner Contact data of the poster
2028          *
2029          * @return string XML for the salmon
2030          */
2031         public static function salmon($item,$owner) {
2032
2033                 $doc = new DOMDocument('1.0', 'utf-8');
2034                 $doc->formatOutput = true;
2035
2036                 if (Config::get('system', 'ostatus_debug')) {
2037                         $item['body'] .= '🐟';
2038                 }
2039
2040                 $entry = self::entry($doc, $item, $owner, true);
2041
2042                 $doc->appendChild($entry);
2043
2044                 return trim($doc->saveXML());
2045         }
2046 }