| [ Index ] |
PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008] |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @file CAS/client.php 5 * Main class of the phpCAS library 6 */ 7 8 // include internationalization stuff 9 include_once(dirname(__FILE__).'/languages/languages.php'); 10 11 // include PGT storage classes 12 include_once(dirname(__FILE__).'/PGTStorage/pgt-main.php'); 13 14 /** 15 * @class CASClient 16 * The CASClient class is a client interface that provides CAS authentication 17 * to PHP applications. 18 * 19 * @author Pascal Aubry <pascal.aubry at univ-rennes1.fr> 20 */ 21 22 class CASClient 23 { 24 25 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 26 // XX XX 27 // XX CONFIGURATION XX 28 // XX XX 29 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 30 31 // ######################################################################## 32 // HTML OUTPUT 33 // ######################################################################## 34 /** 35 * @addtogroup internalOutput 36 * @{ 37 */ 38 39 /** 40 * This method filters a string by replacing special tokens by appropriate values 41 * and prints it. The corresponding tokens are taken into account: 42 * - __CAS_VERSION__ 43 * - __PHPCAS_VERSION__ 44 * - __SERVER_BASE_URL__ 45 * 46 * Used by CASClient::PrintHTMLHeader() and CASClient::printHTMLFooter(). 47 * 48 * @param $str the string to filter and output 49 * 50 * @private 51 */ 52 function HTMLFilterOutput($str) 53 { 54 $str = str_replace('__CAS_VERSION__',$this->getServerVersion(),$str); 55 $str = str_replace('__PHPCAS_VERSION__',phpCAS::getVersion(),$str); 56 $str = str_replace('__SERVER_BASE_URL__',$this->getServerBaseURL(),$str); 57 echo $str; 58 } 59 60 /** 61 * A string used to print the header of HTML pages. Written by CASClient::setHTMLHeader(), 62 * read by CASClient::printHTMLHeader(). 63 * 64 * @hideinitializer 65 * @private 66 * @see CASClient::setHTMLHeader, CASClient::printHTMLHeader() 67 */ 68 var $_output_header = ''; 69 70 /** 71 * This method prints the header of the HTML output (after filtering). If 72 * CASClient::setHTMLHeader() was not used, a default header is output. 73 * 74 * @param $title the title of the page 75 * 76 * @see HTMLFilterOutput() 77 * @private 78 */ 79 function printHTMLHeader($title) 80 { 81 $this->HTMLFilterOutput(str_replace('__TITLE__', 82 $title, 83 (empty($this->_output_header) 84 ? '<html><head><title>__TITLE__</title></head><body><h1>__TITLE__</h1>' 85 : $this->_output_header) 86 ) 87 ); 88 } 89 90 /** 91 * A string used to print the footer of HTML pages. Written by CASClient::setHTMLFooter(), 92 * read by printHTMLFooter(). 93 * 94 * @hideinitializer 95 * @private 96 * @see CASClient::setHTMLFooter, CASClient::printHTMLFooter() 97 */ 98 var $_output_footer = ''; 99 100 /** 101 * This method prints the footer of the HTML output (after filtering). If 102 * CASClient::setHTMLFooter() was not used, a default footer is output. 103 * 104 * @see HTMLFilterOutput() 105 * @private 106 */ 107 function printHTMLFooter() 108 { 109 $this->HTMLFilterOutput(empty($this->_output_footer) 110 ?('<hr><address>phpCAS __PHPCAS_VERSION__ '.$this->getString(CAS_STR_USING_SERVER).' <a href="__SERVER_BASE_URL__">__SERVER_BASE_URL__</a> (CAS __CAS_VERSION__)</a></address></body></html>') 111 :$this->_output_footer); 112 } 113 114 /** 115 * This method set the HTML header used for all outputs. 116 * 117 * @param $header the HTML header. 118 * 119 * @public 120 */ 121 function setHTMLHeader($header) 122 { 123 $this->_output_header = $header; 124 } 125 126 /** 127 * This method set the HTML footer used for all outputs. 128 * 129 * @param $footer the HTML footer. 130 * 131 * @public 132 */ 133 function setHTMLFooter($footer) 134 { 135 $this->_output_footer = $footer; 136 } 137 138 /** @} */ 139 // ######################################################################## 140 // INTERNATIONALIZATION 141 // ######################################################################## 142 /** 143 * @addtogroup internalLang 144 * @{ 145 */ 146 /** 147 * A string corresponding to the language used by phpCAS. Written by 148 * CASClient::setLang(), read by CASClient::getLang(). 149 150 * @note debugging information is always in english (debug purposes only). 151 * 152 * @hideinitializer 153 * @private 154 * @sa CASClient::_strings, CASClient::getString() 155 */ 156 var $_lang = ''; 157 158 /** 159 * This method returns the language used by phpCAS. 160 * 161 * @return a string representing the language 162 * 163 * @private 164 */ 165 function getLang() 166 { 167 if ( empty($this->_lang) ) 168 $this->setLang(PHPCAS_LANG_DEFAULT); 169 return $this->_lang; 170 } 171 172 /** 173 * array containing the strings used by phpCAS. Written by CASClient::setLang(), read by 174 * CASClient::getString() and used by CASClient::setLang(). 175 * 176 * @note This array is filled by instructions in CAS/languages/<$this->_lang>.php 177 * 178 * @private 179 * @see CASClient::_lang, CASClient::getString(), CASClient::setLang(), CASClient::getLang() 180 */ 181 var $_strings; 182 183 /** 184 * This method returns a string depending on the language. 185 * 186 * @param $str the index of the string in $_string. 187 * 188 * @return the string corresponding to $index in $string. 189 * 190 * @private 191 */ 192 function getString($str) 193 { 194 // call CASclient::getLang() to be sure the language is initialized 195 $this->getLang(); 196 197 if ( !isset($this->_strings[$str]) ) { 198 trigger_error('string `'.$str.'\' not defined for language `'.$this->getLang().'\'',E_USER_ERROR); 199 } 200 return $this->_strings[$str]; 201 } 202 203 /** 204 * This method is used to set the language used by phpCAS. 205 * @note Can be called only once. 206 * 207 * @param $lang a string representing the language. 208 * 209 * @public 210 * @sa CAS_LANG_FRENCH, CAS_LANG_ENGLISH 211 */ 212 function setLang($lang) 213 { 214 // include the corresponding language file 215 include_once(dirname(__FILE__).'/languages/'.$lang.'.php'); 216 217 if ( !is_array($this->_strings) ) { 218 trigger_error('language `'.$lang.'\' is not implemented',E_USER_ERROR); 219 } 220 $this->_lang = $lang; 221 } 222 223 /** @} */ 224 // ######################################################################## 225 // CAS SERVER CONFIG 226 // ######################################################################## 227 /** 228 * @addtogroup internalConfig 229 * @{ 230 */ 231 232 /** 233 * a record to store information about the CAS server. 234 * - $_server["version"]: the version of the CAS server 235 * - $_server["hostname"]: the hostname of the CAS server 236 * - $_server["port"]: the port the CAS server is running on 237 * - $_server["uri"]: the base URI the CAS server is responding on 238 * - $_server["base_url"]: the base URL of the CAS server 239 * - $_server["login_url"]: the login URL of the CAS server 240 * - $_server["service_validate_url"]: the service validating URL of the CAS server 241 * - $_server["proxy_url"]: the proxy URL of the CAS server 242 * - $_server["proxy_validate_url"]: the proxy validating URL of the CAS server 243 * - $_server["logout_url"]: the logout URL of the CAS server 244 * 245 * $_server["version"], $_server["hostname"], $_server["port"] and $_server["uri"] 246 * are written by CASClient::CASClient(), read by CASClient::getServerVersion(), 247 * CASClient::getServerHostname(), CASClient::getServerPort() and CASClient::getServerURI(). 248 * 249 * The other fields are written and read by CASClient::getServerBaseURL(), 250 * CASClient::getServerLoginURL(), CASClient::getServerServiceValidateURL(), 251 * CASClient::getServerProxyValidateURL() and CASClient::getServerLogoutURL(). 252 * 253 * @hideinitializer 254 * @private 255 */ 256 var $_server = array( 257 'version' => -1, 258 'hostname' => 'none', 259 'port' => -1, 260 'uri' => 'none' 261 ); 262 263 /** 264 * This method is used to retrieve the version of the CAS server. 265 * @return the version of the CAS server. 266 * @private 267 */ 268 function getServerVersion() 269 { 270 return $this->_server['version']; 271 } 272 273 /** 274 * This method is used to retrieve the hostname of the CAS server. 275 * @return the hostname of the CAS server. 276 * @private 277 */ 278 function getServerHostname() 279 { return $this->_server['hostname']; } 280 281 /** 282 * This method is used to retrieve the port of the CAS server. 283 * @return the port of the CAS server. 284 * @private 285 */ 286 function getServerPort() 287 { return $this->_server['port']; } 288 289 /** 290 * This method is used to retrieve the URI of the CAS server. 291 * @return a URI. 292 * @private 293 */ 294 function getServerURI() 295 { return $this->_server['uri']; } 296 297 /** 298 * This method is used to retrieve the base URL of the CAS server. 299 * @return a URL. 300 * @private 301 */ 302 function getServerBaseURL() 303 { 304 // the URL is build only when needed 305 if ( empty($this->_server['base_url']) ) { 306 $this->_server['base_url'] = 'https://' 307 .$this->getServerHostname() 308 .':' 309 .$this->getServerPort() 310 .$this->getServerURI(); 311 } 312 return $this->_server['base_url']; 313 } 314 315 /** 316 * This method is used to retrieve the login URL of the CAS server. 317 * @param $gateway true to check authentication, false to force it 318 * @return a URL. 319 * @private 320 */ 321 function getServerLoginURL($gateway=false) 322 { 323 phpCAS::traceBegin(); 324 // the URL is build only when needed 325 if ( empty($this->_server['login_url']) ) { 326 $this->_server['login_url'] = $this->getServerBaseURL(); 327 $this->_server['login_url'] .= 'login?service='; 328 // $this->_server['login_url'] .= preg_replace('/&/','%26',$this->getURL()); 329 $this->_server['login_url'] .= urlencode($this->getURL()); 330 if ($gateway) { 331 $this->_server['login_url'] .= '&gateway=true'; 332 } 333 } 334 phpCAS::traceEnd($this->_server['login_url']); 335 return $this->_server['login_url']; 336 } 337 338 /** 339 * This method sets the login URL of the CAS server. 340 * @param $url the login URL 341 * @private 342 * @since 0.4.21 by Wyman Chan 343 */ 344 function setServerLoginURL($url) 345 { 346 return $this->_server['login_url'] = $url; 347 } 348 349 /** 350 * This method is used to retrieve the service validating URL of the CAS server. 351 * @return a URL. 352 * @private 353 */ 354 function getServerServiceValidateURL() 355 { 356 // the URL is build only when needed 357 if ( empty($this->_server['service_validate_url']) ) { 358 switch ($this->getServerVersion()) { 359 case CAS_VERSION_1_0: 360 $this->_server['service_validate_url'] = $this->getServerBaseURL().'validate'; 361 break; 362 case CAS_VERSION_2_0: 363 $this->_server['service_validate_url'] = $this->getServerBaseURL().'serviceValidate'; 364 break; 365 } 366 } 367 // return $this->_server['service_validate_url'].'?service='.preg_replace('/&/','%26',$this->getURL()); 368 return $this->_server['service_validate_url'].'?service='.urlencode($this->getURL()); 369 } 370 371 /** 372 * This method is used to retrieve the proxy validating URL of the CAS server. 373 * @return a URL. 374 * @private 375 */ 376 function getServerProxyValidateURL() 377 { 378 // the URL is build only when needed 379 if ( empty($this->_server['proxy_validate_url']) ) { 380 switch ($this->getServerVersion()) { 381 case CAS_VERSION_1_0: 382 $this->_server['proxy_validate_url'] = ''; 383 break; 384 case CAS_VERSION_2_0: 385 $this->_server['proxy_validate_url'] = $this->getServerBaseURL().'proxyValidate'; 386 break; 387 } 388 } 389 // return $this->_server['proxy_validate_url'].'?service='.preg_replace('/&/','%26',$this->getURL()); 390 return $this->_server['proxy_validate_url'].'?service='.urlencode($this->getURL()); 391 } 392 393 /** 394 * This method is used to retrieve the proxy URL of the CAS server. 395 * @return a URL. 396 * @private 397 */ 398 function getServerProxyURL() 399 { 400 // the URL is build only when needed 401 if ( empty($this->_server['proxy_url']) ) { 402 switch ($this->getServerVersion()) { 403 case CAS_VERSION_1_0: 404 $this->_server['proxy_url'] = ''; 405 break; 406 case CAS_VERSION_2_0: 407 $this->_server['proxy_url'] = $this->getServerBaseURL().'proxy'; 408 break; 409 } 410 } 411 return $this->_server['proxy_url']; 412 } 413 414 /** 415 * This method is used to retrieve the logout URL of the CAS server. 416 * @return a URL. 417 * @private 418 */ 419 function getServerLogoutURL() 420 { 421 // the URL is build only when needed 422 if ( empty($this->_server['logout_url']) ) { 423 $this->_server['logout_url'] = $this->getServerBaseURL().'logout'; 424 } 425 return $this->_server['logout_url']; 426 } 427 428 /** 429 * This method sets the logout URL of the CAS server. 430 * @param $url the logout URL 431 * @private 432 * @since 0.4.21 by Wyman Chan 433 */ 434 function setServerLogoutURL($url) 435 { 436 return $this->_server['logout_url'] = $url; 437 } 438 439 /** 440 * This method checks to see if the request is secured via HTTPS 441 * @return true if https, false otherwise 442 * @private 443 */ 444 function isHttps() { 445 //if ( isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) ) { 446 //0.4.24 by Hinnack 447 if ( isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { 448 return true; 449 } else { 450 return false; 451 } 452 } 453 454 // ######################################################################## 455 // CONSTRUCTOR 456 // ######################################################################## 457 /** 458 * CASClient constructor. 459 * 460 * @param $server_version the version of the CAS server 461 * @param $proxy TRUE if the CAS client is a CAS proxy, FALSE otherwise 462 * @param $server_hostname the hostname of the CAS server 463 * @param $server_port the port the CAS server is running on 464 * @param $server_uri the URI the CAS server is responding on 465 * @param $start_session Have phpCAS start PHP sessions (default true) 466 * 467 * @return a newly created CASClient object 468 * 469 * @public 470 */ 471 function CASClient( 472 $server_version, 473 $proxy, 474 $server_hostname, 475 $server_port, 476 $server_uri, 477 $start_session = true) { 478 479 phpCAS::traceBegin(); 480 481 //activate session mechanism if desired 482 if ($start_session) { 483 session_start(); 484 } 485 486 $this->_proxy = $proxy; 487 488 //check version 489 switch ($server_version) { 490 case CAS_VERSION_1_0: 491 if ( $this->isProxy() ) 492 phpCAS::error('CAS proxies are not supported in CAS ' 493 .$server_version); 494 break; 495 case CAS_VERSION_2_0: 496 break; 497 default: 498 phpCAS::error('this version of CAS (`' 499 .$server_version 500 .'\') is not supported by phpCAS ' 501 .phpCAS::getVersion()); 502 } 503 $this->_server['version'] = $server_version; 504 505 //check hostname 506 if ( empty($server_hostname) 507 || !preg_match('/[\.\d\-abcdefghijklmnopqrstuvwxyz]*/',$server_hostname) ) { 508 phpCAS::error('bad CAS server hostname (`'.$server_hostname.'\')'); 509 } 510 $this->_server['hostname'] = $server_hostname; 511 512 //check port 513 if ( $server_port == 0 514 || !is_int($server_port) ) { 515 phpCAS::error('bad CAS server port (`'.$server_hostname.'\')'); 516 } 517 $this->_server['port'] = $server_port; 518 519 //check URI 520 if ( !preg_match('/[\.\d\-_abcdefghijklmnopqrstuvwxyz\/]*/',$server_uri) ) { 521 phpCAS::error('bad CAS server URI (`'.$server_uri.'\')'); 522 } 523 //add leading and trailing `/' and remove doubles 524 $server_uri = preg_replace('/\/\//','/','/'.$server_uri.'/'); 525 $this->_server['uri'] = $server_uri; 526 527 //set to callback mode if PgtIou and PgtId CGI GET parameters are provided 528 if ( $this->isProxy() ) { 529 $this->setCallbackMode(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId'])); 530 } 531 532 if ( $this->isCallbackMode() ) { 533 //callback mode: check that phpCAS is secured 534 if ( !$this->isHttps() ) { 535 phpCAS::error('CAS proxies must be secured to use phpCAS; PGT\'s will not be received from the CAS server'); 536 } 537 } else { 538 //normal mode: get ticket and remove it from CGI parameters for developpers 539 $ticket = (isset($_GET['ticket']) ? $_GET['ticket'] : null); 540 switch ($this->getServerVersion()) { 541 case CAS_VERSION_1_0: // check for a Service Ticket 542 if( preg_match('/^ST-/',$ticket) ) { 543 phpCAS::trace('ST \''.$ticket.'\' found'); 544 //ST present 545 $this->setST($ticket); 546 //ticket has been taken into account, unset it to hide it to applications 547 unset($_GET['ticket']); 548 } else if ( !empty($ticket) ) { 549 //ill-formed ticket, halt 550 phpCAS::error('ill-formed ticket found in the URL (ticket=`'.htmlentities($ticket).'\')'); 551 } 552 break; 553 case CAS_VERSION_2_0: // check for a Service or Proxy Ticket 554 if( preg_match('/^[SP]T-/',$ticket) ) { 555 phpCAS::trace('ST or PT \''.$ticket.'\' found'); 556 $this->setPT($ticket); 557 unset($_GET['ticket']); 558 } else if ( !empty($ticket) ) { 559 //ill-formed ticket, halt 560 phpCAS::error('ill-formed ticket found in the URL (ticket=`'.htmlentities($ticket).'\')'); 561 } 562 break; 563 } 564 } 565 phpCAS::traceEnd(); 566 } 567 568 /** @} */ 569 570 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 571 // XX XX 572 // XX AUTHENTICATION XX 573 // XX XX 574 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 575 576 /** 577 * @addtogroup internalAuthentication 578 * @{ 579 */ 580 581 /** 582 * The Authenticated user. Written by CASClient::setUser(), read by CASClient::getUser(). 583 * @attention client applications should use phpCAS::getUser(). 584 * 585 * @hideinitializer 586 * @private 587 */ 588 var $_user = ''; 589 590 /** 591 * This method sets the CAS user's login name. 592 * 593 * @param $user the login name of the authenticated user. 594 * 595 * @private 596 */ 597 function setUser($user) 598 { 599 $this->_user = $user; 600 } 601 602 /** 603 * This method returns the CAS user's login name. 604 * @warning should be called only after CASClient::forceAuthentication() or 605 * CASClient::isAuthenticated(), otherwise halt with an error. 606 * 607 * @return the login name of the authenticated user 608 */ 609 function getUser() 610 { 611 if ( empty($this->_user) ) { 612 phpCAS::error('this method should be used only after '.__CLASS__.'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()'); 613 } 614 return $this->_user; 615 } 616 617 /** 618 * This method is called to be sure that the user is authenticated. When not 619 * authenticated, halt by redirecting to the CAS server; otherwise return TRUE. 620 * @return TRUE when the user is authenticated; otherwise halt. 621 * @public 622 */ 623 function forceAuthentication() 624 { 625 phpCAS::traceBegin(); 626 627 if ( $this->isAuthenticated() ) { 628 // the user is authenticated, nothing to be done. 629 phpCAS::trace('no need to authenticate'); 630 $res = TRUE; 631 632 } else { 633 // the user is not authenticated, redirect to the CAS server 634 unset($_SESSION['phpCAS']['auth_checked']); 635 $this->redirectToCas(FALSE/* no gateway */); 636 // never reached 637 $res = FALSE; 638 } 639 phpCAS::traceEnd($res); 640 return $res; 641 } 642 643 /** 644 * An integer that gives the number of times authentication will be cached before rechecked. 645 * 646 * @hideinitializer 647 * @private 648 */ 649 var $_cache_times_for_auth_recheck = 0; 650 651 /** 652 * Set the number of times authentication will be cached before rechecked. 653 * 654 * @param $n an integer. 655 * 656 * @public 657 */ 658 function setCacheTimesForAuthRequest($n) 659 { 660 $this->_cache_times_for_auth_recheck = n; 661 } 662 663 /** 664 * This method is called to check whether the user is authenticated or not. 665 * @return TRUE when the user is authenticated, FALSE otherwise. 666 * @public 667 */ 668 function checkAuthentication() 669 { 670 phpCAS::traceBegin(); 671 672 if ( $this->isAuthenticated() ) { 673 phpCAS::trace('user is authenticated'); 674 $res = TRUE; 675 } else if (isset($_SESSION['phpCAS']['auth_checked'])) { 676 // the previous request has redirected the client to the CAS server with gateway=true 677 unset($_SESSION['phpCAS']['auth_checked']); 678 $res = FALSE; 679 } else { 680 // $_SESSION['phpCAS']['auth_checked'] = true; 681 // $this->redirectToCas(TRUE/* gateway */); 682 // // never reached 683 // $res = FALSE; 684 // avoid a check against CAS on every request 685 if (! isset($_SESSION['phpCAS']['unauth_count']) ) 686 $_SESSION['phpCAS']['unauth_count'] = -2; // uninitialized 687 688 if (($_SESSION['phpCAS']['unauth_count'] != -2 && $this->_cache_times_for_auth_recheck == -1) 689 || ($_SESSION['phpCAS']['unauth_count'] >= 0 && $_SESSION['phpCAS']['unauth_count'] < $this->_cache_times_for_auth_recheck)) 690 { 691 $res = FALSE; 692 693 if ($this->_cache_times_for_auth_recheck != -1) 694 { 695 $_SESSION['phpCAS']['unauth_count']++; 696 phpCAS::trace('user is not authenticated (cached for '.$_SESSION['phpCAS']['unauth_count'].' times of '.$this->_cache_times_for_auth_recheck.')'); 697 } 698 else 699 { 700 phpCAS::trace('user is not authenticated (cached for until login pressed)'); 701 } 702 } 703 else 704 { 705 $_SESSION['phpCAS']['unauth_count'] = 0; 706 $_SESSION['phpCAS']['auth_checked'] = true; 707 phpCAS::trace('user is not authenticated (cache reset)'); 708 $this->redirectToCas(TRUE/* gateway */); 709 // never reached 710 $res = FALSE; 711 } 712 } 713 phpCAS::traceEnd($res); 714 return $res; 715 } 716 717 /** 718 * This method is called to check if the user is authenticated (previously or by 719 * tickets given in the URL). 720 * 721 * @return TRUE when the user is authenticated. 722 * 723 * @public 724 */ 725 function isAuthenticated() 726 { 727 phpCAS::traceBegin(); 728 $res = FALSE; 729 $validate_url = ''; 730 731 if ( $this->wasPreviouslyAuthenticated() ) { 732 // the user has already (previously during the session) been 733 // authenticated, nothing to be done. 734 phpCAS::trace('user was already authenticated, no need to look for tickets'); 735 $res = TRUE; 736 } 737 elseif ( $this->hasST() ) { 738 // if a Service Ticket was given, validate it 739 phpCAS::trace('ST `'.$this->getST().'\' is present'); 740 $this->validateST($validate_url,$text_response,$tree_response); // if it fails, it halts 741 phpCAS::trace('ST `'.$this->getST().'\' was validated'); 742 if ( $this->isProxy() ) { 743 $this->validatePGT($validate_url,$text_response,$tree_response); // idem 744 phpCAS::trace('PGT `'.$this->getPGT().'\' was validated'); 745 $_SESSION['phpCAS']['pgt'] = $this->getPGT(); 746 } 747 $_SESSION['phpCAS']['user'] = $this->getUser(); 748 $res = TRUE; 749 } 750 elseif ( $this->hasPT() ) { 751 // if a Proxy Ticket was given, validate it 752 phpCAS::trace('PT `'.$this->getPT().'\' is present'); 753 $this->validatePT($validate_url,$text_response,$tree_response); // note: if it fails, it halts 754 phpCAS::trace('PT `'.$this->getPT().'\' was validated'); 755 756 if ( $this->isProxy() ) { 757 758 $this->validatePGT($validate_url,$text_response,$tree_response); // idem 759 phpCAS::trace('PGT `'.$this->getPGT().'\' was validated'); 760 $_SESSION['phpCAS']['pgt'] = $this->getPGT(); 761 } 762 $_SESSION['phpCAS']['user'] = $this->getUser(); 763 $res = TRUE; 764 } 765 else { 766 // no ticket given, not authenticated 767 phpCAS::trace('no ticket found'); 768 } 769 770 phpCAS::traceEnd($res); 771 return $res; 772 } 773 774 /** 775 * This method tells if the current session is authenticated. 776 * @return true if authenticated based soley on $_SESSION variable 777 * @since 0.4.22 by Brendan Arnold 778 */ 779 function isSessionAuthenticated () 780 { 781 return !empty($_SESSION['phpCAS']['user']); 782 } 783 784 /** 785 * This method tells if the user has already been (previously) authenticated 786 * by looking into the session variables. 787 * 788 * @note This function switches to callback mode when needed. 789 * 790 * @return TRUE when the user has already been authenticated; FALSE otherwise. 791 * 792 * @private 793 */ 794 function wasPreviouslyAuthenticated() 795 { 796 phpCAS::traceBegin(); 797 798 if ( $this->isCallbackMode() ) { 799 800 $this->callback(); 801 } 802 803 $auth = FALSE; 804 805 if ( $this->isProxy() ) { 806 // CAS proxy: username and PGT must be present 807 if ( $this->isSessionAuthenticated() && !empty($_SESSION['phpCAS']['pgt']) ) { 808 // authentication already done 809 $this->setUser($_SESSION['phpCAS']['user']); 810 $this->setPGT($_SESSION['phpCAS']['pgt']); 811 phpCAS::trace('user = `'.$_SESSION['phpCAS']['user'].'\', PGT = `'.$_SESSION['phpCAS']['pgt'].'\''); 812 $auth = TRUE; 813 } elseif ( $this->isSessionAuthenticated() && empty($_SESSION['phpCAS']['pgt']) ) { 814 // these two variables should be empty or not empty at the same time 815 phpCAS::trace('username found (`'.$_SESSION['phpCAS']['user'].'\') but PGT is empty'); 816 // unset all tickets to enforce authentication 817 unset($_SESSION['phpCAS']); 818 $this->setST(''); 819 $this->setPT(''); 820 } elseif ( !$this->isSessionAuthenticated() && !empty($_SESSION['phpCAS']['pgt']) ) { 821 // these two variables should be empty or not empty at the same time 822 phpCAS::trace('PGT found (`'.$_SESSION['phpCAS']['pgt'].'\') but username is empty'); 823 // unset all tickets to enforce authentication 824 unset($_SESSION['phpCAS']); 825 $this->setST(''); 826 $this->setPT(''); 827 } else { 828 phpCAS::trace('neither user not PGT found'); 829 } 830 } else { 831 // `simple' CAS client (not a proxy): username must be present 832 if ( $this->isSessionAuthenticated() ) { 833 // authentication already done 834 $this->setUser($_SESSION['phpCAS']['user']); 835 phpCAS::trace('user = `'.$_SESSION['phpCAS']['user'].'\''); 836 $auth = TRUE; 837 } else { 838 phpCAS::trace('no user found'); 839 } 840 } 841 842 phpCAS::traceEnd($auth); 843 return $auth; 844 } 845 846 /** 847 * This method is used to redirect the client to the CAS server. 848 * It is used by CASClient::forceAuthentication() and CASClient::checkAuthentication(). 849 * @param $gateway true to check authentication, false to force it 850 * @public 851 */ 852 function redirectToCas($gateway=false) 853 { 854 phpCAS::traceBegin(); 855 $cas_url = $this->getServerLoginURL($gateway); 856 header('Location: '.$cas_url); 857 $this->printHTMLHeader($this->getString(CAS_STR_AUTHENTICATION_WANTED)); 858 printf('<p>'.$this->getString(CAS_STR_SHOULD_HAVE_BEEN_REDIRECTED).'</p>',$cas_url); 859 $this->printHTMLFooter(); 860 phpCAS::traceExit(); 861 exit(); 862 } 863 864 /** 865 * This method is used to logout from CAS. 866 * @param $url a URL that will be transmitted to the CAS server (to come back to when logged out) 867 * @public 868 */ 869 function logout($url = "") 870 { 871 phpCAS::traceBegin(); 872 $cas_url = $this->getServerLogoutURL(); 873 // v0.4.14 sebastien.gougeon at univ-rennes1.fr 874 // header('Location: '.$cas_url); 875 if ( $url != "" ) { 876 $url = '?service=' . $url; 877 } 878 header('Location: '.$cas_url . $url); 879 session_unset(); 880 session_destroy(); 881 $this->printHTMLHeader($this->getString(CAS_STR_LOGOUT)); 882 printf('<p>'.$this->getString(CAS_STR_SHOULD_HAVE_BEEN_REDIRECTED).'</p>',$cas_url); 883 $this->printHTMLFooter(); 884 phpCAS::traceExit(); 885 exit(); 886 } 887 888 /** @} */ 889 890 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 891 // XX XX 892 // XX BASIC CLIENT FEATURES (CAS 1.0) XX 893 // XX XX 894 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 895 896 // ######################################################################## 897 // ST 898 // ######################################################################## 899 /** 900 * @addtogroup internalBasic 901 * @{ 902 */ 903 904 /** 905 * the Service Ticket provided in the URL of the request if present 906 * (empty otherwise). Written by CASClient::CASClient(), read by 907 * CASClient::getST() and CASClient::hasPGT(). 908 * 909 * @hideinitializer 910 * @private 911 */ 912 var $_st = ''; 913 914 /** 915 * This method returns the Service Ticket provided in the URL of the request. 916 * @return The service ticket. 917 * @private 918 */ 919 function getST() 920 { return $this->_st; } 921 922 /** 923 * This method stores the Service Ticket. 924 * @param $st The Service Ticket. 925 * @private 926 */ 927 function setST($st) 928 { $this->_st = $st; } 929 930 /** 931 * This method tells if a Service Ticket was stored. 932 * @return TRUE if a Service Ticket has been stored. 933 * @private 934 */ 935 function hasST() 936 { return !empty($this->_st); } 937 938 /** @} */ 939 940 // ######################################################################## 941 // ST VALIDATION 942 // ######################################################################## 943 /** 944 * @addtogroup internalBasic 945 * @{ 946 */ 947 948 /** 949 * This method is used to validate a ST; halt on failure, and sets $validate_url, 950 * $text_reponse and $tree_response on success. These parameters are used later 951 * by CASClient::validatePGT() for CAS proxies. 952 * 953 * @param $validate_url the URL of the request to the CAS server. 954 * @param $text_response the response of the CAS server, as is (XML text). 955 * @param $tree_response the response of the CAS server, as a DOM XML tree. 956 * 957 * @return bool TRUE when successfull, halt otherwise by calling CASClient::authError(). 958 * 959 * @private 960 */ 961 function validateST($validate_url,&$text_response,&$tree_response) 962 { 963 phpCAS::traceBegin(); 964 // build the URL to validate the ticket 965 $validate_url = $this->getServerServiceValidateURL().'&ticket='.$this->getST(); 966 if ( $this->isProxy() ) { 967 // pass the callback url for CAS proxies 968 $validate_url .= '&pgtUrl='.$this->getCallbackURL(); 969 } 970 971 // open and read the URL 972 if ( !$this->readURL($validate_url,''/*cookies*/,$headers,$text_response,$err_msg) ) { 973 phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'); 974 $this->authError('ST not validated', 975 $validate_url, 976 TRUE/*$no_response*/); 977 } 978 979 // analyze the result depending on the version 980 switch ($this->getServerVersion()) { 981 case CAS_VERSION_1_0: 982 if (preg_match('/^no\n/',$text_response)) { 983 phpCAS::trace('ST has not been validated'); 984 $this->authError('ST not validated', 985 $validate_url, 986 FALSE/*$no_response*/, 987 FALSE/*$bad_response*/, 988 $text_response); 989 } 990 if (!preg_match('/^yes\n/',$text_response)) { 991 phpCAS::trace('ill-formed response'); 992 $this->authError('ST not validated', 993 $validate_url, 994 FALSE/*$no_response*/, 995 TRUE/*$bad_response*/, 996 $text_response); 997 } 998 // ST has been validated, extract the user name 999 $arr = preg_split('/\n/',$text_response); 1000 $this->setUser(trim($arr[1])); 1001 break; 1002 case CAS_VERSION_2_0: 1003 // read the response of the CAS server into a DOM object 1004 if ( !($dom = domxml_open_mem($text_response))) { 1005 phpCAS::trace('domxml_open_mem() failed'); 1006 $this->authError('ST not validated', 1007 $validate_url, 1008 FALSE/*$no_response*/, 1009 TRUE/*$bad_response*/, 1010 $text_response); 1011 } 1012 // read the root node of the XML tree 1013 if ( !($tree_response = $dom->document_element()) ) { 1014 phpCAS::trace('document_element() failed'); 1015 $this->authError('ST not validated', 1016 $validate_url, 1017 FALSE/*$no_response*/, 1018 TRUE/*$bad_response*/, 1019 $text_response); 1020 } 1021 // insure that tag name is 'serviceResponse' 1022 if ( $tree_response->node_name() != 'serviceResponse' ) { 1023 phpCAS::trace('bad XML root node (should be `serviceResponse\' instead of `'.$tree_response->node_name().'\''); 1024 $this->authError('ST not validated', 1025 $validate_url, 1026 FALSE/*$no_response*/, 1027 TRUE/*$bad_response*/, 1028 $text_response); 1029 } 1030 if ( sizeof($success_elements = $tree_response->get_elements_by_tagname("authenticationSuccess")) != 0) { 1031 // authentication succeded, extract the user name 1032 if ( sizeof($user_elements = $success_elements[0]->get_elements_by_tagname("user")) == 0) { 1033 phpCAS::trace('<authenticationSuccess> found, but no <user>'); 1034 $this->authError('ST not validated', 1035 $validate_url, 1036 FALSE/*$no_response*/, 1037 TRUE/*$bad_response*/, 1038 $text_response); 1039 } 1040 $user = trim($user_elements[0]->get_content()); 1041 phpCAS::trace('user = `'.$user); 1042 $this->setUser($user); 1043 1044 } else if ( sizeof($failure_elements = $tree_response->get_elements_by_tagname("authenticationFailure")) != 0) { 1045 phpCAS::trace('<authenticationFailure> found'); 1046 // authentication failed, extract the error code and message 1047 $this->authError('ST not validated', 1048 $validate_url, 1049 FALSE/*$no_response*/, 1050 FALSE/*$bad_response*/, 1051 $text_response, 1052 $failure_elements[0]->get_attribute('code')/*$err_code*/, 1053 trim($failure_elements[0]->get_content())/*$err_msg*/); 1054 } else { 1055 phpCAS::trace('neither <authenticationSuccess> nor <authenticationFailure> found'); 1056 $this->authError('ST not validated', 1057 $validate_url, 1058 FALSE/*$no_response*/, 1059 TRUE/*$bad_response*/, 1060 $text_response); 1061 } 1062 break; 1063 } 1064 1065 // at this step, ST has been validated and $this->_user has been set, 1066 phpCAS::traceEnd(TRUE); 1067 return TRUE; 1068 } 1069 1070 /** @} */ 1071 1072 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1073 // XX XX 1074 // XX PROXY FEATURES (CAS 2.0) XX 1075 // XX XX 1076 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1077 1078 // ######################################################################## 1079 // PROXYING 1080 // ######################################################################## 1081 /** 1082 * @addtogroup internalProxy 1083 * @{ 1084 */ 1085 1086 /** 1087 * A boolean telling if the client is a CAS proxy or not. Written by CASClient::CASClient(), 1088 * read by CASClient::isProxy(). 1089 * 1090 * @private 1091 */ 1092 var $_proxy; 1093 1094 /** 1095 * Tells if a CAS client is a CAS proxy or not 1096 * 1097 * @return TRUE when the CAS client is a CAs proxy, FALSE otherwise 1098 * 1099 * @private 1100 */ 1101 function isProxy() 1102 { 1103 return $this->_proxy; 1104 } 1105 1106 /** @} */ 1107 // ######################################################################## 1108 // PGT 1109 // ######################################################################## 1110 /** 1111 * @addtogroup internalProxy 1112 * @{ 1113 */ 1114 1115 /** 1116 * the Proxy Grnting Ticket given by the CAS server (empty otherwise). 1117 * Written by CASClient::setPGT(), read by CASClient::getPGT() and CASClient::hasPGT(). 1118 * 1119 * @hideinitializer 1120 * @private 1121 */ 1122 var $_pgt = ''; 1123 1124 /** 1125 * This method returns the Proxy Granting Ticket given by the CAS server. 1126 * @return The Proxy Granting Ticket. 1127 * @private 1128 */ 1129 function getPGT() 1130 { return $this->_pgt; } 1131 1132 /** 1133 * This method stores the Proxy Granting Ticket. 1134 * @param $pgt The Proxy Granting Ticket. 1135 * @private 1136 */ 1137 function setPGT($pgt) 1138 { $this->_pgt = $pgt; } 1139 1140 /** 1141 * This method tells if a Proxy Granting Ticket was stored. 1142 * @return TRUE if a Proxy Granting Ticket has been stored. 1143 * @private 1144 */ 1145 function hasPGT() 1146 { return !empty($this->_pgt); } 1147 1148 /** @} */ 1149 1150 // ######################################################################## 1151 // CALLBACK MODE 1152 // ######################################################################## 1153 /** 1154 * @addtogroup internalCallback 1155 * @{ 1156 */ 1157 /** 1158 * each PHP script using phpCAS in proxy mode is its own callback to get the 1159 * PGT back from the CAS server. callback_mode is detected by the constructor 1160 * thanks to the GET parameters. 1161 */ 1162 1163 /** 1164 * a boolean to know if the CAS client is running in callback mode. Written by 1165 * CASClient::setCallBackMode(), read by CASClient::isCallbackMode(). 1166 * 1167 * @hideinitializer 1168 * @private 1169 */ 1170 var $_callback_mode = FALSE; 1171 1172 /** 1173 * This method sets/unsets callback mode. 1174 * 1175 * @param $callback_mode TRUE to set callback mode, FALSE otherwise. 1176 * 1177 * @private 1178 */ 1179 function setCallbackMode($callback_mode) 1180 { 1181 $this->_callback_mode = $callback_mode; 1182 } 1183 1184 /** 1185 * This method returns TRUE when the CAs client is running i callback mode, 1186 * FALSE otherwise. 1187 * 1188 * @return A boolean. 1189 * 1190 * @private 1191 */ 1192 function isCallbackMode() 1193 { 1194 return $this->_callback_mode; 1195 } 1196 1197 /** 1198 * the URL that should be used for the PGT callback (in fact the URL of the 1199 * current request without any CGI parameter). Written and read by 1200 * CASClient::getCallbackURL(). 1201 * 1202 * @hideinitializer 1203 * @private 1204 */ 1205 var $_callback_url = ''; 1206 1207 /** 1208 * This method returns the URL that should be used for the PGT callback (in 1209 * fact the URL of the current request without any CGI parameter, except if 1210 * phpCAS::setFixedCallbackURL() was used). 1211 * 1212 * @return The callback URL 1213 * 1214 * @private 1215 */ 1216 function getCallbackURL() 1217 { 1218 // the URL is built when needed only 1219 if ( empty($this->_callback_url) ) { 1220 $final_uri = ''; 1221 // remove the ticket if present in the URL 1222 $final_uri = 'https://'; 1223 /* replaced by Julien Marchal - v0.4.6 1224 * $this->uri .= $_SERVER['SERVER_NAME']; 1225 */ 1226 if(empty($_SERVER['HTTP_X_FORWARDED_SERVER'])){ 1227 /* replaced by teedog - v0.4.12 1228 * $final_uri .= $_SERVER['SERVER_NAME']; 1229 */ 1230 if (empty($_SERVER['SERVER_NAME'])) { 1231 $final_uri .= $_SERVER['HTTP_HOST']; 1232 } else { 1233 $final_uri .= $_SERVER['SERVER_NAME']; 1234 } 1235 } else { 1236 $final_uri .= $_SERVER['HTTP_X_FORWARDED_SERVER']; 1237 } 1238 if ( ($this->isHttps() && $_SERVER['SERVER_PORT']!=443) 1239 || (!$this->isHttps() && $_SERVER['SERVER_PORT']!=80) ) { 1240 $final_uri .= ':'; 1241 $final_uri .= $_SERVER['SERVER_PORT']; 1242 } 1243 $request_uri = $_SERVER['REQUEST_URI']; 1244 $request_uri = preg_replace('/\?.*$/','',$request_uri); 1245 $final_uri .= $request_uri; 1246 $this->setCallbackURL($final_uri); 1247 } 1248 return $this->_callback_url; 1249 } 1250 1251 /** 1252 * This method sets the callback url. 1253 * 1254 * @param $callback_url url to set callback 1255 * 1256 * @private 1257 */ 1258 function setCallbackURL($url) 1259 { 1260 return $this->_callback_url = $url; 1261 } 1262 1263 /** 1264 * This method is called by CASClient::CASClient() when running in callback 1265 * mode. It stores the PGT and its PGT Iou, prints its output and halts. 1266 * 1267 * @private 1268 */ 1269 function callback() 1270 { 1271 phpCAS::traceBegin(); 1272 $this->printHTMLHeader('phpCAS callback'); 1273 $pgt_iou = $_GET['pgtIou']; 1274 $pgt = $_GET['pgtId']; 1275 phpCAS::trace('Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\')'); 1276 echo '<p>Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\').</p>'; 1277 $this->storePGT($pgt,$pgt_iou); 1278 $this->printHTMLFooter(); 1279 phpCAS::traceExit(); 1280 } 1281 1282 /** @} */ 1283 1284 // ######################################################################## 1285 // PGT STORAGE 1286 // ######################################################################## 1287 /** 1288 * @addtogroup internalPGTStorage 1289 * @{ 1290 */ 1291 1292 /** 1293 * an instance of a class inheriting of PGTStorage, used to deal with PGT 1294 * storage. Created by CASClient::setPGTStorageFile() or CASClient::setPGTStorageDB(), used 1295 * by CASClient::setPGTStorageFile(), CASClient::setPGTStorageDB() and CASClient::initPGTStorage(). 1296 * 1297 * @hideinitializer 1298 * @private 1299 */ 1300 var $_pgt_storage = null; 1301 1302 /** 1303 * This method is used to initialize the storage of PGT's. 1304 * Halts on error. 1305 * 1306 * @private 1307 */ 1308 function initPGTStorage() 1309 { 1310 // if no SetPGTStorageXxx() has been used, default to file 1311 if ( !is_object($this->_pgt_storage) ) { 1312 $this->setPGTStorageFile(); 1313 } 1314 1315 // initializes the storage 1316 $this->_pgt_storage->init(); 1317 } 1318 1319 /** 1320 * This method stores a PGT. Halts on error. 1321 * 1322 * @param $pgt the PGT to store 1323 * @param $pgt_iou its corresponding Iou 1324 * 1325 * @private 1326 */ 1327 function storePGT($pgt,$pgt_iou) 1328 { 1329 // ensure that storage is initialized 1330 $this->initPGTStorage(); 1331 1332 // writes the PGT 1333 $this->_pgt_storage->write($pgt,$pgt_iou); 1334 } 1335 1336 /** 1337 * This method reads a PGT from its Iou and deletes the corresponding storage entry. 1338 * 1339 * @param $pgt_iou the PGT Iou 1340 * 1341 * @return The PGT corresponding to the Iou, FALSE when not found. 1342 * 1343 * @private 1344 */ 1345 function loadPGT($pgt_iou) 1346 { 1347 // ensure that storage is initialized 1348 $this->initPGTStorage(); 1349 1350 // read the PGT 1351 return $this->_pgt_storage->read($pgt_iou); 1352 } 1353 1354 /** 1355 * This method is used to tell phpCAS to store the response of the 1356 * CAS server to PGT requests onto the filesystem. 1357 * 1358 * @param $format the format used to store the PGT's (`plain' and `xml' allowed) 1359 * @param $path the path where the PGT's should be stored 1360 * 1361 * @public 1362 */ 1363 function setPGTStorageFile($format='', 1364 $path='') 1365 { 1366 // check that the storage has not already been set 1367 if ( is_object($this->_pgt_storage) ) { 1368 phpCAS::error('PGT storage already defined'); 1369 } 1370 1371 // create the storage object 1372 $this->_pgt_storage = &new PGTStorageFile($this,$format,$path); 1373 } 1374 1375 /** 1376 * This method is used to tell phpCAS to store the response of the 1377 * CAS server to PGT requests into a database. 1378 * @note The connection to the database is done only when needed. 1379 * As a consequence, bad parameters are detected only when 1380 * initializing PGT storage. 1381 * 1382 * @param $user the user to access the data with 1383 * @param $password the user's password 1384 * @param $database_type the type of the database hosting the data 1385 * @param $hostname the server hosting the database 1386 * @param $port the port the server is listening on 1387 * @param $database the name of the database 1388 * @param $table the name of the table storing the data 1389 * 1390 * @public 1391 */ 1392 function setPGTStorageDB($user, 1393 $password, 1394 $database_type, 1395 $hostname, 1396 $port, 1397 $database, 1398 $table) 1399 { 1400 // check that the storage has not already been set 1401 if ( is_object($this->_pgt_storage) ) { 1402 phpCAS::error('PGT storage already defined'); 1403 } 1404 1405 // warn the user that he should use file storage... 1406 trigger_error('PGT storage into database is an experimental feature, use at your own risk',E_USER_WARNING); 1407 1408 // create the storage object 1409 $this->_pgt_storage = & new PGTStorageDB($this,$user,$password,$database_type,$hostname,$port,$database,$table); 1410 } 1411 1412 // ######################################################################## 1413 // PGT VALIDATION 1414 // ######################################################################## 1415 /** 1416 * This method is used to validate a PGT; halt on failure. 1417 * 1418 * @param $validate_url the URL of the request to the CAS server. 1419 * @param $text_response the response of the CAS server, as is (XML text); result 1420 * of CASClient::validateST() or CASClient::validatePT(). 1421 * @param $tree_response the response of the CAS server, as a DOM XML tree; result 1422 * of CASClient::validateST() or CASClient::validatePT(). 1423 * 1424 * @return bool TRUE when successfull, halt otherwise by calling CASClient::authError(). 1425 * 1426 * @private 1427 */ 1428 function validatePGT(&$validate_url,$text_response,$tree_response) 1429 { 1430 phpCAS::traceBegin(); 1431 if ( sizeof($arr = $tree_response->get_elements_by_tagname("proxyGrantingTicket")) == 0) { 1432 phpCAS::trace('<proxyGrantingTicket> not found'); 1433 // authentication succeded, but no PGT Iou was transmitted 1434 $this->authError('Ticket validated but no PGT Iou transmitted', 1435 $validate_url, 1436 FALSE/*$no_response*/, 1437 FALSE/*$bad_response*/, 1438 $text_response); 1439 } else { 1440 // PGT Iou transmitted, extract it 1441 $pgt_iou = trim($arr[0]->get_content()); 1442 $pgt = $this->loadPGT($pgt_iou); 1443 if ( $pgt == FALSE ) { 1444 1445 phpCAS::trace('could not load PGT'); 1446 1447 $this->authError('PGT Iou was transmitted but PGT could not be retrieved', 1448 $validate_url, 1449 FALSE/*$no_response*/, 1450 FALSE/*$bad_response*/, 1451 $text_response); 1452 1453 1454 } 1455 $this->setPGT($pgt); 1456 } 1457 phpCAS::traceEnd(TRUE); 1458 return TRUE; 1459 } 1460 1461 // ######################################################################## 1462 // PGT VALIDATION 1463 // ######################################################################## 1464 1465 /** 1466 * This method is used to retrieve PT's from the CAS server thanks to a PGT. 1467 * 1468 * @param $target_service the service to ask for with the PT. 1469 * @param $err_code an error code (PHPCAS_SERVICE_OK on success). 1470 * @param $err_msg an error message (empty on success). 1471 * 1472 * @return a Proxy Ticket, or FALSE on error. 1473 * 1474 * @private 1475 */ 1476 function retrievePT($target_service,&$err_code,&$err_msg) 1477 { 1478 phpCAS::traceBegin(); 1479 1480 // by default, $err_msg is set empty and $pt to TRUE. On error, $pt is 1481 // set to false and $err_msg to an error message. At the end, if $pt is FALSE 1482 // and $error_msg is still empty, it is set to 'invalid response' (the most 1483 // commonly encountered error). 1484 $err_msg = ''; 1485 1486 // build the URL to retrieve the PT 1487 // $cas_url = $this->getServerProxyURL().'?targetService='.preg_replace('/&/','%26',$target_service).'&pgt='.$this->getPGT(); 1488 $cas_url = $this->getServerProxyURL().'?targetService='.urlencode($target_service).'&pgt='.$this->getPGT(); 1489 1490 // open and read the URL 1491 if ( !$this->readURL($cas_url,''/*cookies*/,$headers,$cas_response,$err_msg) ) { 1492 phpCAS::trace('could not open URL \''.$cas_url.'\' to validate ('.$err_msg.')'); 1493 $err_code = PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE; 1494 $err_msg = 'could not retrieve PT (no response from the CAS server)'; 1495 phpCAS::traceEnd(FALSE); 1496 return FALSE; 1497 } 1498 1499 $bad_response = FALSE; 1500 1501 if ( !$bad_response ) { 1502 // read the response of the CAS server into a DOM object 1503 if ( !($dom = @domxml_open_mem($cas_response))) { 1504 phpCAS::trace('domxml_open_mem() failed'); 1505 // read failed 1506 $bad_response = TRUE; 1507 } 1508 } 1509 1510 if ( !$bad_response ) { 1511 // read the root node of the XML tree 1512 if ( !($root = $dom->document_element()) ) { 1513 phpCAS::trace('document_element() failed'); 1514 // read failed 1515 $bad_response = TRUE; 1516 } 1517 } 1518 1519 if ( !$bad_response ) { 1520 // insure that tag name is 'serviceResponse' 1521 if ( $root->node_name() != 'serviceResponse' ) { 1522 phpCAS::trace('node_name() failed'); 1523 // bad root node 1524 $bad_response = TRUE; 1525 } 1526 } 1527 1528 if ( !$bad_response ) { 1529 // look for a proxySuccess tag 1530 if ( sizeof($arr = $root->get_elements_by_tagname("proxySuccess")) != 0) { 1531 // authentication succeded, look for a proxyTicket tag 1532 if ( sizeof($arr = $root->get_elements_by_tagname("proxyTicket")) != 0) { 1533 $err_code = PHPCAS_SERVICE_OK; 1534 $err_msg = ''; 1535 phpCAS::trace('original PT: '.trim($arr[0]->get_content())); 1536 $pt = trim($arr[0]->get_content()); 1537 phpCAS::traceEnd($pt); 1538 return $pt; 1539 } else { 1540 phpCAS::trace('<proxySuccess> was found, but not <proxyTicket>'); 1541 } 1542 } 1543 // look for a proxyFailure tag 1544 else if ( sizeof($arr = $root->get_elements_by_tagname("proxyFailure")) != 0) { 1545 // authentication failed, extract the error 1546 $err_code = PHPCAS_SERVICE_PT_FAILURE; 1547 $err_msg = 'PT retrieving failed (code=`' 1548 .$arr[0]->get_attribute('code') 1549 .'\', message=`' 1550 .trim($arr[0]->get_content()) 1551 .'\')'; 1552 phpCAS::traceEnd(FALSE); 1553 return FALSE; 1554 } else { 1555 phpCAS::trace('neither <proxySuccess> nor <proxyFailure> found'); 1556 } 1557 } 1558 1559 // at this step, we are sure that the response of the CAS server was ill-formed 1560 $err_code = PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE; 1561 $err_msg = 'Invalid response from the CAS server (response=`'.$cas_response.'\')'; 1562 1563 phpCAS::traceEnd(FALSE); 1564 return FALSE; 1565 } 1566 1567 // ######################################################################## 1568 // ACCESS TO EXTERNAL SERVICES 1569 // ######################################################################## 1570 1571 /** 1572 * This method is used to acces a remote URL. 1573 * 1574 * @param $url the URL to access. 1575 * @param $cookies an array containing cookies strings such as 'name=val' 1576 * @param $headers an array containing the HTTP header lines of the response 1577 * (an empty array on failure). 1578 * @param $body the body of the response, as a string (empty on failure). 1579 * @param $err_msg an error message, filled on failure. 1580 * 1581 * @return TRUE on success, FALSE otherwise (in this later case, $err_msg 1582 * contains an error message). 1583 * 1584 * @private 1585 */ 1586 function readURL($url,$cookies,&$headers,&$body,&$err_msg) 1587 { 1588 phpCAS::traceBegin(); 1589 $headers = ''; 1590 $body = ''; 1591 $err_msg = ''; 1592 1593 $res = TRUE; 1594 1595 // initialize the CURL session 1596 $ch = curl_init($url); 1597 1598 // verify the the server's certificate corresponds to its name 1599 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1); 1600 // but do not verify the certificate itself 1601 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); 1602 1603 // return the CURL output into a variable 1604 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 1605 // include the HTTP header with the body 1606 curl_setopt($ch, CURLOPT_HEADER, 1); 1607 // add cookies headers 1608 if ( is_array($cookies) ) { 1609 curl_setopt($ch,CURLOPT_COOKIE,implode(';',$cookies)); 1610 } 1611 // perform the query 1612 $buf = curl_exec ($ch); 1613 if ( $buf === FALSE ) { 1614 phpCAS::trace('cur_exec() failed'); 1615 $err_msg = 'CURL error #'.curl_errno($ch).': '.curl_error($ch); 1616 // close the CURL session 1617 curl_close ($ch); 1618 $res = FALSE; 1619 } else { 1620 // close the CURL session 1621 curl_close ($ch); 1622 1623 // find the end of the headers 1624 // note: strpos($str,"\n\r\n\r") does not work (?) 1625 $pos = FALSE; 1626 for ($i=0; $i<strlen($buf); $i++) { 1627 if ( $buf[$i] == chr(13) ) 1628 if ( $buf[$i+1] == chr(10) ) 1629 if ( $buf[$i+2] == chr(13) ) 1630 if ( $buf[$i+3] == chr(10) ) { 1631 // header found 1632 $pos = $i; 1633 break; 1634 } 1635 } 1636 1637 if ( $pos === FALSE ) { 1638 // end of header not found 1639 $err_msg = 'no header found'; 1640 phpCAS::trace($err_msg); 1641 $res = FALSE; 1642 } else { 1643 // extract headers into an array 1644 $headers = preg_split ("/[\n\r]+/",substr($buf,0,$pos)); 1645 // extract body into a string 1646 $body = substr($buf,$pos+4); 1647 } 1648 } 1649 1650 phpCAS::traceEnd($res); 1651 return $res; 1652 } 1653 1654 /** 1655 * This method is used to access an HTTP[S] service. 1656 * 1657 * @param $url the service to access. 1658 * @param $err_code an error code Possible values are PHPCAS_SERVICE_OK (on 1659 * success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, 1660 * PHPCAS_SERVICE_PT_FAILURE, PHPCAS_SERVICE_NOT AVAILABLE. 1661 * @param $output the output of the service (also used to give an error 1662 * message on failure). 1663 * 1664 * @return TRUE on success, FALSE otherwise (in this later case, $err_code 1665 * gives the reason why it failed and $output contains an error message). 1666 * 1667 * @public 1668 */ 1669 function serviceWeb($url,&$err_code,&$output) 1670 { 1671 phpCAS::traceBegin(); 1672 // at first retrieve a PT 1673 $pt = $this->retrievePT($url,$err_code,$output); 1674 1675 $res = TRUE; 1676 1677 // test if PT was retrieved correctly 1678 if ( !$pt ) { 1679 // note: $err_code and $err_msg are filled by CASClient::retrievePT() 1680 phpCAS::trace('PT was not retrieved correctly'); 1681 $res = FALSE; 1682 } else { 1683 // add cookies if necessary 1684 if ( is_array($_SESSION['phpCAS']['services'][$url]['cookies']) ) { 1685 foreach ( $_SESSION['phpCAS']['services'][$url]['cookies'] as $name => $val ) { 1686 $cookies[] = $name.'='.$val; 1687 } 1688 } 1689 1690 // build the URL including the PT 1691 if ( strstr($url,'?') === FALSE ) { 1692 $service_url = $url.'?ticket='.$pt; 1693 } else { 1694 $service_url = $url.'&ticket='.$pt; 1695 } 1696 1697 phpCAS::trace('reading URL`'.$service_url.'\''); 1698 if ( !$this->readURL($service_url,$cookies,$headers,$output,$err_msg) ) { 1699 phpCAS::trace('could not read URL`'.$service_url.'\''); 1700 $err_code = PHPCAS_SERVICE_NOT_AVAILABLE; 1701 // give an error message 1702 $output = sprintf($this->getString(CAS_STR_SERVICE_UNAVAILABLE), 1703 $service_url, 1704 $err_msg); 1705 $res = FALSE; 1706 } else { 1707 // URL has been fetched, extract the cookies 1708 phpCAS::trace('URL`'.$service_url.'\' has been read, storing cookies:'); 1709 foreach ( $headers as $header ) { 1710 // test if the header is a cookie 1711 if ( preg_match('/^Set-Cookie:/',$header) ) { 1712 // the header is a cookie, remove the beginning 1713 $header_val = preg_replace('/^Set-Cookie: */','',$header); 1714 // extract interesting information 1715 $name_val = strtok($header_val,'; '); 1716 // extract the name and the value of the cookie 1717 $cookie_name = strtok($name_val,'='); 1718 $cookie_val = strtok('='); 1719 // store the cookie 1720 $_SESSION['phpCAS']['services'][$url]['cookies'][$cookie_name] = $cookie_val; 1721 phpCAS::trace($cookie_name.' -> '.$cookie_val); 1722 } 1723 } 1724 } 1725 } 1726 1727 phpCAS::traceEnd($res); 1728 return $res; 1729 } 1730 1731 /** 1732 * This method is used to access an IMAP/POP3/NNTP service. 1733 * 1734 * @param $url a string giving the URL of the service, including the mailing box 1735 * for IMAP URLs, as accepted by imap_open(). 1736 * @param $flags options given to imap_open(). 1737 * @param $err_code an error code Possible values are PHPCAS_SERVICE_OK (on 1738 * success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, 1739 * PHPCAS_SERVICE_PT_FAILURE, PHPCAS_SERVICE_NOT AVAILABLE. 1740 * @param $err_msg an error message on failure 1741 * @param $pt the Proxy Ticket (PT) retrieved from the CAS server to access the URL 1742 * on success, FALSE on error). 1743 * 1744 * @return an IMAP stream on success, FALSE otherwise (in this later case, $err_code 1745 * gives the reason why it failed and $err_msg contains an error message). 1746 * 1747 * @public 1748 */ 1749 function serviceMail($url,$flags,&$err_code,&$err_msg,&$pt) 1750 { 1751 phpCAS::traceBegin(); 1752 // at first retrieve a PT 1753 $pt = $this->retrievePT($target_service,$err_code,$output); 1754 1755 $stream = FALSE; 1756 1757 // test if PT was retrieved correctly 1758 if ( !$pt ) { 1759 // note: $err_code and $err_msg are filled by CASClient::retrievePT() 1760 phpCAS::trace('PT was not retrieved correctly'); 1761 } else { 1762 phpCAS::trace('opening IMAP URL `'.$url.'\'...'); 1763 $stream = @imap_open($url,$this->getUser(),$pt,$flags); 1764 if ( !$stream ) { 1765 phpCAS::trace('could not open URL'); 1766 $err_code = PHPCAS_SERVICE_NOT_AVAILABLE; 1767 // give an error message 1768 $err_msg = sprintf($this->getString(CAS_STR_SERVICE_UNAVAILABLE), 1769 $service_url, 1770 var_export(imap_errors(),TRUE)); 1771 $pt = FALSE; 1772 $stream = FALSE; 1773 } else { 1774 phpCAS::trace('ok'); 1775 } 1776 } 1777 1778 phpCAS::traceEnd($stream); 1779 return $stream; 1780 } 1781 1782 /** @} */ 1783 1784 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1785 // XX XX 1786 // XX PROXIED CLIENT FEATURES (CAS 2.0) XX 1787 // XX XX 1788 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1789 1790 // ######################################################################## 1791 // PT 1792 // ######################################################################## 1793 /** 1794 * @addtogroup internalProxied 1795 * @{ 1796 */ 1797 1798 /** 1799 * the Proxy Ticket provided in the URL of the request if present 1800 * (empty otherwise). Written by CASClient::CASClient(), read by 1801 * CASClient::getPT() and CASClient::hasPGT(). 1802 * 1803 * @hideinitializer 1804 * @private 1805 */ 1806 var $_pt = ''; 1807 1808 /** 1809 * This method returns the Proxy Ticket provided in the URL of the request. 1810 * @return The proxy ticket. 1811 * @private 1812 */ 1813 function getPT() 1814 { 1815 return 'ST'.substr($this->_pt, 2); 1816 } 1817 1818 /** 1819 * This method stores the Proxy Ticket. 1820 * @param $pt The Proxy Ticket. 1821 * @private 1822 */ 1823 function setPT($pt) 1824 { $this->_pt = $pt; } 1825 1826 /** 1827 * This method tells if a Proxy Ticket was stored. 1828 * @return TRUE if a Proxy Ticket has been stored. 1829 * @private 1830 */ 1831 function hasPT() 1832 { return !empty($this->_pt); } 1833 1834 /** @} */ 1835 // ######################################################################## 1836 // PT VALIDATION 1837 // ######################################################################## 1838 /** 1839 * @addtogroup internalProxied 1840 * @{ 1841 */ 1842 1843 /** 1844 * This method is used to validate a PT; halt on failure 1845 * 1846 * @return bool TRUE when successfull, halt otherwise by calling CASClient::authError(). 1847 * 1848 * @private 1849 */ 1850 function validatePT(&$validate_url,&$text_response,&$tree_response) 1851 { 1852 phpCAS::traceBegin(); 1853 // build the URL to validate the ticket 1854 $validate_url = $this->getServerProxyValidateURL().'&ticket='.$this->getPT(); 1855 1856 if ( $this->isProxy() ) { 1857 // pass the callback url for CAS proxies 1858 $validate_url .= '&pgtUrl='.$this->getCallbackURL(); 1859 } 1860 1861 // open and read the URL 1862 if ( !$this->readURL($validate_url,''/*cookies*/,$headers,$text_response,$err_msg) ) { 1863 phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'); 1864 $this->authError('PT not validated', 1865 $validate_url, 1866 TRUE/*$no_response*/); 1867 } 1868 1869 // read the response of the CAS server into a DOM object 1870 if ( !($dom = domxml_open_mem($text_response))) { 1871 // read failed 1872 $this->authError('PT not validated', 1873 $validate_url, 1874 FALSE/*$no_response*/, 1875 TRUE/*$bad_response*/, 1876 $text_response); 1877 } 1878 // read the root node of the XML tree 1879 if ( !($tree_response = $dom->document_element()) ) { 1880 // read failed 1881 $this->authError('PT not validated', 1882 $validate_url, 1883 FALSE/*$no_response*/, 1884 TRUE/*$bad_response*/, 1885 $text_response); 1886 } 1887 // insure that tag name is 'serviceResponse' 1888 if ( $tree_response->node_name() != 'serviceResponse' ) { 1889 // bad root node 1890 $this->authError('PT not validated', 1891 $validate_url, 1892 FALSE/*$no_response*/, 1893 TRUE/*$bad_response*/, 1894 $text_response); 1895 } 1896 if ( sizeof($arr = $tree_response->get_elements_by_tagname("authenticationSuccess")) != 0) { 1897 // authentication succeded, extract the user name 1898 if ( sizeof($arr = $tree_response->get_elements_by_tagname("user")) == 0) { 1899 // no user specified => error 1900 $this->authError('PT not validated', 1901 $validate_url, 1902 FALSE/*$no_response*/, 1903 TRUE/*$bad_response*/, 1904 $text_response); 1905 } 1906 $this->setUser(trim($arr[0]->get_content())); 1907 1908 } else if ( sizeof($arr = $tree_response->get_elements_by_tagname("authenticationFailure")) != 0) { 1909 // authentication succeded, extract the error code and message 1910 $this->authError('PT not validated', 1911 $validate_url, 1912 FALSE/*$no_response*/, 1913 FALSE/*$bad_response*/, 1914 $text_response, 1915 $arr[0]->get_attribute('code')/*$err_code*/, 1916 trim($arr[0]->get_content())/*$err_msg*/); 1917 } else { 1918 $this->authError('PT not validated', 1919 $validate_url, 1920 FALSE/*$no_response*/, 1921 TRUE/*$bad_response*/, 1922 $text_response); 1923 } 1924 1925 // at this step, PT has been validated and $this->_user has been set, 1926 1927 phpCAS::traceEnd(TRUE); 1928 return TRUE; 1929 } 1930 1931 /** @} */ 1932 1933 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1934 // XX XX 1935 // XX MISC XX 1936 // XX XX 1937 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1938 1939 /** 1940 * @addtogroup internalMisc 1941 * @{ 1942 */ 1943 1944 // ######################################################################## 1945 // URL 1946 // ######################################################################## 1947 /** 1948 * the URL of the current request (without any ticket CGI parameter). Written 1949 * and read by CASClient::getURL(). 1950 * 1951 * @hideinitializer 1952 * @private 1953 */ 1954 var $_url = ''; 1955 1956 /** 1957 * This method returns the URL of the current request (without any ticket 1958 * CGI parameter). 1959 * 1960 * @return The URL 1961 * 1962 * @private 1963 */ 1964 function getURL() 1965 { 1966 phpCAS::traceBegin(); 1967 // the URL is built when needed only 1968 if ( empty($this->_url) ) { 1969 $final_uri = ''; 1970 // remove the ticket if present in the URL 1971 $final_uri = ($this->isHttps()) ? 'https' : 'http'; 1972 $final_uri .= '://'; 1973 /* replaced by Julien Marchal - v0.4.6 1974 * $this->_url .= $_SERVER['SERVER_NAME']; 1975 */ 1976 if(empty($_SERVER['HTTP_X_FORWARDED_SERVER'])){ 1977 /* replaced by teedog - v0.4.12 1978 * $this->_url .= $_SERVER['SERVER_NAME']; 1979 */ 1980 if (empty($_SERVER['SERVER_NAME'])) { 1981 $server_name = $_SERVER['HTTP_HOST']; 1982 } else { 1983 $server_name = $_SERVER['SERVER_NAME']; 1984 } 1985 } else { 1986 $server_name = $_SERVER['HTTP_X_FORWARDED_SERVER']; 1987 } 1988 $final_uri .= $server_name; 1989 if (!strpos($server_name, ':')) { 1990 if ( ($this->isHttps() && $_SERVER['SERVER_PORT']!=443) 1991 || (!$this->isHttps() && $_SERVER['SERVER_PORT']!=80) ) { 1992 $final_uri .= ':'; 1993 $final_uri .= $_SERVER['SERVER_PORT']; 1994 } 1995 } 1996 1997 $final_uri .= strtok($_SERVER['REQUEST_URI'],"?"); 1998 $cgi_params = '?'.strtok("?"); 1999 // remove the ticket if present in the CGI parameters 2000 $cgi_params = preg_replace('/&ticket=[^&]*/','',$cgi_params); 2001 $cgi_params = preg_replace('/\?ticket=[^&;]*/','?',$cgi_params); 2002 $cgi_params = preg_replace('/\?%26/','?',$cgi_params); 2003 $cgi_params = preg_replace('/\?&/','?',$cgi_params); 2004 $cgi_params = preg_replace('/\?$/','',$cgi_params); 2005 $final_uri .= $cgi_params; 2006 $this->setURL($final_uri); 2007 } 2008 phpCAS::traceEnd($this->_url); 2009 return $this->_url; 2010 } 2011 2012 /** 2013 * This method sets the URL of the current request 2014 * 2015 * @param $url url to set for service 2016 * 2017 * @private 2018 */ 2019 function setURL($url) 2020 { 2021 $this->_url = $url; 2022 } 2023 2024 // ######################################################################## 2025 // AUTHENTICATION ERROR HANDLING 2026 // ######################################################################## 2027 /** 2028 * This method is used to print the HTML output when the user was not authenticated. 2029 * 2030 * @param $failure the failure that occured 2031 * @param $cas_url the URL the CAS server was asked for 2032 * @param $no_response the response from the CAS server (other 2033 * parameters are ignored if TRUE) 2034 * @param $bad_response bad response from the CAS server ($err_code 2035 * and $err_msg ignored if TRUE) 2036 * @param $cas_response the response of the CAS server 2037 * @param $err_code the error code given by the CAS server 2038 * @param $err_msg the error message given by the CAS server 2039 * 2040 * @private 2041 */ 2042 function authError($failure,$cas_url,$no_response,$bad_response='',$cas_response='',$err_code='',$err_msg='') 2043 { 2044 phpCAS::traceBegin(); 2045 2046 $this->printHTMLHeader($this->getString(CAS_STR_AUTHENTICATION_FAILED)); 2047 printf($this->getString(CAS_STR_YOU_WERE_NOT_AUTHENTICATED),$this->getURL(),$_SERVER['SERVER_ADMIN']); 2048 phpCAS::trace('CAS URL: '.$cas_url); 2049 phpCAS::trace('Authentication failure: '.$failure); 2050 if ( $no_response ) { 2051 phpCAS::trace('Reason: no response from the CAS server'); 2052 } else { 2053 if ( $bad_response ) { 2054 phpCAS::trace('Reason: bad response from the CAS server'); 2055 } else { 2056 switch ($this->getServerVersion()) { 2057 case CAS_VERSION_1_0: 2058 phpCAS::trace('Reason: CAS error'); 2059 break; 2060 case CAS_VERSION_2_0: 2061 if ( empty($err_code) ) 2062 phpCAS::trace('Reason: no CAS error'); 2063 else 2064 phpCAS::trace('Reason: ['.$err_code.'] CAS error: '.$err_msg); 2065 break; 2066 } 2067 } 2068 phpCAS::trace('CAS response: '.$cas_response); 2069 } 2070 $this->printHTMLFooter(); 2071 phpCAS::traceExit(); 2072 exit(); 2073 } 2074 2075 /** @} */ 2076 } 2077 2078 ?>
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Wed Jan 14 11:33:29 2009 | Cross-referenced by PHPXref 0.7 |