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