Yadis.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <?php
  2. /**
  3. * The core PHP Yadis implementation.
  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. * Need both fetcher types so we can use the right one based on the
  16. * presence or absence of CURL.
  17. */
  18. require_once "Auth/Yadis/PlainHTTPFetcher.php";
  19. require_once "Auth/Yadis/ParanoidHTTPFetcher.php";
  20. /**
  21. * Need this for parsing HTML (looking for META tags).
  22. */
  23. require_once "Auth/Yadis/ParseHTML.php";
  24. /**
  25. * Need this to parse the XRDS document during Yadis discovery.
  26. */
  27. require_once "Auth/Yadis/XRDS.php";
  28. /**
  29. * XRDS (yadis) content type
  30. */
  31. define('Auth_Yadis_CONTENT_TYPE', 'application/xrds+xml');
  32. /**
  33. * Yadis header
  34. */
  35. define('Auth_Yadis_HEADER_NAME', 'X-XRDS-Location');
  36. /**
  37. * Contains the result of performing Yadis discovery on a URI.
  38. *
  39. * @package OpenID
  40. */
  41. class Auth_Yadis_DiscoveryResult {
  42. // The URI that was passed to the fetcher
  43. var $request_uri = null;
  44. // The result of following redirects from the request_uri
  45. var $normalized_uri = null;
  46. // The URI from which the response text was returned (set to
  47. // None if there was no XRDS document found)
  48. var $xrds_uri = null;
  49. var $xrds = null;
  50. // The content-type returned with the response_text
  51. var $content_type = null;
  52. // The document returned from the xrds_uri
  53. var $response_text = null;
  54. // Did the discovery fail miserably?
  55. var $failed = false;
  56. function Auth_Yadis_DiscoveryResult($request_uri)
  57. {
  58. // Initialize the state of the object
  59. // sets all attributes to None except the request_uri
  60. $this->request_uri = $request_uri;
  61. }
  62. function fail()
  63. {
  64. $this->failed = true;
  65. }
  66. function isFailure()
  67. {
  68. return $this->failed;
  69. }
  70. /**
  71. * Returns the list of service objects as described by the XRDS
  72. * document, if this yadis object represents a successful Yadis
  73. * discovery.
  74. *
  75. * @return array $services An array of {@link Auth_Yadis_Service}
  76. * objects
  77. */
  78. function services()
  79. {
  80. if ($this->xrds) {
  81. return $this->xrds->services();
  82. }
  83. return null;
  84. }
  85. function usedYadisLocation()
  86. {
  87. // Was the Yadis protocol's indirection used?
  88. return ($this->xrds_uri && $this->normalized_uri != $this->xrds_uri);
  89. }
  90. function isXRDS()
  91. {
  92. // Is the response text supposed to be an XRDS document?
  93. return ($this->usedYadisLocation() ||
  94. $this->content_type == Auth_Yadis_CONTENT_TYPE);
  95. }
  96. }
  97. /**
  98. *
  99. * Perform the Yadis protocol on the input URL and return an iterable
  100. * of resulting endpoint objects.
  101. *
  102. * input_url: The URL on which to perform the Yadis protocol
  103. *
  104. * @return: The normalized identity URL and an iterable of endpoint
  105. * objects generated by the filter function.
  106. *
  107. * xrds_parse_func: a callback which will take (uri, xrds_text) and
  108. * return an array of service endpoint objects or null. Usually
  109. * array('Auth_OpenID_ServiceEndpoint', 'fromXRDS').
  110. *
  111. * discover_func: if not null, a callback which should take (uri) and
  112. * return an Auth_Yadis_Yadis object or null.
  113. */
  114. function Auth_Yadis_getServiceEndpoints($input_url, $xrds_parse_func,
  115. $discover_func=null, $fetcher=null)
  116. {
  117. if ($discover_func === null) {
  118. $discover_function = array('Auth_Yadis_Yadis', 'discover');
  119. }
  120. $yadis_result = call_user_func_array($discover_func,
  121. array($input_url, &$fetcher));
  122. if ($yadis_result === null) {
  123. return array($input_url, array());
  124. }
  125. $endpoints = call_user_func_array($xrds_parse_func,
  126. array($yadis_result->normalized_uri,
  127. $yadis_result->response_text));
  128. if ($endpoints === null) {
  129. $endpoints = array();
  130. }
  131. return array($yadis_result->normalized_uri, $endpoints);
  132. }
  133. /**
  134. * This is the core of the PHP Yadis library. This is the only class
  135. * a user needs to use to perform Yadis discovery. This class
  136. * performs the discovery AND stores the result of the discovery.
  137. *
  138. * First, require this library into your program source:
  139. *
  140. * <pre> require_once "Auth/Yadis/Yadis.php";</pre>
  141. *
  142. * To perform Yadis discovery, first call the "discover" method
  143. * statically with a URI parameter:
  144. *
  145. * <pre> $http_response = array();
  146. * $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
  147. * $yadis_object = Auth_Yadis_Yadis::discover($uri,
  148. * $http_response, $fetcher);</pre>
  149. *
  150. * If the discovery succeeds, $yadis_object will be an instance of
  151. * {@link Auth_Yadis_Yadis}. If not, it will be null. The XRDS
  152. * document found during discovery should have service descriptions,
  153. * which can be accessed by calling
  154. *
  155. * <pre> $service_list = $yadis_object->services();</pre>
  156. *
  157. * which returns an array of objects which describe each service.
  158. * These objects are instances of Auth_Yadis_Service. Each object
  159. * describes exactly one whole Service element, complete with all of
  160. * its Types and URIs (no expansion is performed). The common use
  161. * case for using the service objects returned by services() is to
  162. * write one or more filter functions and pass those to services():
  163. *
  164. * <pre> $service_list = $yadis_object->services(
  165. * array("filterByURI",
  166. * "filterByExtension"));</pre>
  167. *
  168. * The filter functions (whose names appear in the array passed to
  169. * services()) take the following form:
  170. *
  171. * <pre> function myFilter($service) {
  172. * // Query $service object here. Return true if the service
  173. * // matches your query; false if not.
  174. * }</pre>
  175. *
  176. * This is an example of a filter which uses a regular expression to
  177. * match the content of URI tags (note that the Auth_Yadis_Service
  178. * class provides a getURIs() method which you should use instead of
  179. * this contrived example):
  180. *
  181. * <pre>
  182. * function URIMatcher($service) {
  183. * foreach ($service->getElements('xrd:URI') as $uri) {
  184. * if (preg_match("/some_pattern/",
  185. * $service->parser->content($uri))) {
  186. * return true;
  187. * }
  188. * }
  189. * return false;
  190. * }</pre>
  191. *
  192. * The filter functions you pass will be called for each service
  193. * object to determine which ones match the criteria your filters
  194. * specify. The default behavior is that if a given service object
  195. * matches ANY of the filters specified in the services() call, it
  196. * will be returned. You can specify that a given service object will
  197. * be returned ONLY if it matches ALL specified filters by changing
  198. * the match mode of services():
  199. *
  200. * <pre> $yadis_object->services(array("filter1", "filter2"),
  201. * SERVICES_YADIS_MATCH_ALL);</pre>
  202. *
  203. * See {@link SERVICES_YADIS_MATCH_ALL} and {@link
  204. * SERVICES_YADIS_MATCH_ANY}.
  205. *
  206. * Services described in an XRDS should have a library which you'll
  207. * probably be using. Those libraries are responsible for defining
  208. * filters that can be used with the "services()" call. If you need
  209. * to write your own filter, see the documentation for {@link
  210. * Auth_Yadis_Service}.
  211. *
  212. * @package OpenID
  213. */
  214. class Auth_Yadis_Yadis {
  215. /**
  216. * Returns an HTTP fetcher object. If the CURL extension is
  217. * present, an instance of {@link Auth_Yadis_ParanoidHTTPFetcher}
  218. * is returned. If not, an instance of
  219. * {@link Auth_Yadis_PlainHTTPFetcher} is returned.
  220. *
  221. * If Auth_Yadis_CURL_OVERRIDE is defined, this method will always
  222. * return a {@link Auth_Yadis_PlainHTTPFetcher}.
  223. */
  224. static function getHTTPFetcher($timeout = 20)
  225. {
  226. if (Auth_Yadis_Yadis::curlPresent() &&
  227. (!defined('Auth_Yadis_CURL_OVERRIDE'))) {
  228. $fetcher = new Auth_Yadis_ParanoidHTTPFetcher($timeout);
  229. } else {
  230. $fetcher = new Auth_Yadis_PlainHTTPFetcher($timeout);
  231. }
  232. return $fetcher;
  233. }
  234. static function curlPresent()
  235. {
  236. return function_exists('curl_init');
  237. }
  238. /**
  239. * @access private
  240. */
  241. static function _getHeader($header_list, $names)
  242. {
  243. foreach ($header_list as $name => $value) {
  244. foreach ($names as $n) {
  245. if (strtolower($name) == strtolower($n)) {
  246. return $value;
  247. }
  248. }
  249. }
  250. return null;
  251. }
  252. /**
  253. * @access private
  254. */
  255. static function _getContentType($content_type_header)
  256. {
  257. if ($content_type_header) {
  258. $parts = explode(";", $content_type_header);
  259. return strtolower($parts[0]);
  260. }
  261. }
  262. /**
  263. * This should be called statically and will build a Yadis
  264. * instance if the discovery process succeeds. This implements
  265. * Yadis discovery as specified in the Yadis specification.
  266. *
  267. * @param string $uri The URI on which to perform Yadis discovery.
  268. *
  269. * @param array $http_response An array reference where the HTTP
  270. * response object will be stored (see {@link
  271. * Auth_Yadis_HTTPResponse}.
  272. *
  273. * @param Auth_Yadis_HTTPFetcher $fetcher An instance of a
  274. * Auth_Yadis_HTTPFetcher subclass.
  275. *
  276. * @param array $extra_ns_map An array which maps namespace names
  277. * to namespace URIs to be used when parsing the Yadis XRDS
  278. * document.
  279. *
  280. * @param integer $timeout An optional fetcher timeout, in seconds.
  281. *
  282. * @return mixed $obj Either null or an instance of
  283. * Auth_Yadis_Yadis, depending on whether the discovery
  284. * succeeded.
  285. */
  286. static function discover($uri, $fetcher,
  287. $extra_ns_map = null, $timeout = 20)
  288. {
  289. $result = new Auth_Yadis_DiscoveryResult($uri);
  290. $request_uri = $uri;
  291. $headers = array("Accept: " . Auth_Yadis_CONTENT_TYPE .
  292. ', text/html; q=0.3, application/xhtml+xml; q=0.5');
  293. if ($fetcher === null) {
  294. $fetcher = Auth_Yadis_Yadis::getHTTPFetcher($timeout);
  295. }
  296. $response = $fetcher->get($uri, $headers);
  297. if (!$response || ($response->status != 200 and
  298. $response->status != 206)) {
  299. $result->fail();
  300. return $result;
  301. }
  302. $result->normalized_uri = $response->final_url;
  303. $result->content_type = Auth_Yadis_Yadis::_getHeader(
  304. $response->headers,
  305. array('content-type'));
  306. if ($result->content_type &&
  307. (Auth_Yadis_Yadis::_getContentType($result->content_type) ==
  308. Auth_Yadis_CONTENT_TYPE)) {
  309. $result->xrds_uri = $result->normalized_uri;
  310. } else {
  311. $yadis_location = Auth_Yadis_Yadis::_getHeader(
  312. $response->headers,
  313. array(Auth_Yadis_HEADER_NAME));
  314. if (!$yadis_location) {
  315. $parser = new Auth_Yadis_ParseHTML();
  316. $yadis_location = $parser->getHTTPEquiv($response->body);
  317. }
  318. if ($yadis_location) {
  319. $result->xrds_uri = $yadis_location;
  320. $response = $fetcher->get($yadis_location);
  321. if ((!$response) || ($response->status != 200 and
  322. $response->status != 206)) {
  323. $result->fail();
  324. return $result;
  325. }
  326. $result->content_type = Auth_Yadis_Yadis::_getHeader(
  327. $response->headers,
  328. array('content-type'));
  329. }
  330. }
  331. $result->response_text = $response->body;
  332. return $result;
  333. }
  334. }