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: }
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: