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: */
20: require_once "Auth/Yadis/PlainHTTPFetcher.php";
21: require_once "Auth/Yadis/ParanoidHTTPFetcher.php";
22:
23: /**
24: * Need this for parsing HTML (looking for META tags).
25: */
26: require_once "Auth/Yadis/ParseHTML.php";
27:
28: /**
29: * Need this to parse the XRDS document during Yadis discovery.
30: */
31: require_once "Auth/Yadis/XRDS.php";
32:
33: /**
34: * XRDS (yadis) content type
35: */
36: define('Auth_Yadis_CONTENT_TYPE', 'application/xrds+xml');
37:
38: /**
39: * Yadis header
40: */
41: define('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: */
48: class 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: */
136: function 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: */
242: class 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: