XRDS.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <?php
  2. /**
  3. * This module contains the XRDS parsing code.
  4. *
  5. * PHP versions 4 and 5
  6. *
  7. * LICENSE: See the COPYING file included in this distribution.
  8. *
  9. * @package OpenID
  10. * @author JanRain, Inc. <openid@janrain.com>
  11. * @copyright 2005-2008 Janrain, Inc.
  12. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
  13. */
  14. /**
  15. * Require the XPath implementation.
  16. */
  17. require_once 'Auth/Yadis/XML.php';
  18. /**
  19. * This match mode means a given service must match ALL filters passed
  20. * to the Auth_Yadis_XRDS::services() call.
  21. */
  22. define('SERVICES_YADIS_MATCH_ALL', 101);
  23. /**
  24. * This match mode means a given service must match ANY filters (at
  25. * least one) passed to the Auth_Yadis_XRDS::services() call.
  26. */
  27. define('SERVICES_YADIS_MATCH_ANY', 102);
  28. /**
  29. * The priority value used for service elements with no priority
  30. * specified.
  31. */
  32. define('SERVICES_YADIS_MAX_PRIORITY', pow(2, 30));
  33. /**
  34. * XRD XML namespace
  35. */
  36. define('Auth_Yadis_XMLNS_XRD_2_0', 'xri://$xrd*($v*2.0)');
  37. /**
  38. * XRDS XML namespace
  39. */
  40. define('Auth_Yadis_XMLNS_XRDS', 'xri://$xrds');
  41. function Auth_Yadis_getNSMap()
  42. {
  43. return array('xrds' => Auth_Yadis_XMLNS_XRDS,
  44. 'xrd' => Auth_Yadis_XMLNS_XRD_2_0);
  45. }
  46. /**
  47. * @access private
  48. */
  49. function Auth_Yadis_array_scramble($arr)
  50. {
  51. $result = array();
  52. while (count($arr)) {
  53. $index = array_rand($arr, 1);
  54. $result[] = $arr[$index];
  55. unset($arr[$index]);
  56. }
  57. return $result;
  58. }
  59. /**
  60. * This class represents a <Service> element in an XRDS document.
  61. * Objects of this type are returned by
  62. * Auth_Yadis_XRDS::services() and
  63. * Auth_Yadis_Yadis::services(). Each object corresponds directly
  64. * to a <Service> element in the XRDS and supplies a
  65. * getElements($name) method which you should use to inspect the
  66. * element's contents. See {@link Auth_Yadis_Yadis} for more
  67. * information on the role this class plays in Yadis discovery.
  68. *
  69. * @package OpenID
  70. */
  71. class Auth_Yadis_Service {
  72. /**
  73. * Creates an empty service object.
  74. */
  75. function Auth_Yadis_Service()
  76. {
  77. $this->element = null;
  78. $this->parser = null;
  79. }
  80. /**
  81. * Return the URIs in the "Type" elements, if any, of this Service
  82. * element.
  83. *
  84. * @return array $type_uris An array of Type URI strings.
  85. */
  86. function getTypes()
  87. {
  88. $t = array();
  89. foreach ($this->getElements('xrd:Type') as $elem) {
  90. $c = $this->parser->content($elem);
  91. if ($c) {
  92. $t[] = $c;
  93. }
  94. }
  95. return $t;
  96. }
  97. function matchTypes($type_uris)
  98. {
  99. $result = array();
  100. foreach ($this->getTypes() as $typ) {
  101. if (in_array($typ, $type_uris)) {
  102. $result[] = $typ;
  103. }
  104. }
  105. return $result;
  106. }
  107. /**
  108. * Return the URIs in the "URI" elements, if any, of this Service
  109. * element. The URIs are returned sorted in priority order.
  110. *
  111. * @return array $uris An array of URI strings.
  112. */
  113. function getURIs()
  114. {
  115. $uris = array();
  116. $last = array();
  117. foreach ($this->getElements('xrd:URI') as $elem) {
  118. $uri_string = $this->parser->content($elem);
  119. $attrs = $this->parser->attributes($elem);
  120. if ($attrs &&
  121. array_key_exists('priority', $attrs)) {
  122. $priority = intval($attrs['priority']);
  123. if (!array_key_exists($priority, $uris)) {
  124. $uris[$priority] = array();
  125. }
  126. $uris[$priority][] = $uri_string;
  127. } else {
  128. $last[] = $uri_string;
  129. }
  130. }
  131. $keys = array_keys($uris);
  132. sort($keys);
  133. // Rebuild array of URIs.
  134. $result = array();
  135. foreach ($keys as $k) {
  136. $new_uris = Auth_Yadis_array_scramble($uris[$k]);
  137. $result = array_merge($result, $new_uris);
  138. }
  139. $result = array_merge($result,
  140. Auth_Yadis_array_scramble($last));
  141. return $result;
  142. }
  143. /**
  144. * Returns the "priority" attribute value of this <Service>
  145. * element, if the attribute is present. Returns null if not.
  146. *
  147. * @return mixed $result Null or integer, depending on whether
  148. * this Service element has a 'priority' attribute.
  149. */
  150. function getPriority()
  151. {
  152. $attributes = $this->parser->attributes($this->element);
  153. if (array_key_exists('priority', $attributes)) {
  154. return intval($attributes['priority']);
  155. }
  156. return null;
  157. }
  158. /**
  159. * Used to get XML elements from this object's <Service> element.
  160. *
  161. * This is what you should use to get all custom information out
  162. * of this element. This is used by service filter functions to
  163. * determine whether a service element contains specific tags,
  164. * etc. NOTE: this only considers elements which are direct
  165. * children of the <Service> element for this object.
  166. *
  167. * @param string $name The name of the element to look for
  168. * @return array $list An array of elements with the specified
  169. * name which are direct children of the <Service> element. The
  170. * nodes returned by this function can be passed to $this->parser
  171. * methods (see {@link Auth_Yadis_XMLParser}).
  172. */
  173. function getElements($name)
  174. {
  175. return $this->parser->evalXPath($name, $this->element);
  176. }
  177. }
  178. /*
  179. * Return the expiration date of this XRD element, or None if no
  180. * expiration was specified.
  181. *
  182. * @param $default The value to use as the expiration if no expiration
  183. * was specified in the XRD.
  184. */
  185. function Auth_Yadis_getXRDExpiration($xrd_element, $default=null)
  186. {
  187. $expires_element = $xrd_element->$parser->evalXPath('/xrd:Expires');
  188. if ($expires_element === null) {
  189. return $default;
  190. } else {
  191. $expires_string = $expires_element->text;
  192. // Will raise ValueError if the string is not the expected
  193. // format
  194. $t = strptime($expires_string, "%Y-%m-%dT%H:%M:%SZ");
  195. if ($t === false) {
  196. return false;
  197. }
  198. // [int $hour [, int $minute [, int $second [,
  199. // int $month [, int $day [, int $year ]]]]]]
  200. return mktime($t['tm_hour'], $t['tm_min'], $t['tm_sec'],
  201. $t['tm_mon'], $t['tm_day'], $t['tm_year']);
  202. }
  203. }
  204. /**
  205. * This class performs parsing of XRDS documents.
  206. *
  207. * You should not instantiate this class directly; rather, call
  208. * parseXRDS statically:
  209. *
  210. * <pre> $xrds = Auth_Yadis_XRDS::parseXRDS($xml_string);</pre>
  211. *
  212. * If the XRDS can be parsed and is valid, an instance of
  213. * Auth_Yadis_XRDS will be returned. Otherwise, null will be
  214. * returned. This class is used by the Auth_Yadis_Yadis::discover
  215. * method.
  216. *
  217. * @package OpenID
  218. */
  219. class Auth_Yadis_XRDS {
  220. /**
  221. * Instantiate a Auth_Yadis_XRDS object. Requires an XPath
  222. * instance which has been used to parse a valid XRDS document.
  223. */
  224. function Auth_Yadis_XRDS($xmlParser, $xrdNodes)
  225. {
  226. $this->parser = $xmlParser;
  227. $this->xrdNode = $xrdNodes[count($xrdNodes) - 1];
  228. $this->allXrdNodes = $xrdNodes;
  229. $this->serviceList = array();
  230. $this->_parse();
  231. }
  232. /**
  233. * Parse an XML string (XRDS document) and return either a
  234. * Auth_Yadis_XRDS object or null, depending on whether the
  235. * XRDS XML is valid.
  236. *
  237. * @param string $xml_string An XRDS XML string.
  238. * @return mixed $xrds An instance of Auth_Yadis_XRDS or null,
  239. * depending on the validity of $xml_string
  240. */
  241. static function parseXRDS($xml_string, $extra_ns_map = null)
  242. {
  243. $_null = null;
  244. if (!$xml_string) {
  245. return $_null;
  246. }
  247. $parser = Auth_Yadis_getXMLParser();
  248. $ns_map = Auth_Yadis_getNSMap();
  249. if ($extra_ns_map && is_array($extra_ns_map)) {
  250. $ns_map = array_merge($ns_map, $extra_ns_map);
  251. }
  252. if (!($parser && $parser->init($xml_string, $ns_map))) {
  253. return $_null;
  254. }
  255. // Try to get root element.
  256. $root = $parser->evalXPath('/xrds:XRDS[1]');
  257. if (!$root) {
  258. return $_null;
  259. }
  260. if (is_array($root)) {
  261. $root = $root[0];
  262. }
  263. $attrs = $parser->attributes($root);
  264. if (array_key_exists('xmlns:xrd', $attrs) &&
  265. $attrs['xmlns:xrd'] != Auth_Yadis_XMLNS_XRDS) {
  266. return $_null;
  267. } else if (array_key_exists('xmlns', $attrs) &&
  268. preg_match('/xri/', $attrs['xmlns']) &&
  269. $attrs['xmlns'] != Auth_Yadis_XMLNS_XRD_2_0) {
  270. return $_null;
  271. }
  272. // Get the last XRD node.
  273. $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD');
  274. if (!$xrd_nodes) {
  275. return $_null;
  276. }
  277. $xrds = new Auth_Yadis_XRDS($parser, $xrd_nodes);
  278. return $xrds;
  279. }
  280. /**
  281. * @access private
  282. */
  283. function _addService($priority, $service)
  284. {
  285. $priority = intval($priority);
  286. if (!array_key_exists($priority, $this->serviceList)) {
  287. $this->serviceList[$priority] = array();
  288. }
  289. $this->serviceList[$priority][] = $service;
  290. }
  291. /**
  292. * Creates the service list using nodes from the XRDS XML
  293. * document.
  294. *
  295. * @access private
  296. */
  297. function _parse()
  298. {
  299. $this->serviceList = array();
  300. $services = $this->parser->evalXPath('xrd:Service', $this->xrdNode);
  301. foreach ($services as $node) {
  302. $s = new Auth_Yadis_Service();
  303. $s->element = $node;
  304. $s->parser = $this->parser;
  305. $priority = $s->getPriority();
  306. if ($priority === null) {
  307. $priority = SERVICES_YADIS_MAX_PRIORITY;
  308. }
  309. $this->_addService($priority, $s);
  310. }
  311. }
  312. /**
  313. * Returns a list of service objects which correspond to <Service>
  314. * elements in the XRDS XML document for this object.
  315. *
  316. * Optionally, an array of filter callbacks may be given to limit
  317. * the list of returned service objects. Furthermore, the default
  318. * mode is to return all service objects which match ANY of the
  319. * specified filters, but $filter_mode may be
  320. * SERVICES_YADIS_MATCH_ALL if you want to be sure that the
  321. * returned services match all the given filters. See {@link
  322. * Auth_Yadis_Yadis} for detailed usage information on filter
  323. * functions.
  324. *
  325. * @param mixed $filters An array of callbacks to filter the
  326. * returned services, or null if all services are to be returned.
  327. * @param integer $filter_mode SERVICES_YADIS_MATCH_ALL or
  328. * SERVICES_YADIS_MATCH_ANY, depending on whether the returned
  329. * services should match ALL or ANY of the specified filters,
  330. * respectively.
  331. * @return mixed $services An array of {@link
  332. * Auth_Yadis_Service} objects if $filter_mode is a valid
  333. * mode; null if $filter_mode is an invalid mode (i.e., not
  334. * SERVICES_YADIS_MATCH_ANY or SERVICES_YADIS_MATCH_ALL).
  335. */
  336. function services($filters = null,
  337. $filter_mode = SERVICES_YADIS_MATCH_ANY)
  338. {
  339. $pri_keys = array_keys($this->serviceList);
  340. sort($pri_keys, SORT_NUMERIC);
  341. // If no filters are specified, return the entire service
  342. // list, ordered by priority.
  343. if (!$filters ||
  344. (!is_array($filters))) {
  345. $result = array();
  346. foreach ($pri_keys as $pri) {
  347. $result = array_merge($result, $this->serviceList[$pri]);
  348. }
  349. return $result;
  350. }
  351. // If a bad filter mode is specified, return null.
  352. if (!in_array($filter_mode, array(SERVICES_YADIS_MATCH_ANY,
  353. SERVICES_YADIS_MATCH_ALL))) {
  354. return null;
  355. }
  356. // Otherwise, use the callbacks in the filter list to
  357. // determine which services are returned.
  358. $filtered = array();
  359. foreach ($pri_keys as $priority_value) {
  360. $service_obj_list = $this->serviceList[$priority_value];
  361. foreach ($service_obj_list as $service) {
  362. $matches = 0;
  363. foreach ($filters as $filter) {
  364. if (call_user_func_array($filter, array($service))) {
  365. $matches++;
  366. if ($filter_mode == SERVICES_YADIS_MATCH_ANY) {
  367. $pri = $service->getPriority();
  368. if ($pri === null) {
  369. $pri = SERVICES_YADIS_MAX_PRIORITY;
  370. }
  371. if (!array_key_exists($pri, $filtered)) {
  372. $filtered[$pri] = array();
  373. }
  374. $filtered[$pri][] = $service;
  375. break;
  376. }
  377. }
  378. }
  379. if (($filter_mode == SERVICES_YADIS_MATCH_ALL) &&
  380. ($matches == count($filters))) {
  381. $pri = $service->getPriority();
  382. if ($pri === null) {
  383. $pri = SERVICES_YADIS_MAX_PRIORITY;
  384. }
  385. if (!array_key_exists($pri, $filtered)) {
  386. $filtered[$pri] = array();
  387. }
  388. $filtered[$pri][] = $service;
  389. }
  390. }
  391. }
  392. $pri_keys = array_keys($filtered);
  393. sort($pri_keys, SORT_NUMERIC);
  394. $result = array();
  395. foreach ($pri_keys as $pri) {
  396. $result = array_merge($result, $filtered[$pri]);
  397. }
  398. return $result;
  399. }
  400. }