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