Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

1223 行
35KB

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