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 string $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 string $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 (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 */
76class Auth_Yadis_SessionLoader {
77    /**
78     * Override this.
79     *
80     * @access private
81     */
82    function check($data)
83    {
84        return true;
85    }
86
87    /**
88     * Given a session data value (an array), this creates an object
89     * (returned by $this->newObject()) whose attributes and values
90     * are those in $data.  Returns null if $data lacks keys found in
91     * $this->requiredKeys().  Returns null if $this->check($data)
92     * evaluates to false.  Returns null if $this->newObject()
93     * evaluates to false.
94     *
95     * @access private
96     */
97    function fromSession($data)
98    {
99        if (!$data) {
100            return null;
101        }
102
103        $required = $this->requiredKeys();
104
105        foreach ($required as $k) {
106            if (!array_key_exists($k, $data)) {
107                return null;
108            }
109        }
110
111        if (!$this->check($data)) {
112            return null;
113        }
114
115        $data = array_merge($data, $this->prepareForLoad($data));
116        $obj = $this->newObject($data);
117
118        if (!$obj) {
119            return null;
120        }
121
122        foreach ($required as $k) {
123            $obj->$k = $data[$k];
124        }
125
126        return $obj;
127    }
128
129    /**
130     * Prepares the data array by making any necessary changes.
131     * Returns an array whose keys and values will be used to update
132     * the original data array before calling $this->newObject($data).
133     *
134     * @access private
135     */
136    function prepareForLoad($data)
137    {
138        return array();
139    }
140
141    /**
142     * Returns a new instance of this loader's class, using the
143     * session data to construct it if necessary.  The object need
144     * only be created; $this->fromSession() will take care of setting
145     * the object's attributes.
146     *
147     * @access private
148     */
149    function newObject($data)
150    {
151        return null;
152    }
153
154    /**
155     * Returns an array of keys and values built from the attributes
156     * of $obj.  If $this->prepareForSave($obj) returns an array, its keys
157     * and values are used to update the $data array of attributes
158     * from $obj.
159     *
160     * @access private
161     */
162    function toSession($obj)
163    {
164        $data = array();
165        foreach ($obj as $k => $v) {
166            $data[$k] = $v;
167        }
168
169        $extra = $this->prepareForSave($obj);
170
171        if ($extra && is_array($extra)) {
172            foreach ($extra as $k => $v) {
173                $data[$k] = $v;
174            }
175        }
176
177        return $data;
178    }
179
180    /**
181     * Override this.
182     *
183     * @access private
184     */
185    function prepareForSave($obj)
186    {
187        return array();
188    }
189}
190
191/**
192 * A concrete loader implementation for Auth_OpenID_ServiceEndpoints.
193 *
194 * @package OpenID
195 */
196class Auth_OpenID_ServiceEndpointLoader extends Auth_Yadis_SessionLoader {
197    function newObject($data)
198    {
199        return new Auth_OpenID_ServiceEndpoint();
200    }
201
202    function requiredKeys()
203    {
204        $obj = new Auth_OpenID_ServiceEndpoint();
205        $data = array();
206        foreach ($obj as $k => $v) {
207            $data[] = $k;
208        }
209        return $data;
210    }
211
212    function check($data)
213    {
214        return is_array($data['type_uris']);
215    }
216}
217
218/**
219 * A concrete loader implementation for Auth_Yadis_Managers.
220 *
221 * @package OpenID
222 */
223class Auth_Yadis_ManagerLoader extends Auth_Yadis_SessionLoader {
224    function requiredKeys()
225    {
226        return array('starting_url',
227                     'yadis_url',
228                     'services',
229                     'session_key',
230                     '_current',
231                     'stale');
232    }
233
234    function newObject($data)
235    {
236        return new Auth_Yadis_Manager($data['starting_url'],
237                                          $data['yadis_url'],
238                                          $data['services'],
239                                          $data['session_key']);
240    }
241
242    function check($data)
243    {
244        return is_array($data['services']);
245    }
246
247    function prepareForLoad($data)
248    {
249        $loader = new Auth_OpenID_ServiceEndpointLoader();
250        $services = array();
251        foreach ($data['services'] as $s) {
252            $services[] = $loader->fromSession($s);
253        }
254        return array('services' => $services);
255    }
256
257    function prepareForSave($obj)
258    {
259        $loader = new Auth_OpenID_ServiceEndpointLoader();
260        $services = array();
261        foreach ($obj->services as $s) {
262            $services[] = $loader->toSession($s);
263        }
264        return array('services' => $services);
265    }
266}
267
268/**
269 * The Yadis service manager which stores state in a session and
270 * iterates over <Service> elements in a Yadis XRDS document and lets
271 * a caller attempt to use each one.  This is used by the Yadis
272 * library internally.
273 *
274 * @package OpenID
275 */
276class Auth_Yadis_Manager {
277
278    /**
279     * Intialize a new yadis service manager.
280     *
281     * @access private
282     */
283    function Auth_Yadis_Manager($starting_url, $yadis_url,
284                                    $services, $session_key)
285    {
286        // The URL that was used to initiate the Yadis protocol
287        $this->starting_url = $starting_url;
288
289        // The URL after following redirects (the identifier)
290        $this->yadis_url = $yadis_url;
291
292        // List of service elements
293        $this->services = $services;
294
295        $this->session_key = $session_key;
296
297        // Reference to the current service object
298        $this->_current = null;
299
300        // Stale flag for cleanup if PHP lib has trouble.
301        $this->stale = false;
302    }
303
304    /**
305     * @access private
306     */
307    function length()
308    {
309        // How many untried services remain?
310        return count($this->services);
311    }
312
313    /**
314     * Return the next service
315     *
316     * $this->current() will continue to return that service until the
317     * next call to this method.
318     */
319    function nextService()
320    {
321
322        if ($this->services) {
323            $this->_current = array_shift($this->services);
324        } else {
325            $this->_current = null;
326        }
327
328        return $this->_current;
329    }
330
331    /**
332     * @access private
333     */
334    function current()
335    {
336        // Return the current service.
337        // Returns None if there are no services left.
338        return $this->_current;
339    }
340
341    /**
342     * @access private
343     */
344    function forURL($url)
345    {
346        return in_array($url, array($this->starting_url, $this->yadis_url));
347    }
348
349    /**
350     * @access private
351     */
352    function started()
353    {
354        // Has the first service been returned?
355        return $this->_current !== null;
356    }
357}
358
359/**
360 * State management for discovery.
361 *
362 * High-level usage pattern is to call .getNextService(discover) in
363 * order to find the next available service for this user for this
364 * session. Once a request completes, call .cleanup() to clean up the
365 * session state.
366 *
367 * @package OpenID
368 */
369class Auth_Yadis_Discovery {
370
371    /**
372     * @access private
373     */
374    var $DEFAULT_SUFFIX = 'auth';
375
376    /**
377     * @access private
378     */
379    var $PREFIX = '_yadis_services_';
380
381    /**
382     * Initialize a discovery object.
383     *
384     * @param Auth_Yadis_PHPSession $session An object which
385     * implements the Auth_Yadis_PHPSession API.
386     * @param string $url The URL on which to attempt discovery.
387     * @param string $session_key_suffix The optional session key
388     * suffix override.
389     */
390    function Auth_Yadis_Discovery($session, $url,
391                                      $session_key_suffix = null)
392    {
393        /// Initialize a discovery object
394        $this->session = $session;
395        $this->url = $url;
396        if ($session_key_suffix === null) {
397            $session_key_suffix = $this->DEFAULT_SUFFIX;
398        }
399
400        $this->session_key_suffix = $session_key_suffix;
401        $this->session_key = $this->PREFIX . $this->session_key_suffix;
402    }
403
404    /**
405     * Return the next authentication service for the pair of
406     * user_input and session. This function handles fallback.
407     */
408    function getNextService($discover_cb, $fetcher)
409    {
410        $manager = $this->getManager();
411        if (!$manager || (!$manager->services)) {
412            $this->destroyManager();
413
414            list($yadis_url, $services) = call_user_func($discover_cb,
415                                                         $this->url,
416                                                         &$fetcher);
417
418            $manager = $this->createManager($services, $yadis_url);
419        }
420
421        if ($manager) {
422            $loader = new Auth_Yadis_ManagerLoader();
423            $service = $manager->nextService();
424            $this->session->set($this->session_key,
425                                serialize($loader->toSession($manager)));
426        } else {
427            $service = null;
428        }
429
430        return $service;
431    }
432
433    /**
434     * Clean up Yadis-related services in the session and return the
435     * most-recently-attempted service from the manager, if one
436     * exists.
437     *
438     * @param $force True if the manager should be deleted regardless
439     * of whether it's a manager for $this->url.
440     */
441    function cleanup($force=false)
442    {
443        $manager = $this->getManager($force);
444        if ($manager) {
445            $service = $manager->current();
446            $this->destroyManager($force);
447        } else {
448            $service = null;
449        }
450
451        return $service;
452    }
453
454    /**
455     * @access private
456     */
457    function getSessionKey()
458    {
459        // Get the session key for this starting URL and suffix
460        return $this->PREFIX . $this->session_key_suffix;
461    }
462
463    /**
464     * @access private
465     *
466     * @param $force True if the manager should be returned regardless
467     * of whether it's a manager for $this->url.
468     */
469    function getManager($force=false)
470    {
471        // Extract the YadisServiceManager for this object's URL and
472        // suffix from the session.
473
474        $manager_str = $this->session->get($this->getSessionKey());
475        $manager = null;
476
477        if ($manager_str !== null) {
478            $loader = new Auth_Yadis_ManagerLoader();
479            $manager = $loader->fromSession(unserialize($manager_str));
480        }
481
482        if ($manager && ($manager->forURL($this->url) || $force)) {
483            return $manager;
484        }
485    }
486
487    /**
488     * @access private
489     */
490    function createManager($services, $yadis_url = null)
491    {
492        $key = $this->getSessionKey();
493        if ($this->getManager()) {
494            return $this->getManager();
495        }
496
497        if ($services) {
498            $loader = new Auth_Yadis_ManagerLoader();
499            $manager = new Auth_Yadis_Manager($this->url, $yadis_url,
500                                              $services, $key);
501            $this->session->set($this->session_key,
502                                serialize($loader->toSession($manager)));
503            return $manager;
504        }
505    }
506
507    /**
508     * @access private
509     *
510     * @param $force True if the manager should be deleted regardless
511     * of whether it's a manager for $this->url.
512     */
513    function destroyManager($force=false)
514    {
515        if ($this->getManager($force) !== null) {
516            $key = $this->getSessionKey();
517            $this->session->del($key);
518        }
519    }
520}
521
522