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