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