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