import
[web.mtrack] / inc / lib / Auth / Yadis / Manager.php
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  */
17 class 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  */
76 class 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  */
196 class 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  */
223 class 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  */
276 class 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  */
369 class 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         } else {
485             $unused = null;
486             return $unused;
487         }
488     }
489
490     /**
491      * @access private
492      */
493     function &createManager($services, $yadis_url = null)
494     {
495         $key = $this->getSessionKey();
496         if ($this->getManager()) {
497             return $this->getManager();
498         }
499
500         if ($services) {
501             $loader = new Auth_Yadis_ManagerLoader();
502             $manager = new Auth_Yadis_Manager($this->url, $yadis_url,
503                                               $services, $key);
504             $this->session->set($this->session_key,
505                                 serialize($loader->toSession($manager)));
506             return $manager;
507         } else {
508             // Oh, PHP.
509             $unused = null;
510             return $unused;
511         }
512     }
513
514     /**
515      * @access private
516      *
517      * @param $force True if the manager should be deleted regardless
518      * of whether it's a manager for $this->url.
519      */
520     function destroyManager($force=false)
521     {
522         if ($this->getManager($force) !== null) {
523             $key = $this->getSessionKey();
524             $this->session->del($key);
525         }
526     }
527 }
528
529 ?>