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