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