1<?php
2
3/**
4 * Yadis service manager to be used during yadis-driven authentication
5 * attempts.
6 *
7 * @package OpenID
8 */
9
10/**
11 * The base session class used by the Auth_Yadis_Manager.  This
12 * class wraps the default PHP session machinery and should be
13 * subclassed if your application doesn't use PHP sessioning.
14 *
15 * @package OpenID
16 */
17class Auth_Yadis_PHPSession {
18    /**
19     * Set a session key/value pair.
20     *
21     * @param string $name The name of the session key to add.
22     * @param mixed $value The value to add to the session.
23     */
24    function set($name, $value)
25    {
26        $_SESSION[$name] = $value;
27    }
28
29    /**
30     * Get a key's value from the session.
31     *
32     * @param string $name The name of the key to retrieve.
33     * @param string $default The optional value to return if the key
34     * is not found in the session.
35     * @return mixed $result The key's value in the session or
36     * $default if it isn't found.
37     */
38    function get($name, $default=null)
39    {
40        if (isset($_SESSION) && array_key_exists($name, $_SESSION)) {
41            return $_SESSION[$name];
42        } else {
43            return $default;
44        }
45    }
46
47    /**
48     * Remove a key/value pair from the session.
49     *
50     * @param string $name The name of the key to remove.
51     */
52    function del($name)
53    {
54        unset($_SESSION[$name]);
55    }
56
57    /**
58     * Return the contents of the session in array form.
59     */
60    function contents()
61    {
62        return $_SESSION;
63    }
64}
65
66/**
67 * A session helper class designed to translate between arrays and
68 * objects.  Note that the class used must have a constructor that
69 * takes no parameters.  This is not a general solution, but it works
70 * for dumb objects that just need to have attributes set.  The idea
71 * is that you'll subclass this and override $this->check($data) ->
72 * bool to implement your own session data validation.
73 *
74 * @package OpenID
75 */
76abstract class Auth_Yadis_SessionLoader {
77    /**
78     * Override this.
79     *
80     * @access private
81     * @param array $data
82     * @return bool
83     */
84    function check($data)
85    {
86        return true;
87    }
88
89    public abstract function requiredKeys();
90
91    /**
92     * Given a session data value (an array), this creates an object
93     * (returned by $this->newObject()) whose attributes and values
94     * are those in $data.  Returns null if $data lacks keys found in
95     * $this->requiredKeys().  Returns null if $this->check($data)
96     * evaluates to false.  Returns null if $this->newObject()
97     * evaluates to false.
98     *
99     * @access private
100     * @param array $data
101     * @return null
102     */
103    function fromSession($data)
104    {
105        if (!$data) {
106            return null;
107        }
108
109        $required = $this->requiredKeys();
110
111        foreach ($required as $k) {
112            if (!array_key_exists($k, $data)) {
113                return null;
114            }
115        }
116
117        if (!$this->check($data)) {
118            return null;
119        }
120
121        $data = array_merge($data, $this->prepareForLoad($data));
122        $obj = $this->newObject($data);
123
124        if (!$obj) {
125            return null;
126        }
127
128        foreach ($required as $k) {
129            $obj->$k = $data[$k];
130        }
131
132        return $obj;
133    }
134
135    /**
136     * Prepares the data array by making any necessary changes.
137     * Returns an array whose keys and values will be used to update
138     * the original data array before calling $this->newObject($data).
139     *
140     * @access private
141     * @param array $data
142     * @return array
143     */
144    function prepareForLoad($data)
145    {
146        return [];
147    }
148
149    /**
150     * Returns a new instance of this loader's class, using the
151     * session data to construct it if necessary.  The object need
152     * only be created; $this->fromSession() will take care of setting
153     * the object's attributes.
154     *
155     * @access private
156     * @param array $data
157     * @return null
158     */
159    function newObject($data)
160    {
161        return null;
162    }
163
164    /**
165     * Returns an array of keys and values built from the attributes
166     * of $obj.  If $this->prepareForSave($obj) returns an array, its keys
167     * and values are used to update the $data array of attributes
168     * from $obj.
169     *
170     * @access private
171     * @param object $obj
172     * @return array
173     */
174    function toSession($obj)
175    {
176        $data = [];
177        foreach ($obj as $k => $v) {
178            $data[$k] = $v;
179        }
180
181        $extra = $this->prepareForSave($obj);
182
183        if ($extra && is_array($extra)) {
184            foreach ($extra as $k => $v) {
185                $data[$k] = $v;
186            }
187        }
188
189        return $data;
190    }
191
192    /**
193     * Override this.
194     *
195     * @access private
196     * @param object $obj
197     * @return array
198     */
199    function prepareForSave($obj)
200    {
201        return [];
202    }
203}
204
205/**
206 * A concrete loader implementation for Auth_OpenID_ServiceEndpoints.
207 *
208 * @package OpenID
209 */
210class Auth_OpenID_ServiceEndpointLoader extends Auth_Yadis_SessionLoader {
211    function newObject($data)
212    {
213        return new Auth_OpenID_ServiceEndpoint();
214    }
215
216    function requiredKeys()
217    {
218        $obj = new Auth_OpenID_ServiceEndpoint();
219        $data = [];
220        foreach ($obj as $k => $v) {
221            $data[] = $k;
222        }
223        return $data;
224    }
225
226    function check($data)
227    {
228        return is_array($data['type_uris']);
229    }
230}
231
232/**
233 * A concrete loader implementation for Auth_Yadis_Managers.
234 *
235 * @package OpenID
236 */
237class Auth_Yadis_ManagerLoader extends Auth_Yadis_SessionLoader {
238    function requiredKeys()
239    {
240        return [
241            'starting_url',
242            'yadis_url',
243            'services',
244            'session_key',
245            '_current',
246            'stale',
247        ];
248    }
249
250    function newObject($data)
251    {
252        return new Auth_Yadis_Manager($data['starting_url'],
253            $data['yadis_url'],
254            $data['services'],
255            $data['session_key']);
256    }
257
258    function check($data)
259    {
260        return is_array($data['services']);
261    }
262
263    function prepareForLoad($data)
264    {
265        $loader = new Auth_OpenID_ServiceEndpointLoader();
266        $services = [];
267        foreach ($data['services'] as $s) {
268            $services[] = $loader->fromSession($s);
269        }
270        return ['services' => $services];
271    }
272
273    function prepareForSave($obj)
274    {
275        $loader = new Auth_OpenID_ServiceEndpointLoader();
276        $services = [];
277        foreach ($obj->services as $s) {
278            $services[] = $loader->toSession($s);
279        }
280        return ['services' => $services];
281    }
282}
283
284/**
285 * The Yadis service manager which stores state in a session and
286 * iterates over <Service> elements in a Yadis XRDS document and lets
287 * a caller attempt to use each one.  This is used by the Yadis
288 * library internally.
289 *
290 * @package OpenID
291 */
292class Auth_Yadis_Manager {
293
294    /** @var string */
295    public $starting_url;
296
297    /** @var string */
298    public $yadis_url;
299
300    /** @var array */
301    public $services;
302
303    /** @var string */
304    public $session_key;
305
306    /** @var Auth_OpenID_ServiceEndpoint */
307    public $_current;
308
309    /**
310     * Intialize a new yadis service manager.
311     *
312     * @access private
313     * @param string $starting_url
314     * @param string $yadis_url
315     * @param array $services
316     * @param string $session_key
317     */
318    function __construct($starting_url, $yadis_url,
319                                    $services, $session_key)
320    {
321        // The URL that was used to initiate the Yadis protocol
322        $this->starting_url = $starting_url;
323
324        // The URL after following redirects (the identifier)
325        $this->yadis_url = $yadis_url;
326
327        // List of service elements
328        $this->services = $services;
329
330        $this->session_key = $session_key;
331
332        // Reference to the current service object
333        $this->_current = null;
334
335        // Stale flag for cleanup if PHP lib has trouble.
336        $this->stale = false;
337    }
338
339    /**
340     * @access private
341     */
342    function length()
343    {
344        // How many untried services remain?
345        return count($this->services);
346    }
347
348    /**
349     * Return the next service
350     *
351     * $this->current() will continue to return that service until the
352     * next call to this method.
353     */
354    function nextService()
355    {
356
357        if ($this->services) {
358            $this->_current = array_shift($this->services);
359        } else {
360            $this->_current = null;
361        }
362
363        return $this->_current;
364    }
365
366    /**
367     * @access private
368     */
369    function current()
370    {
371        // Return the current service.
372        // Returns None if there are no services left.
373        return $this->_current;
374    }
375
376    /**
377     * @access private
378     * @param string $url
379     * @return bool
380     */
381    function forURL($url)
382    {
383        return in_array($url, [$this->starting_url, $this->yadis_url]);
384    }
385
386    /**
387     * @access private
388     */
389    function started()
390    {
391        // Has the first service been returned?
392        return $this->_current !== null;
393    }
394}
395
396/**
397 * State management for discovery.
398 *
399 * High-level usage pattern is to call .getNextService(discover) in
400 * order to find the next available service for this user for this
401 * session. Once a request completes, call .cleanup() to clean up the
402 * session state.
403 *
404 * @package OpenID
405 */
406class Auth_Yadis_Discovery {
407
408    /**
409     * @access private
410     */
411    public $DEFAULT_SUFFIX = 'auth';
412
413    /**
414     * @access private
415     */
416    public $PREFIX = '_yadis_services_';
417
418    /**
419     * Initialize a discovery object.
420     *
421     * @param Auth_Yadis_PHPSession $session An object which
422     * implements the Auth_Yadis_PHPSession API.
423     * @param string $url The URL on which to attempt discovery.
424     * @param string $session_key_suffix The optional session key
425     * suffix override.
426     */
427    function __construct($session, $url,
428                                      $session_key_suffix = null)
429    {
430        /// Initialize a discovery object
431        $this->session = $session;
432        $this->url = $url;
433        if ($session_key_suffix === null) {
434            $session_key_suffix = $this->DEFAULT_SUFFIX;
435        }
436
437        $this->session_key_suffix = $session_key_suffix;
438        $this->session_key = $this->PREFIX . $this->session_key_suffix;
439    }
440
441    /**
442     * Return the next authentication service for the pair of
443     * user_input and session. This function handles fallback.
444     *
445     * @param callback $discover_cb
446     * @param object $fetcher
447     * @return null|Auth_OpenID_ServiceEndpoint
448     */
449    function getNextService($discover_cb, $fetcher)
450    {
451        $manager = $this->getManager();
452        if (!$manager || (!$manager->services)) {
453            $this->destroyManager();
454
455            list($yadis_url, $services) = call_user_func_array($discover_cb,
456                                                               [
457                                                                $this->url,
458                                                                $fetcher,
459                                                               ]);
460
461            $manager = $this->createManager($services, $yadis_url);
462        }
463
464        if ($manager) {
465            $loader = new Auth_Yadis_ManagerLoader();
466            $service = $manager->nextService();
467            $this->session->set($this->session_key,
468                                serialize($loader->toSession($manager)));
469        } else {
470            $service = null;
471        }
472
473        return $service;
474    }
475
476    /**
477     * Clean up Yadis-related services in the session and return the
478     * most-recently-attempted service from the manager, if one
479     * exists.
480     *
481     * @param bool $force True if the manager should be deleted regardless
482     * of whether it's a manager for $this->url.
483     * @return null|Auth_OpenID_ServiceEndpoint
484     */
485    function cleanup($force=false)
486    {
487        $manager = $this->getManager($force);
488        if ($manager) {
489            $service = $manager->current();
490            $this->destroyManager($force);
491        } else {
492            $service = null;
493        }
494
495        return $service;
496    }
497
498    /**
499     * @access private
500     */
501    function getSessionKey()
502    {
503        // Get the session key for this starting URL and suffix
504        return $this->PREFIX . $this->session_key_suffix;
505    }
506
507    /**
508     * @access private
509     *
510     * @param bool $force True if the manager should be returned regardless
511     * of whether it's a manager for $this->url.
512     * @return null|Auth_Yadis_Manager
513     */
514    function getManager($force=false)
515    {
516        // Extract the YadisServiceManager for this object's URL and
517        // suffix from the session.
518
519        $manager_str = $this->session->get($this->getSessionKey());
520        /** @var Auth_Yadis_Manager $manager */
521        $manager = null;
522
523        if ($manager_str !== null) {
524            $loader = new Auth_Yadis_ManagerLoader();
525            $manager = $loader->fromSession(unserialize($manager_str));
526        }
527
528        if ($manager && ($manager->forURL($this->url) || $force)) {
529            return $manager;
530        }
531        return null;
532    }
533
534    /**
535     * @access private
536     * @param array $services
537     * @param null|string $yadis_url
538     * @return Auth_Yadis_Manager|null
539     */
540    function createManager($services, $yadis_url = null)
541    {
542        $key = $this->getSessionKey();
543        if ($this->getManager()) {
544            return $this->getManager();
545        }
546
547        if ($services) {
548            $loader = new Auth_Yadis_ManagerLoader();
549            $manager = new Auth_Yadis_Manager($this->url, $yadis_url,
550                                              $services, $key);
551            $this->session->set($this->session_key,
552                                serialize($loader->toSession($manager)));
553            return $manager;
554        }
555        return null;
556    }
557
558    /**
559     * @access private
560     *
561     * @param bool $force True if the manager should be deleted regardless
562     * of whether it's a manager for $this->url.
563     */
564    function destroyManager($force=false)
565    {
566        if ($this->getManager($force) !== null) {
567            $key = $this->getSessionKey();
568            $this->session->del($key);
569        }
570    }
571}
572
573