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.

1223 lines
35KB

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