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