]> git.mxchange.org Git - friendica.git/blob - include/Probe.php
Merge branch 'develop' into improvement/move-app-to-src-2
[friendica.git] / include / Probe.php
1 <?php
2 /**
3  * @file include/Probe.php
4  * @brief Functions for probing URL
5  *
6  */
7
8 use Friendica\App;
9 use Friendica\Core\Config;
10 use Friendica\Core\PConfig;
11
12 require_once("include/feed.php");
13 require_once('include/email.php');
14 require_once('include/network.php');
15
16 /**
17  * @brief This class contain functions for probing URL
18  *
19  */
20 class Probe {
21
22         private static $baseurl;
23
24         /**
25          * @brief Rearrange the array so that it always has the same order
26          *
27          * @param array $data Unordered data
28          *
29          * @return array Ordered data
30          */
31         private function rearrange_data($data) {
32                 $fields = array("name", "nick", "guid", "url", "addr", "alias",
33                                 "photo", "community", "keywords", "location", "about",
34                                 "batch", "notify", "poll", "request", "confirm", "poco",
35                                 "priority", "network", "pubkey", "baseurl");
36
37                 $newdata = array();
38                 foreach ($fields AS $field)
39                         if (isset($data[$field]))
40                                 $newdata[$field] = $data[$field];
41                         else
42                                 $newdata[$field] = "";
43
44                 // We don't use the "priority" field anymore and replace it with a dummy.
45                 $newdata["priority"] = 0;
46
47                 return $newdata;
48         }
49
50         /**
51          * @brief Probes for XRD data
52          *
53          * @return array
54          *      'lrdd' => Link to LRDD endpoint
55          *      'lrdd-xml' => Link to LRDD endpoint in XML format
56          *      'lrdd-json' => Link to LRDD endpoint in JSON format
57          */
58         private function xrd($host) {
59
60                 // Reset the static variable
61                 self::$baseurl = '';
62
63                 $ssl_url = "https://".$host."/.well-known/host-meta";
64                 $url = "http://".$host."/.well-known/host-meta";
65
66                 $xrd_timeout = Config::get('system','xrd_timeout', 20);
67                 $redirects = 0;
68
69                 $ret = z_fetch_url($ssl_url, false, $redirects, array('timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml'));
70                 if ($ret['errno'] == CURLE_OPERATION_TIMEDOUT) {
71                         return false;
72                 }
73                 $xml = $ret['body'];
74
75                 $xrd = parse_xml_string($xml, false);
76
77                 if (!is_object($xrd)) {
78                         $ret = z_fetch_url($url, false, $redirects, array('timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml'));
79                         if ($ret['errno'] == CURLE_OPERATION_TIMEDOUT) {
80                                 return false;
81                         }
82                         $xml = $ret['body'];
83                         $xrd = parse_xml_string($xml, false);
84                 }
85                 if (!is_object($xrd))
86                         return false;
87
88                 $links = xml::element_to_array($xrd);
89                 if (!isset($links["xrd"]["link"]))
90                         return false;
91
92                 $xrd_data = array();
93
94                 foreach ($links["xrd"]["link"] AS $value => $link) {
95                         if (isset($link["@attributes"]))
96                                 $attributes = $link["@attributes"];
97                         elseif ($value == "@attributes")
98                                 $attributes = $link;
99                         else
100                                 continue;
101
102                         if (($attributes["rel"] == "lrdd") AND
103                                 ($attributes["type"] == "application/xrd+xml"))
104                                 $xrd_data["lrdd-xml"] = $attributes["template"];
105                         elseif (($attributes["rel"] == "lrdd") AND
106                                 ($attributes["type"] == "application/json"))
107                                 $xrd_data["lrdd-json"] = $attributes["template"];
108                         elseif ($attributes["rel"] == "lrdd")
109                                 $xrd_data["lrdd"] = $attributes["template"];
110                 }
111
112                 self::$baseurl = "http://".$host;
113
114                 return $xrd_data;
115         }
116
117         /**
118          * @brief Perform Webfinger lookup and return DFRN data
119          *
120          * Given an email style address, perform webfinger lookup and
121          * return the resulting DFRN profile URL, or if no DFRN profile URL
122          * is located, returns an OStatus subscription template (prefixed
123          * with the string 'stat:' to identify it as on OStatus template).
124          * If this isn't an email style address just return $webbie.
125          * Return an empty string if email-style addresses but webfinger fails,
126          * or if the resultant personal XRD doesn't contain a supported
127          * subscription/friend-request attribute.
128          *
129          * amended 7/9/2011 to return an hcard which could save potentially loading
130          * a lengthy content page to scrape dfrn attributes
131          *
132          * @param string $webbie Address that should be probed
133          * @param string $hcard Link to the hcard - is returned by reference
134          *
135          * @return string profile link
136          */
137
138         public static function webfinger_dfrn($webbie, &$hcard) {
139
140                 $profile_link = '';
141
142                 $links = self::lrdd($webbie);
143                 logger('webfinger_dfrn: '.$webbie.':'.print_r($links,true), LOGGER_DATA);
144                 if (count($links)) {
145                         foreach ($links as $link) {
146                                 if ($link['@attributes']['rel'] === NAMESPACE_DFRN)
147                                         $profile_link = $link['@attributes']['href'];
148                                 if (($link['@attributes']['rel'] === NAMESPACE_OSTATUSSUB) AND ($profile_link == ""))
149                                         $profile_link = 'stat:'.$link['@attributes']['template'];
150                                 if ($link['@attributes']['rel'] === 'http://microformats.org/profile/hcard')
151                                         $hcard = $link['@attributes']['href'];
152                         }
153                 }
154                 return $profile_link;
155         }
156
157         /**
158          * @brief Check an URI for LRDD data
159          *
160          * this is a replacement for the "lrdd" function in include/network.php.
161          * It isn't used in this class and has some redundancies in the code.
162          * When time comes we can check the existing calls for "lrdd" if we can rework them.
163          *
164          * @param string $uri Address that should be probed
165          *
166          * @return array uri data
167          */
168         public static function lrdd($uri) {
169
170                 $lrdd = self::xrd($uri);
171
172                 if (!$lrdd) {
173                         $parts = @parse_url($uri);
174                         if (!$parts)
175                                 return array();
176
177                         $host = $parts["host"];
178                         if (isset($parts["port"])) {
179                                 $host .= ':'.$parts["port"];
180                         }
181
182                         $path_parts = explode("/", trim($parts["path"], "/"));
183
184                         $nick = array_pop($path_parts);
185
186                         do {
187                                 $lrdd = self::xrd($host);
188                                 $host .= "/".array_shift($path_parts);
189                         } while (!$lrdd AND (sizeof($path_parts) > 0));
190                 }
191
192                 if (!$lrdd)
193                         return array();
194
195                 foreach ($lrdd AS $key => $link) {
196                         if ($webfinger)
197                                 continue;
198
199                         if (!in_array($key, array("lrdd", "lrdd-xml", "lrdd-json")))
200                                 continue;
201
202                         $path = str_replace('{uri}', urlencode($uri), $link);
203                         $webfinger = self::webfinger($path);
204
205                         if (!$webfinger AND (strstr($uri, "@"))) {
206                                 $path = str_replace('{uri}', urlencode("acct:".$uri), $link);
207                                 $webfinger = self::webfinger($path);
208                         }
209
210                         // Special treatment for Mastodon
211                         // Problem is that Mastodon uses an URL format like http://domain.tld/@nick
212                         // But the webfinger for this format fails.
213                         if (!$webfinger AND isset($nick)) {
214                                 // Mastodon uses a "@" as prefix for usernames in their url format
215                                 $nick = ltrim($nick, '@');
216
217                                 $addr = $nick."@".$host;
218
219                                 $path = str_replace('{uri}', urlencode("acct:".$addr), $link);
220                                 $webfinger = self::webfinger($path);
221                         }
222                 }
223
224                 if (!is_array($webfinger["links"]))
225                         return false;
226
227                 $data = array();
228
229                 foreach ($webfinger["links"] AS $link)
230                         $data[] = array("@attributes" => $link);
231
232                 if (is_array($webfinger["aliases"]))
233                         foreach ($webfinger["aliases"] AS $alias)
234                                 $data[] = array("@attributes" =>
235                                                         array("rel" => "alias",
236                                                                 "href" => $alias));
237
238                 return $data;
239         }
240
241         /**
242          * @brief Fetch information (protocol endpoints and user information) about a given uri
243          *
244          * @param string $uri Address that should be probed
245          * @param string $network Test for this specific network
246          * @param integer $uid User ID for the probe (only used for mails)
247          * @param boolean $cache Use cached values?
248          *
249          * @return array uri data
250          */
251         public static function uri($uri, $network = "", $uid = 0, $cache = true) {
252
253                 if ($cache) {
254                         $result = Cache::get("probe_url:".$network.":".$uri);
255                         if (!is_null($result)) {
256                                 return $result;
257                         }
258                 }
259
260                 if ($uid == 0)
261                         $uid = local_user();
262
263                 $data = self::detect($uri, $network, $uid);
264
265                 if (!isset($data["url"]))
266                         $data["url"] = $uri;
267
268                 if ($data["photo"] != "")
269                         $data["baseurl"] = matching_url(normalise_link($data["baseurl"]), normalise_link($data["photo"]));
270                 else
271                         $data["photo"] = App::get_baseurl().'/images/person-175.jpg';
272
273                 if (!isset($data["name"]) OR ($data["name"] == "")) {
274                         if (isset($data["nick"]))
275                                 $data["name"] = $data["nick"];
276
277                         if ($data["name"] == "")
278                                 $data["name"] = $data["url"];
279                 }
280
281                 if (!isset($data["nick"]) OR ($data["nick"] == "")) {
282                         $data["nick"] = strtolower($data["name"]);
283
284                         if (strpos($data['nick'], ' '))
285                                 $data['nick'] = trim(substr($data['nick'], 0, strpos($data['nick'], ' ')));
286                 }
287
288                 if (self::$baseurl != "") {
289                         $data["baseurl"] = self::$baseurl;
290                 }
291
292                 if (!isset($data["network"])) {
293                         $data["network"] = NETWORK_PHANTOM;
294                 }
295
296                 $data = self::rearrange_data($data);
297
298                 // Only store into the cache if the value seems to be valid
299                 if (!in_array($data['network'], array(NETWORK_PHANTOM, NETWORK_MAIL))) {
300                         Cache::set("probe_url:".$network.":".$uri, $data, CACHE_DAY);
301
302                         /// @todo temporary fix - we need a real contact update function that updates only changing fields
303                         /// The biggest problem is the avatar picture that could have a reduced image size.
304                         /// It should only be updated if the existing picture isn't existing anymore.
305                         if (($data['network'] != NETWORK_FEED) AND ($mode == PROBE_NORMAL) AND
306                                 $data["name"] AND $data["nick"] AND $data["url"] AND $data["addr"] AND $data["poll"])
307                                 q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `url` = '%s', `addr` = '%s',
308                                                 `notify` = '%s', `poll` = '%s', `alias` = '%s', `success_update` = '%s'
309                                         WHERE `nurl` = '%s' AND NOT `self` AND `uid` = 0",
310                                         dbesc($data["name"]),
311                                         dbesc($data["nick"]),
312                                         dbesc($data["url"]),
313                                         dbesc($data["addr"]),
314                                         dbesc($data["notify"]),
315                                         dbesc($data["poll"]),
316                                         dbesc($data["alias"]),
317                                         dbesc(datetime_convert()),
318                                         dbesc(normalise_link($data['url']))
319                         );
320                 }
321
322                 return $data;
323         }
324
325         /**
326          * @brief Fetch information (protocol endpoints and user information) about a given uri
327          *
328          * This function is only called by the "uri" function that adds caching and rearranging of data.
329          *
330          * @param string $uri Address that should be probed
331          * @param string $network Test for this specific network
332          * @param integer $uid User ID for the probe (only used for mails)
333          *
334          * @return array uri data
335          */
336         private function detect($uri, $network, $uid) {
337                 $parts = parse_url($uri);
338
339                 if (isset($parts["scheme"]) AND isset($parts["host"]) AND isset($parts["path"])) {
340
341                         $host = $parts["host"];
342                         if (isset($parts["port"])) {
343                                 $host .= ':'.$parts["port"];
344                         }
345
346                         if ($host == 'twitter.com') {
347                                 return array("network" => NETWORK_TWITTER);
348                         }
349                         $lrdd = self::xrd($host);
350
351                         $path_parts = explode("/", trim($parts["path"], "/"));
352
353                         while (!$lrdd AND (sizeof($path_parts) > 1)) {
354                                 $host .= "/".array_shift($path_parts);
355                                 $lrdd = self::xrd($host);
356                         }
357                         if (!$lrdd) {
358                                 return self::feed($uri);
359                         }
360                         $nick = array_pop($path_parts);
361
362                         // Mastodon uses a "@" as prefix for usernames in their url format
363                         $nick = ltrim($nick, '@');
364
365                         $addr = $nick."@".$host;
366                 } elseif (strstr($uri, '@')) {
367                         // If the URI starts with "mailto:" then jump directly to the mail detection
368                         if (strpos($url,'mailto:') !== false) {
369                                 $uri = str_replace('mailto:', '', $url);
370                                 return self::mail($uri, $uid);
371                         }
372
373                         if ($network == NETWORK_MAIL) {
374                                 return self::mail($uri, $uid);
375                         }
376                         // Remove "acct:" from the URI
377                         $uri = str_replace('acct:', '', $uri);
378
379                         $host = substr($uri,strpos($uri, '@') + 1);
380                         $nick = substr($uri,0, strpos($uri, '@'));
381
382                         if (strpos($uri, '@twitter.com')) {
383                                 return array("network" => NETWORK_TWITTER);
384                         }
385                         $lrdd = self::xrd($host);
386
387                         if (!$lrdd) {
388                                 return self::mail($uri, $uid);
389                         }
390                         $addr = $uri;
391                 } else {
392                         return false;
393                 }
394
395                 $webfinger = false;
396
397                 /// @todo Do we need the prefix "acct:" or "acct://"?
398
399                 foreach ($lrdd AS $key => $link) {
400                         if ($webfinger) {
401                                 continue;
402                         }
403                         if (!in_array($key, array("lrdd", "lrdd-xml", "lrdd-json"))) {
404                                 continue;
405                         }
406                         // At first try it with the given uri
407                         $path = str_replace('{uri}', urlencode($uri), $link);
408                         $webfinger = self::webfinger($path);
409
410                         // We cannot be sure that the detected address was correct, so we don't use the values
411                         if ($webfinger AND ($uri != $addr)) {
412                                 $nick = "";
413                                 $addr = "";
414                         }
415
416                         // Try webfinger with the address (user@domain.tld)
417                         if (!$webfinger) {
418                                 $path = str_replace('{uri}', urlencode($addr), $link);
419                                 $webfinger = self::webfinger($path);
420                         }
421
422                         // Mastodon needs to have it with "acct:"
423                         if (!$webfinger) {
424                                 $path = str_replace('{uri}', urlencode("acct:".$addr), $link);
425                                 $webfinger = self::webfinger($path);
426                         }
427                 }
428                 if (!$webfinger) {
429                         return self::feed($uri);
430                 }
431
432                 $result = false;
433
434                 logger("Probing ".$uri, LOGGER_DEBUG);
435
436                 if (in_array($network, array("", NETWORK_DFRN)))
437                         $result = self::dfrn($webfinger);
438                 if ((!$result AND ($network == "")) OR ($network == NETWORK_DIASPORA))
439                         $result = self::diaspora($webfinger);
440                 if ((!$result AND ($network == "")) OR ($network == NETWORK_OSTATUS))
441                         $result = self::ostatus($webfinger);
442                 if ((!$result AND ($network == "")) OR ($network == NETWORK_PUMPIO))
443                         $result = self::pumpio($webfinger);
444                 if ((!$result AND ($network == "")) OR ($network == NETWORK_FEED))
445                         $result = self::feed($uri);
446                 else {
447                         // We overwrite the detected nick with our try if the previois routines hadn't detected it.
448                         // Additionally it is overwritten when the nickname doesn't make sense (contains spaces).
449                         if ((!isset($result["nick"]) OR ($result["nick"] == "") OR (strstr($result["nick"], " "))) AND ($nick != ""))
450                                 $result["nick"] = $nick;
451
452                         if ((!isset($result["addr"]) OR ($result["addr"] == "")) AND ($addr != ""))
453                                 $result["addr"] = $addr;
454                 }
455
456                 logger($uri." is ".$result["network"], LOGGER_DEBUG);
457
458                 if (!isset($result["baseurl"]) OR ($result["baseurl"] == "")) {
459                         $pos = strpos($result["url"], $host);
460                         if ($pos)
461                                 $result["baseurl"] = substr($result["url"], 0, $pos).$host;
462                 }
463
464                 return $result;
465         }
466
467         /**
468          * @brief Perform a webfinger request.
469          *
470          * For details see RFC 7033: <https://tools.ietf.org/html/rfc7033>
471          *
472          * @param string $url Address that should be probed
473          *
474          * @return array webfinger data
475          */
476         private function webfinger($url) {
477
478                 $xrd_timeout = Config::get('system','xrd_timeout', 20);
479                 $redirects = 0;
480
481                 $ret = z_fetch_url($url, false, $redirects, array('timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml'));
482                 if ($ret['errno'] == CURLE_OPERATION_TIMEDOUT) {
483                         return false;
484                 }
485                 $data = $ret['body'];
486
487                 $xrd = parse_xml_string($data, false);
488
489                 if (!is_object($xrd)) {
490                         // If it is not XML, maybe it is JSON
491                         $webfinger = json_decode($data, true);
492
493                         if (!isset($webfinger["links"]))
494                                 return false;
495
496                         return $webfinger;
497                 }
498
499                 $xrd_arr = xml::element_to_array($xrd);
500                 if (!isset($xrd_arr["xrd"]["link"]))
501                         return false;
502
503                 $webfinger = array();
504
505                 if (isset($xrd_arr["xrd"]["subject"]))
506                         $webfinger["subject"] = $xrd_arr["xrd"]["subject"];
507
508                 if (isset($xrd_arr["xrd"]["alias"]))
509                         $webfinger["aliases"] = $xrd_arr["xrd"]["alias"];
510
511                 $webfinger["links"] = array();
512
513                 foreach ($xrd_arr["xrd"]["link"] AS $value => $data) {
514                         if (isset($data["@attributes"]))
515                                 $attributes = $data["@attributes"];
516                         elseif ($value == "@attributes")
517                                 $attributes = $data;
518                         else
519                                 continue;
520
521                         $webfinger["links"][] = $attributes;
522                 }
523                 return $webfinger;
524         }
525
526         /**
527          * @brief Poll the Friendica specific noscrape page.
528          *
529          * "noscrape" is a faster alternative to fetch the data from the hcard.
530          * This functionality was originally created for the directory.
531          *
532          * @param string $noscrape Link to the noscrape page
533          * @param array $data The already fetched data
534          *
535          * @return array noscrape data
536          */
537         private function poll_noscrape($noscrape, $data) {
538                 $ret = z_fetch_url($noscrape);
539                 if ($ret['errno'] == CURLE_OPERATION_TIMEDOUT) {
540                         return false;
541                 }
542                 $content = $ret['body'];
543                 if (!$content) {
544                         return false;
545                 }
546
547                 $json = json_decode($content, true);
548                 if (!is_array($json))
549                         return false;
550
551                 if (isset($json["fn"]))
552                         $data["name"] = $json["fn"];
553
554                 if (isset($json["addr"]))
555                         $data["addr"] = $json["addr"];
556
557                 if (isset($json["nick"]))
558                         $data["nick"] = $json["nick"];
559
560                 if (isset($json["comm"]))
561                         $data["community"] = $json["comm"];
562
563                 if (isset($json["tags"])) {
564                         $keywords = implode(" ", $json["tags"]);
565                         if ($keywords != "")
566                                 $data["keywords"] = $keywords;
567                 }
568
569                 $location = formatted_location($json);
570                 if ($location)
571                         $data["location"] = $location;
572
573                 if (isset($json["about"]))
574                         $data["about"] = $json["about"];
575
576                 if (isset($json["key"]))
577                         $data["pubkey"] = $json["key"];
578
579                 if (isset($json["photo"]))
580                         $data["photo"] = $json["photo"];
581
582                 if (isset($json["dfrn-request"]))
583                         $data["request"] = $json["dfrn-request"];
584
585                 if (isset($json["dfrn-confirm"]))
586                         $data["confirm"] = $json["dfrn-confirm"];
587
588                 if (isset($json["dfrn-notify"]))
589                         $data["notify"] = $json["dfrn-notify"];
590
591                 if (isset($json["dfrn-poll"]))
592                         $data["poll"] = $json["dfrn-poll"];
593
594                 return $data;
595         }
596
597         /**
598          * @brief Check for valid DFRN data
599          *
600          * @param array $data DFRN data
601          *
602          * @return int Number of errors
603          */
604         public static function valid_dfrn($data) {
605                 $errors = 0;
606                 if(!isset($data['key']))
607                         $errors ++;
608                 if(!isset($data['dfrn-request']))
609                         $errors ++;
610                 if(!isset($data['dfrn-confirm']))
611                         $errors ++;
612                 if(!isset($data['dfrn-notify']))
613                         $errors ++;
614                 if(!isset($data['dfrn-poll']))
615                         $errors ++;
616                 return $errors;
617         }
618
619         /**
620          * @brief Fetch data from a DFRN profile page and via "noscrape"
621          *
622          * @param string $profile Link to the profile page
623          *
624          * @return array profile data
625          */
626         public static function profile($profile) {
627
628                 $data = array();
629
630                 logger("Check profile ".$profile, LOGGER_DEBUG);
631
632                 // Fetch data via noscrape - this is faster
633                 $noscrape = str_replace(array("/hcard/", "/profile/"), "/noscrape/", $profile);
634                 $data = self::poll_noscrape($noscrape, $data);
635
636                 if (!isset($data["notify"]) OR !isset($data["confirm"]) OR
637                         !isset($data["request"]) OR !isset($data["poll"]) OR
638                         !isset($data["poco"]) OR !isset($data["name"]) OR
639                         !isset($data["photo"]))
640                         $data = self::poll_hcard($profile, $data, true);
641
642                 $prof_data = array();
643                 $prof_data["addr"] = $data["addr"];
644                 $prof_data["nick"] = $data["nick"];
645                 $prof_data["dfrn-request"] = $data["request"];
646                 $prof_data["dfrn-confirm"] = $data["confirm"];
647                 $prof_data["dfrn-notify"] = $data["notify"];
648                 $prof_data["dfrn-poll"] = $data["poll"];
649                 $prof_data["dfrn-poco"] = $data["poco"];
650                 $prof_data["photo"] = $data["photo"];
651                 $prof_data["fn"] = $data["name"];
652                 $prof_data["key"] = $data["pubkey"];
653
654                 logger("Result for profile ".$profile.": ".print_r($prof_data, true), LOGGER_DEBUG);
655
656                 return $prof_data;
657         }
658
659         /**
660          * @brief Check for DFRN contact
661          *
662          * @param array $webfinger Webfinger data
663          *
664          * @return array DFRN data
665          */
666         private function dfrn($webfinger) {
667
668                 $hcard = "";
669                 $data = array();
670                 foreach ($webfinger["links"] AS $link) {
671                         if (($link["rel"] == NAMESPACE_DFRN) AND ($link["href"] != ""))
672                                 $data["network"] = NETWORK_DFRN;
673                         elseif (($link["rel"] == NAMESPACE_FEED) AND ($link["href"] != ""))
674                                 $data["poll"] = $link["href"];
675                         elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") AND
676                                 ($link["type"] == "text/html") AND ($link["href"] != ""))
677                                 $data["url"] = $link["href"];
678                         elseif (($link["rel"] == "http://microformats.org/profile/hcard") AND ($link["href"] != ""))
679                                 $hcard = $link["href"];
680                         elseif (($link["rel"] == NAMESPACE_POCO) AND ($link["href"] != ""))
681                                 $data["poco"] = $link["href"];
682                         elseif (($link["rel"] == "http://webfinger.net/rel/avatar") AND ($link["href"] != ""))
683                                 $data["photo"] = $link["href"];
684
685                         elseif (($link["rel"] == "http://joindiaspora.com/seed_location") AND ($link["href"] != ""))
686                                 $data["baseurl"] = trim($link["href"], '/');
687                         elseif (($link["rel"] == "http://joindiaspora.com/guid") AND ($link["href"] != ""))
688                                 $data["guid"] = $link["href"];
689                         elseif (($link["rel"] == "diaspora-public-key") AND ($link["href"] != "")) {
690                                 $data["pubkey"] = base64_decode($link["href"]);
691
692                                 //if (strstr($data["pubkey"], 'RSA ') OR ($link["type"] == "RSA"))
693                                 if (strstr($data["pubkey"], 'RSA '))
694                                         $data["pubkey"] = rsatopem($data["pubkey"]);
695                         }
696                 }
697
698                 if (!isset($data["network"]) OR ($hcard == ""))
699                         return false;
700
701                 // Fetch data via noscrape - this is faster
702                 $noscrape = str_replace("/hcard/", "/noscrape/", $hcard);
703                 $data = self::poll_noscrape($noscrape, $data);
704
705                 if (isset($data["notify"]) AND isset($data["confirm"]) AND isset($data["request"]) AND
706                         isset($data["poll"]) AND isset($data["name"]) AND isset($data["photo"]))
707                         return $data;
708
709                 $data = self::poll_hcard($hcard, $data, true);
710
711                 return $data;
712         }
713
714         /**
715          * @brief Poll the hcard page (Diaspora and Friendica specific)
716          *
717          * @param string $hcard Link to the hcard page
718          * @param array $data The already fetched data
719          * @param boolean $dfrn Poll DFRN specific data
720          *
721          * @return array hcard data
722          */
723         private function poll_hcard($hcard, $data, $dfrn = false) {
724                 $ret = z_fetch_url($hcard);
725                 if ($ret['errno'] == CURLE_OPERATION_TIMEDOUT) {
726                         return false;
727                 }
728                 $content = $ret['body'];
729                 if (!$content) {
730                         return false;
731                 }
732
733                 $doc = new DOMDocument();
734                 if (!@$doc->loadHTML($content))
735                         return false;
736
737                 $xpath = new DomXPath($doc);
738
739                 $vcards = $xpath->query("//div[contains(concat(' ', @class, ' '), ' vcard ')]");
740                 if (!is_object($vcards))
741                         return false;
742
743                 if ($vcards->length > 0) {
744                         $vcard = $vcards->item(0);
745
746                         // We have to discard the guid from the hcard in favour of the guid from lrdd
747                         // Reason: Hubzilla doesn't use the value "uid" in the hcard like Diaspora does.
748                         $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' uid ')]", $vcard); // */
749                         if (($search->length > 0) AND ($data["guid"] == ""))
750                                 $data["guid"] = $search->item(0)->nodeValue;
751
752                         $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' nickname ')]", $vcard); // */
753                         if ($search->length > 0)
754                                 $data["nick"] = $search->item(0)->nodeValue;
755
756                         $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' fn ')]", $vcard); // */
757                         if ($search->length > 0)
758                                 $data["name"] = $search->item(0)->nodeValue;
759
760                         $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' searchable ')]", $vcard); // */
761                         if ($search->length > 0)
762                                 $data["searchable"] = $search->item(0)->nodeValue;
763
764                         $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' key ')]", $vcard); // */
765                         if ($search->length > 0) {
766                                 $data["pubkey"] = $search->item(0)->nodeValue;
767                                 if (strstr($data["pubkey"], 'RSA '))
768                                         $data["pubkey"] = rsatopem($data["pubkey"]);
769                         }
770
771                         $search = $xpath->query("//*[@id='pod_location']", $vcard); // */
772                         if ($search->length > 0)
773                                 $data["baseurl"] = trim($search->item(0)->nodeValue, "/");
774                 }
775
776                 $avatar = array();
777                 $photos = $xpath->query("//*[contains(concat(' ', @class, ' '), ' photo ') or contains(concat(' ', @class, ' '), ' avatar ')]", $vcard); // */
778                 foreach ($photos AS $photo) {
779                         $attr = array();
780                         foreach ($photo->attributes as $attribute) {
781                                 $attr[$attribute->name] = trim($attribute->value);
782                         }
783
784                         if (isset($attr["src"]) AND isset($attr["width"])) {
785                                 $avatar[$attr["width"]] = $attr["src"];
786                         }
787
788                         // We don't have a width. So we just take everything that we got.
789                         // This is a Hubzilla workaround which doesn't send a width.
790                         if ((sizeof($avatar) == 0) AND isset($attr["src"])) {
791                                 $avatar[] = $attr["src"];
792                         }
793                 }
794
795                 if (sizeof($avatar)) {
796                         ksort($avatar);
797                         $data["photo"] = self::fix_avatar(array_pop($avatar), $data["baseurl"]);
798                 }
799
800                 if ($dfrn) {
801                         // Poll DFRN specific data
802                         $search = $xpath->query("//link[contains(concat(' ', @rel), ' dfrn-')]");
803                         if ($search->length > 0) {
804                                 foreach ($search AS $link) {
805                                         //$data["request"] = $search->item(0)->nodeValue;
806                                         $attr = array();
807                                         foreach ($link->attributes as $attribute)
808                                                 $attr[$attribute->name] = trim($attribute->value);
809
810                                         $data[substr($attr["rel"], 5)] = $attr["href"];
811                                 }
812                         }
813
814                         // Older Friendica versions had used the "uid" field differently than newer versions
815                         if ($data["nick"] == $data["guid"])
816                                 unset($data["guid"]);
817                 }
818
819
820                 return $data;
821         }
822
823         /**
824          * @brief Check for Diaspora contact
825          *
826          * @param array $webfinger Webfinger data
827          *
828          * @return array Diaspora data
829          */
830         private function diaspora($webfinger) {
831
832                 $hcard = "";
833                 $data = array();
834                 foreach ($webfinger["links"] AS $link) {
835                         if (($link["rel"] == "http://microformats.org/profile/hcard") AND ($link["href"] != ""))
836                                 $hcard = $link["href"];
837                         elseif (($link["rel"] == "http://joindiaspora.com/seed_location") AND ($link["href"] != ""))
838                                 $data["baseurl"] = trim($link["href"], '/');
839                         elseif (($link["rel"] == "http://joindiaspora.com/guid") AND ($link["href"] != ""))
840                                 $data["guid"] = $link["href"];
841                         elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") AND
842                                 ($link["type"] == "text/html") AND ($link["href"] != ""))
843                                 $data["url"] = $link["href"];
844                         elseif (($link["rel"] == NAMESPACE_FEED) AND ($link["href"] != ""))
845                                 $data["poll"] = $link["href"];
846                         elseif (($link["rel"] == NAMESPACE_POCO) AND ($link["href"] != ""))
847                                 $data["poco"] = $link["href"];
848                         elseif (($link["rel"] == "salmon") AND ($link["href"] != ""))
849                                 $data["notify"] = $link["href"];
850                         elseif (($link["rel"] == "diaspora-public-key") AND ($link["href"] != "")) {
851                                 $data["pubkey"] = base64_decode($link["href"]);
852
853                                 //if (strstr($data["pubkey"], 'RSA ') OR ($link["type"] == "RSA"))
854                                 if (strstr($data["pubkey"], 'RSA '))
855                                         $data["pubkey"] = rsatopem($data["pubkey"]);
856                         }
857                 }
858
859                 if (!isset($data["url"]) OR ($hcard == ""))
860                         return false;
861
862                 if (is_array($webfinger["aliases"]))
863                         foreach ($webfinger["aliases"] AS $alias)
864                                 if (normalise_link($alias) != normalise_link($data["url"]) AND !strstr($alias, "@"))
865                                         $data["alias"] = $alias;
866
867                 // Fetch further information from the hcard
868                 $data = self::poll_hcard($hcard, $data);
869
870                 if (!$data)
871                         return false;
872
873                 if (isset($data["url"]) AND isset($data["guid"]) AND isset($data["baseurl"]) AND
874                         isset($data["pubkey"]) AND ($hcard != "")) {
875                         $data["network"] = NETWORK_DIASPORA;
876
877                         // The Diaspora handle must always be lowercase
878                         $data["addr"] = strtolower($data["addr"]);
879
880                         // We have to overwrite the detected value for "notify" since Hubzilla doesn't send it
881                         $data["notify"] = $data["baseurl"]."/receive/users/".$data["guid"];
882                         $data["batch"] = $data["baseurl"]."/receive/public";
883                 } else
884                         return false;
885
886                 return $data;
887         }
888
889         /**
890          * @brief Check for OStatus contact
891          *
892          * @param array $webfinger Webfinger data
893          *
894          * @return array OStatus data
895          */
896         private function ostatus($webfinger) {
897                 $data = array();
898                 if (is_array($webfinger["aliases"])) {
899                         foreach ($webfinger["aliases"] AS $alias) {
900                                 if (strstr($alias, "@")) {
901                                         $data["addr"] = str_replace('acct:', '', $alias);
902                                 }
903                         }
904                 }
905
906                 if (is_string($webfinger["subject"]) AND strstr($webfinger["subject"], "@")) {
907                         $data["addr"] = str_replace('acct:', '', $webfinger["subject"]);
908                 }
909                 $pubkey = "";
910                 foreach ($webfinger["links"] AS $link) {
911                         if (($link["rel"] == "http://webfinger.net/rel/profile-page") AND
912                                 ($link["type"] == "text/html") AND ($link["href"] != "")) {
913                                 $data["url"] = $link["href"];
914                         } elseif (($link["rel"] == "salmon") AND ($link["href"] != "")) {
915                                 $data["notify"] = $link["href"];
916                         } elseif (($link["rel"] == NAMESPACE_FEED) AND ($link["href"] != "")) {
917                                 $data["poll"] = $link["href"];
918                         } elseif (($link["rel"] == "magic-public-key") AND ($link["href"] != "")) {
919                                 $pubkey = $link["href"];
920
921                                 if (substr($pubkey, 0, 5) === 'data:') {
922                                         if (strstr($pubkey, ',')) {
923                                                 $pubkey = substr($pubkey, strpos($pubkey, ',') + 1);
924                                         } else {
925                                                 $pubkey = substr($pubkey, 5);
926                                         }
927                                 } elseif (normalise_link($pubkey) == 'http://') {
928                                         $ret = z_fetch_url($pubkey);
929                                         if ($ret['errno'] == CURLE_OPERATION_TIMEDOUT) {
930                                                 return false;
931                                         }
932                                         $pubkey = $ret['body'];
933                                 }
934
935                                 $key = explode(".", $pubkey);
936
937                                 if (sizeof($key) >= 3) {
938                                         $m = base64url_decode($key[1]);
939                                         $e = base64url_decode($key[2]);
940                                         $data["pubkey"] = metopem($m,$e);
941                                 }
942                         }
943                 }
944
945                 if (isset($data["notify"]) AND isset($data["pubkey"]) AND
946                         isset($data["poll"]) AND isset($data["url"])) {
947                         $data["network"] = NETWORK_OSTATUS;
948                 } else {
949                         return false;
950                 }
951                 // Fetch all additional data from the feed
952                 $ret = z_fetch_url($data["poll"]);
953                 if ($ret['errno'] == CURLE_OPERATION_TIMEDOUT) {
954                         return false;
955                 }
956                 $feed = $ret['body'];
957                 $feed_data = feed_import($feed,$dummy1,$dummy2, $dummy3, true);
958                 if (!$feed_data) {
959                         return false;
960                 }
961                 if ($feed_data["header"]["author-name"] != "") {
962                         $data["name"] = $feed_data["header"]["author-name"];
963                 }
964                 if ($feed_data["header"]["author-nick"] != "") {
965                         $data["nick"] = $feed_data["header"]["author-nick"];
966                 }
967                 if ($feed_data["header"]["author-avatar"] != "") {
968                         $data["photo"] = self::fix_avatar($feed_data["header"]["author-avatar"], $data["url"]);
969                 }
970                 if ($feed_data["header"]["author-id"] != "") {
971                         $data["alias"] = $feed_data["header"]["author-id"];
972                 }
973                 if ($feed_data["header"]["author-location"] != "") {
974                         $data["location"] = $feed_data["header"]["author-location"];
975                 }
976                 if ($feed_data["header"]["author-about"] != "") {
977                         $data["about"] = $feed_data["header"]["author-about"];
978                 }
979                 // OStatus has serious issues when the the url doesn't fit (ssl vs. non ssl)
980                 // So we take the value that we just fetched, although the other one worked as well
981                 if ($feed_data["header"]["author-link"] != "") {
982                         $data["url"] = $feed_data["header"]["author-link"];
983                 }
984                 /// @todo Fetch location and "about" from the feed as well
985                 return $data;
986         }
987
988         /**
989          * @brief Fetch data from a pump.io profile page
990          *
991          * @param string $profile Link to the profile page
992          *
993          * @return array profile data
994          */
995         private function pumpio_profile_data($profile) {
996
997                 $doc = new DOMDocument();
998                 if (!@$doc->loadHTMLFile($profile))
999                         return false;
1000
1001                 $xpath = new DomXPath($doc);
1002
1003                 $data = array();
1004
1005                 // This is ugly - but pump.io doesn't seem to know a better way for it
1006                 $data["name"] = trim($xpath->query("//h1[@class='media-header']")->item(0)->nodeValue);
1007                 $pos = strpos($data["name"], chr(10));
1008                 if ($pos)
1009                         $data["name"] = trim(substr($data["name"], 0, $pos));
1010
1011                 $avatar = $xpath->query("//img[@class='img-rounded media-object']")->item(0);
1012                 if ($avatar)
1013                         foreach ($avatar->attributes as $attribute)
1014                                 if ($attribute->name == "src")
1015                                         $data["photo"] = trim($attribute->value);
1016
1017                 $data["location"] = $xpath->query("//p[@class='location']")->item(0)->nodeValue;
1018                 $data["about"] = $xpath->query("//p[@class='summary']")->item(0)->nodeValue;
1019
1020                 return $data;
1021         }
1022
1023         /**
1024          * @brief Check for pump.io contact
1025          *
1026          * @param array $webfinger Webfinger data
1027          *
1028          * @return array pump.io data
1029          */
1030         private function pumpio($webfinger) {
1031
1032                 $data = array();
1033                 foreach ($webfinger["links"] AS $link) {
1034                         if (($link["rel"] == "http://webfinger.net/rel/profile-page") AND
1035                                 ($link["type"] == "text/html") AND ($link["href"] != ""))
1036                                 $data["url"] = $link["href"];
1037                         elseif (($link["rel"] == "activity-inbox") AND ($link["href"] != ""))
1038                                 $data["notify"] = $link["href"];
1039                         elseif (($link["rel"] == "activity-outbox") AND ($link["href"] != ""))
1040                                 $data["poll"] = $link["href"];
1041                         elseif (($link["rel"] == "dialback") AND ($link["href"] != ""))
1042                                 $data["dialback"] = $link["href"];
1043                 }
1044                 if (isset($data["poll"]) AND isset($data["notify"]) AND
1045                         isset($data["dialback"]) AND isset($data["url"])) {
1046
1047                         // by now we use these fields only for the network type detection
1048                         // So we unset all data that isn't used at the moment
1049                         unset($data["dialback"]);
1050
1051                         $data["network"] = NETWORK_PUMPIO;
1052                 } else
1053                         return false;
1054
1055                 $profile_data = self::pumpio_profile_data($data["url"]);
1056
1057                 if (!$profile_data)
1058                         return false;
1059
1060                 $data = array_merge($data, $profile_data);
1061
1062                 return $data;
1063         }
1064
1065         /**
1066          * @brief Check page for feed link
1067          *
1068          * @param string $url Page link
1069          *
1070          * @return string feed link
1071          */
1072         private function get_feed_link($url) {
1073                 $doc = new DOMDocument();
1074
1075                 if (!@$doc->loadHTMLFile($url))
1076                         return false;
1077
1078                 $xpath = new DomXPath($doc);
1079
1080                 //$feeds = $xpath->query("/html/head/link[@type='application/rss+xml']");
1081                 $feeds = $xpath->query("/html/head/link[@type='application/rss+xml' and @rel='alternate']");
1082                 if (!is_object($feeds))
1083                         return false;
1084
1085                 if ($feeds->length == 0)
1086                         return false;
1087
1088                 $feed_url = "";
1089
1090                 foreach ($feeds AS $feed) {
1091                         $attr = array();
1092                         foreach ($feed->attributes as $attribute)
1093                         $attr[$attribute->name] = trim($attribute->value);
1094
1095                         if ($feed_url == "")
1096                                 $feed_url = $attr["href"];
1097                 }
1098
1099                 return $feed_url;
1100         }
1101
1102         /**
1103          * @brief Check for feed contact
1104          *
1105          * @param string $url Profile link
1106          * @param boolean $probe Do a probe if the page contains a feed link
1107          *
1108          * @return array feed data
1109          */
1110         private function feed($url, $probe = true) {
1111                 $ret = z_fetch_url($url);
1112                 if ($ret['errno'] == CURLE_OPERATION_TIMEDOUT) {
1113                         return false;
1114                 }
1115                 $feed = $ret['body'];
1116                 $feed_data = feed_import($feed, $dummy1, $dummy2, $dummy3, true);
1117
1118                 if (!$feed_data) {
1119                         if (!$probe)
1120                                 return false;
1121
1122                         $feed_url = self::get_feed_link($url);
1123
1124                         if (!$feed_url)
1125                                 return false;
1126
1127                         return self::feed($feed_url, false);
1128                 }
1129
1130                 if ($feed_data["header"]["author-name"] != "")
1131                         $data["name"] = $feed_data["header"]["author-name"];
1132
1133                 if ($feed_data["header"]["author-nick"] != "")
1134                         $data["nick"] = $feed_data["header"]["author-nick"];
1135
1136                 if ($feed_data["header"]["author-avatar"] != "")
1137                         $data["photo"] = $feed_data["header"]["author-avatar"];
1138
1139                 if ($feed_data["header"]["author-id"] != "")
1140                         $data["alias"] = $feed_data["header"]["author-id"];
1141
1142                 $data["url"] = $url;
1143                 $data["poll"] = $url;
1144
1145                 if ($feed_data["header"]["author-link"] != "")
1146                         $data["baseurl"] = $feed_data["header"]["author-link"];
1147                 else
1148                         $data["baseurl"] = $data["url"];
1149
1150                 $data["network"] = NETWORK_FEED;
1151
1152                 return $data;
1153         }
1154
1155         /**
1156          * @brief Check for mail contact
1157          *
1158          * @param string $uri Profile link
1159          * @param integer $uid User ID
1160          *
1161          * @return array mail data
1162          */
1163         private function mail($uri, $uid) {
1164
1165                 if (!validate_email($uri))
1166                         return false;
1167
1168                 $x = q("SELECT `prvkey` FROM `user` WHERE `uid` = %d LIMIT 1", intval($uid));
1169
1170                 $r = q("SELECT * FROM `mailacct` WHERE `uid` = %d AND `server` != '' LIMIT 1", intval($uid));
1171
1172                 if (dbm::is_result($x) && dbm::is_result($r)) {
1173                         $mailbox = construct_mailbox_name($r[0]);
1174                         $password = '';
1175                         openssl_private_decrypt(hex2bin($r[0]['pass']), $password,$x[0]['prvkey']);
1176                         $mbox = email_connect($mailbox,$r[0]['user'], $password);
1177                         if(!mbox)
1178                                 return false;
1179                 }
1180
1181                 $msgs = email_poll($mbox, $uri);
1182                 logger('searching '.$uri.', '.count($msgs).' messages found.', LOGGER_DEBUG);
1183
1184                 if (!count($msgs))
1185                         return false;
1186
1187                 $data = array();
1188
1189                 $data["addr"] = $uri;
1190                 $data["network"] = NETWORK_MAIL;
1191                 $data["name"] = substr($uri, 0, strpos($uri,'@'));
1192                 $data["nick"] = $data["name"];
1193                 $data["photo"] = avatar_img($uri);
1194
1195                 $phost = substr($uri, strpos($uri,'@') + 1);
1196                 $data["url"] = 'http://'.$phost."/".$data["nick"];
1197                 $data["notify"] = 'smtp '.random_string();
1198                 $data["poll"] = 'email '.random_string();
1199
1200                 $x = email_msg_meta($mbox, $msgs[0]);
1201                 if(stristr($x[0]->from, $uri))
1202                         $adr = imap_rfc822_parse_adrlist($x[0]->from, '');
1203                 elseif(stristr($x[0]->to, $uri))
1204                         $adr = imap_rfc822_parse_adrlist($x[0]->to, '');
1205                 if(isset($adr)) {
1206                         foreach($adr as $feadr) {
1207                                 if((strcasecmp($feadr->mailbox, $data["name"]) == 0)
1208                                         &&(strcasecmp($feadr->host, $phost) == 0)
1209                                         && (strlen($feadr->personal))) {
1210
1211                                         $personal = imap_mime_header_decode($feadr->personal);
1212                                         $data["name"] = "";
1213                                         foreach($personal as $perspart)
1214                                                 if ($perspart->charset != "default")
1215                                                         $data["name"] .= iconv($perspart->charset, 'UTF-8//IGNORE', $perspart->text);
1216                                                 else
1217                                                         $data["name"] .= $perspart->text;
1218
1219                                         $data["name"] = notags($data["name"]);
1220                                 }
1221                         }
1222                 }
1223                 imap_close($mbox);
1224
1225                 return $data;
1226         }
1227
1228         /**
1229          * @brief Mix two paths together to possibly fix missing parts
1230          *
1231          * @param string $avatar Path to the avatar
1232          * @param string $base Another path that is hopefully complete
1233          *
1234          * @return string fixed avatar path
1235          */
1236         public static function fix_avatar($avatar, $base) {
1237                 $base_parts = parse_url($base);
1238
1239                 // Remove all parts that could create a problem
1240                 unset($base_parts['path']);
1241                 unset($base_parts['query']);
1242                 unset($base_parts['fragment']);
1243
1244                 $avatar_parts = parse_url($avatar);
1245
1246                 // Now we mix them
1247                 $parts = array_merge($base_parts, $avatar_parts);
1248
1249                 // And put them together again
1250                 $scheme   = isset($parts['scheme']) ? $parts['scheme'] . '://' : '';
1251                 $host     = isset($parts['host']) ? $parts['host'] : '';
1252                 $port     = isset($parts['port']) ? ':' . $parts['port'] : '';
1253                 $path     = isset($parts['path']) ? $parts['path'] : '';
1254                 $query    = isset($parts['query']) ? '?' . $parts['query'] : '';
1255                 $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';
1256
1257                 $fixed = $scheme.$host.$port.$path.$query.$fragment;
1258
1259                 logger('Base: '.$base.' - Avatar: '.$avatar.' - Fixed: '.$fixed, LOGGER_DATA);
1260
1261                 return $fixed;
1262         }
1263
1264 }