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