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