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