]> git.mxchange.org Git - friendica.git/blob - src/Util/ParseUrl.php
Add Temporal::utcNow()
[friendica.git] / src / Util / ParseUrl.php
1 <?php
2 /**
3  * @file src/Util/ParseUrl.php
4  * @brief Get informations about a given URL
5  */
6 namespace Friendica\Util;
7
8 use Friendica\Content\OEmbed;
9 use Friendica\Core\Addon;
10 use Friendica\Object\Image;
11 use Friendica\Util\Network;
12 use Friendica\Util\XML;
13
14 use dba;
15 use DOMXPath;
16 use DOMDocument;
17
18 require_once 'include/dba.php';
19
20 /**
21  * @brief Class with methods for extracting certain content from an url
22  */
23 class ParseUrl
24 {
25         /**
26          * @brief Search for chached embeddable data of an url otherwise fetch it
27          *
28          * @param string $url         The url of the page which should be scraped
29          * @param bool $no_guessing If true the parse doens't search for
30          *                          preview pictures
31          * @param bool $do_oembed   The false option is used by the function fetch_oembed()
32          *                          to avoid endless loops
33          *
34          * @return array which contains needed data for embedding
35          *    string 'url' => The url of the parsed page
36          *    string 'type' => Content type
37          *    string 'title' => The title of the content
38          *    string 'text' => The description for the content
39          *    string 'image' => A preview image of the content (only available
40          *                if $no_geuessing = false
41          *    array'images' = Array of preview pictures
42          *    string 'keywords' => The tags which belong to the content
43          *
44          * @see ParseUrl::getSiteinfo() for more information about scraping
45          * embeddable content
46          */
47         public static function getSiteinfoCached($url, $no_guessing = false, $do_oembed = true)
48         {
49                 if ($url == "") {
50                         return false;
51                 }
52
53                 $r = q(
54                         "SELECT * FROM `parsed_url` WHERE `url` = '%s' AND `guessing` = %d AND `oembed` = %d",
55                         dbesc(normalise_link($url)),
56                         intval(!$no_guessing),
57                         intval($do_oembed)
58                 );
59
60                 if ($r) {
61                         $data = $r[0]["content"];
62                 }
63
64                 if (!is_null($data)) {
65                         $data = unserialize($data);
66                         return $data;
67                 }
68
69                 $data = self::getSiteinfo($url, $no_guessing, $do_oembed);
70
71                 dba::insert(
72                         'parsed_url',
73                         [
74                                 'url' => normalise_link($url), 'guessing' => !$no_guessing,
75                                 'oembed' => $do_oembed, 'content' => serialize($data),
76                                 'created' => Temporal::utcNow()
77                         ],
78                         true
79                 );
80
81                 return $data;
82         }
83         /**
84          * @brief Parse a page for embeddable content information
85          *
86          * This method parses to url for meta data which can be used to embed
87          * the content. If available it prioritizes Open Graph meta tags.
88          * If this is not available it uses the twitter cards meta tags.
89          * As fallback it uses standard html elements with meta informations
90          * like \<title\>Awesome Title\</title\> or
91          * \<meta name="description" content="An awesome description"\>
92          *
93          * @param string $url         The url of the page which should be scraped
94          * @param bool $no_guessing If true the parse doens't search for
95          *                          preview pictures
96          * @param bool $do_oembed   The false option is used by the function fetch_oembed()
97          *                          to avoid endless loops
98          * @param int $count       Internal counter to avoid endless loops
99          *
100          * @return array which contains needed data for embedding
101          *    string 'url' => The url of the parsed page
102          *    string 'type' => Content type
103          *    string 'title' => The title of the content
104          *    string 'text' => The description for the content
105          *    string 'image' => A preview image of the content (only available
106          *                if $no_geuessing = false
107          *    array'images' = Array of preview pictures
108          *    string 'keywords' => The tags which belong to the content
109          *
110          * @todo https://developers.google.com/+/plugins/snippet/
111          * @verbatim
112          * <meta itemprop="name" content="Awesome title">
113          * <meta itemprop="description" content="An awesome description">
114          * <meta itemprop="image" content="http://maple.libertreeproject.org/images/tree-icon.png">
115          *
116          * <body itemscope itemtype="http://schema.org/Product">
117          *   <h1 itemprop="name">Shiny Trinket</h1>
118          *   <img itemprop="image" src="{image-url}" />
119          *   <p itemprop="description">Shiny trinkets are shiny.</p>
120          * </body>
121          * @endverbatim
122          */
123         public static function getSiteinfo($url, $no_guessing = false, $do_oembed = true, $count = 1)
124         {
125                 $a = get_app();
126
127                 $siteinfo = [];
128
129                 // Check if the URL does contain a scheme
130                 $scheme = parse_url($url, PHP_URL_SCHEME);
131
132                 if ($scheme == "") {
133                         $url = "http://".trim($url, "/");
134                 }
135
136                 if ($count > 10) {
137                         logger("parseurl_getsiteinfo: Endless loop detected for ".$url, LOGGER_DEBUG);
138                         return($siteinfo);
139                 }
140
141                 $url = trim($url, "'");
142                 $url = trim($url, '"');
143
144                 $url = Network::stripTrackingQueryParams($url);
145
146                 $siteinfo["url"] = $url;
147                 $siteinfo["type"] = "link";
148
149                 $data = Network::curl($url);
150                 if (!$data['success']) {
151                         return($siteinfo);
152                 }
153
154                 // If the file is too large then exit
155                 if ($data["info"]["download_content_length"] > 1000000) {
156                         return($siteinfo);
157                 }
158
159                 // If it isn't a HTML file then exit
160                 if (($data["info"]["content_type"] != "") && !strstr(strtolower($data["info"]["content_type"]), "html")) {
161                         return($siteinfo);
162                 }
163
164                 $header = $data["header"];
165                 $body = $data["body"];
166
167                 if ($do_oembed) {
168                         $oembed_data = OEmbed::fetchURL($url);
169
170                         if (!in_array($oembed_data->type, ["error", "rich", ""])) {
171                                 $siteinfo["type"] = $oembed_data->type;
172                         }
173
174                         if (($oembed_data->type == "link") && ($siteinfo["type"] != "photo")) {
175                                 if (isset($oembed_data->title)) {
176                                         $siteinfo["title"] = trim($oembed_data->title);
177                                 }
178                                 if (isset($oembed_data->description)) {
179                                         $siteinfo["text"] = trim($oembed_data->description);
180                                 }
181                                 if (isset($oembed_data->thumbnail_url)) {
182                                         $siteinfo["image"] = $oembed_data->thumbnail_url;
183                                 }
184                         }
185                 }
186
187                 // Fetch the first mentioned charset. Can be in body or header
188                 $charset = "";
189                 if (preg_match('/charset=(.*?)['."'".'"\s\n]/', $header, $matches)) {
190                         $charset = trim(trim(trim(array_pop($matches)), ';,'));
191                 }
192
193                 if ($charset == "") {
194                         $charset = "utf-8";
195                 }
196
197                 if (($charset != "") && (strtoupper($charset) != "UTF-8")) {
198                         logger("parseurl_getsiteinfo: detected charset ".$charset, LOGGER_DEBUG);
199                         //$body = mb_convert_encoding($body, "UTF-8", $charset);
200                         $body = iconv($charset, "UTF-8//TRANSLIT", $body);
201                 }
202
203                 $body = mb_convert_encoding($body, 'HTML-ENTITIES', "UTF-8");
204
205                 $doc = new DOMDocument();
206                 @$doc->loadHTML($body);
207
208                 XML::deleteNode($doc, "style");
209                 XML::deleteNode($doc, "script");
210                 XML::deleteNode($doc, "option");
211                 XML::deleteNode($doc, "h1");
212                 XML::deleteNode($doc, "h2");
213                 XML::deleteNode($doc, "h3");
214                 XML::deleteNode($doc, "h4");
215                 XML::deleteNode($doc, "h5");
216                 XML::deleteNode($doc, "h6");
217                 XML::deleteNode($doc, "ol");
218                 XML::deleteNode($doc, "ul");
219
220                 $xpath = new DOMXPath($doc);
221
222                 $list = $xpath->query("//meta[@content]");
223                 foreach ($list as $node) {
224                         $attr = [];
225                         if ($node->attributes->length) {
226                                 foreach ($node->attributes as $attribute) {
227                                         $attr[$attribute->name] = $attribute->value;
228                                 }
229                         }
230
231                         if (@$attr["http-equiv"] == "refresh") {
232                                 $path = $attr["content"];
233                                 $pathinfo = explode(";", $path);
234                                 $content = "";
235                                 foreach ($pathinfo as $value) {
236                                         if (substr(strtolower($value), 0, 4) == "url=") {
237                                                 $content = substr($value, 4);
238                                         }
239                                 }
240                                 if ($content != "") {
241                                         $siteinfo = self::getSiteinfo($content, $no_guessing, $do_oembed, ++$count);
242                                         return($siteinfo);
243                                 }
244                         }
245                 }
246
247                 $list = $xpath->query("//title");
248                 if ($list->length > 0) {
249                         $siteinfo["title"] = trim($list->item(0)->nodeValue);
250                 }
251
252                 //$list = $xpath->query("head/meta[@name]");
253                 $list = $xpath->query("//meta[@name]");
254                 foreach ($list as $node) {
255                         $attr = [];
256                         if ($node->attributes->length) {
257                                 foreach ($node->attributes as $attribute) {
258                                         $attr[$attribute->name] = $attribute->value;
259                                 }
260                         }
261
262                         $attr["content"] = trim(html_entity_decode($attr["content"], ENT_QUOTES, "UTF-8"));
263
264                         if ($attr["content"] != "") {
265                                 switch (strtolower($attr["name"])) {
266                                         case "fulltitle":
267                                                 $siteinfo["title"] = trim($attr["content"]);
268                                                 break;
269                                         case "description":
270                                                 $siteinfo["text"] = trim($attr["content"]);
271                                                 break;
272                                         case "thumbnail":
273                                                 $siteinfo["image"] = $attr["content"];
274                                                 break;
275                                         case "twitter:image":
276                                                 $siteinfo["image"] = $attr["content"];
277                                                 break;
278                                         case "twitter:image:src":
279                                                 $siteinfo["image"] = $attr["content"];
280                                                 break;
281                                         case "twitter:card":
282                                                 if (($siteinfo["type"] == "") || ($attr["content"] == "photo")) {
283                                                         $siteinfo["type"] = $attr["content"];
284                                                 }
285                                                 break;
286                                         case "twitter:description":
287                                                 $siteinfo["text"] = trim($attr["content"]);
288                                                 break;
289                                         case "twitter:title":
290                                                 $siteinfo["title"] = trim($attr["content"]);
291                                                 break;
292                                         case "dc.title":
293                                                 $siteinfo["title"] = trim($attr["content"]);
294                                                 break;
295                                         case "dc.description":
296                                                 $siteinfo["text"] = trim($attr["content"]);
297                                                 break;
298                                         case "keywords":
299                                                 $keywords = explode(",", $attr["content"]);
300                                                 break;
301                                         case "news_keywords":
302                                                 $keywords = explode(",", $attr["content"]);
303                                                 break;
304                                 }
305                         }
306                         if ($siteinfo["type"] == "summary") {
307                                 $siteinfo["type"] = "link";
308                         }
309                 }
310
311                 if (isset($keywords)) {
312                         $siteinfo["keywords"] = [];
313                         foreach ($keywords as $keyword) {
314                                 if (!in_array(trim($keyword), $siteinfo["keywords"])) {
315                                         $siteinfo["keywords"][] = trim($keyword);
316                                 }
317                         }
318                 }
319
320                 //$list = $xpath->query("head/meta[@property]");
321                 $list = $xpath->query("//meta[@property]");
322                 foreach ($list as $node) {
323                         $attr = [];
324                         if ($node->attributes->length) {
325                                 foreach ($node->attributes as $attribute) {
326                                         $attr[$attribute->name] = $attribute->value;
327                                 }
328                         }
329
330                         $attr["content"] = trim(html_entity_decode($attr["content"], ENT_QUOTES, "UTF-8"));
331
332                         if ($attr["content"] != "") {
333                                 switch (strtolower($attr["property"])) {
334                                         case "og:image":
335                                                 $siteinfo["image"] = $attr["content"];
336                                                 break;
337                                         case "og:title":
338                                                 $siteinfo["title"] = trim($attr["content"]);
339                                                 break;
340                                         case "og:description":
341                                                 $siteinfo["text"] = trim($attr["content"]);
342                                                 break;
343                                 }
344                         }
345                 }
346
347                 if ((@$siteinfo["image"] == "") && !$no_guessing) {
348                         $list = $xpath->query("//img[@src]");
349                         foreach ($list as $node) {
350                                 $attr = [];
351                                 if ($node->attributes->length) {
352                                         foreach ($node->attributes as $attribute) {
353                                                 $attr[$attribute->name] = $attribute->value;
354                                         }
355                                 }
356
357                                 $src = self::completeUrl($attr["src"], $url);
358                                 $photodata = Image::getInfoFromURL($src);
359
360                                 if (($photodata) && ($photodata[0] > 150) && ($photodata[1] > 150)) {
361                                         if ($photodata[0] > 300) {
362                                                 $photodata[1] = round($photodata[1] * (300 / $photodata[0]));
363                                                 $photodata[0] = 300;
364                                         }
365                                         if ($photodata[1] > 300) {
366                                                 $photodata[0] = round($photodata[0] * (300 / $photodata[1]));
367                                                 $photodata[1] = 300;
368                                         }
369                                         $siteinfo["images"][] = ["src" => $src,
370                                                                         "width" => $photodata[0],
371                                                                         "height" => $photodata[1]];
372                                 }
373                         }
374                 } elseif ($siteinfo["image"] != "") {
375                         $src = self::completeUrl($siteinfo["image"], $url);
376
377                         unset($siteinfo["image"]);
378
379                         $photodata = Image::getInfoFromURL($src);
380
381                         if (($photodata) && ($photodata[0] > 10) && ($photodata[1] > 10)) {
382                                 $siteinfo["images"][] = ["src" => $src,
383                                                                 "width" => $photodata[0],
384                                                                 "height" => $photodata[1]];
385                         }
386                 }
387
388                 if ((@$siteinfo["text"] == "") && (@$siteinfo["title"] != "") && !$no_guessing) {
389                         $text = "";
390
391                         $list = $xpath->query("//div[@class='article']");
392                         foreach ($list as $node) {
393                                 if (strlen($node->nodeValue) > 40) {
394                                         $text .= " ".trim($node->nodeValue);
395                                 }
396                         }
397
398                         if ($text == "") {
399                                 $list = $xpath->query("//div[@class='content']");
400                                 foreach ($list as $node) {
401                                         if (strlen($node->nodeValue) > 40) {
402                                                 $text .= " ".trim($node->nodeValue);
403                                         }
404                                 }
405                         }
406
407                         // If none text was found then take the paragraph content
408                         if ($text == "") {
409                                 $list = $xpath->query("//p");
410                                 foreach ($list as $node) {
411                                         if (strlen($node->nodeValue) > 40) {
412                                                 $text .= " ".trim($node->nodeValue);
413                                         }
414                                 }
415                         }
416
417                         if ($text != "") {
418                                 $text = trim(str_replace(["\n", "\r"], [" ", " "], $text));
419
420                                 while (strpos($text, "  ")) {
421                                         $text = trim(str_replace("  ", " ", $text));
422                                 }
423
424                                 $siteinfo["text"] = trim(html_entity_decode(substr($text, 0, 350), ENT_QUOTES, "UTF-8").'...');
425                         }
426                 }
427
428                 logger("parseurl_getsiteinfo: Siteinfo for ".$url." ".print_r($siteinfo, true), LOGGER_DEBUG);
429
430                 Addon::callHooks("getsiteinfo", $siteinfo);
431
432                 return($siteinfo);
433         }
434
435         /**
436          * @brief Convert tags from CSV to an array
437          *
438          * @param string $string Tags
439          * @return array with formatted Hashtags
440          */
441         public static function convertTagsToArray($string)
442         {
443                 $arr_tags = str_getcsv($string);
444                 if (count($arr_tags)) {
445                         // add the # sign to every tag
446                         array_walk($arr_tags, ["self", "arrAddHashes"]);
447
448                         return $arr_tags;
449                 }
450         }
451
452         /**
453          * @brief Add a hasht sign to a string
454          *
455          *  This method is used as callback function
456          *
457          * @param string $tag The pure tag name
458          * @param int    $k   Counter for internal use
459          * @return void
460          */
461         private static function arrAddHashes(&$tag, $k)
462         {
463                 $tag = "#" . $tag;
464         }
465
466         /**
467          * @brief Add a scheme to an url
468          *
469          * The src attribute of some html elements (e.g. images)
470          * can miss the scheme so we need to add the correct
471          * scheme
472          *
473          * @param string $url    The url which possibly does have
474          *                       a missing scheme (a link to an image)
475          * @param string $scheme The url with a correct scheme
476          *                       (e.g. the url from the webpage which does contain the image)
477          *
478          * @return string The url with a scheme
479          */
480         private static function completeUrl($url, $scheme)
481         {
482                 $urlarr = parse_url($url);
483
484                 // If the url does allready have an scheme
485                 // we can stop the process here
486                 if (isset($urlarr["scheme"])) {
487                         return($url);
488                 }
489
490                 $schemearr = parse_url($scheme);
491
492                 $complete = $schemearr["scheme"]."://".$schemearr["host"];
493
494                 if (@$schemearr["port"] != "") {
495                         $complete .= ":".$schemearr["port"];
496                 }
497
498                 if (strpos($urlarr["path"], "/") !== 0) {
499                         $complete .= "/";
500                 }
501
502                 $complete .= $urlarr["path"];
503
504                 if (@$urlarr["query"] != "") {
505                         $complete .= "?".$urlarr["query"];
506                 }
507
508                 if (@$urlarr["fragment"] != "") {
509                         $complete .= "#".$urlarr["fragment"];
510                 }
511
512                 return($complete);
513         }
514 }