PageRenderTime 49ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/www/discovery.inc.php

https://gitlab.com/pirati.cz/simpleid
PHP | 647 lines | 258 code | 73 blank | 316 comment | 84 complexity | 255530454f12113d590edec49b215426 MD5 | raw file
  1. <?php
  2. /*
  3. * SimpleID
  4. *
  5. * Copyright (C) Kelvin Mo 2007-9
  6. *
  7. * This program is free software; you can redistribute it and/or
  8. * modify it under the terms of the GNU General Public
  9. * License as published by the Free Software Foundation; either
  10. * version 2 of the License, or (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. * General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public
  18. * License along with this program; if not, write to the Free
  19. * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  20. *
  21. * $Id$
  22. */
  23. /**
  24. * Support for XRDS based discovery.
  25. *
  26. * The functions for this file supports HTTP-based identifiers. For XRIs, the
  27. * resolution service xri.net is used to resolve to HTTP-based URLs.
  28. *
  29. * @package simpleid
  30. * @since 0.7
  31. * @filesource
  32. */
  33. include_once "http.inc.php";
  34. /**
  35. * The namespace identifier for an XRDS document.
  36. */
  37. define('XRDS_NS', 'xri://$xrds');
  38. /**
  39. * The namespace identifier for XRDS version 2.
  40. */
  41. define('XRD2_NS', 'xri://$xrd*($v*2.0)');
  42. /**
  43. * The namespace identifier for XRDS Simple.
  44. */
  45. define('XRDS_SIMPLE_NS', 'http://xrds-simple.net/core/1.0');
  46. /**
  47. * The type identifier for XRDS Simple.
  48. */
  49. define('XRDS_SIMPLE_TYPE', 'xri://$xrds*simple');
  50. /**
  51. * The namespace identifier for OpenID services.
  52. */
  53. define('XRD_OPENID_NS', 'http://openid.net/xmlns/1.0');
  54. /**
  55. * Obtains the services for particular identifier.
  56. *
  57. * This function attempts to discover and obtain the XRDS document associated
  58. * with the identifier, parses the XRDS document and returns an array of
  59. * services.
  60. *
  61. * If an XRDS document is not found, and $openid is set to true, this function
  62. * will also attempt to discover OpenID services by looking for link elements
  63. * with rel of openid.server or openid2.provider in the discovered HTML document.
  64. *
  65. * @param string $identifier the identifier
  66. * @param bool $openid if true, performs additional discovery of OpenID services
  67. * by looking for link elements within the discovered document
  68. * @return array an array of discovered services, or an empty array if no services
  69. * are found
  70. */
  71. function discovery_xrds_discover($identifier, $openid = FALSE) {
  72. $identifier = discovery_xrds_normalize($identifier);
  73. $url = discovery_xrds_url($identifier);
  74. $xrds = discovery_xrds_get($url);
  75. if ($xrds) {
  76. return discovery_xrds_parse($xrds);
  77. } else {
  78. if ($openid) return discovery_html_get_services($url);
  79. return array();
  80. }
  81. }
  82. /**
  83. * Given an array of discovered services, obtains information on services of
  84. * a particular type.
  85. *
  86. * @param array $services the discovered services
  87. * @param string $type the URI of the type of service to obtain
  88. * @return array an array of matching services, or an empty array of no services
  89. * match
  90. */
  91. function discovery_xrds_services_by_type($services, $type) {
  92. $matches = array();
  93. foreach ($services as $service) {
  94. foreach ($service['type'] as $service_type) {
  95. if ($service_type == $type) $matches[] = $service;
  96. }
  97. }
  98. return $matches;
  99. }
  100. /**
  101. * Given an array of discovered services, obtains information on the service of
  102. * a specified ID.
  103. *
  104. * @param array $services the discovered services
  105. * @param string $id the XML ID of the service in the XRDS document
  106. * @return array the matching service, or NULL of no services
  107. * are found
  108. */
  109. function discovery_xrds_service_by_id($services, $id) {
  110. foreach ($services as $service) {
  111. if ($service['#id'] == $id) return $service;
  112. }
  113. return NULL;
  114. }
  115. /**
  116. * Obtains a XRDS document at a particular URL. Performs Yadis discovery if
  117. * the URL does not produce a XRDS document.
  118. *
  119. * @param string $url the URL
  120. * @param bool $check whether to check the content type of the response is
  121. * application/xrds+xml
  122. * @param int $retries the number of tries to make
  123. * @return string the contents of the XRDS document
  124. */
  125. function discovery_xrds_get($url, $check = TRUE, $retries = 5) {
  126. if ($retries == 0) return NULL;
  127. $response = http_make_request($url, array('Accept' => 'application/xrds+xml'));
  128. if (isset($response['http-error'])) return NULL;
  129. if (($response['content-type'] == 'application/xrds+xml') || ($check == FALSE)) {
  130. return $response['data'];
  131. } elseif (isset($response['headers']['x-xrds-location'])) {
  132. return discovery_xrds_get($response['headers']['x-xrds-location'], false, $retries - 1);
  133. } else {
  134. $location = _discovery_meta_httpequiv('X-XRDS-Location', $response['data']);
  135. if ($location) {
  136. return discovery_xrds_get($location, false, $retries - 1);
  137. }
  138. return NULL;
  139. }
  140. }
  141. /**
  142. * Normalises an identifier for discovery.
  143. *
  144. * If the identifier begins with xri://, acct: or mailto:, this is stripped out. If the identifier
  145. * does not begin with a valid URI scheme, http:// is assumed and added to the
  146. * identifier.
  147. *
  148. * @param string $identifier the identifier to normalise
  149. * @return string the normalised identifier
  150. */
  151. function discovery_xrds_normalize($identifier) {
  152. $normalized = $identifier;
  153. if (discovery_is_xri($identifier)) {
  154. if (stristr($identifier, 'xri://') !== false) $normalized = substr($identifier, 6);
  155. } elseif (discovery_is_email($identifier)) {
  156. if (stristr($identifier, 'acct:') !== false) $normalized = substr($identifier, 5);
  157. if (stristr($identifier, 'mailto:') !== false) $normalized = substr($identifier, 7);
  158. } else {
  159. if (stristr($identifier, '://') === false) $normalized = 'http://'. $identifier;
  160. if (substr_count($normalized, '/') < 3) $normalized .= '/';
  161. }
  162. return $normalized;
  163. }
  164. /**
  165. * Obtains a URL for an identifier. If the identifier is a XRI, the XRI resolution
  166. * service is used to convert the identifier to a URL.
  167. *
  168. * @param string $identifier the identifier
  169. * @return string the URL
  170. */
  171. function discovery_xrds_url($identifier) {
  172. if (discovery_is_xri($identifier)) {
  173. return 'http://xri.net/' . $identifier;
  174. } elseif (discovery_is_email($identifier)) {
  175. //list($user, $host) = explode('@', $identifier, 2);
  176. //$host_meta = 'http://' . $host . '/.well-known/host-meta';
  177. } else {
  178. return $identifier;
  179. }
  180. }
  181. /**
  182. * Determines whether an identifier is an XRI.
  183. *
  184. * XRI identifiers either start with xri:// or with @, =, +, $ or !.
  185. *
  186. * @param string $identifier the parameter to test
  187. * @return bool true if the identifier is an XRI
  188. */
  189. function discovery_is_xri($identifier) {
  190. $firstchar = substr($identifier, 0, 1);
  191. if ($firstchar == "@" || $firstchar == "=" || $firstchar == "+" || $firstchar == "\$" || $firstchar == "!") return true;
  192. if (stristr($identifier, 'xri://') !== FALSE) return true;
  193. return false;
  194. }
  195. /**
  196. * Determines whether an identifier is an e-mail address.
  197. *
  198. * An identifier is an e-mail address if it:
  199. *
  200. * - has a single @ character
  201. * - does not have a slash character
  202. *
  203. * @param string $identifier the parameter to test
  204. * @return bool true if the identifier is an e-mail address
  205. */
  206. function discovery_is_email($identifier) {
  207. // If it begins with acct: or mailto:, strip it out
  208. if (stristr($identifier, 'acct:') !== false) $identifier = substr($identifier, 5);
  209. if (stristr($identifier, 'mailto:') !== false) $identifier = substr($identifier, 7);
  210. // If it contains a slash, it is not an e-mail address
  211. if (strpos($identifier, "/") !== false) return false;
  212. $at = strpos($identifier, "@");
  213. // If it does not contain a @, it is not an e-mail address
  214. if ($at === false) return false;
  215. // If it contains more than one @, it is not an e-mail
  216. if (strrpos($identifier, "@") != $at) return false;
  217. return true;
  218. }
  219. /**
  220. * Callback function to sort service and URI elements based on priorities
  221. * specified in the XRDS document.
  222. *
  223. * The XRDS specification allows multiple instances of certain elements, such
  224. * as Service and URI. The specification allows an attribute called priority
  225. * so that the document creator can specify the order the elements should be used.
  226. *
  227. * @param array $a
  228. * @param array $b
  229. * @return int
  230. */
  231. function discovery_xrds_priority_sort($a, $b) {
  232. if (!isset($a['#priority']) && !isset($b['#priority'])) return 0;
  233. // if #priority is missing, #priority is assumed to be infinity
  234. if (!isset($a['#priority'])) return 1;
  235. if (!isset($b['#priority'])) return -1;
  236. if ($a['#priority'] == $b['#priority']) return 0;
  237. return ($a['#priority'] < $b['#priority']) ? -1 : 1;
  238. }
  239. /**
  240. * Parses an XRDS document to return services available.
  241. *
  242. * @param string $xrds the XRDS document
  243. * @return array the parsed structure
  244. *
  245. * @see XRDSParser
  246. */
  247. function discovery_xrds_parse($xrds) {
  248. $parser = new XRDSParser();
  249. $parser->parse($xrds);
  250. $parser->free();
  251. $services = $parser->services();
  252. uasort($services, 'discovery_xrds_priority_sort');
  253. return $services;
  254. }
  255. /**
  256. * Obtains the OpenID services for particular identifier by scanning for link
  257. * elements in the returned document.
  258. *
  259. * Note that this function does not use the YADIS protocol to scan for services.
  260. * To use the YADIS protocol, use {@link discovery_get_services()}.
  261. *
  262. * @param string $url the URL
  263. * @return array an array of discovered services, or an empty array if no services
  264. * are found
  265. */
  266. function discovery_html_get_services($url) {
  267. $services = array();
  268. $response = http_make_request($url);
  269. $html = $response['data'];
  270. $uri = _discovery_link_rel('openid2.provider', $html);
  271. $delegate = _discovery_link_rel('openid2.local_id', $html);
  272. if ($uri) {
  273. $service = array(
  274. 'type' => 'http://specs.openid.net/auth/2.0/signon',
  275. 'uri' => $uri
  276. );
  277. if ($delegate) $service['localid'] = $delegate;
  278. $services[] = $service;
  279. }
  280. $uri = _discovery_link_rel('openid.server', $html);
  281. $delegate = _discovery_link_rel('openid.delegate', $html);
  282. if ($uri) {
  283. $service = array(
  284. 'type' => 'http://openid.net/signon/1.0',
  285. 'uri' => $uri
  286. );
  287. if ($delegate) $service['localid'] = $delegate;
  288. $services[] = $service;
  289. }
  290. return $services;
  291. }
  292. /**
  293. * Searches through an HTML document to obtain the value of a meta
  294. * element with a specified http-equiv attribute.
  295. *
  296. * @param string $equiv the http-equiv attribute for which to search
  297. * @param string $html the HTML document to search
  298. * @return mixed the value of the meta element, or FALSE if the element is not
  299. * found
  300. */
  301. function _discovery_meta_httpequiv($equiv, $html) {
  302. $html = preg_replace('/<!(?:--(?:[^-]*|-[^-]+)*--\s*)>/', '', $html); // Strip html comments
  303. $equiv = preg_quote($equiv);
  304. preg_match('|<meta\s+http-equiv=["\']'. $equiv .'["\'](.*)/?>|iUs', $html, $matches);
  305. if (isset($matches[1])) {
  306. preg_match('|content=["\']([^"]+)["\']|iUs', $matches[1], $content);
  307. if (isset($content[1])) {
  308. return $content[1];
  309. }
  310. }
  311. return FALSE;
  312. }
  313. /**
  314. * Searches through an HTML document to obtain the value of a link
  315. * element with a specified rel attribute.
  316. *
  317. * @param string $rel the rel attribute for which to search
  318. * @param string $html the HTML document to search
  319. * @return mixed the href of the link element, or FALSE if the element is not
  320. * found
  321. */
  322. function _discovery_link_rel($rel, $html) {
  323. $html = preg_replace('/<!(?:--(?:[^-]*|-[^-]+)*--\s*)>/s', '', $html); // Strip html comments
  324. $rel = preg_quote($rel);
  325. preg_match('|<link\s+rel=["\'](.*)'. $rel .'(.*)["\'](.*)/?>|iUs', $html, $matches);
  326. if (isset($matches[3])) {
  327. preg_match('|href=["\']([^"]+)["\']|iU', $matches[3], $href);
  328. return trim($href[1]);
  329. }
  330. return FALSE;
  331. }
  332. /**
  333. * A simple XRDS parser.
  334. *
  335. * This parser uses the classic expat functions available in PHP to parse the
  336. * XRDS Simple XML document.
  337. *
  338. * The result is an array of discovered services.
  339. *
  340. * @link http://xrds-simple.net/
  341. */
  342. class XRDSParser {
  343. /**
  344. * XML parser
  345. * @var resource
  346. * @access private
  347. */
  348. var $parser;
  349. /**
  350. * Discovered services
  351. * @var array
  352. * @access private
  353. */
  354. var $services = array();
  355. /**
  356. * State: are we parsing a service element?
  357. * @var bool
  358. * @access private
  359. */
  360. var $in_service = FALSE;
  361. /**
  362. * CDATA buffer
  363. * @var string
  364. * @access private
  365. */
  366. var $_buffer;
  367. /**
  368. * Attributes buffer
  369. * @var array
  370. * @access private
  371. */
  372. var $_attribs = array();
  373. /**
  374. * priority attribute buffer
  375. * @var string
  376. * @access private
  377. */
  378. var $priority = NULL;
  379. /**
  380. * Currently parsed service buffer
  381. * @var array
  382. * @access private
  383. */
  384. var $service = array();
  385. /**
  386. * Creates an instance of the XRDS parser.
  387. *
  388. * This constructor also initialises the underlying XML parser.
  389. */
  390. function XRDSParser() {
  391. $this->parser = xml_parser_create_ns();
  392. xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING,0);
  393. xml_set_object($this->parser, $this);
  394. xml_set_element_handler($this->parser, 'element_start', 'element_end');
  395. xml_set_character_data_handler($this->parser, 'cdata');
  396. }
  397. /**
  398. * Frees memory associated with the underlying XML parser.
  399. *
  400. * Note that only the memory associated with the underlying XML parser is
  401. * freed. Memory associated with the class itself is not freed.
  402. *
  403. * @access public
  404. */
  405. function free() {
  406. xml_parser_free($this->parser);
  407. }
  408. /**
  409. * Parses an XRDS document.
  410. *
  411. * Once the parsing is complete, use {@link XRDSParser::services()} to obtain
  412. * the services extracted from the document.
  413. *
  414. * @param string $xml the XML document to parse
  415. * @access public
  416. */
  417. function parse($xml) {
  418. xml_parse($this->parser, $xml);
  419. }
  420. /**
  421. * Gets an array of discovered services.
  422. *
  423. * @return array an array of discovered services, or an empty array
  424. * @access public
  425. * @see XRDSParser::parse()
  426. */
  427. function services() {
  428. return $this->services;
  429. }
  430. /**
  431. * XML parser callback
  432. *
  433. * @access private
  434. */
  435. function element_start(&$parser, $qualified, $attribs) {
  436. list($ns, $name) = $this->parse_namespace($qualified);
  437. // Strictly speaking, XML namespace URIs are semi-case sensitive
  438. // (i.e. the scheme and host are not case sensitive, but other elements
  439. // are). However, the XRDS-Simple specifications defines a
  440. // namespace URI for XRD (xri://$XRD*($v*2.0) rather than xri://$xrd*($v*2.0))
  441. // with an unusual case.
  442. if ((strtolower($ns) == strtolower(XRD2_NS)) && ($name == 'Service')) {
  443. $this->in_service = TRUE;
  444. $this->service = array();
  445. if (in_array('priority', $attribs)) {
  446. $this->service['#priority'] = $attribs['priority'];
  447. }
  448. if (in_array('id', $attribs)) {
  449. $this->service['#id'] = $attribs['id'];
  450. }
  451. }
  452. if ((strtolower($ns) == strtolower(XRD2_NS)) && ($this->in_service)) {
  453. switch ($name) {
  454. case 'Type':
  455. case 'LocalID':
  456. case 'URI':
  457. if (in_array('priority', $attribs)) {
  458. $this->priority = $attribs['priority'];
  459. } else {
  460. $this->priority = NULL;
  461. }
  462. }
  463. }
  464. $this->_buffer = '';
  465. $this->_attribs = $attribs;
  466. }
  467. /**
  468. * XML parser callback
  469. *
  470. * @access private
  471. */
  472. function element_end(&$parser, $qualified) {
  473. list($ns, $name) = $this->parse_namespace($qualified);
  474. if ((strtolower($ns) == strtolower(XRD2_NS)) && ($this->in_service)) {
  475. switch ($name) {
  476. case 'Service':
  477. foreach (array('type', 'localid', 'uri') as $key) {
  478. if (!isset($this->service[$key])) continue;
  479. $this->service[$key] = $this->flatten_uris($this->service[$key]);
  480. }
  481. $this->services[] = $this->service;
  482. $this->in_service = FALSE;
  483. break;
  484. case 'Type':
  485. case 'LocalID':
  486. case 'URI':
  487. $key = strtolower($name);
  488. if (!isset($this->service[$key])) {
  489. $this->service[$key] = array();
  490. }
  491. if ($this->priority != NULL) {
  492. $this->service[$key][] = array('#uri' => trim($this->_buffer), '#priority' => $this->priority);
  493. } else {
  494. $this->service[$key][] = array('#uri' => trim($this->_buffer));
  495. }
  496. $this->priority = NULL;
  497. break;
  498. }
  499. }
  500. if ((strtolower($ns) == strtolower(XRD_OPENID_NS)) && ($this->in_service)) {
  501. switch ($name) {
  502. case 'Delegate':
  503. $this->service['delegate'] = trim($this->_buffer);
  504. }
  505. }
  506. $this->_attribs = array();
  507. }
  508. /**
  509. * XML parser callback
  510. *
  511. * @access private
  512. */
  513. function cdata(&$parser, $data) {
  514. $this->_buffer .= $data;
  515. }
  516. /**
  517. * Parses a namespace-qualified element name.
  518. *
  519. * @param string $qualified the qualified name
  520. * @return array an array with two elements - the first element contains
  521. * the namespace qualifier (or an empty string), the second element contains
  522. * the element name
  523. * @access protected
  524. */
  525. function parse_namespace($qualified) {
  526. $pos = strrpos($qualified, ':');
  527. if ($pos !== FALSE) return array(substr($qualified, 0, $pos), substr($qualified, $pos + 1, strlen($qualified)));
  528. return array('', $qualified);
  529. }
  530. /**
  531. * Flattens the service array.
  532. *
  533. * In an XRDS document, child elements of the service element often contains
  534. * a list of URIs, with the priority specified in the priority attribute.
  535. *
  536. * When the document is parsed in this class, the URI and the priority are first
  537. * extracted into the #uri and the #priority keys respectively. This function
  538. * takes this array, sorts the elements using the #priority keys (if $sort is
  539. * true), then collapses the array using the value associated with the #uri key.
  540. *
  541. * @param array $array the service array, with URIs and priorities
  542. * @param bool $sort whether to sort the service array using the #priority
  543. * keys
  544. * @return array the services array with URIs sorted by priority
  545. * @access protected
  546. */
  547. function flatten_uris($array, $sort = TRUE) {
  548. $result = array();
  549. if ($sort) uasort($array, 'discovery_xrds_priority_sort');
  550. for ($i = 0; $i < count($array); $i++) {
  551. $result[] = $array[$i]['#uri'];
  552. }
  553. return $result;
  554. }
  555. }
  556. if (!function_exists('rfc3986_urlencode')) {
  557. /**
  558. * Encodes a URL using RFC 3986.
  559. *
  560. * PHP's rawurlencode function encodes a URL using RFC 1738. RFC 1738 has been
  561. * updated by RFC 3986, which change the list of characters which needs to be
  562. * encoded.
  563. *
  564. * Strictly correct encoding is required for various purposes, such as OAuth
  565. * signature base strings.
  566. *
  567. * @param string $s the URL to encode
  568. * @return string the encoded URL
  569. */
  570. function rfc3986_urlencode($s) {
  571. return str_replace('%7E', '~', rawurlencode($s));
  572. }
  573. }
  574. ?>