Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1144 lines
33KB

  1. <?php
  2. /**
  3. * @file include/Probe.php
  4. * @brief Functions for probing URL
  5. *
  6. */
  7. use \Friendica\Core\Config;
  8. use \Friendica\Core\PConfig;
  9. require_once("include/feed.php");
  10. require_once('include/email.php');
  11. require_once('include/network.php');
  12. /**
  13. * @brief This class contain functions for probing URL
  14. *
  15. */
  16. class Probe {
  17. /**
  18. * @brief Rearrange the array so that it always has the same order
  19. *
  20. * @param array $data Unordered data
  21. *
  22. * @return array Ordered data
  23. */
  24. private function rearrange_data($data) {
  25. $fields = array("name", "nick", "guid", "url", "addr", "alias",
  26. "photo", "community", "keywords", "location", "about",
  27. "batch", "notify", "poll", "request", "confirm", "poco",
  28. "priority", "network", "pubkey", "baseurl");
  29. $newdata = array();
  30. foreach ($fields AS $field)
  31. if (isset($data[$field]))
  32. $newdata[$field] = $data[$field];
  33. else
  34. $newdata[$field] = "";
  35. // We don't use the "priority" field anymore and replace it with a dummy.
  36. $newdata["priority"] = 0;
  37. return $newdata;
  38. }
  39. /**
  40. * @brief Probes for XRD data
  41. *
  42. * @return array
  43. * 'lrdd' => Link to LRDD endpoint
  44. * 'lrdd-xml' => Link to LRDD endpoint in XML format
  45. * 'lrdd-json' => Link to LRDD endpoint in JSON format
  46. */
  47. private function xrd($host) {
  48. $ssl_url = "https://".$host."/.well-known/host-meta";
  49. $url = "http://".$host."/.well-known/host-meta";
  50. $xrd_timeout = Config::get('system','xrd_timeout', 20);
  51. $redirects = 0;
  52. $xml = fetch_url($ssl_url, false, $redirects, $xrd_timeout, "application/xrd+xml");
  53. $xrd = parse_xml_string($xml, false);
  54. if (!is_object($xrd)) {
  55. $xml = fetch_url($url, false, $redirects, $xrd_timeout, "application/xrd+xml");
  56. $xrd = parse_xml_string($xml, false);
  57. }
  58. if (!is_object($xrd))
  59. return false;
  60. $links = xml::element_to_array($xrd);
  61. if (!isset($links["xrd"]["link"]))
  62. return false;
  63. $xrd_data = array();
  64. foreach ($links["xrd"]["link"] AS $value => $link) {
  65. if (isset($link["@attributes"]))
  66. $attributes = $link["@attributes"];
  67. elseif ($value == "@attributes")
  68. $attributes = $link;
  69. else
  70. continue;
  71. if (($attributes["rel"] == "lrdd") AND
  72. ($attributes["type"] == "application/xrd+xml"))
  73. $xrd_data["lrdd-xml"] = $attributes["template"];
  74. elseif (($attributes["rel"] == "lrdd") AND
  75. ($attributes["type"] == "application/json"))
  76. $xrd_data["lrdd-json"] = $attributes["template"];
  77. elseif ($attributes["rel"] == "lrdd")
  78. $xrd_data["lrdd"] = $attributes["template"];
  79. }
  80. return $xrd_data;
  81. }
  82. /**
  83. * @brief Perform Webfinger lookup and return DFRN data
  84. *
  85. * Given an email style address, perform webfinger lookup and
  86. * return the resulting DFRN profile URL, or if no DFRN profile URL
  87. * is located, returns an OStatus subscription template (prefixed
  88. * with the string 'stat:' to identify it as on OStatus template).
  89. * If this isn't an email style address just return $webbie.
  90. * Return an empty string if email-style addresses but webfinger fails,
  91. * or if the resultant personal XRD doesn't contain a supported
  92. * subscription/friend-request attribute.
  93. *
  94. * amended 7/9/2011 to return an hcard which could save potentially loading
  95. * a lengthy content page to scrape dfrn attributes
  96. *
  97. * @param string $webbie Address that should be probed
  98. * @param string $hcard Link to the hcard - is returned by reference
  99. *
  100. * @return string profile link
  101. */
  102. public static function webfinger_dfrn($webbie, &$hcard) {
  103. $profile_link = '';
  104. $links = self::lrdd($webbie);
  105. logger('webfinger_dfrn: '.$webbie.':'.print_r($links,true), LOGGER_DATA);
  106. if (count($links)) {
  107. foreach ($links as $link) {
  108. if ($link['@attributes']['rel'] === NAMESPACE_DFRN)
  109. $profile_link = $link['@attributes']['href'];
  110. if (($link['@attributes']['rel'] === NAMESPACE_OSTATUSSUB) AND ($profile_link == ""))
  111. $profile_link = 'stat:'.$link['@attributes']['template'];
  112. if ($link['@attributes']['rel'] === 'http://microformats.org/profile/hcard')
  113. $hcard = $link['@attributes']['href'];
  114. }
  115. }
  116. return $profile_link;
  117. }
  118. /**
  119. * @brief Check an URI for LRDD data
  120. *
  121. * this is a replacement for the "lrdd" function in include/network.php.
  122. * It isn't used in this class and has some redundancies in the code.
  123. * When time comes we can check the existing calls for "lrdd" if we can rework them.
  124. *
  125. * @param string $uri Address that should be probed
  126. *
  127. * @return array uri data
  128. */
  129. public static function lrdd($uri) {
  130. $lrdd = self::xrd($uri);
  131. if (!$lrdd) {
  132. $parts = @parse_url($uri);
  133. if (!$parts)
  134. return array();
  135. $host = $parts["host"];
  136. $path_parts = explode("/", trim($parts["path"], "/"));
  137. do {
  138. $lrdd = self::xrd($host);
  139. $host .= "/".array_shift($path_parts);
  140. } while (!$lrdd AND (sizeof($path_parts) > 0));
  141. }
  142. if (!$lrdd)
  143. return array();
  144. foreach ($lrdd AS $key => $link) {
  145. if ($webfinger)
  146. continue;
  147. if (!in_array($key, array("lrdd", "lrdd-xml", "lrdd-json")))
  148. continue;
  149. $path = str_replace('{uri}', urlencode($uri), $link);
  150. $webfinger = self::webfinger($path);
  151. if (!$webfinger AND (strstr($uri, "@"))) {
  152. $path = str_replace('{uri}', urlencode("acct:".$uri), $link);
  153. $webfinger = self::webfinger($path);
  154. }
  155. }
  156. if (!is_array($webfinger["links"]))
  157. return false;
  158. $data = array();
  159. foreach ($webfinger["links"] AS $link)
  160. $data[] = array("@attributes" => $link);
  161. if (is_array($webfinger["aliases"]))
  162. foreach ($webfinger["aliases"] AS $alias)
  163. $data[] = array("@attributes" =>
  164. array("rel" => "alias",
  165. "href" => $alias));
  166. return $data;
  167. }
  168. /**
  169. * @brief Fetch information (protocol endpoints and user information) about a given uri
  170. *
  171. * @param string $uri Address that should be probed
  172. * @param string $network Test for this specific network
  173. * @param integer $uid User ID for the probe (only used for mails)
  174. * @param boolean $cache Use cached values?
  175. *
  176. * @return array uri data
  177. */
  178. public static function uri($uri, $network = "", $uid = 0, $cache = true) {
  179. if ($cache) {
  180. $result = Cache::get("probe_url:".$network.":".$uri);
  181. if (!is_null($result)) {
  182. return $result;
  183. }
  184. }
  185. if ($uid == 0)
  186. $uid = local_user();
  187. $data = self::detect($uri, $network, $uid);
  188. if (!isset($data["url"]))
  189. $data["url"] = $uri;
  190. if ($data["photo"] != "")
  191. $data["baseurl"] = matching_url(normalise_link($data["baseurl"]), normalise_link($data["photo"]));
  192. else
  193. $data["photo"] = $a->get_baseurl().'/images/person-175.jpg';
  194. if (!isset($data["name"]) OR ($data["name"] == "")) {
  195. if (isset($data["nick"]))
  196. $data["name"] = $data["nick"];
  197. if ($data["name"] == "")
  198. $data["name"] = $data["url"];
  199. }
  200. if (!isset($data["nick"]) OR ($data["nick"] == "")) {
  201. $data["nick"] = strtolower($data["name"]);
  202. if (strpos($data['nick'], ' '))
  203. $data['nick'] = trim(substr($data['nick'], 0, strpos($data['nick'], ' ')));
  204. }
  205. if (!isset($data["network"]))
  206. $data["network"] = NETWORK_PHANTOM;
  207. $data = self::rearrange_data($data);
  208. // Only store into the cache if the value seems to be valid
  209. if (!in_array($data['network'], array(NETWORK_PHANTOM, NETWORK_MAIL))) {
  210. Cache::set("probe_url:".$network.":".$uri, $data, CACHE_DAY);
  211. /// @todo temporary fix - we need a real contact update function that updates only changing fields
  212. /// The biggest problem is the avatar picture that could have a reduced image size.
  213. /// It should only be updated if the existing picture isn't existing anymore.
  214. if (($data['network'] != NETWORK_FEED) AND ($mode == PROBE_NORMAL) AND
  215. $data["name"] AND $data["nick"] AND $data["url"] AND $data["addr"] AND $data["poll"])
  216. q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `url` = '%s', `addr` = '%s',
  217. `notify` = '%s', `poll` = '%s', `alias` = '%s', `success_update` = '%s'
  218. WHERE `nurl` = '%s' AND NOT `self` AND `uid` = 0",
  219. dbesc($data["name"]),
  220. dbesc($data["nick"]),
  221. dbesc($data["url"]),
  222. dbesc($data["addr"]),
  223. dbesc($data["notify"]),
  224. dbesc($data["poll"]),
  225. dbesc($data["alias"]),
  226. dbesc(datetime_convert()),
  227. dbesc(normalise_link($data['url']))
  228. );
  229. }
  230. return $data;
  231. }
  232. /**
  233. * @brief Fetch information (protocol endpoints and user information) about a given uri
  234. *
  235. * This function is only called by the "uri" function that adds caching and rearranging of data.
  236. *
  237. * @param string $uri Address that should be probed
  238. * @param string $network Test for this specific network
  239. * @param integer $uid User ID for the probe (only used for mails)
  240. *
  241. * @return array uri data
  242. */
  243. private function detect($uri, $network, $uid) {
  244. if (strstr($uri, '@')) {
  245. // If the URI starts with "mailto:" then jump directly to the mail detection
  246. if (strpos($url,'mailto:') !== false) {
  247. $uri = str_replace('mailto:', '', $url);
  248. return self::mail($uri, $uid);
  249. }
  250. if ($network == NETWORK_MAIL)
  251. return self::mail($uri, $uid);
  252. // Remove "acct:" from the URI
  253. $uri = str_replace('acct:', '', $uri);
  254. $host = substr($uri,strpos($uri, '@') + 1);
  255. $nick = substr($uri,0, strpos($uri, '@'));
  256. if (strpos($uri, '@twitter.com'))
  257. return array("network" => NETWORK_TWITTER);
  258. $lrdd = self::xrd($host);
  259. if (!$lrdd)
  260. return self::mail($uri, $uid);
  261. $addr = $uri;
  262. } else {
  263. $parts = parse_url($uri);
  264. if (!isset($parts["scheme"]) OR
  265. !isset($parts["host"]) OR
  266. !isset($parts["path"]))
  267. return false;
  268. // todo: Ports?
  269. $host = $parts["host"];
  270. if ($host == 'twitter.com')
  271. return array("network" => NETWORK_TWITTER);
  272. $lrdd = self::xrd($host);
  273. $path_parts = explode("/", trim($parts["path"], "/"));
  274. while (!$lrdd AND (sizeof($path_parts) > 1)) {
  275. $host .= "/".array_shift($path_parts);
  276. $lrdd = self::xrd($host);
  277. }
  278. if (!$lrdd)
  279. return self::feed($uri);
  280. $nick = array_pop($path_parts);
  281. $addr = $nick."@".$host;
  282. }
  283. $webfinger = false;
  284. /// @todo Do we need the prefix "acct:" or "acct://"?
  285. foreach ($lrdd AS $key => $link) {
  286. if ($webfinger)
  287. continue;
  288. if (!in_array($key, array("lrdd", "lrdd-xml", "lrdd-json")))
  289. continue;
  290. // Try webfinger with the address (user@domain.tld)
  291. $path = str_replace('{uri}', urlencode($addr), $link);
  292. $webfinger = self::webfinger($path);
  293. // Mastodon needs to have it with "acct:"
  294. if (!$webfinger) {
  295. $path = str_replace('{uri}', urlencode("acct:".$addr), $link);
  296. $webfinger = self::webfinger($path);
  297. }
  298. // If webfinger wasn't successful then try it with the URL - possibly in the format https://...
  299. if (!$webfinger AND ($uri != $addr)) {
  300. $path = str_replace('{uri}', urlencode($uri), $link);
  301. $webfinger = self::webfinger($path);
  302. // Since the detection with the address wasn't successful, we delete it.
  303. if ($webfinger) {
  304. $nick = "";
  305. $addr = "";
  306. }
  307. }
  308. }
  309. if (!$webfinger)
  310. return self::feed($uri);
  311. $result = false;
  312. logger("Probing ".$uri, LOGGER_DEBUG);
  313. if (in_array($network, array("", NETWORK_DFRN)))
  314. $result = self::dfrn($webfinger);
  315. if ((!$result AND ($network == "")) OR ($network == NETWORK_DIASPORA))
  316. $result = self::diaspora($webfinger);
  317. if ((!$result AND ($network == "")) OR ($network == NETWORK_OSTATUS))
  318. $result = self::ostatus($webfinger);
  319. if ((!$result AND ($network == "")) OR ($network == NETWORK_PUMPIO))
  320. $result = self::pumpio($webfinger);
  321. if ((!$result AND ($network == "")) OR ($network == NETWORK_FEED))
  322. $result = self::feed($uri);
  323. else {
  324. // We overwrite the detected nick with our try if the previois routines hadn't detected it.
  325. // Additionally it is overwritten when the nickname doesn't make sense (contains spaces).
  326. if ((!isset($result["nick"]) OR ($result["nick"] == "") OR (strstr($result["nick"], " "))) AND ($nick != ""))
  327. $result["nick"] = $nick;
  328. if ((!isset($result["addr"]) OR ($result["addr"] == "")) AND ($addr != ""))
  329. $result["addr"] = $addr;
  330. }
  331. logger($uri." is ".$result["network"], LOGGER_DEBUG);
  332. if (!isset($result["baseurl"]) OR ($result["baseurl"] == "")) {
  333. $pos = strpos($result["url"], $host);
  334. if ($pos)
  335. $result["baseurl"] = substr($result["url"], 0, $pos).$host;
  336. }
  337. return $result;
  338. }
  339. /**
  340. * @brief Perform a webfinger request.
  341. *
  342. * For details see RFC 7033: <https://tools.ietf.org/html/rfc7033>
  343. *
  344. * @param string $url Address that should be probed
  345. *
  346. * @return array webfinger data
  347. */
  348. private function webfinger($url) {
  349. $xrd_timeout = Config::get('system','xrd_timeout', 20);
  350. $redirects = 0;
  351. $data = fetch_url($url, false, $redirects, $xrd_timeout, "application/xrd+xml");
  352. $xrd = parse_xml_string($data, false);
  353. if (!is_object($xrd)) {
  354. // If it is not XML, maybe it is JSON
  355. $webfinger = json_decode($data, true);
  356. if (!isset($webfinger["links"]))
  357. return false;
  358. return $webfinger;
  359. }
  360. $xrd_arr = xml::element_to_array($xrd);
  361. if (!isset($xrd_arr["xrd"]["link"]))
  362. return false;
  363. $webfinger = array();
  364. if (isset($xrd_arr["xrd"]["subject"]))
  365. $webfinger["subject"] = $xrd_arr["xrd"]["subject"];
  366. if (isset($xrd_arr["xrd"]["alias"]))
  367. $webfinger["aliases"] = $xrd_arr["xrd"]["alias"];
  368. $webfinger["links"] = array();
  369. foreach ($xrd_arr["xrd"]["link"] AS $value => $data) {
  370. if (isset($data["@attributes"]))
  371. $attributes = $data["@attributes"];
  372. elseif ($value == "@attributes")
  373. $attributes = $data;
  374. else
  375. continue;
  376. $webfinger["links"][] = $attributes;
  377. }
  378. return $webfinger;
  379. }
  380. /**
  381. * @brief Poll the Friendica specific noscrape page.
  382. *
  383. * "noscrape" is a faster alternative to fetch the data from the hcard.
  384. * This functionality was originally created for the directory.
  385. *
  386. * @param string $noscrape Link to the noscrape page
  387. * @param array $data The already fetched data
  388. *
  389. * @return array noscrape data
  390. */
  391. private function poll_noscrape($noscrape, $data) {
  392. $content = fetch_url($noscrape);
  393. if (!$content)
  394. return false;
  395. $json = json_decode($content, true);
  396. if (!is_array($json))
  397. return false;
  398. if (isset($json["fn"]))
  399. $data["name"] = $json["fn"];
  400. if (isset($json["addr"]))
  401. $data["addr"] = $json["addr"];
  402. if (isset($json["nick"]))
  403. $data["nick"] = $json["nick"];
  404. if (isset($json["comm"]))
  405. $data["community"] = $json["comm"];
  406. if (isset($json["tags"])) {
  407. $keywords = implode(" ", $json["tags"]);
  408. if ($keywords != "")
  409. $data["keywords"] = $keywords;
  410. }
  411. $location = formatted_location($json);
  412. if ($location)
  413. $data["location"] = $location;
  414. if (isset($json["about"]))
  415. $data["about"] = $json["about"];
  416. if (isset($json["key"]))
  417. $data["pubkey"] = $json["key"];
  418. if (isset($json["photo"]))
  419. $data["photo"] = $json["photo"];
  420. if (isset($json["dfrn-request"]))
  421. $data["request"] = $json["dfrn-request"];
  422. if (isset($json["dfrn-confirm"]))
  423. $data["confirm"] = $json["dfrn-confirm"];
  424. if (isset($json["dfrn-notify"]))
  425. $data["notify"] = $json["dfrn-notify"];
  426. if (isset($json["dfrn-poll"]))
  427. $data["poll"] = $json["dfrn-poll"];
  428. return $data;
  429. }
  430. /**
  431. * @brief Check for valid DFRN data
  432. *
  433. * @param array $data DFRN data
  434. *
  435. * @return int Number of errors
  436. */
  437. public static function valid_dfrn($data) {
  438. $errors = 0;
  439. if(!isset($data['key']))
  440. $errors ++;
  441. if(!isset($data['dfrn-request']))
  442. $errors ++;
  443. if(!isset($data['dfrn-confirm']))
  444. $errors ++;
  445. if(!isset($data['dfrn-notify']))
  446. $errors ++;
  447. if(!isset($data['dfrn-poll']))
  448. $errors ++;
  449. return $errors;
  450. }
  451. /**
  452. * @brief Fetch data from a DFRN profile page and via "noscrape"
  453. *
  454. * @param string $profile Link to the profile page
  455. *
  456. * @return array profile data
  457. */
  458. public static function profile($profile) {
  459. $data = array();
  460. logger("Check profile ".$profile, LOGGER_DEBUG);
  461. // Fetch data via noscrape - this is faster
  462. $noscrape = str_replace(array("/hcard/", "/profile/"), "/noscrape/", $profile);
  463. $data = self::poll_noscrape($noscrape, $data);
  464. if (!isset($data["notify"]) OR !isset($data["confirm"]) OR
  465. !isset($data["request"]) OR !isset($data["poll"]) OR
  466. !isset($data["poco"]) OR !isset($data["name"]) OR
  467. !isset($data["photo"]))
  468. $data = self::poll_hcard($profile, $data, true);
  469. $prof_data = array();
  470. $prof_data["addr"] = $data["addr"];
  471. $prof_data["nick"] = $data["nick"];
  472. $prof_data["dfrn-request"] = $data["request"];
  473. $prof_data["dfrn-confirm"] = $data["confirm"];
  474. $prof_data["dfrn-notify"] = $data["notify"];
  475. $prof_data["dfrn-poll"] = $data["poll"];
  476. $prof_data["dfrn-poco"] = $data["poco"];
  477. $prof_data["photo"] = $data["photo"];
  478. $prof_data["fn"] = $data["name"];
  479. $prof_data["key"] = $data["pubkey"];
  480. logger("Result for profile ".$profile.": ".print_r($prof_data, true), LOGGER_DEBUG);
  481. return $prof_data;
  482. }
  483. /**
  484. * @brief Check for DFRN contact
  485. *
  486. * @param array $webfinger Webfinger data
  487. *
  488. * @return array DFRN data
  489. */
  490. private function dfrn($webfinger) {
  491. $hcard = "";
  492. $data = array();
  493. foreach ($webfinger["links"] AS $link) {
  494. if (($link["rel"] == NAMESPACE_DFRN) AND ($link["href"] != ""))
  495. $data["network"] = NETWORK_DFRN;
  496. elseif (($link["rel"] == NAMESPACE_FEED) AND ($link["href"] != ""))
  497. $data["poll"] = $link["href"];
  498. elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") AND
  499. ($link["type"] == "text/html") AND ($link["href"] != ""))
  500. $data["url"] = $link["href"];
  501. elseif (($link["rel"] == "http://microformats.org/profile/hcard") AND ($link["href"] != ""))
  502. $hcard = $link["href"];
  503. elseif (($link["rel"] == NAMESPACE_POCO) AND ($link["href"] != ""))
  504. $data["poco"] = $link["href"];
  505. elseif (($link["rel"] == "http://webfinger.net/rel/avatar") AND ($link["href"] != ""))
  506. $data["photo"] = $link["href"];
  507. elseif (($link["rel"] == "http://joindiaspora.com/seed_location") AND ($link["href"] != ""))
  508. $data["baseurl"] = trim($link["href"], '/');
  509. elseif (($link["rel"] == "http://joindiaspora.com/guid") AND ($link["href"] != ""))
  510. $data["guid"] = $link["href"];
  511. elseif (($link["rel"] == "diaspora-public-key") AND ($link["href"] != "")) {
  512. $data["pubkey"] = base64_decode($link["href"]);
  513. //if (strstr($data["pubkey"], 'RSA ') OR ($link["type"] == "RSA"))
  514. if (strstr($data["pubkey"], 'RSA '))
  515. $data["pubkey"] = rsatopem($data["pubkey"]);
  516. }
  517. }
  518. if (!isset($data["network"]) OR ($hcard == ""))
  519. return false;
  520. // Fetch data via noscrape - this is faster
  521. $noscrape = str_replace("/hcard/", "/noscrape/", $hcard);
  522. $data = self::poll_noscrape($noscrape, $data);
  523. if (isset($data["notify"]) AND isset($data["confirm"]) AND isset($data["request"]) AND
  524. isset($data["poll"]) AND isset($data["name"]) AND isset($data["photo"]))
  525. return $data;
  526. $data = self::poll_hcard($hcard, $data, true);
  527. return $data;
  528. }
  529. /**
  530. * @brief Poll the hcard page (Diaspora and Friendica specific)
  531. *
  532. * @param string $hcard Link to the hcard page
  533. * @param array $data The already fetched data
  534. * @param boolean $dfrn Poll DFRN specific data
  535. *
  536. * @return array hcard data
  537. */
  538. private function poll_hcard($hcard, $data, $dfrn = false) {
  539. $content = fetch_url($hcard);
  540. if (!$content)
  541. return false;
  542. $doc = new DOMDocument();
  543. if (!@$doc->loadHTML($content))
  544. return false;
  545. $xpath = new DomXPath($doc);
  546. $vcards = $xpath->query("//div[contains(concat(' ', @class, ' '), ' vcard ')]");
  547. if (!is_object($vcards))
  548. return false;
  549. if ($vcards->length > 0) {
  550. $vcard = $vcards->item(0);
  551. // We have to discard the guid from the hcard in favour of the guid from lrdd
  552. // Reason: Hubzilla doesn't use the value "uid" in the hcard like Diaspora does.
  553. $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' uid ')]", $vcard); // */
  554. if (($search->length > 0) AND ($data["guid"] == ""))
  555. $data["guid"] = $search->item(0)->nodeValue;
  556. $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' nickname ')]", $vcard); // */
  557. if ($search->length > 0)
  558. $data["nick"] = $search->item(0)->nodeValue;
  559. $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' fn ')]", $vcard); // */
  560. if ($search->length > 0)
  561. $data["name"] = $search->item(0)->nodeValue;
  562. $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' searchable ')]", $vcard); // */
  563. if ($search->length > 0)
  564. $data["searchable"] = $search->item(0)->nodeValue;
  565. $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' key ')]", $vcard); // */
  566. if ($search->length > 0) {
  567. $data["pubkey"] = $search->item(0)->nodeValue;
  568. if (strstr($data["pubkey"], 'RSA '))
  569. $data["pubkey"] = rsatopem($data["pubkey"]);
  570. }
  571. $search = $xpath->query("//*[@id='pod_location']", $vcard); // */
  572. if ($search->length > 0)
  573. $data["baseurl"] = trim($search->item(0)->nodeValue, "/");
  574. }
  575. $avatar = array();
  576. $photos = $xpath->query("//*[contains(concat(' ', @class, ' '), ' photo ') or contains(concat(' ', @class, ' '), ' avatar ')]", $vcard); // */
  577. foreach ($photos AS $photo) {
  578. $attr = array();
  579. foreach ($photo->attributes as $attribute)
  580. $attr[$attribute->name] = trim($attribute->value);
  581. if (isset($attr["src"]) AND isset($attr["width"]))
  582. $avatar[$attr["width"]] = $attr["src"];
  583. }
  584. if (sizeof($avatar)) {
  585. ksort($avatar);
  586. $data["photo"] = array_pop($avatar);
  587. }
  588. if ($dfrn) {
  589. // Poll DFRN specific data
  590. $search = $xpath->query("//link[contains(concat(' ', @rel), ' dfrn-')]");
  591. if ($search->length > 0) {
  592. foreach ($search AS $link) {
  593. //$data["request"] = $search->item(0)->nodeValue;
  594. $attr = array();
  595. foreach ($link->attributes as $attribute)
  596. $attr[$attribute->name] = trim($attribute->value);
  597. $data[substr($attr["rel"], 5)] = $attr["href"];
  598. }
  599. }
  600. // Older Friendica versions had used the "uid" field differently than newer versions
  601. if ($data["nick"] == $data["guid"])
  602. unset($data["guid"]);
  603. }
  604. return $data;
  605. }
  606. /**
  607. * @brief Check for Diaspora contact
  608. *
  609. * @param array $webfinger Webfinger data
  610. *
  611. * @return array Diaspora data
  612. */
  613. private function diaspora($webfinger) {
  614. $hcard = "";
  615. $data = array();
  616. foreach ($webfinger["links"] AS $link) {
  617. if (($link["rel"] == "http://microformats.org/profile/hcard") AND ($link["href"] != ""))
  618. $hcard = $link["href"];
  619. elseif (($link["rel"] == "http://joindiaspora.com/seed_location") AND ($link["href"] != ""))
  620. $data["baseurl"] = trim($link["href"], '/');
  621. elseif (($link["rel"] == "http://joindiaspora.com/guid") AND ($link["href"] != ""))
  622. $data["guid"] = $link["href"];
  623. elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") AND
  624. ($link["type"] == "text/html") AND ($link["href"] != ""))
  625. $data["url"] = $link["href"];
  626. elseif (($link["rel"] == NAMESPACE_FEED) AND ($link["href"] != ""))
  627. $data["poll"] = $link["href"];
  628. elseif (($link["rel"] == NAMESPACE_POCO) AND ($link["href"] != ""))
  629. $data["poco"] = $link["href"];
  630. elseif (($link["rel"] == "salmon") AND ($link["href"] != ""))
  631. $data["notify"] = $link["href"];
  632. elseif (($link["rel"] == "diaspora-public-key") AND ($link["href"] != "")) {
  633. $data["pubkey"] = base64_decode($link["href"]);
  634. //if (strstr($data["pubkey"], 'RSA ') OR ($link["type"] == "RSA"))
  635. if (strstr($data["pubkey"], 'RSA '))
  636. $data["pubkey"] = rsatopem($data["pubkey"]);
  637. }
  638. }
  639. if (!isset($data["url"]) OR ($hcard == ""))
  640. return false;
  641. if (is_array($webfinger["aliases"]))
  642. foreach ($webfinger["aliases"] AS $alias)
  643. if (normalise_link($alias) != normalise_link($data["url"]) AND !strstr($alias, "@"))
  644. $data["alias"] = $alias;
  645. // Fetch further information from the hcard
  646. $data = self::poll_hcard($hcard, $data);
  647. if (!$data)
  648. return false;
  649. if (isset($data["url"]) AND isset($data["guid"]) AND isset($data["baseurl"]) AND
  650. isset($data["pubkey"]) AND ($hcard != "")) {
  651. $data["network"] = NETWORK_DIASPORA;
  652. // The Diaspora handle must always be lowercase
  653. $data["addr"] = strtolower($data["addr"]);
  654. // We have to overwrite the detected value for "notify" since Hubzilla doesn't send it
  655. $data["notify"] = $data["baseurl"]."/receive/users/".$data["guid"];
  656. $data["batch"] = $data["baseurl"]."/receive/public";
  657. } else
  658. return false;
  659. return $data;
  660. }
  661. /**
  662. * @brief Check for OStatus contact
  663. *
  664. * @param array $webfinger Webfinger data
  665. *
  666. * @return array OStatus data
  667. */
  668. private function ostatus($webfinger) {
  669. $data = array();
  670. if (is_array($webfinger["aliases"]))
  671. foreach($webfinger["aliases"] AS $alias)
  672. if (strstr($alias, "@"))
  673. $data["addr"] = str_replace('acct:', '', $alias);
  674. if (is_string($webfinger["subject"]) AND strstr($webfinger["subject"], "@"))
  675. $data["addr"] = str_replace('acct:', '', $webfinger["subject"]);
  676. $pubkey = "";
  677. foreach ($webfinger["links"] AS $link) {
  678. if (($link["rel"] == "http://webfinger.net/rel/profile-page") AND
  679. ($link["type"] == "text/html") AND ($link["href"] != ""))
  680. $data["url"] = $link["href"];
  681. elseif (($link["rel"] == "salmon") AND ($link["href"] != ""))
  682. $data["notify"] = $link["href"];
  683. elseif (($link["rel"] == NAMESPACE_FEED) AND ($link["href"] != ""))
  684. $data["poll"] = $link["href"];
  685. elseif (($link["rel"] == "magic-public-key") AND ($link["href"] != "")) {
  686. $pubkey = $link["href"];
  687. if (substr($pubkey, 0, 5) === 'data:') {
  688. if (strstr($pubkey, ','))
  689. $pubkey = substr($pubkey, strpos($pubkey, ',') + 1);
  690. else
  691. $pubkey = substr($pubkey, 5);
  692. } elseif (normalise_link($pubkey) == 'http://')
  693. $pubkey = fetch_url($pubkey);
  694. $key = explode(".", $pubkey);
  695. if (sizeof($key) >= 3) {
  696. $m = base64url_decode($key[1]);
  697. $e = base64url_decode($key[2]);
  698. $data["pubkey"] = metopem($m,$e);
  699. }
  700. }
  701. }
  702. if (isset($data["notify"]) AND isset($data["pubkey"]) AND
  703. isset($data["poll"]) AND isset($data["url"])) {
  704. $data["network"] = NETWORK_OSTATUS;
  705. } else
  706. return false;
  707. // Fetch all additional data from the feed
  708. $feed = fetch_url($data["poll"]);
  709. $feed_data = feed_import($feed,$dummy1,$dummy2, $dummy3, true);
  710. if (!$feed_data)
  711. return false;
  712. if ($feed_data["header"]["author-name"] != "")
  713. $data["name"] = $feed_data["header"]["author-name"];
  714. if ($feed_data["header"]["author-nick"] != "")
  715. $data["nick"] = $feed_data["header"]["author-nick"];
  716. if ($feed_data["header"]["author-avatar"] != "")
  717. $data["photo"] = $feed_data["header"]["author-avatar"];
  718. if ($feed_data["header"]["author-id"] != "")
  719. $data["alias"] = $feed_data["header"]["author-id"];
  720. if ($feed_data["header"]["author-location"] != "")
  721. $data["location"] = $feed_data["header"]["author-location"];
  722. if ($feed_data["header"]["author-about"] != "")
  723. $data["about"] = $feed_data["header"]["author-about"];
  724. // OStatus has serious issues when the the url doesn't fit (ssl vs. non ssl)
  725. // So we take the value that we just fetched, although the other one worked as well
  726. if ($feed_data["header"]["author-link"] != "")
  727. $data["url"] = $feed_data["header"]["author-link"];
  728. /// @todo Fetch location and "about" from the feed as well
  729. return $data;
  730. }
  731. /**
  732. * @brief Fetch data from a pump.io profile page
  733. *
  734. * @param string $profile Link to the profile page
  735. *
  736. * @return array profile data
  737. */
  738. private function pumpio_profile_data($profile) {
  739. $doc = new DOMDocument();
  740. if (!@$doc->loadHTMLFile($profile))
  741. return false;
  742. $xpath = new DomXPath($doc);
  743. $data = array();
  744. // This is ugly - but pump.io doesn't seem to know a better way for it
  745. $data["name"] = trim($xpath->query("//h1[@class='media-header']")->item(0)->nodeValue);
  746. $pos = strpos($data["name"], chr(10));
  747. if ($pos)
  748. $data["name"] = trim(substr($data["name"], 0, $pos));
  749. $avatar = $xpath->query("//img[@class='img-rounded media-object']")->item(0);
  750. if ($avatar)
  751. foreach ($avatar->attributes as $attribute)
  752. if ($attribute->name == "src")
  753. $data["photo"] = trim($attribute->value);
  754. $data["location"] = $xpath->query("//p[@class='location']")->item(0)->nodeValue;
  755. $data["about"] = $xpath->query("//p[@class='summary']")->item(0)->nodeValue;
  756. return $data;
  757. }
  758. /**
  759. * @brief Check for pump.io contact
  760. *
  761. * @param array $webfinger Webfinger data
  762. *
  763. * @return array pump.io data
  764. */
  765. private function pumpio($webfinger) {
  766. $data = array();
  767. foreach ($webfinger["links"] AS $link) {
  768. if (($link["rel"] == "http://webfinger.net/rel/profile-page") AND
  769. ($link["type"] == "text/html") AND ($link["href"] != ""))
  770. $data["url"] = $link["href"];
  771. elseif (($link["rel"] == "activity-inbox") AND ($link["href"] != ""))
  772. $data["notify"] = $link["href"];
  773. elseif (($link["rel"] == "activity-outbox") AND ($link["href"] != ""))
  774. $data["poll"] = $link["href"];
  775. elseif (($link["rel"] == "dialback") AND ($link["href"] != ""))
  776. $data["dialback"] = $link["href"];
  777. }
  778. if (isset($data["poll"]) AND isset($data["notify"]) AND
  779. isset($data["dialback"]) AND isset($data["url"])) {
  780. // by now we use these fields only for the network type detection
  781. // So we unset all data that isn't used at the moment
  782. unset($data["dialback"]);
  783. $data["network"] = NETWORK_PUMPIO;
  784. } else
  785. return false;
  786. $profile_data = self::pumpio_profile_data($data["url"]);
  787. if (!$profile_data)
  788. return false;
  789. $data = array_merge($data, $profile_data);
  790. return $data;
  791. }
  792. /**
  793. * @brief Check page for feed link
  794. *
  795. * @param string $url Page link
  796. *
  797. * @return string feed link
  798. */
  799. private function get_feed_link($url) {
  800. $doc = new DOMDocument();
  801. if (!@$doc->loadHTMLFile($url))
  802. return false;
  803. $xpath = new DomXPath($doc);
  804. //$feeds = $xpath->query("/html/head/link[@type='application/rss+xml']");
  805. $feeds = $xpath->query("/html/head/link[@type='application/rss+xml' and @rel='alternate']");
  806. if (!is_object($feeds))
  807. return false;
  808. if ($feeds->length == 0)
  809. return false;
  810. $feed_url = "";
  811. foreach ($feeds AS $feed) {
  812. $attr = array();
  813. foreach ($feed->attributes as $attribute)
  814. $attr[$attribute->name] = trim($attribute->value);
  815. if ($feed_url == "")
  816. $feed_url = $attr["href"];
  817. }
  818. return $feed_url;
  819. }
  820. /**
  821. * @brief Check for feed contact
  822. *
  823. * @param string $url Profile link
  824. * @param boolean $probe Do a probe if the page contains a feed link
  825. *
  826. * @return array feed data
  827. */
  828. private function feed($url, $probe = true) {
  829. $feed = fetch_url($url);
  830. $feed_data = feed_import($feed, $dummy1, $dummy2, $dummy3, true);
  831. if (!$feed_data) {
  832. if (!$probe)
  833. return false;
  834. $feed_url = self::get_feed_link($url);
  835. if (!$feed_url)
  836. return false;
  837. return self::feed($feed_url, false);
  838. }
  839. if ($feed_data["header"]["author-name"] != "")
  840. $data["name"] = $feed_data["header"]["author-name"];
  841. if ($feed_data["header"]["author-nick"] != "")
  842. $data["nick"] = $feed_data["header"]["author-nick"];
  843. if ($feed_data["header"]["author-avatar"] != "")
  844. $data["photo"] = $feed_data["header"]["author-avatar"];
  845. if ($feed_data["header"]["author-id"] != "")
  846. $data["alias"] = $feed_data["header"]["author-id"];
  847. $data["url"] = $url;
  848. $data["poll"] = $url;
  849. if ($feed_data["header"]["author-link"] != "")
  850. $data["baseurl"] = $feed_data["header"]["author-link"];
  851. else
  852. $data["baseurl"] = $data["url"];
  853. $data["network"] = NETWORK_FEED;
  854. return $data;
  855. }
  856. /**
  857. * @brief Check for mail contact
  858. *
  859. * @param string $uri Profile link
  860. * @param integer $uid User ID
  861. *
  862. * @return array mail data
  863. */
  864. private function mail($uri, $uid) {
  865. if (!validate_email($uri))
  866. return false;
  867. $x = q("SELECT `prvkey` FROM `user` WHERE `uid` = %d LIMIT 1", intval($uid));
  868. $r = q("SELECT * FROM `mailacct` WHERE `uid` = %d AND `server` != '' LIMIT 1", intval($uid));
  869. if(count($x) && count($r)) {
  870. $mailbox = construct_mailbox_name($r[0]);
  871. $password = '';
  872. openssl_private_decrypt(hex2bin($r[0]['pass']), $password,$x[0]['prvkey']);
  873. $mbox = email_connect($mailbox,$r[0]['user'], $password);
  874. if(!mbox)
  875. return false;
  876. }
  877. $msgs = email_poll($mbox, $uri);
  878. logger('searching '.$uri.', '.count($msgs).' messages found.', LOGGER_DEBUG);
  879. if (!count($msgs))
  880. return false;
  881. $data = array();
  882. $data["addr"] = $uri;
  883. $data["network"] = NETWORK_MAIL;
  884. $data["name"] = substr($uri, 0, strpos($uri,'@'));
  885. $data["nick"] = $data["name"];
  886. $data["photo"] = avatar_img($uri);
  887. $phost = substr($uri, strpos($uri,'@') + 1);
  888. $data["url"] = 'http://'.$phost."/".$data["nick"];
  889. $data["notify"] = 'smtp '.random_string();
  890. $data["poll"] = 'email '.random_string();
  891. $x = email_msg_meta($mbox, $msgs[0]);
  892. if(stristr($x[0]->from, $uri))
  893. $adr = imap_rfc822_parse_adrlist($x[0]->from, '');
  894. elseif(stristr($x[0]->to, $uri))
  895. $adr = imap_rfc822_parse_adrlist($x[0]->to, '');
  896. if(isset($adr)) {
  897. foreach($adr as $feadr) {
  898. if((strcasecmp($feadr->mailbox, $data["name"]) == 0)
  899. &&(strcasecmp($feadr->host, $phost) == 0)
  900. && (strlen($feadr->personal))) {
  901. $personal = imap_mime_header_decode($feadr->personal);
  902. $data["name"] = "";
  903. foreach($personal as $perspart)
  904. if ($perspart->charset != "default")
  905. $data["name"] .= iconv($perspart->charset, 'UTF-8//IGNORE', $perspart->text);
  906. else
  907. $data["name"] .= $perspart->text;
  908. $data["name"] = notags($data["name"]);
  909. }
  910. }
  911. }
  912. imap_close($mbox);
  913. return $data;
  914. }
  915. }
  916. ?>