]> git.mxchange.org Git - friendica.git/blob - src/Protocol/Feed.php
Changed constant name to keep compatible with PHP 5
[friendica.git] / src / Protocol / Feed.php
1 <?php
2 /**
3  * @file src/Protocol/Feed.php
4  * @brief Imports RSS/RDF/Atom feeds
5  *
6  */
7 namespace Friendica\Protocol;
8
9 use DOMDocument;
10 use DOMXPath;
11 use Friendica\Content\Text\HTML;
12 use Friendica\Core\Protocol;
13 use Friendica\Core\System;
14 use Friendica\Database\DBA;
15 use Friendica\Model\Item;
16 use Friendica\Util\Network;
17 use Friendica\Util\XML;
18
19 require_once 'include/dba.php';
20 require_once 'include/items.php';
21
22 /**
23  * @brief This class contain functions to import feeds
24  *
25  */
26 class Feed {
27         /**
28          * @brief Read a RSS/RDF/Atom feed and create an item entry for it
29          *
30          * @param string $xml The feed data
31          * @param array $importer The user record of the importer
32          * @param array $contact The contact record of the feed
33          * @param string $hub Unused dummy value for compatibility reasons
34          * @param bool $simulate If enabled, no data is imported
35          *
36          * @return array In simulation mode it returns the header and the first item
37          */
38         public static function import($xml, $importer, &$contact, &$hub, $simulate = false) {
39
40                 $a = get_app();
41
42                 if (!$simulate) {
43                         logger("Import Atom/RSS feed '".$contact["name"]."' (Contact ".$contact["id"].") for user ".$importer["uid"], LOGGER_DEBUG);
44                 } else {
45                         logger("Test Atom/RSS feed", LOGGER_DEBUG);
46                 }
47                 if (empty($xml)) {
48                         logger('XML is empty.', LOGGER_DEBUG);
49                         return;
50                 }
51
52                 if (!empty($contact['poll'])) {
53                         $basepath = $contact['poll'];
54                 } elseif (!empty($contact['url'])) {
55                         $basepath = $contact['url'];
56                 } else {
57                         $basepath = '';
58                 }
59
60                 $doc = new DOMDocument();
61                 @$doc->loadXML(trim($xml));
62                 $xpath = new DOMXPath($doc);
63                 $xpath->registerNamespace('atom', NAMESPACE_ATOM1);
64                 $xpath->registerNamespace('dc', "http://purl.org/dc/elements/1.1/");
65                 $xpath->registerNamespace('content', "http://purl.org/rss/1.0/modules/content/");
66                 $xpath->registerNamespace('rdf', "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
67                 $xpath->registerNamespace('rss', "http://purl.org/rss/1.0/");
68                 $xpath->registerNamespace('media', "http://search.yahoo.com/mrss/");
69                 $xpath->registerNamespace('poco', NAMESPACE_POCO);
70
71                 $author = [];
72                 $entries = null;
73
74                 // Is it RDF?
75                 if ($xpath->query('/rdf:RDF/rss:channel')->length > 0) {
76                         $author["author-link"] = XML::getFirstNodeValue($xpath, '/rdf:RDF/rss:channel/rss:link/text()');
77                         $author["author-name"] = XML::getFirstNodeValue($xpath, '/rdf:RDF/rss:channel/rss:title/text()');
78
79                         if (empty($author["author-name"])) {
80                                 $author["author-name"] = XML::getFirstNodeValue($xpath, '/rdf:RDF/rss:channel/rss:description/text()');
81                         }
82                         $entries = $xpath->query('/rdf:RDF/rss:item');
83                 }
84
85                 // Is it Atom?
86                 if ($xpath->query('/atom:feed')->length > 0) {
87                         $alternate = XML::getFirstAttributes($xpath, "atom:link[@rel='alternate']");
88                         if (is_object($alternate)) {
89                                 foreach ($alternate AS $attribute) {
90                                         if ($attribute->name == "href") {
91                                                 $author["author-link"] = $attribute->textContent;
92                                         }
93                                 }
94                         }
95
96                         if (empty($author["author-link"])) {
97                                 $self = XML::getFirstAttributes($xpath, "atom:link[@rel='self']");
98                                 if (is_object($self)) {
99                                         foreach ($self AS $attribute) {
100                                                 if ($attribute->name == "href") {
101                                                         $author["author-link"] = $attribute->textContent;
102                                                 }
103                                         }
104                                 }
105                         }
106
107                         if (empty($author["author-link"])) {
108                                 $author["author-link"] = XML::getFirstNodeValue($xpath, '/atom:feed/atom:id/text()');
109                         }
110                         $author["author-avatar"] = XML::getFirstNodeValue($xpath, '/atom:feed/atom:logo/text()');
111
112                         $author["author-name"] = XML::getFirstNodeValue($xpath, '/atom:feed/atom:title/text()');
113
114                         if (empty($author["author-name"])) {
115                                 $author["author-name"] = XML::getFirstNodeValue($xpath, '/atom:feed/atom:subtitle/text()');
116                         }
117                         if (empty($author["author-name"])) {
118                                 $author["author-name"] = XML::getFirstNodeValue($xpath, '/atom:feed/atom:author/atom:name/text()');
119                         }
120                         $value = XML::getFirstNodeValue($xpath, 'atom:author/poco:displayName/text()');
121                         if ($value != "") {
122                                 $author["author-name"] = $value;
123                         }
124                         if ($simulate) {
125                                 $author["author-id"] = XML::getFirstNodeValue($xpath, '/atom:feed/atom:author/atom:uri/text()');
126
127                                 $value = XML::getFirstNodeValue($xpath, 'atom:author/poco:preferredUsername/text()');
128                                 if ($value != "") {
129                                         $author["author-nick"] = $value;
130                                 }
131                                 $value = XML::getFirstNodeValue($xpath, 'atom:author/poco:address/poco:formatted/text()');
132                                 if ($value != "") {
133                                         $author["author-location"] = $value;
134                                 }
135                                 $value = XML::getFirstNodeValue($xpath, 'atom:author/poco:note/text()');
136                                 if ($value != "") {
137                                         $author["author-about"] = $value;
138                                 }
139                                 $avatar = XML::getFirstAttributes($xpath, "atom:author/atom:link[@rel='avatar']");
140                                 if (is_object($avatar)) {
141                                         foreach ($avatar AS $attribute) {
142                                                 if ($attribute->name == "href") {
143                                                         $author["author-avatar"] = $attribute->textContent;
144                                                 }
145                                         }
146                                 }
147                         }
148
149                         $author["edited"] = $author["created"] = XML::getFirstNodeValue($xpath, '/atom:feed/atom:updated/text()');
150
151                         $author["app"] = XML::getFirstNodeValue($xpath, '/atom:feed/atom:generator/text()');
152
153                         $entries = $xpath->query('/atom:feed/atom:entry');
154                 }
155
156                 // Is it RSS?
157                 if ($xpath->query('/rss/channel')->length > 0) {
158                         $author["author-link"] = XML::getFirstNodeValue($xpath, '/rss/channel/link/text()');
159
160                         $author["author-name"] = XML::getFirstNodeValue($xpath, '/rss/channel/title/text()');
161                         $author["author-avatar"] = XML::getFirstNodeValue($xpath, '/rss/channel/image/url/text()');
162
163                         if (empty($author["author-name"])) {
164                                 $author["author-name"] = XML::getFirstNodeValue($xpath, '/rss/channel/copyright/text()');
165                         }
166                         if (empty($author["author-name"])) {
167                                 $author["author-name"] = XML::getFirstNodeValue($xpath, '/rss/channel/description/text()');
168                         }
169                         $author["edited"] = $author["created"] = XML::getFirstNodeValue($xpath, '/rss/channel/pubDate/text()');
170
171                         $author["app"] = XML::getFirstNodeValue($xpath, '/rss/channel/generator/text()');
172
173                         $entries = $xpath->query('/rss/channel/item');
174                 }
175
176                 if (!$simulate) {
177                         $author["author-link"] = $contact["url"];
178
179                         if (empty($author["author-name"])) {
180                                 $author["author-name"] = $contact["name"];
181                         }
182                         $author["author-avatar"] = $contact["thumb"];
183
184                         $author["owner-link"] = $contact["url"];
185                         $author["owner-name"] = $contact["name"];
186                         $author["owner-avatar"] = $contact["thumb"];
187                 }
188
189                 $header = [];
190                 $header["uid"] = $importer["uid"];
191                 $header["network"] = Protocol::FEED;
192                 $header["wall"] = 0;
193                 $header["origin"] = 0;
194                 $header["gravity"] = GRAVITY_PARENT;
195                 $header["private"] = 2;
196                 $header["verb"] = ACTIVITY_POST;
197                 $header["object-type"] = ACTIVITY_OBJ_NOTE;
198
199                 $header["contact-id"] = $contact["id"];
200
201                 if (!is_object($entries)) {
202                         logger("There are no entries in this feed.", LOGGER_DEBUG);
203                         return;
204                 }
205
206                 $items = [];
207                 // Importing older entries first
208                 for($i = $entries->length - 1; $i >= 0;--$i) {
209                         $entry = $entries->item($i);
210
211                         $item = array_merge($header, $author);
212
213                         $alternate = XML::getFirstAttributes($xpath, "atom:link[@rel='alternate']", $entry);
214                         if (!is_object($alternate)) {
215                                 $alternate = XML::getFirstAttributes($xpath, "atom:link", $entry);
216                         }
217                         if (is_object($alternate)) {
218                                 foreach ($alternate AS $attribute) {
219                                         if ($attribute->name == "href") {
220                                                 $item["plink"] = $attribute->textContent;
221                                         }
222                                 }
223                         }
224                         if (empty($item["plink"])) {
225                                 $item["plink"] = XML::getFirstNodeValue($xpath, 'link/text()', $entry);
226                         }
227                         if (empty($item["plink"])) {
228                                 $item["plink"] = XML::getFirstNodeValue($xpath, 'rss:link/text()', $entry);
229                         }
230
231                         $item["uri"] = XML::getFirstNodeValue($xpath, 'atom:id/text()', $entry);
232
233                         if (empty($item["uri"])) {
234                                 $item["uri"] = XML::getFirstNodeValue($xpath, 'guid/text()', $entry);
235                         }
236                         if (empty($item["uri"])) {
237                                 $item["uri"] = $item["plink"];
238                         }
239
240                         $orig_plink = $item["plink"];
241
242                         $item["plink"] = Network::finalUrl($item["plink"]);
243
244                         $item["parent-uri"] = $item["uri"];
245
246                         if (!$simulate) {
247                                 $condition = ["`uid` = ? AND `uri` = ? AND `network` IN (?, ?)",
248                                         $importer["uid"], $item["uri"], Protocol::FEED, Protocol::DFRN];
249                                 $previous = Item::selectFirst(['id'], $condition);
250                                 if (DBA::isResult($previous)) {
251                                         logger("Item with uri ".$item["uri"]." for user ".$importer["uid"]." already existed under id ".$previous["id"], LOGGER_DEBUG);
252                                         continue;
253                                 }
254                         }
255
256                         $item["title"] = XML::getFirstNodeValue($xpath, 'atom:title/text()', $entry);
257
258                         if (empty($item["title"])) {
259                                 $item["title"] = XML::getFirstNodeValue($xpath, 'title/text()', $entry);
260                         }
261                         if (empty($item["title"])) {
262                                 $item["title"] = XML::getFirstNodeValue($xpath, 'rss:title/text()', $entry);
263                         }
264                         $published = XML::getFirstNodeValue($xpath, 'atom:published/text()', $entry);
265
266                         if (empty($published)) {
267                                 $published = XML::getFirstNodeValue($xpath, 'pubDate/text()', $entry);
268                         }
269                         if (empty($published)) {
270                                 $published = XML::getFirstNodeValue($xpath, 'dc:date/text()', $entry);
271                         }
272                         $updated = XML::getFirstNodeValue($xpath, 'atom:updated/text()', $entry);
273
274                         if (empty($updated) && !empty($published)) {
275                                 $updated = $published;
276                         }
277
278                         if (empty($published) && !empty($updated)) {
279                                 $published = $updated;
280                         }
281
282                         if ($published != "") {
283                                 $item["created"] = $published;
284                         }
285                         if ($updated != "") {
286                                 $item["edited"] = $updated;
287                         }
288                         $creator = XML::getFirstNodeValue($xpath, 'author/text()', $entry);
289
290                         if (empty($creator)) {
291                                 $creator = XML::getFirstNodeValue($xpath, 'atom:author/atom:name/text()', $entry);
292                         }
293                         if (empty($creator)) {
294                                 $creator = XML::getFirstNodeValue($xpath, 'dc:creator/text()', $entry);
295                         }
296                         if ($creator != "") {
297                                 $item["author-name"] = $creator;
298                         }
299                         $creator = XML::getFirstNodeValue($xpath, 'dc:creator/text()', $entry);
300
301                         if ($creator != "") {
302                                 $item["author-name"] = $creator;
303                         }
304
305                         /// @TODO ?
306                         // <category>Ausland</category>
307                         // <media:thumbnail width="152" height="76" url="http://www.taz.de/picture/667875/192/14388767.jpg"/>
308
309                         $attachments = [];
310
311                         $enclosures = $xpath->query("enclosure|atom:link[@rel='enclosure']", $entry);
312                         foreach ($enclosures AS $enclosure) {
313                                 $href = "";
314                                 $length = "";
315                                 $type = "";
316                                 $title = "";
317
318                                 foreach ($enclosure->attributes AS $attribute) {
319                                         if (in_array($attribute->name, ["url", "href"])) {
320                                                 $href = $attribute->textContent;
321                                         } elseif ($attribute->name == "length") {
322                                                 $length = $attribute->textContent;
323                                         } elseif ($attribute->name == "type") {
324                                                 $type = $attribute->textContent;
325                                         }
326                                 }
327                                 if (!empty($item["attach"])) {
328                                         $item["attach"] .= ',';
329                                 } else {
330                                         $item["attach"] = '';
331                                 }
332
333                                 $attachments[] = ["link" => $href, "type" => $type, "length" => $length];
334
335                                 $item["attach"] .= '[attach]href="'.$href.'" length="'.$length.'" type="'.$type.'"[/attach]';
336                         }
337
338                         $tags = '';
339                         $categories = $xpath->query("category", $entry);
340                         foreach ($categories AS $category) {
341                                 $hashtag = $category->nodeValue;
342                                 if ($tags != '') {
343                                         $tags .= ', ';
344                                 }
345
346                                 $taglink = "#[url=" . System::baseUrl() . "/search?tag=" . rawurlencode($hashtag) . "]" . $hashtag . "[/url]";
347                                 $tags .= $taglink;
348                         }
349
350                         $body = trim(XML::getFirstNodeValue($xpath, 'atom:content/text()', $entry));
351
352                         if (empty($body)) {
353                                 $body = trim(XML::getFirstNodeValue($xpath, 'content:encoded/text()', $entry));
354                         }
355                         if (empty($body)) {
356                                 $body = trim(XML::getFirstNodeValue($xpath, 'description/text()', $entry));
357                         }
358                         if (empty($body)) {
359                                 $body = trim(XML::getFirstNodeValue($xpath, 'atom:summary/text()', $entry));
360                         }
361
362                         // remove the content of the title if it is identically to the body
363                         // This helps with auto generated titles e.g. from tumblr
364                         if (self::titleIsBody($item["title"], $body)) {
365                                 $item["title"] = "";
366                         }
367                         $item["body"] = HTML::toBBCode($body, $basepath);
368
369                         if (($item["body"] == '') && ($item["title"] != '')) {
370                                 $item["body"] = $item["title"];
371                                 $item["title"] = '';
372                         }
373
374                         $preview = '';
375                         if (!empty($contact["fetch_further_information"]) && ($contact["fetch_further_information"] < 3)) {
376                                 // Handle enclosures and treat them as preview picture
377                                 foreach ($attachments AS $attachment) {
378                                         if ($attachment["type"] == "image/jpeg") {
379                                                 $preview = $attachment["link"];
380                                         }
381                                 }
382
383                                 // Remove a possible link to the item itself
384                                 $item["body"] = str_replace($item["plink"], '', $item["body"]);
385                                 $item["body"] = preg_replace('/\[url\=\](\w+.*?)\[\/url\]/i', '', $item["body"]);
386
387                                 // Replace the content when the title is longer than the body
388                                 $replace = (strlen($item["title"]) > strlen($item["body"]));
389
390                                 // Replace it, when there is an image in the body
391                                 if (strstr($item["body"], '[/img]')) {
392                                         $replace = true;
393                                 }
394
395                                 // Replace it, when there is a link in the body
396                                 if (strstr($item["body"], '[/url]')) {
397                                         $replace = true;
398                                 }
399
400                                 if ($replace) {
401                                         $item["body"] = $item["title"];
402                                 }
403                                 // We always strip the title since it will be added in the page information
404                                 $item["title"] = "";
405                                 $item["body"] = $item["body"].add_page_info($item["plink"], false, $preview, ($contact["fetch_further_information"] == 2), $contact["ffi_keyword_blacklist"]);
406                                 $item["tag"] = add_page_keywords($item["plink"], $preview, ($contact["fetch_further_information"] == 2), $contact["ffi_keyword_blacklist"]);
407                                 $item["object-type"] = ACTIVITY_OBJ_BOOKMARK;
408                                 unset($item["attach"]);
409                         } else {
410                                 if ($contact["fetch_further_information"] == 3) {
411                                         if (!empty($tags)) {
412                                                 $item["tag"] = $tags;
413                                         } else {
414                                                 // @todo $preview is never set in this case, is it intended? - @MrPetovan 2018-02-13
415                                                 $item["tag"] = add_page_keywords($item["plink"], $preview, true, $contact["ffi_keyword_blacklist"]);
416                                         }
417                                         $item["body"] .= "\n".$item['tag'];
418                                 }
419                                 // Add the link to the original feed entry if not present in feed
420                                 if (($item['plink'] != '') && !strstr($item["body"], $item['plink'])) {
421                                         $item["body"] .= "[hr][url]".$item['plink']."[/url]";
422                                 }
423                         }
424
425                         if (!$simulate) {
426                                 logger("Stored feed: ".print_r($item, true), LOGGER_DEBUG);
427
428                                 $notify = Item::isRemoteSelf($contact, $item);
429
430                                 // Distributed items should have a well formatted URI.
431                                 // Additionally we have to avoid conflicts with identical URI between imported feeds and these items.
432                                 if ($notify) {
433                                         $item['guid'] = Item::guidFromUri($orig_plink, $a->get_hostname());
434                                         unset($item['uri']);
435                                         unset($item['parent-uri']);
436
437                                         // Set the delivery priority for "remote self" to "medium"
438                                         $notify = PRIORITY_MEDIUM;
439                                 }
440
441                                 $id = Item::insert($item, false, $notify);
442
443                                 logger("Feed for contact ".$contact["url"]." stored under id ".$id);
444                         } else {
445                                 $items[] = $item;
446                         }
447                         if ($simulate) {
448                                 break;
449                         }
450                 }
451
452                 if ($simulate) {
453                         return ["header" => $author, "items" => $items];
454                 }
455         }
456
457         private static function titleIsBody($title, $body)
458         {
459                 $title = strip_tags($title);
460                 $title = trim($title);
461                 $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
462                 $title = str_replace(["\n", "\r", "\t", " "], ["", "", "", ""], $title);
463
464                 $body = strip_tags($body);
465                 $body = trim($body);
466                 $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
467                 $body = str_replace(["\n", "\r", "\t", " "], ["", "", "", ""], $body);
468
469                 if (strlen($title) < strlen($body)) {
470                         $body = substr($body, 0, strlen($title));
471                 }
472
473                 if (($title != $body) && (substr($title, -3) == "...")) {
474                         $pos = strrpos($title, "...");
475                         if ($pos > 0) {
476                                 $title = substr($title, 0, $pos);
477                                 $body = substr($body, 0, $pos);
478                         }
479                 }
480                 return ($title == $body);
481         }
482 }