]> git.mxchange.org Git - friendica.git/blob - src/Protocol/DFRN.php
Merge pull request #10817 from MrPetovan/task/refactor-notifications
[friendica.git] / src / Protocol / DFRN.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Protocol;
23
24 use DOMDocument;
25 use DOMXPath;
26 use Friendica\Content\Text\BBCode;
27 use Friendica\Core\Logger;
28 use Friendica\Core\Protocol;
29 use Friendica\Database\DBA;
30 use Friendica\DI;
31 use Friendica\Model\Contact;
32 use Friendica\Model\Conversation;
33 use Friendica\Model\Event;
34 use Friendica\Model\FContact;
35 use Friendica\Model\GServer;
36 use Friendica\Model\Item;
37 use Friendica\Model\ItemURI;
38 use Friendica\Model\Mail;
39 use Friendica\Model\Notification;
40 use Friendica\Model\Photo;
41 use Friendica\Model\Post;
42 use Friendica\Model\Profile;
43 use Friendica\Model\Tag;
44 use Friendica\Model\User;
45 use Friendica\Network\Probe;
46 use Friendica\Util\Crypto;
47 use Friendica\Util\DateTimeFormat;
48 use Friendica\Util\Images;
49 use Friendica\Util\Network;
50 use Friendica\Util\Proxy;
51 use Friendica\Util\Strings;
52 use Friendica\Util\XML;
53
54 /**
55  * This class contain functions to create and send DFRN XML files
56  */
57 class DFRN
58 {
59
60         const TOP_LEVEL = 0;    // Top level posting
61         const REPLY = 1;                // Regular reply that is stored locally
62         const REPLY_RC = 2;     // Reply that will be relayed
63
64         /**
65          * Generates an array of contact and user for DFRN imports
66          *
67          * This array contains not only the receiver but also the sender of the message.
68          *
69          * @param integer $cid Contact id
70          * @param integer $uid User id
71          *
72          * @return array importer
73          * @throws \Exception
74          */
75         public static function getImporter($cid, $uid = 0)
76         {
77                 $condition = ['id' => $cid, 'blocked' => false, 'pending' => false];
78                 $contact = DBA::selectFirst('contact', [], $condition);
79                 if (!DBA::isResult($contact)) {
80                         return [];
81                 }
82
83                 $contact['cpubkey'] = $contact['pubkey'];
84                 $contact['cprvkey'] = $contact['prvkey'];
85                 $contact['senderName'] = $contact['name'];
86
87                 if ($uid != 0) {
88                         $condition = ['uid' => $uid, 'account_expired' => false, 'account_removed' => false];
89                         $user = DBA::selectFirst('user', [], $condition);
90                         if (!DBA::isResult($user)) {
91                                 return [];
92                         }
93
94                         $user['importer_uid'] = $user['uid'];
95                         $user['uprvkey'] = $user['prvkey'];
96                 } else {
97                         $user = ['importer_uid' => 0, 'uprvkey' => '', 'timezone' => 'UTC',
98                                 'nickname' => '', 'sprvkey' => '', 'spubkey' => '',
99                                 'page-flags' => 0, 'account-type' => 0, 'prvnets' => 0];
100                 }
101
102                 return array_merge($contact, $user);
103         }
104
105         /**
106          * Generates the atom entries for delivery.php
107          *
108          * This function is used whenever content is transmitted via DFRN.
109          *
110          * @param array $items Item elements
111          * @param array $owner Owner record
112          *
113          * @return string DFRN entries
114          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
115          * @throws \ImagickException
116          * @todo  Find proper type-hints
117          */
118         public static function entries($items, $owner)
119         {
120                 $doc = new DOMDocument('1.0', 'utf-8');
121                 $doc->formatOutput = true;
122
123                 $root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
124
125                 if (! count($items)) {
126                         return trim($doc->saveXML());
127                 }
128
129                 foreach ($items as $item) {
130                         // These values aren't sent when sending from the queue.
131                         /// @todo Check if we can set these values from the queue or if they are needed at all.
132                         $item["entry:comment-allow"] = ($item["entry:comment-allow"] ?? '') ?: true;
133                         $item["entry:cid"] = $item["entry:cid"] ?? 0;
134
135                         $entry = self::entry($doc, "text", $item, $owner, $item["entry:comment-allow"], $item["entry:cid"]);
136                         if (isset($entry)) {
137                                 $root->appendChild($entry);
138                         }
139                 }
140
141                 return trim($doc->saveXML());
142         }
143
144         /**
145          * Generate an atom entry for a given uri id and user
146          *
147          * @param int     $uri_id       The uri id
148          * @param int     $uid          The user id
149          * @param boolean $conversation Show the conversation. If false show the single post.
150          *
151          * @return string DFRN feed entry
152          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
153          * @throws \ImagickException
154          */
155         public static function itemFeed(int $uri_id, int $uid, bool $conversation = false)
156         {
157                 if ($conversation) {
158                         $condition = ['parent-uri-id' => $uri_id];
159                 } else {
160                         $condition = ['uri-id' => $uri_id];
161                 }
162
163                 $condition['uid'] = $uid;
164
165                 $items = Post::selectToArray(Item::DELIVER_FIELDLIST, $condition);
166                 if (!DBA::isResult($items)) {
167                         return '';
168                 }
169
170                 $item = $items[0];
171
172                 if ($item['uid'] != 0) {
173                         $owner = User::getOwnerDataById($item['uid']);
174                         if (!$owner) {
175                                 return '';
176                         }
177                 } else {
178                         $owner = ['uid' => 0, 'nick' => 'feed-item'];
179                 }
180
181                 $doc = new DOMDocument('1.0', 'utf-8');
182                 $doc->formatOutput = true;
183                 $type = 'html';
184
185                 if ($conversation) {
186                         $root = $doc->createElementNS(ActivityNamespace::ATOM1, 'feed');
187                         $doc->appendChild($root);
188
189                         $root->setAttribute("xmlns:thr", ActivityNamespace::THREAD);
190                         $root->setAttribute("xmlns:at", ActivityNamespace::TOMB);
191                         $root->setAttribute("xmlns:media", ActivityNamespace::MEDIA);
192                         $root->setAttribute("xmlns:dfrn", ActivityNamespace::DFRN);
193                         $root->setAttribute("xmlns:activity", ActivityNamespace::ACTIVITY);
194                         $root->setAttribute("xmlns:georss", ActivityNamespace::GEORSS);
195                         $root->setAttribute("xmlns:poco", ActivityNamespace::POCO);
196                         $root->setAttribute("xmlns:ostatus", ActivityNamespace::OSTATUS);
197                         $root->setAttribute("xmlns:statusnet", ActivityNamespace::STATUSNET);
198
199                         //$root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
200
201                         foreach ($items as $item) {
202                                 $entry = self::entry($doc, $type, $item, $owner, true, 0);
203                                 if (isset($entry)) {
204                                         $root->appendChild($entry);
205                                 }
206                         }
207                 } else {
208                         self::entry($doc, $type, $item, $owner, true, 0, true);
209                 }
210
211                 $atom = trim($doc->saveXML());
212                 return $atom;
213         }
214
215         /**
216          * Create XML text for DFRN mails
217          *
218          * @param array $mail  Mail record
219          * @param array $owner Owner record
220          *
221          * @return string DFRN mail
222          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
223          * @todo  Find proper type-hints
224          */
225         public static function mail(array $mail, array $owner)
226         {
227                 $doc = new DOMDocument('1.0', 'utf-8');
228                 $doc->formatOutput = true;
229
230                 $root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
231
232                 $mailElement = $doc->createElement("dfrn:mail");
233                 $senderElement = $doc->createElement("dfrn:sender");
234
235                 XML::addElement($doc, $senderElement, "dfrn:name", $owner['name']);
236                 XML::addElement($doc, $senderElement, "dfrn:uri", $owner['url']);
237                 XML::addElement($doc, $senderElement, "dfrn:avatar", $owner['thumb']);
238
239                 $mailElement->appendChild($senderElement);
240
241                 XML::addElement($doc, $mailElement, "dfrn:id", $mail['uri']);
242                 XML::addElement($doc, $mailElement, "dfrn:in-reply-to", $mail['parent-uri']);
243                 XML::addElement($doc, $mailElement, "dfrn:sentdate", DateTimeFormat::utc($mail['created'] . '+00:00', DateTimeFormat::ATOM));
244                 XML::addElement($doc, $mailElement, "dfrn:subject", $mail['title']);
245                 XML::addElement($doc, $mailElement, "dfrn:content", $mail['body']);
246
247                 $root->appendChild($mailElement);
248
249                 return trim($doc->saveXML());
250         }
251
252         /**
253          * Create XML text for DFRN friend suggestions
254          *
255          * @param array $item  suggestion elements
256          * @param array $owner Owner record
257          *
258          * @return string DFRN suggestions
259          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
260          * @todo  Find proper type-hints
261          */
262         public static function fsuggest($item, $owner)
263         {
264                 $doc = new DOMDocument('1.0', 'utf-8');
265                 $doc->formatOutput = true;
266
267                 $root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
268
269                 $suggest = $doc->createElement("dfrn:suggest");
270
271                 XML::addElement($doc, $suggest, "dfrn:url", $item['url']);
272                 XML::addElement($doc, $suggest, "dfrn:name", $item['name']);
273                 XML::addElement($doc, $suggest, "dfrn:photo", $item['photo']);
274                 XML::addElement($doc, $suggest, "dfrn:request", $item['request']);
275                 XML::addElement($doc, $suggest, "dfrn:note", $item['note']);
276
277                 $root->appendChild($suggest);
278
279                 return trim($doc->saveXML());
280         }
281
282         /**
283          * Create XML text for DFRN relocations
284          *
285          * @param array $owner Owner record
286          * @param int   $uid   User ID
287          *
288          * @return string DFRN relocations
289          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
290          * @todo  Find proper type-hints
291          */
292         public static function relocate($owner, $uid)
293         {
294
295                 /* get site pubkey. this could be a new installation with no site keys*/
296                 $pubkey = DI::config()->get('system', 'site_pubkey');
297                 if (! $pubkey) {
298                         $res = Crypto::newKeypair(1024);
299                         DI::config()->set('system', 'site_prvkey', $res['prvkey']);
300                         DI::config()->set('system', 'site_pubkey', $res['pubkey']);
301                 }
302
303                 $profilephotos = Photo::selectToArray(['resource-id' , 'scale'], ['profile' => true, 'uid' => $uid], ['order' => ['scale']]);
304
305                 $photos = [];
306                 $ext = Images::supportedTypes();
307
308                 foreach ($profilephotos as $p) {
309                         $photos[$p['scale']] = DI::baseUrl().'/photo/'.$p['resource-id'].'-'.$p['scale'].'.'.$ext[$p['type']];
310                 }
311
312
313                 $doc = new DOMDocument('1.0', 'utf-8');
314                 $doc->formatOutput = true;
315
316                 $root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
317
318                 $relocate = $doc->createElement("dfrn:relocate");
319
320                 XML::addElement($doc, $relocate, "dfrn:url", $owner['url']);
321                 XML::addElement($doc, $relocate, "dfrn:name", $owner['name']);
322                 XML::addElement($doc, $relocate, "dfrn:addr", $owner['addr']);
323                 XML::addElement($doc, $relocate, "dfrn:avatar", $owner['avatar']);
324                 XML::addElement($doc, $relocate, "dfrn:photo", $photos[4]);
325                 XML::addElement($doc, $relocate, "dfrn:thumb", $photos[5]);
326                 XML::addElement($doc, $relocate, "dfrn:micro", $photos[6]);
327                 XML::addElement($doc, $relocate, "dfrn:request", $owner['request']);
328                 XML::addElement($doc, $relocate, "dfrn:confirm", $owner['confirm']);
329                 XML::addElement($doc, $relocate, "dfrn:notify", $owner['notify']);
330                 XML::addElement($doc, $relocate, "dfrn:poll", $owner['poll']);
331                 XML::addElement($doc, $relocate, "dfrn:sitepubkey", DI::config()->get('system', 'site_pubkey'));
332
333                 $root->appendChild($relocate);
334
335                 return trim($doc->saveXML());
336         }
337
338         /**
339          * Adds the header elements for the DFRN protocol
340          *
341          * @param DOMDocument $doc           XML document
342          * @param array       $owner         Owner record
343          * @param string      $authorelement Element name for the author
344          * @param string      $alternatelink link to profile or category
345          * @param bool        $public        Is it a header for public posts?
346          *
347          * @return object XML root object
348          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
349          * @todo  Find proper type-hints
350          */
351         private static function addHeader(DOMDocument $doc, $owner, $authorelement, $alternatelink = "", $public = false)
352         {
353
354                 if ($alternatelink == "") {
355                         $alternatelink = $owner['url'];
356                 }
357
358                 $root = $doc->createElementNS(ActivityNamespace::ATOM1, 'feed');
359                 $doc->appendChild($root);
360
361                 $root->setAttribute("xmlns:thr", ActivityNamespace::THREAD);
362                 $root->setAttribute("xmlns:at", ActivityNamespace::TOMB);
363                 $root->setAttribute("xmlns:media", ActivityNamespace::MEDIA);
364                 $root->setAttribute("xmlns:dfrn", ActivityNamespace::DFRN);
365                 $root->setAttribute("xmlns:activity", ActivityNamespace::ACTIVITY);
366                 $root->setAttribute("xmlns:georss", ActivityNamespace::GEORSS);
367                 $root->setAttribute("xmlns:poco", ActivityNamespace::POCO);
368                 $root->setAttribute("xmlns:ostatus", ActivityNamespace::OSTATUS);
369                 $root->setAttribute("xmlns:statusnet", ActivityNamespace::STATUSNET);
370
371                 XML::addElement($doc, $root, "id", DI::baseUrl()."/profile/".$owner["nick"]);
372                 XML::addElement($doc, $root, "title", $owner["name"]);
373
374                 $attributes = ["uri" => "https://friendi.ca", "version" => FRIENDICA_VERSION."-".DB_UPDATE_VERSION];
375                 XML::addElement($doc, $root, "generator", FRIENDICA_PLATFORM, $attributes);
376
377                 $attributes = ["rel" => "license", "href" => "http://creativecommons.org/licenses/by/3.0/"];
378                 XML::addElement($doc, $root, "link", "", $attributes);
379
380                 $attributes = ["rel" => "alternate", "type" => "text/html", "href" => $alternatelink];
381                 XML::addElement($doc, $root, "link", "", $attributes);
382
383
384                 if ($public) {
385                         // DFRN itself doesn't uses this. But maybe someone else wants to subscribe to the public feed.
386                         OStatus::hublinks($doc, $root, $owner["nick"]);
387
388                         $attributes = ["rel" => "salmon", "href" => DI::baseUrl()."/salmon/".$owner["nick"]];
389                         XML::addElement($doc, $root, "link", "", $attributes);
390
391                         $attributes = ["rel" => "http://salmon-protocol.org/ns/salmon-replies", "href" => DI::baseUrl()."/salmon/".$owner["nick"]];
392                         XML::addElement($doc, $root, "link", "", $attributes);
393
394                         $attributes = ["rel" => "http://salmon-protocol.org/ns/salmon-mention", "href" => DI::baseUrl()."/salmon/".$owner["nick"]];
395                         XML::addElement($doc, $root, "link", "", $attributes);
396                 }
397
398                 // For backward compatibility we keep this element
399                 if ($owner['page-flags'] == User::PAGE_FLAGS_COMMUNITY) {
400                         XML::addElement($doc, $root, "dfrn:community", 1);
401                 }
402
403                 // The former element is replaced by this one
404                 XML::addElement($doc, $root, "dfrn:account_type", $owner["account-type"]);
405
406                 /// @todo We need a way to transmit the different page flags like "User::PAGE_FLAGS_PRVGROUP"
407
408                 XML::addElement($doc, $root, "updated", DateTimeFormat::utcNow(DateTimeFormat::ATOM));
409
410                 $author = self::addAuthor($doc, $owner, $authorelement, $public);
411                 $root->appendChild($author);
412
413                 return $root;
414         }
415
416         /**
417          * Adds the author element in the header for the DFRN protocol
418          *
419          * @param DOMDocument $doc           XML document
420          * @param array       $owner         Owner record
421          * @param string      $authorelement Element name for the author
422          * @param boolean     $public        boolean
423          *
424          * @return \DOMElement XML author object
425          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
426          * @todo  Find proper type-hints
427          */
428         private static function addAuthor(DOMDocument $doc, array $owner, $authorelement, $public)
429         {
430                 // Should the profile be "unsearchable" in the net? Then add the "hide" element
431                 $hide = DBA::exists('profile', ['uid' => $owner['uid'], 'net-publish' => false]);
432
433                 $author = $doc->createElement($authorelement);
434
435                 $namdate = DateTimeFormat::utc($owner['name-date'].'+00:00', DateTimeFormat::ATOM);
436                 $picdate = DateTimeFormat::utc($owner['avatar-date'].'+00:00', DateTimeFormat::ATOM);
437
438                 $attributes = [];
439
440                 if (!$public || !$hide) {
441                         $attributes = ["dfrn:updated" => $namdate];
442                 }
443
444                 XML::addElement($doc, $author, "name", $owner["name"], $attributes);
445                 XML::addElement($doc, $author, "uri", DI::baseUrl().'/profile/'.$owner["nickname"], $attributes);
446                 XML::addElement($doc, $author, "dfrn:handle", $owner["addr"], $attributes);
447
448                 $attributes = ["rel" => "photo", "type" => "image/jpeg",
449                                         "media:width" => Proxy::PIXEL_SMALL, "media:height" => Proxy::PIXEL_SMALL,
450                                         "href" => User::getAvatarUrl($owner, Proxy::SIZE_SMALL)];
451
452                 if (!$public || !$hide) {
453                         $attributes["dfrn:updated"] = $picdate;
454                 }
455
456                 XML::addElement($doc, $author, "link", "", $attributes);
457
458                 $attributes["rel"] = "avatar";
459                 XML::addElement($doc, $author, "link", "", $attributes);
460
461                 if ($hide) {
462                         XML::addElement($doc, $author, "dfrn:hide", "true");
463                 }
464
465                 // The following fields will only be generated if the data isn't meant for a public feed
466                 if ($public) {
467                         return $author;
468                 }
469
470                 $birthday = feed_birthday($owner['uid'], $owner['timezone']);
471
472                 if ($birthday) {
473                         XML::addElement($doc, $author, "dfrn:birthday", $birthday);
474                 }
475
476                 // Only show contact details when we are allowed to
477                 $profile = DBA::selectFirst('owner-view',
478                         ['about', 'name', 'homepage', 'nickname', 'timezone', 'locality', 'region', 'country-name', 'pub_keywords', 'xmpp', 'dob'],
479                         ['uid' => $owner['uid'], 'hidewall' => false]);
480                 if (DBA::isResult($profile)) {
481                         XML::addElement($doc, $author, "poco:displayName", $profile["name"]);
482                         XML::addElement($doc, $author, "poco:updated", $namdate);
483
484                         if (trim($profile["dob"]) > DBA::NULL_DATE) {
485                                 XML::addElement($doc, $author, "poco:birthday", "0000-".date("m-d", strtotime($profile["dob"])));
486                         }
487
488                         XML::addElement($doc, $author, "poco:note", $profile["about"]);
489                         XML::addElement($doc, $author, "poco:preferredUsername", $profile["nickname"]);
490
491                         $savetz = date_default_timezone_get();
492                         date_default_timezone_set($profile["timezone"]);
493                         XML::addElement($doc, $author, "poco:utcOffset", date("P"));
494                         date_default_timezone_set($savetz);
495
496                         if (trim($profile["homepage"]) != "") {
497                                 $urls = $doc->createElement("poco:urls");
498                                 XML::addElement($doc, $urls, "poco:type", "homepage");
499                                 XML::addElement($doc, $urls, "poco:value", $profile["homepage"]);
500                                 XML::addElement($doc, $urls, "poco:primary", "true");
501                                 $author->appendChild($urls);
502                         }
503
504                         if (trim($profile["pub_keywords"]) != "") {
505                                 $keywords = explode(",", $profile["pub_keywords"]);
506
507                                 foreach ($keywords as $keyword) {
508                                         XML::addElement($doc, $author, "poco:tags", trim($keyword));
509                                 }
510                         }
511
512                         if (trim($profile["xmpp"]) != "") {
513                                 $ims = $doc->createElement("poco:ims");
514                                 XML::addElement($doc, $ims, "poco:type", "xmpp");
515                                 XML::addElement($doc, $ims, "poco:value", $profile["xmpp"]);
516                                 XML::addElement($doc, $ims, "poco:primary", "true");
517                                 $author->appendChild($ims);
518                         }
519
520                         if (trim($profile["locality"].$profile["region"].$profile["country-name"]) != "") {
521                                 $element = $doc->createElement("poco:address");
522
523                                 XML::addElement($doc, $element, "poco:formatted", Profile::formatLocation($profile));
524
525                                 if (trim($profile["locality"]) != "") {
526                                         XML::addElement($doc, $element, "poco:locality", $profile["locality"]);
527                                 }
528
529                                 if (trim($profile["region"]) != "") {
530                                         XML::addElement($doc, $element, "poco:region", $profile["region"]);
531                                 }
532
533                                 if (trim($profile["country-name"]) != "") {
534                                         XML::addElement($doc, $element, "poco:country", $profile["country-name"]);
535                                 }
536
537                                 $author->appendChild($element);
538                         }
539                 }
540
541                 return $author;
542         }
543
544         /**
545          * Adds the author elements in the "entry" elements of the DFRN protocol
546          *
547          * @param DOMDocument $doc         XML document
548          * @param string $element     Element name for the author
549          * @param string $contact_url Link of the contact
550          * @param array  $item        Item elements
551          *
552          * @return \DOMElement XML author object
553          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
554          * @todo  Find proper type-hints
555          */
556         private static function addEntryAuthor(DOMDocument $doc, $element, $contact_url, $item)
557         {
558                 $author = $doc->createElement($element);
559
560                 $contact = Contact::getByURLForUser($contact_url, $item["uid"], false, ['url', 'name', 'addr', 'photo']);
561                 if (!empty($contact)) {
562                         XML::addElement($doc, $author, "name", $contact["name"]);
563                         XML::addElement($doc, $author, "uri", $contact["url"]);
564                         XML::addElement($doc, $author, "dfrn:handle", $contact["addr"]);
565
566                         /// @Todo
567                         /// - Check real image type and image size
568                         /// - Check which of these boths elements we should use
569                         $attributes = [
570                                 "rel" => "photo",
571                                 "type" => "image/jpeg",
572                                 "media:width" => 80,
573                                 "media:height" => 80,
574                                 "href" => $contact["photo"]];
575                         XML::addElement($doc, $author, "link", "", $attributes);
576
577                         $attributes = [
578                                 "rel" => "avatar",
579                                 "type" => "image/jpeg",
580                                 "media:width" => 80,
581                                 "media:height" => 80,
582                                 "href" => $contact["photo"]];
583                         XML::addElement($doc, $author, "link", "", $attributes);
584                 }
585
586                 return $author;
587         }
588
589         /**
590          * Adds the activity elements
591          *
592          * @param DOMDocument $doc      XML document
593          * @param string      $element  Element name for the activity
594          * @param string      $activity activity value
595          * @param int         $uriid    Uri-Id of the post
596          *
597          * @return \DOMElement XML activity object
598          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
599          * @todo  Find proper type-hints
600          */
601         private static function createActivity(DOMDocument $doc, $element, $activity, $uriid)
602         {
603                 if ($activity) {
604                         $entry = $doc->createElement($element);
605
606                         $r = XML::parseString($activity);
607                         if (!$r) {
608                                 return false;
609                         }
610
611                         if ($r->type) {
612                                 XML::addElement($doc, $entry, "activity:object-type", $r->type);
613                         }
614
615                         if ($r->id) {
616                                 XML::addElement($doc, $entry, "id", $r->id);
617                         }
618
619                         if ($r->title) {
620                                 XML::addElement($doc, $entry, "title", $r->title);
621                         }
622
623                         if ($r->link) {
624                                 if (substr($r->link, 0, 1) == '<') {
625                                         if (strstr($r->link, '&') && (! strstr($r->link, '&amp;'))) {
626                                                 $r->link = str_replace('&', '&amp;', $r->link);
627                                         }
628
629                                         $r->link = preg_replace('/\<link(.*?)\"\>/', '<link$1"/>', $r->link);
630
631                                         // XML does need a single element as root element so we add a dummy element here
632                                         $data = XML::parseString("<dummy>" . $r->link . "</dummy>");
633                                         if (is_object($data)) {
634                                                 foreach ($data->link as $link) {
635                                                         $attributes = [];
636                                                         foreach ($link->attributes() as $parameter => $value) {
637                                                                 $attributes[$parameter] = $value;
638                                                         }
639                                                         XML::addElement($doc, $entry, "link", "", $attributes);
640                                                 }
641                                         }
642                                 } else {
643                                         $attributes = ["rel" => "alternate", "type" => "text/html", "href" => $r->link];
644                                         XML::addElement($doc, $entry, "link", "", $attributes);
645                                 }
646                         }
647                         if ($r->content) {
648                                 XML::addElement($doc, $entry, "content", BBCode::convertForUriId($uriid, $r->content, BBCode::EXTERNAL), ["type" => "html"]);
649                         }
650
651                         return $entry;
652                 }
653
654                 return false;
655         }
656
657         /**
658          * Adds the elements for attachments
659          *
660          * @param object $doc  XML document
661          * @param object $root XML root
662          * @param array  $item Item element
663          *
664          * @return void XML attachment object
665          * @todo  Find proper type-hints
666          */
667         private static function getAttachment($doc, $root, $item)
668         {
669                 foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT, Post\Media::UNKNOWN]) as $attachment) {
670                         $attributes = ['rel' => 'enclosure',
671                                 'href' => $attachment['url'],
672                                 'type' => $attachment['mimetype']];
673
674                         if (!empty($attachment['size'])) {
675                                 $attributes['length'] = intval($attachment['size']);
676                         }
677                         if (!empty($attachment['description'])) {
678                                 $attributes['title'] = $attachment['description'];
679                         }
680
681                         XML::addElement($doc, $root, 'link', '', $attributes);
682                 }
683         }
684
685         /**
686          * Adds the "entry" elements for the DFRN protocol
687          *
688          * @param DOMDocument $doc     XML document
689          * @param string      $type    "text" or "html"
690          * @param array       $item    Item element
691          * @param array       $owner   Owner record
692          * @param bool        $comment Trigger the sending of the "comment" element
693          * @param int         $cid     Contact ID of the recipient
694          * @param bool        $single  If set, the entry is created as an XML document with a single "entry" element
695          *
696          * @return null|\DOMElement XML entry object
697          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
698          * @throws \ImagickException
699          * @todo  Find proper type-hints
700          */
701         private static function entry(DOMDocument $doc, $type, array $item, array $owner, $comment = false, $cid = 0, $single = false)
702         {
703                 $mentioned = [];
704
705                 if (!$item['parent']) {
706                         Logger::notice('Item without parent found.', ['type' => $type, 'item' => $item]);
707                         return null;
708                 }
709
710                 if ($item['deleted']) {
711                         $attributes = ["ref" => $item['uri'], "when" => DateTimeFormat::utc($item['edited'] . '+00:00', DateTimeFormat::ATOM)];
712                         return XML::createElement($doc, "at:deleted-entry", "", $attributes);
713                 }
714
715                 if (!$single) {
716                         $entry = $doc->createElement("entry");
717                 } else {
718                         $entry = $doc->createElementNS(ActivityNamespace::ATOM1, 'entry');
719                         $doc->appendChild($entry);
720
721                         $entry->setAttribute("xmlns:thr", ActivityNamespace::THREAD);
722                         $entry->setAttribute("xmlns:at", ActivityNamespace::TOMB);
723                         $entry->setAttribute("xmlns:media", ActivityNamespace::MEDIA);
724                         $entry->setAttribute("xmlns:dfrn", ActivityNamespace::DFRN);
725                         $entry->setAttribute("xmlns:activity", ActivityNamespace::ACTIVITY);
726                         $entry->setAttribute("xmlns:georss", ActivityNamespace::GEORSS);
727                         $entry->setAttribute("xmlns:poco", ActivityNamespace::POCO);
728                         $entry->setAttribute("xmlns:ostatus", ActivityNamespace::OSTATUS);
729                         $entry->setAttribute("xmlns:statusnet", ActivityNamespace::STATUSNET);
730                 }
731
732                 $body = Post\Media::addAttachmentsToBody($item['uri-id'], $item['body'] ?? '');
733
734                 if ($item['private'] == Item::PRIVATE) {
735                         $body = Item::fixPrivatePhotos($body, $owner['uid'], $item, $cid);
736                 }
737
738                 // Remove the abstract element. It is only locally important.
739                 $body = BBCode::stripAbstract($body);
740
741                 $htmlbody = '';
742                 if ($type == 'html') {
743                         $htmlbody = $body;
744
745                         if ($item['title'] != "") {
746                                 $htmlbody = "[b]" . $item['title'] . "[/b]\n\n" . $htmlbody;
747                         }
748
749                         $htmlbody = BBCode::convertForUriId($item['uri-id'], $htmlbody, BBCode::ACTIVITYPUB);
750                 }
751
752                 $author = self::addEntryAuthor($doc, "author", $item["author-link"], $item);
753                 $entry->appendChild($author);
754
755                 $dfrnowner = self::addEntryAuthor($doc, "dfrn:owner", $item["owner-link"], $item);
756                 $entry->appendChild($dfrnowner);
757
758                 if ($item['gravity'] != GRAVITY_PARENT) {
759                         $parent = Post::selectFirst(['guid', 'plink'], ['uri' => $item['thr-parent'], 'uid' => $item['uid']]);
760                         if (DBA::isResult($parent)) {
761                                 $attributes = ["ref" => $item['thr-parent'], "type" => "text/html",
762                                         "href" => $parent['plink'],
763                                         "dfrn:diaspora_guid" => $parent['guid']];
764                                 XML::addElement($doc, $entry, "thr:in-reply-to", "", $attributes);
765                         }
766                 }
767
768                 // Add conversation data. This is used for OStatus
769                 $conversation_href = DI::baseUrl()."/display/".$item["parent-guid"];
770                 $conversation_uri = $conversation_href;
771
772                 if (isset($parent_item)) {
773                         $conversation = DBA::selectFirst('conversation', ['conversation-uri', 'conversation-href'], ['item-uri' => $item['thr-parent']]);
774                         if (DBA::isResult($conversation)) {
775                                 if ($conversation['conversation-uri'] != '') {
776                                         $conversation_uri = $conversation['conversation-uri'];
777                                 }
778                                 if ($conversation['conversation-href'] != '') {
779                                         $conversation_href = $conversation['conversation-href'];
780                                 }
781                         }
782                 }
783
784                 $attributes = [
785                                 "href" => $conversation_href,
786                                 "ref" => $conversation_uri];
787
788                 XML::addElement($doc, $entry, "ostatus:conversation", $conversation_uri, $attributes);
789
790                 XML::addElement($doc, $entry, "id", $item["uri"]);
791                 XML::addElement($doc, $entry, "title", $item["title"]);
792
793                 XML::addElement($doc, $entry, "published", DateTimeFormat::utc($item["created"] . "+00:00", DateTimeFormat::ATOM));
794                 XML::addElement($doc, $entry, "updated", DateTimeFormat::utc($item["edited"] . "+00:00", DateTimeFormat::ATOM));
795
796                 // "dfrn:env" is used to read the content
797                 XML::addElement($doc, $entry, "dfrn:env", Strings::base64UrlEncode($body, true));
798
799                 // The "content" field is not read by the receiver. We could remove it when the type is "text"
800                 // We keep it at the moment, maybe there is some old version that doesn't read "dfrn:env"
801                 XML::addElement($doc, $entry, "content", (($type == 'html') ? $htmlbody : $body), ["type" => $type]);
802
803                 // We save this value in "plink". Maybe we should read it from there as well?
804                 XML::addElement(
805                         $doc,
806                         $entry,
807                         "link",
808                         "",
809                         ["rel" => "alternate", "type" => "text/html",
810                                  "href" => DI::baseUrl() . "/display/" . $item["guid"]]
811                 );
812
813                 // "comment-allow" is some old fashioned stuff for old Friendica versions.
814                 // It is included in the rewritten code for completeness
815                 if ($comment) {
816                         XML::addElement($doc, $entry, "dfrn:comment-allow", 1);
817                 }
818
819                 if ($item['location']) {
820                         XML::addElement($doc, $entry, "dfrn:location", $item['location']);
821                 }
822
823                 if ($item['coord']) {
824                         XML::addElement($doc, $entry, "georss:point", $item['coord']);
825                 }
826
827                 if ($item['private']) {
828                         // Friendica versions prior to 2020.3 can't handle "unlisted" properly. So we can only transmit public and private
829                         XML::addElement($doc, $entry, "dfrn:private", ($item['private'] == Item::PRIVATE ? Item::PRIVATE : Item::PUBLIC));
830                         XML::addElement($doc, $entry, "dfrn:unlisted", $item['private'] == Item::UNLISTED);
831                 }
832
833                 if ($item['extid']) {
834                         XML::addElement($doc, $entry, "dfrn:extid", $item['extid']);
835                 }
836
837                 if ($item['post-type'] == Item::PT_PAGE) {
838                         XML::addElement($doc, $entry, "dfrn:bookmark", "true");
839                 }
840
841                 if ($item['app']) {
842                         XML::addElement($doc, $entry, "statusnet:notice_info", "", ["local_id" => $item['id'], "source" => $item['app']]);
843                 }
844
845                 XML::addElement($doc, $entry, "dfrn:diaspora_guid", $item["guid"]);
846
847                 // The signed text contains the content in Markdown, the sender handle and the signatur for the content
848                 // It is needed for relayed comments to Diaspora.
849                 if ($item['signed_text']) {
850                         $sign = base64_encode(json_encode(['signed_text' => $item['signed_text'],'signature' => '','signer' => '']));
851                         XML::addElement($doc, $entry, "dfrn:diaspora_signature", $sign);
852                 }
853
854                 XML::addElement($doc, $entry, "activity:verb", self::constructVerb($item));
855
856                 if ($item['object-type'] != "") {
857                         XML::addElement($doc, $entry, "activity:object-type", $item['object-type']);
858                 } elseif ($item['gravity'] == GRAVITY_PARENT) {
859                         XML::addElement($doc, $entry, "activity:object-type", Activity\ObjectType::NOTE);
860                 } else {
861                         XML::addElement($doc, $entry, "activity:object-type", Activity\ObjectType::COMMENT);
862                 }
863
864                 $actobj = self::createActivity($doc, "activity:object", $item['object'], $item['uri-id']);
865                 if ($actobj) {
866                         $entry->appendChild($actobj);
867                 }
868
869                 $actarg = self::createActivity($doc, "activity:target", $item['target'], $item['uri-id']);
870                 if ($actarg) {
871                         $entry->appendChild($actarg);
872                 }
873
874                 $tags = Tag::getByURIId($item['uri-id']);
875
876                 if (count($tags)) {
877                         foreach ($tags as $tag) {
878                                 if (($type != 'html') || ($tag['type'] == Tag::HASHTAG)) {
879                                         XML::addElement($doc, $entry, "category", "", ["scheme" => "X-DFRN:" . Tag::TAG_CHARACTER[$tag['type']] . ":" . $tag['url'], "term" => $tag['name']]);
880                                 }
881                                 if ($tag['type'] != Tag::HASHTAG) {
882                                         $mentioned[$tag['url']] = $tag['url'];
883                                 }
884                         }
885                 }
886
887                 foreach ($mentioned as $mention) {
888                         $condition = ['uid' => $owner["uid"], 'nurl' => Strings::normaliseLink($mention)];
889                         $contact = DBA::selectFirst('contact', ['forum', 'prv'], $condition);
890
891                         if (DBA::isResult($contact) && ($contact["forum"] || $contact["prv"])) {
892                                 XML::addElement(
893                                         $doc,
894                                         $entry,
895                                         "link",
896                                         "",
897                                         ["rel" => "mentioned",
898                                                         "ostatus:object-type" => Activity\ObjectType::GROUP,
899                                                         "href" => $mention]
900                                 );
901                         } else {
902                                 XML::addElement(
903                                         $doc,
904                                         $entry,
905                                         "link",
906                                         "",
907                                         ["rel" => "mentioned",
908                                                         "ostatus:object-type" => Activity\ObjectType::PERSON,
909                                                         "href" => $mention]
910                                 );
911                         }
912                 }
913
914                 self::getAttachment($doc, $entry, $item);
915
916                 return $entry;
917         }
918
919         /**
920          * Transmits atom content to the contacts via the Diaspora transport layer
921          *
922          * @param array  $owner   Owner record
923          * @param array  $contact Contact record of the receiver
924          * @param string $atom    Content that will be transmitted
925          *
926          * @param bool   $public_batch
927          * @return int Deliver status. Negative values mean an error.
928          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
929          * @throws \ImagickException
930          */
931         public static function transmit($owner, $contact, $atom, $public_batch = false)
932         {
933                 if (!$public_batch) {
934                         if (empty($contact['addr'])) {
935                                 Logger::log('Empty contact handle for ' . $contact['id'] . ' - ' . $contact['url'] . ' - trying to update it.');
936                                 if (Contact::updateFromProbe($contact['id'])) {
937                                         $new_contact = DBA::selectFirst('contact', ['addr'], ['id' => $contact['id']]);
938                                         $contact['addr'] = $new_contact['addr'];
939                                 }
940
941                                 if (empty($contact['addr'])) {
942                                         Logger::log('Unable to find contact handle for ' . $contact['id'] . ' - ' . $contact['url']);
943                                         return -21;
944                                 }
945                         }
946
947                         $fcontact = FContact::getByURL($contact['addr']);
948                         if (empty($fcontact)) {
949                                 Logger::log('Unable to find contact details for ' . $contact['id'] . ' - ' . $contact['addr']);
950                                 return -22;
951                         }
952                         $pubkey = $fcontact['pubkey'];
953                 } else {
954                         $pubkey = '';
955                 }
956
957                 $envelope = Diaspora::buildMessage($atom, $owner, $contact, $owner['uprvkey'], $pubkey, $public_batch);
958
959                 // Create the endpoint for public posts. This is some WIP and should later be added to the probing
960                 if ($public_batch && empty($contact["batch"])) {
961                         $parts = parse_url($contact["notify"]);
962                         $path_parts = explode('/', $parts['path']);
963                         array_pop($path_parts);
964                         $parts['path'] =  implode('/', $path_parts);
965                         $contact["batch"] = Network::unparseURL($parts);
966                 }
967
968                 $dest_url = ($public_batch ? $contact["batch"] : $contact["notify"]);
969
970                 if (empty($dest_url)) {
971                         Logger::info('Empty destination', ['public' => $public_batch, 'contact' => $contact]);
972                         return -24;
973                 }
974
975                 $content_type = ($public_batch ? "application/magic-envelope+xml" : "application/json");
976
977                 $postResult = DI::httpClient()->post($dest_url, $envelope, ['Content-Type' => $content_type]);
978                 $xml = $postResult->getBody();
979
980                 $curl_stat = $postResult->getReturnCode();
981                 if (empty($curl_stat) || empty($xml)) {
982                         Logger::log('Empty answer from ' . $contact['id'] . ' - ' . $dest_url);
983                         return -9; // timed out
984                 }
985
986                 if (($curl_stat == 503) && $postResult->inHeader('retry-after')) {
987                         return -10;
988                 }
989
990                 if (strpos($xml, '<?xml') === false) {
991                         Logger::log('No valid XML returned from ' . $contact['id'] . ' - ' . $dest_url);
992                         Logger::log('Returned XML: ' . $xml, Logger::DATA);
993                         return 3;
994                 }
995
996                 $res = XML::parseString($xml);
997
998                 if (empty($res->status)) {
999                         return -23;
1000                 }
1001
1002                 if (!empty($res->message)) {
1003                         Logger::log('Transmit to ' . $dest_url . ' returned status '.$res->status.' - '.$res->message, Logger::DEBUG);
1004                 }
1005
1006                 return intval($res->status);
1007         }
1008
1009         /**
1010          * Fetch the author data from head or entry items
1011          *
1012          * @param \DOMXPath $xpath     XPath object
1013          * @param \DOMNode  $context   In which context should the data be searched
1014          * @param array     $importer  Record of the importer user mixed with contact of the content
1015          * @param string    $element   Element name from which the data is fetched
1016          * @param bool      $onlyfetch Should the data only be fetched or should it update the contact record as well
1017          * @param string    $xml       optional, default empty
1018          *
1019          * @return array Relevant data of the author
1020          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1021          * @throws \ImagickException
1022          * @todo  Find good type-hints for all parameter
1023          */
1024         private static function fetchauthor(\DOMXPath $xpath, \DOMNode $context, $importer, $element, $onlyfetch, $xml = "")
1025         {
1026                 $author = [];
1027                 $author["name"] = XML::getFirstNodeValue($xpath, $element."/atom:name/text()", $context);
1028                 $author["link"] = XML::getFirstNodeValue($xpath, $element."/atom:uri/text()", $context);
1029
1030                 $fields = ['id', 'uid', 'url', 'network', 'avatar-date', 'avatar', 'name-date', 'uri-date', 'addr',
1031                         'name', 'nick', 'about', 'location', 'keywords', 'xmpp', 'bdyear', 'bd', 'hidden', 'contact-type'];
1032                 $condition = ["`uid` = ? AND `nurl` = ? AND `network` != ? AND NOT `pending` AND NOT `blocked`",
1033                         $importer["importer_uid"], Strings::normaliseLink($author["link"]), Protocol::STATUSNET];
1034
1035                 if ($importer['account-type'] != User::ACCOUNT_TYPE_COMMUNITY) {
1036                         $condition = DBA::mergeConditions($condition, ['rel' => [Contact::SHARING, Contact::FRIEND]]);
1037                 }
1038
1039                 $contact_old = DBA::selectFirst('contact', $fields, $condition);
1040
1041                 if (DBA::isResult($contact_old)) {
1042                         $author["contact-id"] = $contact_old["id"];
1043                         $author["network"] = $contact_old["network"];
1044                 } else {
1045                         Logger::info('Contact not found', ['condition' => $condition]);
1046
1047                         $author["contact-unknown"] = true;
1048                         $contact = Contact::getByURL($author["link"], null, ["id", "network"]);
1049                         $author["contact-id"] = $contact["id"] ?? $importer["id"];
1050                         $author["network"] = $contact["network"] ?? $importer["network"];
1051                         $onlyfetch = true;
1052                 }
1053
1054                 // Until now we aren't serving different sizes - but maybe later
1055                 $avatarlist = [];
1056                 /// @todo check if "avatar" or "photo" would be the best field in the specification
1057                 $avatars = $xpath->query($element . "/atom:link[@rel='avatar']", $context);
1058                 foreach ($avatars as $avatar) {
1059                         $href = "";
1060                         $width = 0;
1061                         foreach ($avatar->attributes as $attributes) {
1062                                 /// @TODO Rewrite these similar if() to one switch
1063                                 if ($attributes->name == "href") {
1064                                         $href = $attributes->textContent;
1065                                 }
1066                                 if ($attributes->name == "width") {
1067                                         $width = $attributes->textContent;
1068                                 }
1069                                 if ($attributes->name == "updated") {
1070                                         $author["avatar-date"] = $attributes->textContent;
1071                                 }
1072                         }
1073                         if (($width > 0) && ($href != "")) {
1074                                 $avatarlist[$width] = $href;
1075                         }
1076                 }
1077
1078                 if (count($avatarlist) > 0) {
1079                         krsort($avatarlist);
1080                         $author["avatar"] = current($avatarlist);
1081                 }
1082
1083                 if (empty($author['avatar']) && !empty($author['link'])) {
1084                         $cid = Contact::getIdForURL($author['link'], 0);
1085                         if (!empty($cid)) {
1086                                 $contact = DBA::selectFirst('contact', ['avatar'], ['id' => $cid]);
1087                                 if (DBA::isResult($contact)) {
1088                                         $author['avatar'] = $contact['avatar'];
1089                                 }
1090                         }
1091                 }
1092
1093                 if (empty($author['avatar'])) {
1094                         Logger::log('Empty author: ' . $xml);
1095                         $author['avatar'] = '';
1096                 }
1097
1098                 if (DBA::isResult($contact_old) && !$onlyfetch) {
1099                         Logger::log("Check if contact details for contact " . $contact_old["id"] . " (" . $contact_old["nick"] . ") have to be updated.", Logger::DEBUG);
1100
1101                         $poco = ["url" => $contact_old["url"], "network" => $contact_old["network"]];
1102
1103                         // When was the last change to name or uri?
1104                         $name_element = $xpath->query($element . "/atom:name", $context)->item(0);
1105                         foreach ($name_element->attributes as $attributes) {
1106                                 if ($attributes->name == "updated") {
1107                                         $poco["name-date"] = $attributes->textContent;
1108                                 }
1109                         }
1110
1111                         $link_element = $xpath->query($element . "/atom:link", $context)->item(0);
1112                         foreach ($link_element->attributes as $attributes) {
1113                                 if ($attributes->name == "updated") {
1114                                         $poco["uri-date"] = $attributes->textContent;
1115                                 }
1116                         }
1117
1118                         // Update contact data
1119                         $value = XML::getFirstNodeValue($xpath, $element . "/dfrn:handle/text()", $context);
1120                         if ($value != "") {
1121                                 $poco["addr"] = $value;
1122                         }
1123
1124                         $value = XML::getFirstNodeValue($xpath, $element . "/poco:displayName/text()", $context);
1125                         if ($value != "") {
1126                                 $poco["name"] = $value;
1127                         }
1128
1129                         $value = XML::getFirstNodeValue($xpath, $element . "/poco:preferredUsername/text()", $context);
1130                         if ($value != "") {
1131                                 $poco["nick"] = $value;
1132                         }
1133
1134                         $value = XML::getFirstNodeValue($xpath, $element . "/poco:note/text()", $context);
1135                         if ($value != "") {
1136                                 $poco["about"] = $value;
1137                         }
1138
1139                         $value = XML::getFirstNodeValue($xpath, $element . "/poco:address/poco:formatted/text()", $context);
1140                         if ($value != "") {
1141                                 $poco["location"] = $value;
1142                         }
1143
1144                         /// @todo Only search for elements with "poco:type" = "xmpp"
1145                         $value = XML::getFirstNodeValue($xpath, $element . "/poco:ims/poco:value/text()", $context);
1146                         if ($value != "") {
1147                                 $poco["xmpp"] = $value;
1148                         }
1149
1150                         /// @todo Add support for the following fields that we don't support by now in the contact table:
1151                         /// - poco:utcOffset
1152                         /// - poco:urls
1153                         /// - poco:locality
1154                         /// - poco:region
1155                         /// - poco:country
1156
1157                         // If the "hide" element is present then the profile isn't searchable.
1158                         $hide = intval(XML::getFirstNodeValue($xpath, $element . "/dfrn:hide/text()", $context) == "true");
1159
1160                         Logger::log("Hidden status for contact " . $contact_old["url"] . ": " . $hide, Logger::DEBUG);
1161
1162                         // If the contact isn't searchable then set the contact to "hidden".
1163                         // Problem: This can be manually overridden by the user.
1164                         if ($hide) {
1165                                 $contact_old["hidden"] = true;
1166                         }
1167
1168                         // Save the keywords into the contact table
1169                         $tags = [];
1170                         $tagelements = $xpath->evaluate($element . "/poco:tags/text()", $context);
1171                         foreach ($tagelements as $tag) {
1172                                 $tags[$tag->nodeValue] = $tag->nodeValue;
1173                         }
1174
1175                         if (count($tags)) {
1176                                 $poco["keywords"] = implode(", ", $tags);
1177                         }
1178
1179                         // "dfrn:birthday" contains the birthday converted to UTC
1180                         $birthday = XML::getFirstNodeValue($xpath, $element . "/dfrn:birthday/text()", $context);
1181                         try {
1182                                 $birthday_date = new \DateTime($birthday);
1183                                 if ($birthday_date > new \DateTime()) {
1184                                         $poco["bdyear"] = $birthday_date->format("Y");
1185                                 }
1186                         } catch (\Exception $e) {
1187                                 // Invalid birthday
1188                         }
1189
1190                         // "poco:birthday" is the birthday in the format "yyyy-mm-dd"
1191                         $value = XML::getFirstNodeValue($xpath, $element . "/poco:birthday/text()", $context);
1192
1193                         if (!in_array($value, ["", "0000-00-00", DBA::NULL_DATE])) {
1194                                 $bdyear = date("Y");
1195                                 $value = str_replace(["0000", "0001"], $bdyear, $value);
1196
1197                                 if (strtotime($value) < time()) {
1198                                         $value = str_replace($bdyear, $bdyear + 1, $value);
1199                                 }
1200
1201                                 $poco["bd"] = $value;
1202                         }
1203
1204                         $contact = array_merge($contact_old, $poco);
1205
1206                         if ($contact_old["bdyear"] != $contact["bdyear"]) {
1207                                 Event::createBirthday($contact, $birthday);
1208                         }
1209
1210                         $fields = ['name' => $contact['name'], 'nick' => $contact['nick'], 'about' => $contact['about'],
1211                                 'location' => $contact['location'], 'addr' => $contact['addr'], 'keywords' => $contact['keywords'],
1212                                 'bdyear' => $contact['bdyear'], 'bd' => $contact['bd'], 'hidden' => $contact['hidden'],
1213                                 'xmpp' => $contact['xmpp'], 'name-date' => DateTimeFormat::utc($contact['name-date']),
1214                                 'unsearchable' => $contact['hidden'], 'uri-date' => DateTimeFormat::utc($contact['uri-date'])];
1215
1216                         Contact::update($fields, ['id' => $contact['id'], 'network' => $contact['network']], $contact_old);
1217
1218                         // Update the public contact. Don't set the "hidden" value, this is used differently for public contacts
1219                         unset($fields['hidden']);
1220                         $condition = ['uid' => 0, 'nurl' => Strings::normaliseLink($contact_old['url'])];
1221                         Contact::update($fields, $condition, true);
1222
1223                         Contact::updateAvatar($contact['id'], $author['avatar']);
1224
1225                         $pcid = Contact::getIdForURL($contact_old['url']);
1226                         if (!empty($pcid)) {
1227                                 Contact::updateAvatar($pcid, $author['avatar']);
1228                         }
1229                 }
1230
1231                 return $author;
1232         }
1233
1234         /**
1235          * Transforms activity objects into an XML string
1236          *
1237          * @param object $xpath    XPath object
1238          * @param object $activity Activity object
1239          * @param string $element  element name
1240          *
1241          * @return string XML string
1242          * @todo Find good type-hints for all parameter
1243          */
1244         private static function transformActivity($xpath, $activity, $element)
1245         {
1246                 if (!is_object($activity)) {
1247                         return "";
1248                 }
1249
1250                 $obj_doc = new DOMDocument("1.0", "utf-8");
1251                 $obj_doc->formatOutput = true;
1252
1253                 $obj_element = $obj_doc->createElementNS( ActivityNamespace::ATOM1, $element);
1254
1255                 $activity_type = $xpath->query("activity:object-type/text()", $activity)->item(0)->nodeValue;
1256                 XML::addElement($obj_doc, $obj_element, "type", $activity_type);
1257
1258                 $id = $xpath->query("atom:id", $activity)->item(0);
1259                 if (is_object($id)) {
1260                         $obj_element->appendChild($obj_doc->importNode($id, true));
1261                 }
1262
1263                 $title = $xpath->query("atom:title", $activity)->item(0);
1264                 if (is_object($title)) {
1265                         $obj_element->appendChild($obj_doc->importNode($title, true));
1266                 }
1267
1268                 $links = $xpath->query("atom:link", $activity);
1269                 if (is_object($links)) {
1270                         foreach ($links as $link) {
1271                                 $obj_element->appendChild($obj_doc->importNode($link, true));
1272                         }
1273                 }
1274
1275                 $content = $xpath->query("atom:content", $activity)->item(0);
1276                 if (is_object($content)) {
1277                         $obj_element->appendChild($obj_doc->importNode($content, true));
1278                 }
1279
1280                 $obj_doc->appendChild($obj_element);
1281
1282                 $objxml = $obj_doc->saveXML($obj_element);
1283
1284                 /// @todo This isn't totally clean. We should find a way to transform the namespaces
1285                 $objxml = str_replace("<".$element.' xmlns="http://www.w3.org/2005/Atom">', "<".$element.">", $objxml);
1286                 return($objxml);
1287         }
1288
1289         /**
1290          * Processes the mail elements
1291          *
1292          * @param object $xpath    XPath object
1293          * @param object $mail     mail elements
1294          * @param array  $importer Record of the importer user mixed with contact of the content
1295          * @return void
1296          * @throws \Exception
1297          * @todo  Find good type-hints for all parameter
1298          */
1299         private static function processMail($xpath, $mail, $importer)
1300         {
1301                 Logger::log("Processing mails");
1302
1303                 $msg = [];
1304                 $msg["uid"] = $importer["importer_uid"];
1305                 $msg["from-name"] = XML::getFirstValue($xpath, "dfrn:sender/dfrn:name/text()", $mail);
1306                 $msg["from-url"] = XML::getFirstValue($xpath, "dfrn:sender/dfrn:uri/text()", $mail);
1307                 $msg["from-photo"] = XML::getFirstValue($xpath, "dfrn:sender/dfrn:avatar/text()", $mail);
1308                 $msg["contact-id"] = $importer["id"];
1309                 $msg["uri"] = XML::getFirstValue($xpath, "dfrn:id/text()", $mail);
1310                 $msg["parent-uri"] = XML::getFirstValue($xpath, "dfrn:in-reply-to/text()", $mail);
1311                 $msg["created"] = DateTimeFormat::utc(XML::getFirstValue($xpath, "dfrn:sentdate/text()", $mail));
1312                 $msg["title"] = XML::getFirstValue($xpath, "dfrn:subject/text()", $mail);
1313                 $msg["body"] = XML::getFirstValue($xpath, "dfrn:content/text()", $mail);
1314
1315                 Mail::insert($msg);
1316         }
1317
1318         /**
1319          * Processes the suggestion elements
1320          *
1321          * @param object $xpath      XPath object
1322          * @param object $suggestion suggestion elements
1323          * @param array  $importer   Record of the importer user mixed with contact of the content
1324          * @return boolean
1325          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1326          * @todo  Find good type-hints for all parameter
1327          */
1328         private static function processSuggestion($xpath, $suggestion, $importer)
1329         {
1330                 Logger::notice('Processing suggestions');
1331
1332                 $url = $xpath->evaluate('string(dfrn:url[1]/text())', $suggestion);
1333                 $cid = Contact::getIdForURL($url);
1334                 $note = $xpath->evaluate('string(dfrn:note[1]/text())', $suggestion);
1335
1336                 return FContact::addSuggestion($importer['importer_uid'], $cid, $importer['id'], $note);
1337         }
1338
1339         /**
1340          * Processes the relocation elements
1341          *
1342          * @param object $xpath      XPath object
1343          * @param object $relocation relocation elements
1344          * @param array  $importer   Record of the importer user mixed with contact of the content
1345          * @return boolean
1346          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1347          * @throws \ImagickException
1348          * @todo  Find good type-hints for all parameter
1349          */
1350         private static function processRelocation($xpath, $relocation, $importer)
1351         {
1352                 Logger::log("Processing relocations");
1353
1354                 /// @TODO Rewrite this to one statement
1355                 $relocate = [];
1356                 $relocate["uid"] = $importer["importer_uid"];
1357                 $relocate["cid"] = $importer["id"];
1358                 $relocate["url"] = $xpath->query("dfrn:url/text()", $relocation)->item(0)->nodeValue;
1359                 $relocate["addr"] = $xpath->query("dfrn:addr/text()", $relocation)->item(0)->nodeValue;
1360                 $relocate["name"] = $xpath->query("dfrn:name/text()", $relocation)->item(0)->nodeValue;
1361                 $relocate["avatar"] = $xpath->query("dfrn:avatar/text()", $relocation)->item(0)->nodeValue;
1362                 $relocate["photo"] = $xpath->query("dfrn:photo/text()", $relocation)->item(0)->nodeValue;
1363                 $relocate["thumb"] = $xpath->query("dfrn:thumb/text()", $relocation)->item(0)->nodeValue;
1364                 $relocate["micro"] = $xpath->query("dfrn:micro/text()", $relocation)->item(0)->nodeValue;
1365                 $relocate["request"] = $xpath->query("dfrn:request/text()", $relocation)->item(0)->nodeValue;
1366                 $relocate["confirm"] = $xpath->query("dfrn:confirm/text()", $relocation)->item(0)->nodeValue;
1367                 $relocate["notify"] = $xpath->query("dfrn:notify/text()", $relocation)->item(0)->nodeValue;
1368                 $relocate["poll"] = $xpath->query("dfrn:poll/text()", $relocation)->item(0)->nodeValue;
1369                 $relocate["sitepubkey"] = $xpath->query("dfrn:sitepubkey/text()", $relocation)->item(0)->nodeValue;
1370
1371                 if (($relocate["avatar"] == "") && ($relocate["photo"] != "")) {
1372                         $relocate["avatar"] = $relocate["photo"];
1373                 }
1374
1375                 if ($relocate["addr"] == "") {
1376                         $relocate["addr"] = preg_replace("=(https?://)(.*)/profile/(.*)=ism", "$3@$2", $relocate["url"]);
1377                 }
1378
1379                 // update contact
1380                 $old = Contact::selectFirst(['photo', 'url'], ['id' => $importer["id"], 'uid' => $importer["importer_uid"]]);
1381
1382                 if (!DBA::isResult($old)) {
1383                         Logger::notice("Query failed to execute, no result returned in " . __FUNCTION__);
1384                         return false;
1385                 }
1386
1387                 // Update the contact table. We try to find every entry.
1388                 $fields = ['name' => $relocate["name"], 'avatar' => $relocate["avatar"],
1389                         'url' => $relocate["url"], 'nurl' => Strings::normaliseLink($relocate["url"]),
1390                         'addr' => $relocate["addr"], 'request' => $relocate["request"],
1391                         'confirm' => $relocate["confirm"], 'notify' => $relocate["notify"],
1392                         'poll' => $relocate["poll"], 'site-pubkey' => $relocate["sitepubkey"]];
1393                 $condition = ["(`id` = ?) OR (`nurl` = ?)", $importer["id"], Strings::normaliseLink($old["url"])];
1394
1395                 Contact::update($fields, $condition);
1396
1397                 Contact::updateAvatar($importer["id"], $relocate["avatar"], true);
1398
1399                 Logger::log('Contacts are updated.');
1400
1401                 /// @TODO
1402                 /// merge with current record, current contents have priority
1403                 /// update record, set url-updated
1404                 /// update profile photos
1405                 /// schedule a scan?
1406                 return true;
1407         }
1408
1409         /**
1410          * Updates an item
1411          *
1412          * @param array $current   the current item record
1413          * @param array $item      the new item record
1414          * @param array $importer  Record of the importer user mixed with contact of the content
1415          * @param int   $entrytype Is it a toplevel entry, a comment or a relayed comment?
1416          * @return mixed
1417          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1418          * @todo  set proper type-hints (array?)
1419          */
1420         private static function updateContent($current, $item, $importer, $entrytype)
1421         {
1422                 $changed = false;
1423
1424                 if (self::isEditedTimestampNewer($current, $item)) {
1425                         // do not accept (ignore) an earlier edit than one we currently have.
1426                         if (DateTimeFormat::utc($item["edited"]) < $current["edited"]) {
1427                                 return false;
1428                         }
1429
1430                         $fields = ['title' => $item['title'] ?? '', 'body' => $item['body'] ?? '',
1431                                         'changed' => DateTimeFormat::utcNow(),
1432                                         'edited' => DateTimeFormat::utc($item["edited"])];
1433
1434                         $condition = ["`uri` = ? AND `uid` IN (0, ?)", $item["uri"], $importer["importer_uid"]];
1435                         Item::update($fields, $condition);
1436
1437                         $changed = true;
1438                 }
1439                 return $changed;
1440         }
1441
1442         /**
1443          * Detects the entry type of the item
1444          *
1445          * @param array $importer Record of the importer user mixed with contact of the content
1446          * @param array $item     the new item record
1447          *
1448          * @return int Is it a toplevel entry, a comment or a relayed comment?
1449          * @throws \Exception
1450          * @todo  set proper type-hints (array?)
1451          */
1452         private static function getEntryType($importer, $item)
1453         {
1454                 if ($item["thr-parent"] != $item["uri"]) {
1455                         $community = false;
1456
1457                         if ($importer["page-flags"] == User::PAGE_FLAGS_COMMUNITY || $importer["page-flags"] == User::PAGE_FLAGS_PRVGROUP) {
1458                                 $sql_extra = "";
1459                                 $community = true;
1460                                 Logger::log("possible community action");
1461                         } else {
1462                                 $sql_extra = " AND `self` AND `wall`";
1463                         }
1464
1465                         // was the top-level post for this action written by somebody on this site?
1466                         // Specifically, the recipient?
1467                         $parent = Post::selectFirst(['forum_mode', 'wall'],
1468                                 ["`uri` = ? AND `uid` = ?" . $sql_extra, $item["thr-parent"], $importer["importer_uid"]]);
1469
1470                         $is_a_remote_action = DBA::isResult($parent);
1471
1472                         /*
1473                          * Does this have the characteristics of a community or private group action?
1474                          * If it's an action to a wall post on a community/prvgroup page it's a
1475                          * valid community action. Also forum_mode makes it valid for sure.
1476                          * If neither, it's not.
1477                          */
1478                         if ($is_a_remote_action && $community && (!$parent["forum_mode"]) && (!$parent["wall"])) {
1479                                 $is_a_remote_action = false;
1480                                 Logger::log("not a community action");
1481                         }
1482
1483                         if ($is_a_remote_action) {
1484                                 return DFRN::REPLY_RC;
1485                         } else {
1486                                 return DFRN::REPLY;
1487                         }
1488                 } else {
1489                         return DFRN::TOP_LEVEL;
1490                 }
1491         }
1492
1493         /**
1494          * Send a "poke"
1495          *
1496          * @param array $item      The new item record
1497          * @param array $importer  Record of the importer user mixed with contact of the content
1498          * @return void
1499          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1500          * @todo  set proper type-hints (array?)
1501          */
1502         private static function doPoke(array $item, array $importer)
1503         {
1504                 $verb = urldecode(substr($item["verb"], strpos($item["verb"], "#")+1));
1505                 if (!$verb) {
1506                         return;
1507                 }
1508                 $xo = XML::parseString($item["object"]);
1509
1510                 if (($xo->type == Activity\ObjectType::PERSON) && ($xo->id)) {
1511                         // somebody was poked/prodded. Was it me?
1512                         $Blink = '';
1513                         foreach ($xo->link as $l) {
1514                                 $atts = $l->attributes();
1515                                 switch ($atts["rel"]) {
1516                                         case "alternate":
1517                                                 $Blink = $atts["href"];
1518                                                 break;
1519                                         default:
1520                                                 break;
1521                                 }
1522                         }
1523
1524                         if ($Blink && Strings::compareLink($Blink, DI::baseUrl() . "/profile/" . $importer["nickname"])) {
1525                                 $author = DBA::selectFirst('contact', ['id', 'name', 'thumb', 'url'], ['id' => $item['author-id']]);
1526
1527                                 $parent = Post::selectFirst(['id'], ['uri' => $item['thr-parent'], 'uid' => $importer["importer_uid"]]);
1528                                 $item['parent'] = $parent['id'];
1529
1530                                 // send a notification
1531                                 notification(
1532                                         [
1533                                         "type"     => Notification\Type::POKE,
1534                                         "otype"    => Notification\ObjectType::PERSON,
1535                                         "activity" => $verb,
1536                                         "verb"     => $item["verb"],
1537                                         "uid"      => $importer["importer_uid"],
1538                                         "cid"      => $author["id"],
1539                                         "item"     => $item,
1540                                         "link"     => DI::baseUrl() . "/display/" . urlencode($item['guid']),
1541                                         ]
1542                                 );
1543                         }
1544                 }
1545         }
1546
1547         /**
1548          * Processes several actions, depending on the verb
1549          *
1550          * @param int   $entrytype Is it a toplevel entry, a comment or a relayed comment?
1551          * @param array $importer  Record of the importer user mixed with contact of the content
1552          * @param array $item      the new item record
1553          * @param bool  $is_like   Is the verb a "like"?
1554          *
1555          * @return bool Should the processing of the entries be continued?
1556          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1557          * @todo  set proper type-hints (array?)
1558          */
1559         private static function processVerbs($entrytype, $importer, &$item, &$is_like)
1560         {
1561                 Logger::log("Process verb ".$item["verb"]." and object-type ".$item["object-type"]." for entrytype ".$entrytype, Logger::DEBUG);
1562
1563                 if (($entrytype == DFRN::TOP_LEVEL) && !empty($importer['id'])) {
1564                         // The filling of the the "contact" variable is done for legcy reasons
1565                         // The functions below are partly used by ostatus.php as well - where we have this variable
1566                         $contact = Contact::selectFirst([], ['id' => $importer['id']]);
1567
1568                         $activity = DI::activity();
1569
1570                         // Big question: Do we need these functions? They were part of the "consume_feed" function.
1571                         // This function once was responsible for DFRN and OStatus.
1572                         if ($activity->match($item["verb"], Activity::FOLLOW)) {
1573                                 Logger::log("New follower");
1574                                 Contact::addRelationship($importer, $contact, $item);
1575                                 return false;
1576                         }
1577                         if ($activity->match($item["verb"], Activity::UNFOLLOW)) {
1578                                 Logger::log("Lost follower");
1579                                 Contact::removeFollower($contact);
1580                                 return false;
1581                         }
1582                         if ($activity->match($item["verb"], Activity::REQ_FRIEND)) {
1583                                 Logger::log("New friend request");
1584                                 Contact::addRelationship($importer, $contact, $item, true);
1585                                 return false;
1586                         }
1587                         if ($activity->match($item["verb"], Activity::UNFRIEND)) {
1588                                 Logger::log("Lost sharer");
1589                                 Contact::removeSharer($importer, $contact, $item);
1590                                 return false;
1591                         }
1592                 } else {
1593                         if (($item["verb"] == Activity::LIKE)
1594                                 || ($item["verb"] == Activity::DISLIKE)
1595                                 || ($item["verb"] == Activity::ATTEND)
1596                                 || ($item["verb"] == Activity::ATTENDNO)
1597                                 || ($item["verb"] == Activity::ATTENDMAYBE)
1598                                 || ($item["verb"] == Activity::ANNOUNCE)
1599                         ) {
1600                                 $is_like = true;
1601                                 $item["gravity"] = GRAVITY_ACTIVITY;
1602                                 // only one like or dislike per person
1603                                 // split into two queries for performance issues
1604                                 $condition = ['uid' => $item["uid"], 'author-id' => $item["author-id"], 'gravity' => GRAVITY_ACTIVITY,
1605                                         'verb' => $item['verb'], 'parent-uri' => $item['thr-parent']];
1606                                 if (Post::exists($condition)) {
1607                                         return false;
1608                                 }
1609
1610                                 $condition = ['uid' => $item["uid"], 'author-id' => $item["author-id"], 'gravity' => GRAVITY_ACTIVITY,
1611                                         'verb' => $item['verb'], 'thr-parent' => $item['thr-parent']];
1612                                 if (Post::exists($condition)) {
1613                                         return false;
1614                                 }
1615
1616                                 // The owner of an activity must be the author
1617                                 $item["owner-name"] = $item["author-name"];
1618                                 $item["owner-link"] = $item["author-link"];
1619                                 $item["owner-avatar"] = $item["author-avatar"];
1620                                 $item["owner-id"] = $item["author-id"];
1621                         } else {
1622                                 $is_like = false;
1623                         }
1624
1625                         if (($item["verb"] == Activity::TAG) && ($item["object-type"] == Activity\ObjectType::TAGTERM)) {
1626                                 $xo = XML::parseString($item["object"]);
1627                                 $xt = XML::parseString($item["target"]);
1628
1629                                 if ($xt->type == Activity\ObjectType::NOTE) {
1630                                         $item_tag = Post::selectFirst(['id', 'uri-id'], ['uri' => $xt->id, 'uid' => $importer["importer_uid"]]);
1631
1632                                         if (!DBA::isResult($item_tag)) {
1633                                                 Logger::log("Query failed to execute, no result returned in " . __FUNCTION__);
1634                                                 return false;
1635                                         }
1636
1637                                         // extract tag, if not duplicate, add to parent item
1638                                         if ($xo->content) {
1639                                                 Tag::store($item_tag['uri-id'], Tag::HASHTAG, $xo->content);
1640                                         }
1641                                 }
1642                         }
1643                 }
1644                 return true;
1645         }
1646
1647         /**
1648          * Processes the link elements
1649          *
1650          * @param object $links link elements
1651          * @param array  $item  the item record
1652          * @return void
1653          * @todo set proper type-hints
1654          */
1655         private static function parseLinks($links, &$item)
1656         {
1657                 $rel = "";
1658                 $href = "";
1659                 $type = null;
1660                 $length = null;
1661                 $title = null;
1662                 foreach ($links as $link) {
1663                         foreach ($link->attributes as $attributes) {
1664                                 switch ($attributes->name) {
1665                                         case "href"  : $href   = $attributes->textContent; break;
1666                                         case "rel"   : $rel    = $attributes->textContent; break;
1667                                         case "type"  : $type   = $attributes->textContent; break;
1668                                         case "length": $length = $attributes->textContent; break;
1669                                         case "title" : $title  = $attributes->textContent; break;
1670                                 }
1671                         }
1672                         if (($rel != "") && ($href != "")) {
1673                                 switch ($rel) {
1674                                         case "alternate":
1675                                                 $item["plink"] = $href;
1676                                                 break;
1677                                         case "enclosure":
1678                                                 Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::DOCUMENT,
1679                                                         'url' => $href, 'mimetype' => $type, 'size' => $length, 'description' => $title]);
1680                                                 break;
1681                                 }
1682                         }
1683                 }
1684         }
1685
1686         /**
1687          * Checks if an incoming message is wanted
1688          *
1689          * @param array $item
1690          * @return boolean Is the message wanted?
1691          */
1692         private static function isSolicitedMessage(array $item)
1693         {
1694                 if (DBA::exists('contact', ["`nurl` = ? AND `uid` != ? AND `rel` IN (?, ?)",
1695                         Strings::normaliseLink($item["author-link"]), 0, Contact::FRIEND, Contact::SHARING])) {
1696                         Logger::info('Author has got followers - accepted', ['uri' => $item['uri'], 'author' => $item["author-link"]]);
1697                         return true;
1698                 }
1699
1700                 $taglist = Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]);
1701                 $tags = array_column($taglist, 'name');
1702                 return Relay::isSolicitedPost($tags, $item['body'], $item['author-id'], $item['uri'], Protocol::DFRN);
1703         }
1704
1705         /**
1706          * Processes the entry elements which contain the items and comments
1707          *
1708          * @param array  $header   Array of the header elements that always stay the same
1709          * @param object $xpath    XPath object
1710          * @param object $entry    entry elements
1711          * @param array  $importer Record of the importer user mixed with contact of the content
1712          * @param string $xml      xml
1713          * @return void
1714          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1715          * @throws \ImagickException
1716          * @todo  Add type-hints
1717          */
1718         private static function processEntry($header, $xpath, $entry, $importer, $xml, $protocol)
1719         {
1720                 Logger::log("Processing entries");
1721
1722                 $item = $header;
1723
1724                 $item["protocol"] = $protocol;
1725
1726                 $item["source"] = $xml;
1727
1728                 // Get the uri
1729                 $item["uri"] = XML::getFirstNodeValue($xpath, "atom:id/text()", $entry);
1730
1731                 $item["edited"] = XML::getFirstNodeValue($xpath, "atom:updated/text()", $entry);
1732
1733                 $current = Post::selectFirst(['id', 'uid', 'edited', 'body'],
1734                         ['uri' => $item["uri"], 'uid' => $importer["importer_uid"]]
1735                 );
1736                 // Is there an existing item?
1737                 if (DBA::isResult($current) && !self::isEditedTimestampNewer($current, $item)) {
1738                         Logger::log("Item ".$item["uri"]." (".$item['edited'].") already existed.", Logger::DEBUG);
1739                         return;
1740                 }
1741
1742                 // Fetch the owner
1743                 $owner = self::fetchauthor($xpath, $entry, $importer, "dfrn:owner", true, $xml);
1744
1745                 $owner_unknown = (isset($owner["contact-unknown"]) && $owner["contact-unknown"]);
1746
1747                 $item["owner-name"] = $owner["name"];
1748                 $item["owner-link"] = $owner["link"];
1749                 $item["owner-avatar"] = $owner["avatar"];
1750                 $item["owner-id"] = Contact::getIdForURL($owner["link"], 0);
1751
1752                 // fetch the author
1753                 $author = self::fetchauthor($xpath, $entry, $importer, "atom:author", true, $xml);
1754
1755                 $item["author-name"] = $author["name"];
1756                 $item["author-link"] = $author["link"];
1757                 $item["author-avatar"] = $author["avatar"];
1758                 $item["author-id"] = Contact::getIdForURL($author["link"], 0);
1759
1760                 $item["title"] = XML::getFirstNodeValue($xpath, "atom:title/text()", $entry);
1761
1762                 if (!empty($item["title"])) {
1763                         $item["post-type"] = Item::PT_ARTICLE;
1764                 } else {
1765                         $item["post-type"] = Item::PT_NOTE;
1766                 }
1767
1768                 $item["created"] = XML::getFirstNodeValue($xpath, "atom:published/text()", $entry);
1769
1770                 $item["body"] = XML::getFirstNodeValue($xpath, "dfrn:env/text()", $entry);
1771                 $item["body"] = str_replace([' ',"\t","\r","\n"], ['','','',''], $item["body"]);
1772
1773                 $item["body"] = Strings::base64UrlDecode($item["body"]);
1774
1775                 $item["body"] = BBCode::limitBodySize($item["body"]);
1776
1777                 /// @todo We should check for a repeated post and if we know the repeated author.
1778
1779                 // We don't need the content element since "dfrn:env" is always present
1780                 //$item["body"] = $xpath->query("atom:content/text()", $entry)->item(0)->nodeValue;
1781
1782                 $item["location"] = XML::getFirstNodeValue($xpath, "dfrn:location/text()", $entry);
1783
1784                 $item["coord"] = XML::getFirstNodeValue($xpath, "georss:point", $entry);
1785
1786                 $item["private"] = XML::getFirstNodeValue($xpath, "dfrn:private/text()", $entry);
1787
1788                 $unlisted = XML::getFirstNodeValue($xpath, "dfrn:unlisted/text()", $entry);
1789                 if (!empty($unlisted) && ($item['private'] != Item::PRIVATE)) {
1790                         $item['private'] = Item::UNLISTED;
1791                 }
1792
1793                 $item["extid"] = XML::getFirstNodeValue($xpath, "dfrn:extid/text()", $entry);
1794
1795                 if (XML::getFirstNodeValue($xpath, "dfrn:bookmark/text()", $entry) == "true") {
1796                         $item["post-type"] = Item::PT_PAGE;
1797                 }
1798
1799                 $notice_info = $xpath->query("statusnet:notice_info", $entry);
1800                 if ($notice_info && ($notice_info->length > 0)) {
1801                         foreach ($notice_info->item(0)->attributes as $attributes) {
1802                                 if ($attributes->name == "source") {
1803                                         $item["app"] = strip_tags($attributes->textContent);
1804                                 }
1805                         }
1806                 }
1807
1808                 $item["guid"] = XML::getFirstNodeValue($xpath, "dfrn:diaspora_guid/text()", $entry);
1809
1810                 $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]);
1811
1812                 $item["body"] = Item::improveSharedDataInBody($item);
1813
1814                 Tag::storeFromBody($item['uri-id'], $item["body"]);
1815
1816                 // We store the data from "dfrn:diaspora_signature" in a different table, this is done in "Item::insert"
1817                 $dsprsig = XML::unescape(XML::getFirstNodeValue($xpath, "dfrn:diaspora_signature/text()", $entry));
1818                 if ($dsprsig != "") {
1819                         $signature = json_decode(base64_decode($dsprsig));
1820                         // We don't store the old style signatures anymore that also contained the "signature" and "signer"
1821                         if (!empty($signature->signed_text) && empty($signature->signature) && empty($signature->signer)) {
1822                                 $item["diaspora_signed_text"] = $signature->signed_text;
1823                         }
1824                 }
1825
1826                 $item["verb"] = XML::getFirstNodeValue($xpath, "activity:verb/text()", $entry);
1827
1828                 if (XML::getFirstNodeValue($xpath, "activity:object-type/text()", $entry) != "") {
1829                         $item["object-type"] = XML::getFirstNodeValue($xpath, "activity:object-type/text()", $entry);
1830                 }
1831
1832                 $object = $xpath->query("activity:object", $entry)->item(0);
1833                 $item["object"] = self::transformActivity($xpath, $object, "object");
1834
1835                 if (trim($item["object"]) != "") {
1836                         $r = XML::parseString($item["object"]);
1837                         if (isset($r->type)) {
1838                                 $item["object-type"] = $r->type;
1839                         }
1840                 }
1841
1842                 $target = $xpath->query("activity:target", $entry)->item(0);
1843                 $item["target"] = self::transformActivity($xpath, $target, "target");
1844
1845                 $categories = $xpath->query("atom:category", $entry);
1846                 if ($categories) {
1847                         foreach ($categories as $category) {
1848                                 $term = "";
1849                                 $scheme = "";
1850                                 foreach ($category->attributes as $attributes) {
1851                                         if ($attributes->name == "term") {
1852                                                 $term = $attributes->textContent;
1853                                         }
1854
1855                                         if ($attributes->name == "scheme") {
1856                                                 $scheme = $attributes->textContent;
1857                                         }
1858                                 }
1859
1860                                 if (($term != "") && ($scheme != "")) {
1861                                         $parts = explode(":", $scheme);
1862                                         if ((count($parts) >= 4) && (array_shift($parts) == "X-DFRN")) {
1863                                                 $termurl = array_pop($parts);
1864                                                 $termurl = array_pop($parts) . ':' . $termurl;
1865                                                 Tag::store($item['uri-id'], Tag::IMPLICIT_MENTION, $term, $termurl);
1866                                         }
1867                                 }
1868                         }
1869                 }
1870
1871                 $links = $xpath->query("atom:link", $entry);
1872                 if ($links) {
1873                         self::parseLinks($links, $item);
1874                 }
1875
1876                 $item['conversation-uri'] = XML::getFirstNodeValue($xpath, 'ostatus:conversation/text()', $entry);
1877
1878                 $conv = $xpath->query('ostatus:conversation', $entry);
1879                 if (is_object($conv->item(0))) {
1880                         foreach ($conv->item(0)->attributes as $attributes) {
1881                                 if ($attributes->name == "ref") {
1882                                         $item['conversation-uri'] = $attributes->textContent;
1883                                 }
1884                                 if ($attributes->name == "href") {
1885                                         $item['conversation-href'] = $attributes->textContent;
1886                                 }
1887                         }
1888                 }
1889
1890                 // Is it a reply or a top level posting?
1891                 $item['thr-parent'] = $item['uri'];
1892
1893                 $inreplyto = $xpath->query("thr:in-reply-to", $entry);
1894                 if (is_object($inreplyto->item(0))) {
1895                         foreach ($inreplyto->item(0)->attributes as $attributes) {
1896                                 if ($attributes->name == "ref") {
1897                                         $item['thr-parent'] = $attributes->textContent;
1898                                 }
1899                         }
1900                 }
1901
1902                 // Check if the message is wanted
1903                 if (($importer['importer_uid'] == 0) && ($item['uri'] == $item['thr-parent'])) {
1904                         if (!self::isSolicitedMessage($item)) {
1905                                 DBA::delete('item-uri', ['uri' => $item['uri']]);
1906                                 return 403;
1907                         }
1908                 }
1909
1910                 // Get the type of the item (Top level post, reply or remote reply)
1911                 $entrytype = self::getEntryType($importer, $item);
1912
1913                 // Now assign the rest of the values that depend on the type of the message
1914                 if (in_array($entrytype, [DFRN::REPLY, DFRN::REPLY_RC])) {
1915                         if (!isset($item["object-type"])) {
1916                                 $item["object-type"] = Activity\ObjectType::COMMENT;
1917                         }
1918
1919                         if ($item["contact-id"] != $owner["contact-id"]) {
1920                                 $item["contact-id"] = $owner["contact-id"];
1921                         }
1922
1923                         if (($item["network"] != $owner["network"]) && ($owner["network"] != "")) {
1924                                 $item["network"] = $owner["network"];
1925                         }
1926
1927                         if ($item["contact-id"] != $author["contact-id"]) {
1928                                 $item["contact-id"] = $author["contact-id"];
1929                         }
1930
1931                         if (($item["network"] != $author["network"]) && ($author["network"] != "")) {
1932                                 $item["network"] = $author["network"];
1933                         }
1934                 }
1935
1936                 // Ensure to have the correct share data
1937                 $item = Item::addShareDataFromOriginal($item);
1938
1939                 if ($entrytype == DFRN::REPLY_RC) {
1940                         $item["wall"] = 1;
1941                 } elseif ($entrytype == DFRN::TOP_LEVEL) {
1942                         if (!isset($item["object-type"])) {
1943                                 $item["object-type"] = Activity\ObjectType::NOTE;
1944                         }
1945
1946                         // Is it an event?
1947                         if (($item["object-type"] == Activity\ObjectType::EVENT) && !$owner_unknown) {
1948                                 Logger::log("Item ".$item["uri"]." seems to contain an event.", Logger::DEBUG);
1949                                 $ev = Event::fromBBCode($item["body"]);
1950                                 if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) {
1951                                         Logger::log("Event in item ".$item["uri"]." was found.", Logger::DEBUG);
1952                                         $ev["cid"]       = $importer["id"];
1953                                         $ev["uid"]       = $importer["importer_uid"];
1954                                         $ev["uri"]       = $item["uri"];
1955                                         $ev["edited"]    = $item["edited"];
1956                                         $ev["private"]   = $item["private"];
1957                                         $ev["guid"]      = $item["guid"];
1958                                         $ev["plink"]     = $item["plink"];
1959                                         $ev["network"]   = $item["network"];
1960                                         $ev["protocol"]  = $item["protocol"];
1961                                         $ev["direction"] = $item["direction"];
1962                                         $ev["source"]    = $item["source"];
1963
1964                                         $condition = ['uri' => $item["uri"], 'uid' => $importer["importer_uid"]];
1965                                         $event = DBA::selectFirst('event', ['id'], $condition);
1966                                         if (DBA::isResult($event)) {
1967                                                 $ev["id"] = $event["id"];
1968                                         }
1969
1970                                         $event_id = Event::store($ev);
1971                                         Logger::info('Event was stored', ['id' => $event_id]);
1972
1973                                         $item = Event::getItemArrayForImportedId($event_id, $item);
1974                                 }
1975                         }
1976                 }
1977
1978                 if (!self::processVerbs($entrytype, $importer, $item, $is_like)) {
1979                         Logger::log("Exiting because 'processVerbs' told us so", Logger::DEBUG);
1980                         return;
1981                 }
1982
1983                 // This check is done here to be able to receive connection requests in "processVerbs"
1984                 if (($entrytype == DFRN::TOP_LEVEL) && $owner_unknown) {
1985                         Logger::log("Item won't be stored because user " . $importer["importer_uid"] . " doesn't follow " . $item["owner-link"] . ".", Logger::DEBUG);
1986                         return;
1987                 }
1988
1989
1990                 // Update content if 'updated' changes
1991                 if (DBA::isResult($current)) {
1992                         if (self::updateContent($current, $item, $importer, $entrytype)) {
1993                                 Logger::log("Item ".$item["uri"]." was updated.", Logger::DEBUG);
1994                         } else {
1995                                 Logger::log("Item " . $item["uri"] . " already existed.", Logger::DEBUG);
1996                         }
1997                         return;
1998                 }
1999
2000                 if (in_array($entrytype, [DFRN::REPLY, DFRN::REPLY_RC])) {
2001                         // Will be overwritten for sharing accounts in Item::insert
2002                         if (empty($item['post-reason']) && ($entrytype == DFRN::REPLY)) {
2003                                 $item['post-reason'] = Item::PR_COMMENT;
2004                         }
2005
2006                         $posted_id = Item::insert($item);
2007                         if ($posted_id) {
2008                                 Logger::log("Reply from contact ".$item["contact-id"]." was stored with id ".$posted_id, Logger::DEBUG);
2009
2010                                 if ($item['uid'] == 0) {
2011                                         Item::distribute($posted_id);
2012                                 }
2013
2014                                 return true;
2015                         }
2016                 } else { // $entrytype == DFRN::TOP_LEVEL
2017                         if (($importer["uid"] == 0) && ($importer["importer_uid"] != 0)) {
2018                                 Logger::log("Contact ".$importer["id"]." isn't known to user ".$importer["importer_uid"].". The post will be ignored.", Logger::DEBUG);
2019                                 return;
2020                         }
2021                         if (!Strings::compareLink($item["owner-link"], $importer["url"])) {
2022                                 /*
2023                                  * The item owner info is not our contact. It's OK and is to be expected if this is a tgroup delivery,
2024                                  * but otherwise there's a possible data mixup on the sender's system.
2025                                  * the tgroup delivery code called from Item::insert will correct it if it's a forum,
2026                                  * but we're going to unconditionally correct it here so that the post will always be owned by our contact.
2027                                  */
2028                                 Logger::log('Correcting item owner.', Logger::DEBUG);
2029                                 $item["owner-link"] = $importer["url"];
2030                                 $item["owner-id"] = Contact::getIdForURL($importer["url"], 0);
2031                         }
2032
2033                         if (($importer["rel"] == Contact::FOLLOWER) && (!self::tgroupCheck($importer["importer_uid"], $item))) {
2034                                 Logger::log("Contact ".$importer["id"]." is only follower and tgroup check was negative.", Logger::DEBUG);
2035                                 return;
2036                         }
2037
2038                         // This is my contact on another system, but it's really me.
2039                         // Turn this into a wall post.
2040                         $notify = Item::isRemoteSelf($importer, $item);
2041
2042                         $posted_id = Item::insert($item, $notify);
2043
2044                         if ($notify) {
2045                                 $posted_id = $notify;
2046                         }
2047
2048                         Logger::log("Item was stored with id ".$posted_id, Logger::DEBUG);
2049
2050                         if ($item['uid'] == 0) {
2051                                 Item::distribute($posted_id);
2052                         }
2053
2054                         if (stristr($item["verb"], Activity::POKE)) {
2055                                 $item['id'] = $posted_id;
2056                                 self::doPoke($item, $importer);
2057                         }
2058                 }
2059         }
2060
2061         /**
2062          * Deletes items
2063          *
2064          * @param object $xpath    XPath object
2065          * @param object $deletion deletion elements
2066          * @param array  $importer Record of the importer user mixed with contact of the content
2067          * @return void
2068          * @throws \Exception
2069          * @todo  set proper type-hints
2070          */
2071         private static function processDeletion($xpath, $deletion, $importer)
2072         {
2073                 Logger::log("Processing deletions");
2074                 $uri = null;
2075
2076                 foreach ($deletion->attributes as $attributes) {
2077                         if ($attributes->name == "ref") {
2078                                 $uri = $attributes->textContent;
2079                         }
2080                 }
2081
2082                 if (!$uri || !$importer["id"]) {
2083                         return false;
2084                 }
2085
2086                 $condition = ['uri' => $uri, 'uid' => $importer["importer_uid"]];
2087                 $item = Post::selectFirst(['id', 'parent', 'contact-id', 'uri-id', 'deleted', 'gravity'], $condition);
2088                 if (!DBA::isResult($item)) {
2089                         Logger::log("Item with uri " . $uri . " for user " . $importer["importer_uid"] . " wasn't found.", Logger::DEBUG);
2090                         return;
2091                 }
2092
2093                 if (DBA::exists('post-category', ['uri-id' => $item['uri-id'], 'uid' => $importer['importer_uid'], 'type' => Post\Category::FILE])) {
2094                         Logger::notice("Item is filed. It won't be deleted.", ['uri' => $uri, 'uri-id' => $item['uri_id'], 'uid' => $importer["importer_uid"]]);
2095                         return;
2096                 }
2097
2098                 // When it is a starting post it has to belong to the person that wants to delete it
2099                 if (($item['gravity'] == GRAVITY_PARENT) && ($item['contact-id'] != $importer["id"])) {
2100                         Logger::log("Item with uri " . $uri . " don't belong to contact " . $importer["id"] . " - ignoring deletion.", Logger::DEBUG);
2101                         return;
2102                 }
2103
2104                 // Comments can be deleted by the thread owner or comment owner
2105                 if (($item['gravity'] != GRAVITY_PARENT) && ($item['contact-id'] != $importer["id"])) {
2106                         $condition = ['id' => $item['parent'], 'contact-id' => $importer["id"]];
2107                         if (!Post::exists($condition)) {
2108                                 Logger::log("Item with uri " . $uri . " wasn't found or mustn't be deleted by contact " . $importer["id"] . " - ignoring deletion.", Logger::DEBUG);
2109                                 return;
2110                         }
2111                 }
2112
2113                 if ($item["deleted"]) {
2114                         return;
2115                 }
2116
2117                 Logger::log('deleting item '.$item['id'].' uri='.$uri, Logger::DEBUG);
2118
2119                 Item::markForDeletion(['id' => $item['id']]);
2120         }
2121
2122         /**
2123          * Imports a DFRN message
2124          *
2125          * @param string $xml       The DFRN message
2126          * @param array  $importer  Record of the importer user mixed with contact of the content
2127          * @param int    $protocol  Transport protocol
2128          * @param int    $direction Is the message pushed or pulled?
2129          * @return integer Import status
2130          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
2131          * @throws \ImagickException
2132          * @todo  set proper type-hints
2133          */
2134         public static function import($xml, $importer, $protocol, $direction)
2135         {
2136                 if ($xml == "") {
2137                         return 400;
2138                 }
2139
2140                 $doc = new DOMDocument();
2141                 @$doc->loadXML($xml);
2142
2143                 $xpath = new DOMXPath($doc);
2144                 $xpath->registerNamespace("atom", ActivityNamespace::ATOM1);
2145                 $xpath->registerNamespace("thr", ActivityNamespace::THREAD);
2146                 $xpath->registerNamespace("at", ActivityNamespace::TOMB);
2147                 $xpath->registerNamespace("media", ActivityNamespace::MEDIA);
2148                 $xpath->registerNamespace("dfrn", ActivityNamespace::DFRN);
2149                 $xpath->registerNamespace("activity", ActivityNamespace::ACTIVITY);
2150                 $xpath->registerNamespace("georss", ActivityNamespace::GEORSS);
2151                 $xpath->registerNamespace("poco", ActivityNamespace::POCO);
2152                 $xpath->registerNamespace("ostatus", ActivityNamespace::OSTATUS);
2153                 $xpath->registerNamespace("statusnet", ActivityNamespace::STATUSNET);
2154
2155                 $header = [];
2156                 $header["uid"] = $importer["importer_uid"];
2157                 $header["network"] = Protocol::DFRN;
2158                 $header["wall"] = 0;
2159                 $header["origin"] = 0;
2160                 $header["contact-id"] = $importer["id"];
2161                 $header["direction"] = $direction;
2162
2163                 if ($direction === Conversation::RELAY) {
2164                         $header['post-reason'] = Item::PR_RELAY;
2165                 }
2166
2167                 // Update the contact table if the data has changed
2168
2169                 // The "atom:author" is only present in feeds
2170                 if ($xpath->query("/atom:feed/atom:author")->length > 0) {
2171                         self::fetchauthor($xpath, $doc->firstChild, $importer, "atom:author", false, $xml);
2172                 }
2173
2174                 // Only the "dfrn:owner" in the head section contains all data
2175                 if ($xpath->query("/atom:feed/dfrn:owner")->length > 0) {
2176                         self::fetchauthor($xpath, $doc->firstChild, $importer, "dfrn:owner", false, $xml);
2177                 }
2178
2179                 Logger::log("Import DFRN message for user " . $importer["importer_uid"] . " from contact " . $importer["id"], Logger::DEBUG);
2180
2181                 if (!empty($importer['gsid']) && ($protocol == Conversation::PARCEL_DIASPORA_DFRN)) {
2182                         GServer::setProtocol($importer['gsid'], Post\DeliveryData::DFRN);
2183                 }
2184
2185                 // is it a public forum? Private forums aren't exposed with this method
2186                 $forum = intval(XML::getFirstNodeValue($xpath, "/atom:feed/dfrn:community/text()"));
2187
2188                 // The account type is new since 3.5.1
2189                 if ($xpath->query("/atom:feed/dfrn:account_type")->length > 0) {
2190                         // Hint: We are using separate update calls for uid=0 and uid!=0 since a combined call is bad for the database performance
2191
2192                         $accounttype = intval(XML::getFirstNodeValue($xpath, "/atom:feed/dfrn:account_type/text()"));
2193
2194                         if ($accounttype != $importer["contact-type"]) {
2195                                 Contact::update(['contact-type' => $accounttype], ['id' => $importer['id']]);
2196
2197                                 // Updating the public contact as well
2198                                 Contact::update(['contact-type' => $accounttype], ['uid' => 0, 'nurl' => $importer['nurl']]);
2199                         }
2200                         // A forum contact can either have set "forum" or "prv" - but not both
2201                         if ($accounttype == User::ACCOUNT_TYPE_COMMUNITY) {
2202                                 // It's a forum, so either set the public or private forum flag
2203                                 $condition = ['(`forum` != ? OR `prv` != ?) AND `id` = ?', $forum, !$forum, $importer['id']];
2204                                 Contact::update(['forum' => $forum, 'prv' => !$forum], $condition);
2205
2206                                 // Updating the public contact as well
2207                                 $condition = ['(`forum` != ? OR `prv` != ?) AND `uid` = 0 AND `nurl` = ?', $forum, !$forum, $importer['nurl']];
2208                                 Contact::update(['forum' => $forum, 'prv' => !$forum], $condition);
2209                         } else {
2210                                 // It's not a forum, so remove the flags
2211                                 $condition = ['(`forum` OR `prv`) AND `id` = ?', $importer['id']];
2212                                 Contact::update(['forum' => false, 'prv' => false], $condition);
2213
2214                                 // Updating the public contact as well
2215                                 $condition = ['(`forum` OR `prv`) AND `uid` = 0 AND `nurl` = ?', $importer['nurl']];
2216                                 Contact::update(['forum' => false, 'prv' => false], $condition);
2217                         }
2218                 } elseif ($forum != $importer["forum"]) { // Deprecated since 3.5.1
2219                         $condition = ['`forum` != ? AND `id` = ?', $forum, $importer["id"]];
2220                         Contact::update(['forum' => $forum], $condition);
2221
2222                         // Updating the public contact as well
2223                         $condition = ['`forum` != ? AND `uid` = 0 AND `nurl` = ?', $forum, $importer['nurl']];
2224                         Contact::update(['forum' => $forum], $condition);
2225                 }
2226
2227
2228                 // We are processing relocations even if we are ignoring a contact
2229                 $relocations = $xpath->query("/atom:feed/dfrn:relocate");
2230                 foreach ($relocations as $relocation) {
2231                         self::processRelocation($xpath, $relocation, $importer);
2232                 }
2233
2234                 if (($importer["uid"] != 0) && !$importer["readonly"]) {
2235                         $mails = $xpath->query("/atom:feed/dfrn:mail");
2236                         foreach ($mails as $mail) {
2237                                 self::processMail($xpath, $mail, $importer);
2238                         }
2239
2240                         $suggestions = $xpath->query("/atom:feed/dfrn:suggest");
2241                         foreach ($suggestions as $suggestion) {
2242                                 self::processSuggestion($xpath, $suggestion, $importer);
2243                         }
2244                 }
2245
2246                 $deletions = $xpath->query("/atom:feed/at:deleted-entry");
2247                 if (!empty($deletions)) {
2248                         foreach ($deletions as $deletion) {
2249                                 self::processDeletion($xpath, $deletion, $importer);
2250                         }
2251                         if (count($deletions) > 0) {
2252                                 Logger::notice('Deletions had been processed');
2253                                 return 200;
2254                         }
2255                 }
2256
2257                 $entries = $xpath->query("/atom:feed/atom:entry");
2258                 foreach ($entries as $entry) {
2259                         self::processEntry($header, $xpath, $entry, $importer, $xml, $protocol);
2260                 }
2261
2262                 Logger::log("Import done for user " . $importer["importer_uid"] . " from contact " . $importer["id"], Logger::DEBUG);
2263                 return 200;
2264         }
2265
2266         /**
2267          * Returns the activity verb
2268          *
2269          * @param array $item Item array
2270          *
2271          * @return string activity verb
2272          */
2273         private static function constructVerb(array $item)
2274         {
2275                 if ($item['verb']) {
2276                         return $item['verb'];
2277                 }
2278                 return Activity::POST;
2279         }
2280
2281         private static function tgroupCheck($uid, $item)
2282         {
2283                 $mention = false;
2284
2285                 // check that the message originated elsewhere and is a top-level post
2286
2287                 if ($item['wall'] || $item['origin'] || ($item['uri'] != $item['thr-parent'])) {
2288                         return false;
2289                 }
2290
2291                 $user = DBA::selectFirst('user', ['page-flags', 'nickname'], ['uid' => $uid]);
2292                 if (!DBA::isResult($user)) {
2293                         return false;
2294                 }
2295
2296                 $community_page = ($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY);
2297                 $prvgroup = ($user['page-flags'] == User::PAGE_FLAGS_PRVGROUP);
2298
2299                 $link = Strings::normaliseLink(DI::baseUrl() . '/profile/' . $user['nickname']);
2300
2301                 /*
2302                  * Diaspora uses their own hardwired link URL in @-tags
2303                  * instead of the one we supply with webfinger
2304                  */
2305                 $dlink = Strings::normaliseLink(DI::baseUrl() . '/u/' . $user['nickname']);
2306
2307                 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism', $item['body'], $matches, PREG_SET_ORDER);
2308                 if ($cnt) {
2309                         foreach ($matches as $mtch) {
2310                                 if (Strings::compareLink($link, $mtch[1]) || Strings::compareLink($dlink, $mtch[1])) {
2311                                         $mention = true;
2312                                         Logger::log('mention found: ' . $mtch[2]);
2313                                 }
2314                         }
2315                 }
2316
2317                 if (!$mention) {
2318                         return false;
2319                 }
2320
2321                 return $community_page || $prvgroup;
2322         }
2323
2324         /**
2325          * This function returns true if $update has an edited timestamp newer
2326          * than $existing, i.e. $update contains new data which should override
2327          * what's already there.  If there is no timestamp yet, the update is
2328          * assumed to be newer.  If the update has no timestamp, the existing
2329          * item is assumed to be up-to-date.  If the timestamps are equal it
2330          * assumes the update has been seen before and should be ignored.
2331          *
2332          * @param $existing
2333          * @param $update
2334          * @return bool
2335          * @throws \Exception
2336          */
2337         private static function isEditedTimestampNewer($existing, $update)
2338         {
2339                 if (empty($existing['edited'])) {
2340                         return true;
2341                 }
2342                 if (empty($update['edited'])) {
2343                         return false;
2344                 }
2345
2346                 $existing_edited = DateTimeFormat::utc($existing['edited']);
2347                 $update_edited = DateTimeFormat::utc($update['edited']);
2348
2349                 return (strcmp($existing_edited, $update_edited) < 0);
2350         }
2351
2352         /**
2353          * Checks if the given contact url does support DFRN
2354          *
2355          * @param string  $url    profile url
2356          * @return boolean
2357          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
2358          * @throws \ImagickException
2359          */
2360         public static function isSupportedByContactUrl($url)
2361         {
2362                 $probe = Probe::uri($url, Protocol::DFRN);
2363                 return $probe['network'] == Protocol::DFRN;
2364         }
2365 }