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