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