Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   1  <?php
   2  
   3  /**
   4   * Licensed to Jasig under one or more contributor license
   5   * agreements. See the NOTICE file distributed with this work for
   6   * additional information regarding copyright ownership.
   7   *
   8   * Jasig licenses this file to you under the Apache License,
   9   * Version 2.0 (the "License"); you may not use this file except in
  10   * compliance with the License. You may obtain a copy of the License at:
  11   *
  12   * http://www.apache.org/licenses/LICENSE-2.0
  13   *
  14   * Unless required by applicable law or agreed to in writing, software
  15   * distributed under the License is distributed on an "AS IS" BASIS,
  16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17   * See the License for the specific language governing permissions and
  18   * limitations under the License.
  19   *
  20   * PHP Version 5
  21   *
  22   * @file     CAS/Client.php
  23   * @category Authentication
  24   * @package  PhpCAS
  25   * @author   Pascal Aubry <pascal.aubry@univ-rennes1.fr>
  26   * @author   Olivier Berger <olivier.berger@it-sudparis.eu>
  27   * @author   Brett Bieber <brett.bieber@gmail.com>
  28   * @author   Joachim Fritschi <jfritschi@freenet.de>
  29   * @author   Adam Franco <afranco@middlebury.edu>
  30   * @license  http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
  31   * @link     https://wiki.jasig.org/display/CASC/phpCAS
  32   */
  33  
  34  /**
  35   * The CAS_Client class is a client interface that provides CAS authentication
  36   * to PHP applications.
  37   *
  38   * @class    CAS_Client
  39   * @category Authentication
  40   * @package  PhpCAS
  41   * @author   Pascal Aubry <pascal.aubry@univ-rennes1.fr>
  42   * @author   Olivier Berger <olivier.berger@it-sudparis.eu>
  43   * @author   Brett Bieber <brett.bieber@gmail.com>
  44   * @author   Joachim Fritschi <jfritschi@freenet.de>
  45   * @author   Adam Franco <afranco@middlebury.edu>
  46   * @license  http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
  47   * @link     https://wiki.jasig.org/display/CASC/phpCAS
  48   *
  49   */
  50  
  51  class CAS_Client
  52  {
  53  
  54      // ########################################################################
  55      //  HTML OUTPUT
  56      // ########################################################################
  57      /**
  58      * @addtogroup internalOutput
  59      * @{
  60      */
  61  
  62      /**
  63       * This method filters a string by replacing special tokens by appropriate values
  64       * and prints it. The corresponding tokens are taken into account:
  65       * - __CAS_VERSION__
  66       * - __PHPCAS_VERSION__
  67       * - __SERVER_BASE_URL__
  68       *
  69       * Used by CAS_Client::PrintHTMLHeader() and CAS_Client::printHTMLFooter().
  70       *
  71       * @param string $str the string to filter and output
  72       *
  73       * @return void
  74       */
  75      private function _htmlFilterOutput($str)
  76      {
  77          $str = str_replace('__CAS_VERSION__', $this->getServerVersion(), $str);
  78          $str = str_replace('__PHPCAS_VERSION__', phpCAS::getVersion(), $str);
  79          $str = str_replace('__SERVER_BASE_URL__', $this->_getServerBaseURL(), $str);
  80          echo $str;
  81      }
  82  
  83      /**
  84       * A string used to print the header of HTML pages. Written by
  85       * CAS_Client::setHTMLHeader(), read by CAS_Client::printHTMLHeader().
  86       *
  87       * @hideinitializer
  88       * @see CAS_Client::setHTMLHeader, CAS_Client::printHTMLHeader()
  89       */
  90      private $_output_header = '';
  91  
  92      /**
  93       * This method prints the header of the HTML output (after filtering). If
  94       * CAS_Client::setHTMLHeader() was not used, a default header is output.
  95       *
  96       * @param string $title the title of the page
  97       *
  98       * @return void
  99       * @see _htmlFilterOutput()
 100       */
 101      public function printHTMLHeader($title)
 102      {
 103          $this->_htmlFilterOutput(
 104              str_replace(
 105                  '__TITLE__', $title,
 106                  (empty($this->_output_header)
 107                  ? '<html><head><title>__TITLE__</title></head><body><h1>__TITLE__</h1>'
 108                  : $this->_output_header)
 109              )
 110          );
 111      }
 112  
 113      /**
 114       * A string used to print the footer of HTML pages. Written by
 115       * CAS_Client::setHTMLFooter(), read by printHTMLFooter().
 116       *
 117       * @hideinitializer
 118       * @see CAS_Client::setHTMLFooter, CAS_Client::printHTMLFooter()
 119       */
 120      private $_output_footer = '';
 121  
 122      /**
 123       * This method prints the footer of the HTML output (after filtering). If
 124       * CAS_Client::setHTMLFooter() was not used, a default footer is output.
 125       *
 126       * @return void
 127       * @see _htmlFilterOutput()
 128       */
 129      public function printHTMLFooter()
 130      {
 131          $lang = $this->getLangObj();
 132          $this->_htmlFilterOutput(
 133              empty($this->_output_footer)?
 134              (phpCAS::getVerbose())?
 135                  '<hr><address>phpCAS __PHPCAS_VERSION__ '
 136                  .$lang->getUsingServer()
 137                  .' <a href="__SERVER_BASE_URL__">__SERVER_BASE_URL__</a> (CAS __CAS_VERSION__)</a></address></body></html>'
 138                  :'</body></html>'
 139              :$this->_output_footer
 140          );
 141      }
 142  
 143      /**
 144       * This method set the HTML header used for all outputs.
 145       *
 146       * @param string $header the HTML header.
 147       *
 148       * @return void
 149       */
 150      public function setHTMLHeader($header)
 151      {
 152      	 // Argument Validation
 153      	 if (gettype($header) != 'string')
 154          	 throw new CAS_TypeMismatchException($header, '$header', 'string');
 155  
 156          $this->_output_header = $header;
 157      }
 158  
 159      /**
 160       * This method set the HTML footer used for all outputs.
 161       *
 162       * @param string $footer the HTML footer.
 163       *
 164       * @return void
 165       */
 166      public function setHTMLFooter($footer)
 167      {
 168      	 // Argument Validation
 169      	 if (gettype($footer) != 'string')
 170          	 throw new CAS_TypeMismatchException($footer, '$footer', 'string');
 171  
 172          $this->_output_footer = $footer;
 173      }
 174  
 175  
 176      /** @} */
 177  
 178  
 179      // ########################################################################
 180      //  INTERNATIONALIZATION
 181      // ########################################################################
 182      /**
 183      * @addtogroup internalLang
 184      * @{
 185      */
 186      /**
 187       * A string corresponding to the language used by phpCAS. Written by
 188       * CAS_Client::setLang(), read by CAS_Client::getLang().
 189  
 190       * @note debugging information is always in english (debug purposes only).
 191       */
 192      private $_lang = PHPCAS_LANG_DEFAULT;
 193  
 194      /**
 195       * This method is used to set the language used by phpCAS.
 196       *
 197       * @param string $lang representing the language.
 198       *
 199       * @return void
 200       */
 201      public function setLang($lang)
 202      {
 203      	 // Argument Validation
 204      	 if (gettype($lang) != 'string')
 205          	 throw new CAS_TypeMismatchException($lang, '$lang', 'string');
 206  
 207          phpCAS::traceBegin();
 208          $obj = new $lang();
 209          if (!($obj instanceof CAS_Languages_LanguageInterface)) {
 210              throw new CAS_InvalidArgumentException(
 211                  '$className must implement the CAS_Languages_LanguageInterface'
 212              );
 213          }
 214          $this->_lang = $lang;
 215          phpCAS::traceEnd();
 216      }
 217      /**
 218       * Create the language
 219       *
 220       * @return CAS_Languages_LanguageInterface object implementing the class
 221       */
 222      public function getLangObj()
 223      {
 224          $classname = $this->_lang;
 225          return new $classname();
 226      }
 227  
 228      /** @} */
 229      // ########################################################################
 230      //  CAS SERVER CONFIG
 231      // ########################################################################
 232      /**
 233      * @addtogroup internalConfig
 234      * @{
 235      */
 236  
 237      /**
 238       * a record to store information about the CAS server.
 239       * - $_server['version']: the version of the CAS server
 240       * - $_server['hostname']: the hostname of the CAS server
 241       * - $_server['port']: the port the CAS server is running on
 242       * - $_server['uri']: the base URI the CAS server is responding on
 243       * - $_server['base_url']: the base URL of the CAS server
 244       * - $_server['login_url']: the login URL of the CAS server
 245       * - $_server['service_validate_url']: the service validating URL of the
 246       *   CAS server
 247       * - $_server['proxy_url']: the proxy URL of the CAS server
 248       * - $_server['proxy_validate_url']: the proxy validating URL of the CAS server
 249       * - $_server['logout_url']: the logout URL of the CAS server
 250       *
 251       * $_server['version'], $_server['hostname'], $_server['port'] and
 252       * $_server['uri'] are written by CAS_Client::CAS_Client(), read by
 253       * CAS_Client::getServerVersion(), CAS_Client::_getServerHostname(),
 254       * CAS_Client::_getServerPort() and CAS_Client::_getServerURI().
 255       *
 256       * The other fields are written and read by CAS_Client::_getServerBaseURL(),
 257       * CAS_Client::getServerLoginURL(), CAS_Client::getServerServiceValidateURL(),
 258       * CAS_Client::getServerProxyValidateURL() and CAS_Client::getServerLogoutURL().
 259       *
 260       * @hideinitializer
 261       */
 262      private $_server = array(
 263          'version' => '',
 264          'hostname' => 'none',
 265          'port' => -1,
 266          'uri' => 'none');
 267  
 268      /**
 269       * This method is used to retrieve the version of the CAS server.
 270       *
 271       * @return string the version of the CAS server.
 272       */
 273      public function getServerVersion()
 274      {
 275          return $this->_server['version'];
 276      }
 277  
 278      /**
 279       * This method is used to retrieve the hostname of the CAS server.
 280       *
 281       * @return string the hostname of the CAS server.
 282       */
 283      private function _getServerHostname()
 284      {
 285          return $this->_server['hostname'];
 286      }
 287  
 288      /**
 289       * This method is used to retrieve the port of the CAS server.
 290       *
 291       * @return int the port of the CAS server.
 292       */
 293      private function _getServerPort()
 294      {
 295          return $this->_server['port'];
 296      }
 297  
 298      /**
 299       * This method is used to retrieve the URI of the CAS server.
 300       *
 301       * @return string a URI.
 302       */
 303      private function _getServerURI()
 304      {
 305          return $this->_server['uri'];
 306      }
 307  
 308      /**
 309       * This method is used to retrieve the base URL of the CAS server.
 310       *
 311       * @return string a URL.
 312       */
 313      private function _getServerBaseURL()
 314      {
 315          // the URL is build only when needed
 316          if ( empty($this->_server['base_url']) ) {
 317              $this->_server['base_url'] = 'https://' . $this->_getServerHostname();
 318              if ($this->_getServerPort()!=443) {
 319                  $this->_server['base_url'] .= ':'
 320                  .$this->_getServerPort();
 321              }
 322              $this->_server['base_url'] .= $this->_getServerURI();
 323          }
 324          return $this->_server['base_url'];
 325      }
 326  
 327      /**
 328       * This method is used to retrieve the login URL of the CAS server.
 329       *
 330       * @param bool $gateway true to check authentication, false to force it
 331       * @param bool $renew   true to force the authentication with the CAS server
 332       *
 333       * @return string a URL.
 334       * @note It is recommended that CAS implementations ignore the "gateway"
 335       * parameter if "renew" is set
 336       */
 337      public function getServerLoginURL($gateway=false,$renew=false)
 338      {
 339          phpCAS::traceBegin();
 340          // the URL is build only when needed
 341          if ( empty($this->_server['login_url']) ) {
 342              $this->_server['login_url'] = $this->_buildQueryUrl($this->_getServerBaseURL().'login','service='.urlencode($this->getURL()));
 343          }
 344          $url = $this->_server['login_url'];
 345          if ($renew) {
 346              // It is recommended that when the "renew" parameter is set, its
 347              // value be "true"
 348              $url = $this->_buildQueryUrl($url, 'renew=true');
 349          } elseif ($gateway) {
 350              // It is recommended that when the "gateway" parameter is set, its
 351              // value be "true"
 352              $url = $this->_buildQueryUrl($url, 'gateway=true');
 353          }
 354          phpCAS::traceEnd($url);
 355          return $url;
 356      }
 357  
 358      /**
 359       * This method sets the login URL of the CAS server.
 360       *
 361       * @param string $url the login URL
 362       *
 363       * @return string login url
 364       */
 365      public function setServerLoginURL($url)
 366      {
 367      	 // Argument Validation
 368      	 if (gettype($url) != 'string')
 369          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
 370  
 371          return $this->_server['login_url'] = $url;
 372      }
 373  
 374  
 375      /**
 376       * This method sets the serviceValidate URL of the CAS server.
 377       *
 378       * @param string $url the serviceValidate URL
 379       *
 380       * @return string serviceValidate URL
 381       */
 382      public function setServerServiceValidateURL($url)
 383      {
 384      	 // Argument Validation
 385      	 if (gettype($url) != 'string')
 386          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
 387  
 388          return $this->_server['service_validate_url'] = $url;
 389      }
 390  
 391  
 392      /**
 393       * This method sets the proxyValidate URL of the CAS server.
 394       *
 395       * @param string $url the proxyValidate URL
 396       *
 397       * @return string proxyValidate URL
 398       */
 399      public function setServerProxyValidateURL($url)
 400      {
 401      	 // Argument Validation
 402      	 if (gettype($url) != 'string')
 403          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
 404  
 405          return $this->_server['proxy_validate_url'] = $url;
 406      }
 407  
 408  
 409      /**
 410       * This method sets the samlValidate URL of the CAS server.
 411       *
 412       * @param string $url the samlValidate URL
 413       *
 414       * @return string samlValidate URL
 415       */
 416      public function setServerSamlValidateURL($url)
 417      {
 418      	 // Argument Validation
 419      	 if (gettype($url) != 'string')
 420          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
 421  
 422          return $this->_server['saml_validate_url'] = $url;
 423      }
 424  
 425  
 426      /**
 427       * This method is used to retrieve the service validating URL of the CAS server.
 428       *
 429       * @return string serviceValidate URL.
 430       */
 431      public function getServerServiceValidateURL()
 432      {
 433          phpCAS::traceBegin();
 434          // the URL is build only when needed
 435          if ( empty($this->_server['service_validate_url']) ) {
 436              switch ($this->getServerVersion()) {
 437              case CAS_VERSION_1_0:
 438                  $this->_server['service_validate_url'] = $this->_getServerBaseURL()
 439                  .'validate';
 440                  break;
 441              case CAS_VERSION_2_0:
 442                  $this->_server['service_validate_url'] = $this->_getServerBaseURL()
 443                  .'serviceValidate';
 444                  break;
 445              case CAS_VERSION_3_0:
 446                  $this->_server['service_validate_url'] = $this->_getServerBaseURL()
 447                  .'p3/serviceValidate';
 448                  break;
 449              }
 450          }
 451          $url = $this->_buildQueryUrl(
 452              $this->_server['service_validate_url'],
 453              'service='.urlencode($this->getURL())
 454          );
 455          phpCAS::traceEnd($url);
 456          return $url;
 457      }
 458      /**
 459       * This method is used to retrieve the SAML validating URL of the CAS server.
 460       *
 461       * @return string samlValidate URL.
 462       */
 463      public function getServerSamlValidateURL()
 464      {
 465          phpCAS::traceBegin();
 466          // the URL is build only when needed
 467          if ( empty($this->_server['saml_validate_url']) ) {
 468              switch ($this->getServerVersion()) {
 469              case SAML_VERSION_1_1:
 470                  $this->_server['saml_validate_url'] = $this->_getServerBaseURL().'samlValidate';
 471                  break;
 472              }
 473          }
 474  
 475          $url = $this->_buildQueryUrl(
 476              $this->_server['saml_validate_url'],
 477              'TARGET='.urlencode($this->getURL())
 478          );
 479          phpCAS::traceEnd($url);
 480          return $url;
 481      }
 482  
 483      /**
 484       * This method is used to retrieve the proxy validating URL of the CAS server.
 485       *
 486       * @return string proxyValidate URL.
 487       */
 488      public function getServerProxyValidateURL()
 489      {
 490          phpCAS::traceBegin();
 491          // the URL is build only when needed
 492          if ( empty($this->_server['proxy_validate_url']) ) {
 493              switch ($this->getServerVersion()) {
 494              case CAS_VERSION_1_0:
 495                  $this->_server['proxy_validate_url'] = '';
 496                  break;
 497              case CAS_VERSION_2_0:
 498                  $this->_server['proxy_validate_url'] = $this->_getServerBaseURL().'proxyValidate';
 499                  break;
 500              case CAS_VERSION_3_0:
 501                  $this->_server['proxy_validate_url'] = $this->_getServerBaseURL().'p3/proxyValidate';
 502                  break;
 503              }
 504          }
 505          $url = $this->_buildQueryUrl(
 506              $this->_server['proxy_validate_url'],
 507              'service='.urlencode($this->getURL())
 508          );
 509          phpCAS::traceEnd($url);
 510          return $url;
 511      }
 512  
 513  
 514      /**
 515       * This method is used to retrieve the proxy URL of the CAS server.
 516       *
 517       * @return  string proxy URL.
 518       */
 519      public function getServerProxyURL()
 520      {
 521          // the URL is build only when needed
 522          if ( empty($this->_server['proxy_url']) ) {
 523              switch ($this->getServerVersion()) {
 524              case CAS_VERSION_1_0:
 525                  $this->_server['proxy_url'] = '';
 526                  break;
 527              case CAS_VERSION_2_0:
 528              case CAS_VERSION_3_0:
 529                  $this->_server['proxy_url'] = $this->_getServerBaseURL().'proxy';
 530                  break;
 531              }
 532          }
 533          return $this->_server['proxy_url'];
 534      }
 535  
 536      /**
 537       * This method is used to retrieve the logout URL of the CAS server.
 538       *
 539       * @return string logout URL.
 540       */
 541      public function getServerLogoutURL()
 542      {
 543          // the URL is build only when needed
 544          if ( empty($this->_server['logout_url']) ) {
 545              $this->_server['logout_url'] = $this->_getServerBaseURL().'logout';
 546          }
 547          return $this->_server['logout_url'];
 548      }
 549  
 550      /**
 551       * This method sets the logout URL of the CAS server.
 552       *
 553       * @param string $url the logout URL
 554       *
 555       * @return string logout url
 556       */
 557      public function setServerLogoutURL($url)
 558      {
 559      	 // Argument Validation
 560      	 if (gettype($url) != 'string')
 561          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
 562  
 563          return $this->_server['logout_url'] = $url;
 564      }
 565  
 566      /**
 567       * An array to store extra curl options.
 568       */
 569      private $_curl_options = array();
 570  
 571      /**
 572       * This method is used to set additional user curl options.
 573       *
 574       * @param string $key   name of the curl option
 575       * @param string $value value of the curl option
 576       *
 577       * @return void
 578       */
 579      public function setExtraCurlOption($key, $value)
 580      {
 581          $this->_curl_options[$key] = $value;
 582      }
 583  
 584      /** @} */
 585  
 586      // ########################################################################
 587      //  Change the internal behaviour of phpcas
 588      // ########################################################################
 589  
 590      /**
 591       * @addtogroup internalBehave
 592       * @{
 593       */
 594  
 595      /**
 596       * The class to instantiate for making web requests in readUrl().
 597       * The class specified must implement the CAS_Request_RequestInterface.
 598       * By default CAS_Request_CurlRequest is used, but this may be overridden to
 599       * supply alternate request mechanisms for testing.
 600       */
 601      private $_requestImplementation = 'CAS_Request_CurlRequest';
 602  
 603      /**
 604       * Override the default implementation used to make web requests in readUrl().
 605       * This class must implement the CAS_Request_RequestInterface.
 606       *
 607       * @param string $className name of the RequestImplementation class
 608       *
 609       * @return void
 610       */
 611      public function setRequestImplementation ($className)
 612      {
 613          $obj = new $className;
 614          if (!($obj instanceof CAS_Request_RequestInterface)) {
 615              throw new CAS_InvalidArgumentException(
 616                  '$className must implement the CAS_Request_RequestInterface'
 617              );
 618          }
 619          $this->_requestImplementation = $className;
 620      }
 621  
 622      /**
 623       * @var boolean $_clearTicketsFromUrl; If true, phpCAS will clear session
 624       * tickets from the URL after a successful authentication.
 625       */
 626      private $_clearTicketsFromUrl = true;
 627  
 628      /**
 629       * Configure the client to not send redirect headers and call exit() on
 630       * authentication success. The normal redirect is used to remove the service
 631       * ticket from the client's URL, but for running unit tests we need to
 632       * continue without exiting.
 633       *
 634       * Needed for testing authentication
 635       *
 636       * @return void
 637       */
 638      public function setNoClearTicketsFromUrl ()
 639      {
 640          $this->_clearTicketsFromUrl = false;
 641      }
 642  
 643      /**
 644       * @var callback $_attributeParserCallbackFunction;
 645       */
 646      private $_casAttributeParserCallbackFunction = null;
 647  
 648      /**
 649       * @var array $_attributeParserCallbackArgs;
 650       */
 651      private $_casAttributeParserCallbackArgs = array();
 652  
 653      /**
 654       * Set a callback function to be run when parsing CAS attributes
 655       *
 656       * The callback function will be passed a XMLNode as its first parameter,
 657       * followed by any $additionalArgs you pass.
 658       *
 659       * @param string $function       callback function to call
 660       * @param array  $additionalArgs optional array of arguments
 661       *
 662       * @return void
 663       */
 664      public function setCasAttributeParserCallback($function, array $additionalArgs = array())
 665      {
 666          $this->_casAttributeParserCallbackFunction = $function;
 667          $this->_casAttributeParserCallbackArgs = $additionalArgs;
 668      }
 669  
 670      /** @var callable $_postAuthenticateCallbackFunction;
 671       */
 672      private $_postAuthenticateCallbackFunction = null;
 673  
 674      /**
 675       * @var array $_postAuthenticateCallbackArgs;
 676       */
 677      private $_postAuthenticateCallbackArgs = array();
 678  
 679      /**
 680       * Set a callback function to be run when a user authenticates.
 681       *
 682       * The callback function will be passed a $logoutTicket as its first parameter,
 683       * followed by any $additionalArgs you pass. The $logoutTicket parameter is an
 684       * opaque string that can be used to map a session-id to the logout request
 685       * in order to support single-signout in applications that manage their own
 686       * sessions (rather than letting phpCAS start the session).
 687       *
 688       * phpCAS::forceAuthentication() will always exit and forward client unless
 689       * they are already authenticated. To perform an action at the moment the user
 690       * logs in (such as registering an account, performing logging, etc), register
 691       * a callback function here.
 692       *
 693       * @param callable $function       callback function to call
 694       * @param array  $additionalArgs optional array of arguments
 695       *
 696       * @return void
 697       */
 698      public function setPostAuthenticateCallback ($function, array $additionalArgs = array())
 699      {
 700          $this->_postAuthenticateCallbackFunction = $function;
 701          $this->_postAuthenticateCallbackArgs = $additionalArgs;
 702      }
 703  
 704      /**
 705       * @var callable $_signoutCallbackFunction;
 706       */
 707      private $_signoutCallbackFunction = null;
 708  
 709      /**
 710       * @var array $_signoutCallbackArgs;
 711       */
 712      private $_signoutCallbackArgs = array();
 713  
 714      /**
 715       * Set a callback function to be run when a single-signout request is received.
 716       *
 717       * The callback function will be passed a $logoutTicket as its first parameter,
 718       * followed by any $additionalArgs you pass. The $logoutTicket parameter is an
 719       * opaque string that can be used to map a session-id to the logout request in
 720       * order to support single-signout in applications that manage their own sessions
 721       * (rather than letting phpCAS start and destroy the session).
 722       *
 723       * @param callable $function       callback function to call
 724       * @param array  $additionalArgs optional array of arguments
 725       *
 726       * @return void
 727       */
 728      public function setSingleSignoutCallback ($function, array $additionalArgs = array())
 729      {
 730          $this->_signoutCallbackFunction = $function;
 731          $this->_signoutCallbackArgs = $additionalArgs;
 732      }
 733  
 734      // ########################################################################
 735      //  Methods for supplying code-flow feedback to integrators.
 736      // ########################################################################
 737  
 738      /**
 739       * Ensure that this is actually a proxy object or fail with an exception
 740       *
 741       * @throws CAS_OutOfSequenceBeforeProxyException
 742       *
 743       * @return void
 744       */
 745      public function ensureIsProxy()
 746      {
 747          if (!$this->isProxy()) {
 748              throw new CAS_OutOfSequenceBeforeProxyException();
 749          }
 750      }
 751  
 752      /**
 753       * Mark the caller of authentication. This will help client integraters determine
 754       * problems with their code flow if they call a function such as getUser() before
 755       * authentication has occurred.
 756       *
 757       * @param bool $auth True if authentication was successful, false otherwise.
 758       *
 759       * @return null
 760       */
 761      public function markAuthenticationCall ($auth)
 762      {
 763          // store where the authentication has been checked and the result
 764          $dbg = debug_backtrace();
 765          $this->_authentication_caller = array (
 766              'file' => $dbg[1]['file'],
 767              'line' => $dbg[1]['line'],
 768              'method' => $dbg[1]['class'] . '::' . $dbg[1]['function'],
 769              'result' => (boolean)$auth
 770          );
 771      }
 772      private $_authentication_caller;
 773  
 774      /**
 775       * Answer true if authentication has been checked.
 776       *
 777       * @return bool
 778       */
 779      public function wasAuthenticationCalled ()
 780      {
 781          return !empty($this->_authentication_caller);
 782      }
 783  
 784      /**
 785       * Ensure that authentication was checked. Terminate with exception if no
 786       * authentication was performed
 787       *
 788       * @throws CAS_OutOfSequenceBeforeAuthenticationCallException
 789       *
 790       * @return void
 791       */
 792      private function _ensureAuthenticationCalled()
 793      {
 794          if (!$this->wasAuthenticationCalled()) {
 795              throw new CAS_OutOfSequenceBeforeAuthenticationCallException();
 796          }
 797      }
 798  
 799      /**
 800       * Answer the result of the authentication call.
 801       *
 802       * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
 803       * and markAuthenticationCall() didn't happen.
 804       *
 805       * @return bool
 806       */
 807      public function wasAuthenticationCallSuccessful ()
 808      {
 809          $this->_ensureAuthenticationCalled();
 810          return $this->_authentication_caller['result'];
 811      }
 812  
 813  
 814      /**
 815       * Ensure that authentication was checked. Terminate with exception if no
 816       * authentication was performed
 817       *
 818       * @throws CAS_OutOfSequenceException
 819       *
 820       * @return void
 821       */
 822      public function ensureAuthenticationCallSuccessful()
 823      {
 824          $this->_ensureAuthenticationCalled();
 825          if (!$this->_authentication_caller['result']) {
 826              throw new CAS_OutOfSequenceException(
 827                  'authentication was checked (by '
 828                  . $this->getAuthenticationCallerMethod()
 829                  . '() at ' . $this->getAuthenticationCallerFile()
 830                  . ':' . $this->getAuthenticationCallerLine()
 831                  . ') but the method returned false'
 832              );
 833          }
 834      }
 835  
 836      /**
 837       * Answer information about the authentication caller.
 838       *
 839       * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
 840       * and markAuthenticationCall() didn't happen.
 841       *
 842       * @return string the file that called authentication
 843       */
 844      public function getAuthenticationCallerFile ()
 845      {
 846          $this->_ensureAuthenticationCalled();
 847          return $this->_authentication_caller['file'];
 848      }
 849  
 850      /**
 851       * Answer information about the authentication caller.
 852       *
 853       * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
 854       * and markAuthenticationCall() didn't happen.
 855       *
 856       * @return int the line that called authentication
 857       */
 858      public function getAuthenticationCallerLine ()
 859      {
 860          $this->_ensureAuthenticationCalled();
 861          return $this->_authentication_caller['line'];
 862      }
 863  
 864      /**
 865       * Answer information about the authentication caller.
 866       *
 867       * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
 868       * and markAuthenticationCall() didn't happen.
 869       *
 870       * @return string the method that called authentication
 871       */
 872      public function getAuthenticationCallerMethod ()
 873      {
 874          $this->_ensureAuthenticationCalled();
 875          return $this->_authentication_caller['method'];
 876      }
 877  
 878      /** @} */
 879  
 880      // ########################################################################
 881      //  CONSTRUCTOR
 882      // ########################################################################
 883      /**
 884      * @addtogroup internalConfig
 885      * @{
 886      */
 887  
 888      /**
 889       * CAS_Client constructor.
 890       *
 891       * @param string $server_version  the version of the CAS server
 892       * @param bool   $proxy           true if the CAS client is a CAS proxy
 893       * @param string $server_hostname the hostname of the CAS server
 894       * @param int    $server_port     the port the CAS server is running on
 895       * @param string $server_uri      the URI the CAS server is responding on
 896       * @param bool   $changeSessionID Allow phpCAS to change the session_id
 897       *                                (Single Sign Out/handleLogoutRequests
 898       *                                is based on that change)
 899       *
 900       * @return self a newly created CAS_Client object
 901       */
 902      public function __construct(
 903          $server_version,
 904          $proxy,
 905          $server_hostname,
 906          $server_port,
 907          $server_uri,
 908          $changeSessionID = true
 909      ) {
 910  	 	 // Argument validation
 911          if (gettype($server_version) != 'string')
 912          	 throw new CAS_TypeMismatchException($server_version, '$server_version', 'string');
 913          if (gettype($proxy) != 'boolean')
 914          	 throw new CAS_TypeMismatchException($proxy, '$proxy', 'boolean');
 915          if (gettype($server_hostname) != 'string')
 916          	 throw new CAS_TypeMismatchException($server_hostname, '$server_hostname', 'string');
 917          if (gettype($server_port) != 'integer')
 918          	 throw new CAS_TypeMismatchException($server_port, '$server_port', 'integer');
 919          if (gettype($server_uri) != 'string')
 920          	 throw new CAS_TypeMismatchException($server_uri, '$server_uri', 'string');
 921          if (gettype($changeSessionID) != 'boolean')
 922          	 throw new CAS_TypeMismatchException($changeSessionID, '$changeSessionID', 'boolean');
 923  
 924          phpCAS::traceBegin();
 925          // true : allow to change the session_id(), false session_id won't be
 926          // change and logout won't be handle because of that
 927          $this->_setChangeSessionID($changeSessionID);
 928  
 929          // skip Session Handling for logout requests and if don't want it'
 930          if (session_id()=="" && !$this->_isLogoutRequest()) {
 931              session_start();
 932              phpCAS :: trace("Starting a new session " . session_id());
 933          }
 934          // Only for debug purposes
 935          if ($this->isSessionAuthenticated()){
 936              phpCAS :: trace("Session is authenticated as: " . $_SESSION['phpCAS']['user']);
 937          } else {
 938              phpCAS :: trace("Session is not authenticated");
 939          }
 940          // are we in proxy mode ?
 941          $this->_proxy = $proxy;
 942  
 943          // Make cookie handling available.
 944          if ($this->isProxy()) {
 945              if (!isset($_SESSION['phpCAS'])) {
 946                  $_SESSION['phpCAS'] = array();
 947              }
 948              if (!isset($_SESSION['phpCAS']['service_cookies'])) {
 949                  $_SESSION['phpCAS']['service_cookies'] = array();
 950              }
 951              $this->_serviceCookieJar = new CAS_CookieJar(
 952                  $_SESSION['phpCAS']['service_cookies']
 953              );
 954          }
 955  
 956          // check version
 957          $supportedProtocols = phpCAS::getSupportedProtocols();
 958          if (isset($supportedProtocols[$server_version]) === false) {
 959              phpCAS::error(
 960                  'this version of CAS (`'.$server_version
 961                  .'\') is not supported by phpCAS '.phpCAS::getVersion()
 962              );
 963          }
 964  
 965          if ($server_version === CAS_VERSION_1_0 && $this->isProxy()) {
 966              phpCAS::error(
 967                  'CAS proxies are not supported in CAS '.$server_version
 968              );
 969          }
 970  
 971          $this->_server['version'] = $server_version;
 972  
 973          // check hostname
 974          if ( empty($server_hostname)
 975              || !preg_match('/[\.\d\-a-z]*/', $server_hostname)
 976          ) {
 977              phpCAS::error('bad CAS server hostname (`'.$server_hostname.'\')');
 978          }
 979          $this->_server['hostname'] = $server_hostname;
 980  
 981          // check port
 982          if ( $server_port == 0
 983              || !is_int($server_port)
 984          ) {
 985              phpCAS::error('bad CAS server port (`'.$server_hostname.'\')');
 986          }
 987          $this->_server['port'] = $server_port;
 988  
 989          // check URI
 990          if ( !preg_match('/[\.\d\-_a-z\/]*/', $server_uri) ) {
 991              phpCAS::error('bad CAS server URI (`'.$server_uri.'\')');
 992          }
 993          // add leading and trailing `/' and remove doubles
 994          if(strstr($server_uri, '?') === false) $server_uri .= '/';
 995          $server_uri = preg_replace('/\/\//', '/', '/'.$server_uri);
 996          $this->_server['uri'] = $server_uri;
 997  
 998          // set to callback mode if PgtIou and PgtId CGI GET parameters are provided
 999          if ( $this->isProxy() ) {
1000              if(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId'])) {
1001                  $this->_setCallbackMode(true);
1002                  $this->_setCallbackModeUsingPost(false);
1003              } elseif (!empty($_POST['pgtIou'])&&!empty($_POST['pgtId'])) {
1004                  $this->_setCallbackMode(true);
1005                  $this->_setCallbackModeUsingPost(true);
1006              } else {
1007                  $this->_setCallbackMode(false);
1008                  $this->_setCallbackModeUsingPost(false);
1009              }
1010  
1011              
1012          }
1013  
1014          if ( $this->_isCallbackMode() ) {
1015              //callback mode: check that phpCAS is secured
1016              if ( !$this->_isHttps() ) {
1017                  phpCAS::error(
1018                      'CAS proxies must be secured to use phpCAS; PGT\'s will not be received from the CAS server'
1019                  );
1020              }
1021          } else {
1022              //normal mode: get ticket and remove it from CGI parameters for
1023              // developers
1024              $ticket = (isset($_GET['ticket']) ? $_GET['ticket'] : null);
1025              if (preg_match('/^[SP]T-/', $ticket) ) {
1026                  phpCAS::trace('Ticket \''.$ticket.'\' found');
1027                  $this->setTicket($ticket);
1028                  unset($_GET['ticket']);
1029              } else if ( !empty($ticket) ) {
1030                  //ill-formed ticket, halt
1031                  phpCAS::error(
1032                      'ill-formed ticket found in the URL (ticket=`'
1033                      .htmlentities($ticket).'\')'
1034                  );
1035              }
1036  
1037          }
1038          phpCAS::traceEnd();
1039      }
1040  
1041      /** @} */
1042  
1043      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1044      // XX                                                                    XX
1045      // XX                           Session Handling                         XX
1046      // XX                                                                    XX
1047      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1048  
1049      /**
1050       * @addtogroup internalConfig
1051       * @{
1052       */
1053  
1054  
1055      /**
1056       * @var bool A variable to whether phpcas will use its own session handling. Default = true
1057       * @hideinitializer
1058       */
1059      private $_change_session_id = true;
1060  
1061      /**
1062       * Set a parameter whether to allow phpCAS to change session_id
1063       *
1064       * @param bool $allowed allow phpCAS to change session_id
1065       *
1066       * @return void
1067       */
1068      private function _setChangeSessionID($allowed)
1069      {
1070          $this->_change_session_id = $allowed;
1071      }
1072  
1073      /**
1074       * Get whether phpCAS is allowed to change session_id
1075       *
1076       * @return bool
1077       */
1078      public function getChangeSessionID()
1079      {
1080          return $this->_change_session_id;
1081      }
1082  
1083      /** @} */
1084  
1085      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1086      // XX                                                                    XX
1087      // XX                           AUTHENTICATION                           XX
1088      // XX                                                                    XX
1089      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1090  
1091      /**
1092       * @addtogroup internalAuthentication
1093       * @{
1094       */
1095  
1096      /**
1097       * The Authenticated user. Written by CAS_Client::_setUser(), read by
1098       * CAS_Client::getUser().
1099       *
1100       * @hideinitializer
1101       */
1102      private $_user = '';
1103  
1104      /**
1105       * This method sets the CAS user's login name.
1106       *
1107       * @param string $user the login name of the authenticated user.
1108       *
1109       * @return void
1110       */
1111      private function _setUser($user)
1112      {
1113          $this->_user = $user;
1114      }
1115  
1116      /**
1117       * This method returns the CAS user's login name.
1118       *
1119       * @return string the login name of the authenticated user
1120       *
1121       * @warning should be called only after CAS_Client::forceAuthentication() or
1122       * CAS_Client::isAuthenticated(), otherwise halt with an error.
1123       */
1124      public function getUser()
1125      {
1126      	 // Sequence validation
1127      	 $this->ensureAuthenticationCallSuccessful();
1128  
1129      	 return $this->_getUser();
1130      }
1131  
1132      /**
1133       * This method returns the CAS user's login name.
1134       *
1135       * @return string the login name of the authenticated user
1136       *
1137       * @warning should be called only after CAS_Client::forceAuthentication() or
1138       * CAS_Client::isAuthenticated(), otherwise halt with an error.
1139       */
1140      private function _getUser()
1141      {
1142      	 // This is likely a duplicate check that could be removed....
1143          if ( empty($this->_user) ) {
1144              phpCAS::error(
1145                  'this method should be used only after '.__CLASS__
1146                  .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()'
1147              );
1148          }
1149          return $this->_user;
1150      }
1151  
1152      /**
1153       * The Authenticated users attributes. Written by
1154       * CAS_Client::setAttributes(), read by CAS_Client::getAttributes().
1155       * @attention client applications should use phpCAS::getAttributes().
1156       *
1157       * @hideinitializer
1158       */
1159      private $_attributes = array();
1160  
1161      /**
1162       * Set an array of attributes
1163       *
1164       * @param array $attributes a key value array of attributes
1165       *
1166       * @return void
1167       */
1168      public function setAttributes($attributes)
1169      {
1170          $this->_attributes = $attributes;
1171      }
1172  
1173      /**
1174       * Get an key values arry of attributes
1175       *
1176       * @return array of attributes
1177       */
1178      public function getAttributes()
1179      {
1180      	 // Sequence validation
1181      	 $this->ensureAuthenticationCallSuccessful();
1182      	 // This is likely a duplicate check that could be removed....
1183          if ( empty($this->_user) ) {
1184              // if no user is set, there shouldn't be any attributes also...
1185              phpCAS::error(
1186                  'this method should be used only after '.__CLASS__
1187                  .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()'
1188              );
1189          }
1190          return $this->_attributes;
1191      }
1192  
1193      /**
1194       * Check whether attributes are available
1195       *
1196       * @return bool attributes available
1197       */
1198      public function hasAttributes()
1199      {
1200      	 // Sequence validation
1201      	 $this->ensureAuthenticationCallSuccessful();
1202  
1203          return !empty($this->_attributes);
1204      }
1205      /**
1206       * Check whether a specific attribute with a name is available
1207       *
1208       * @param string $key name of attribute
1209       *
1210       * @return bool is attribute available
1211       */
1212      public function hasAttribute($key)
1213      {
1214      	 // Sequence validation
1215      	 $this->ensureAuthenticationCallSuccessful();
1216  
1217          return $this->_hasAttribute($key);
1218      }
1219  
1220      /**
1221       * Check whether a specific attribute with a name is available
1222       *
1223       * @param string $key name of attribute
1224       *
1225       * @return bool is attribute available
1226       */
1227      private function _hasAttribute($key)
1228      {
1229          return (is_array($this->_attributes)
1230              && array_key_exists($key, $this->_attributes));
1231      }
1232  
1233      /**
1234       * Get a specific attribute by name
1235       *
1236       * @param string $key name of attribute
1237       *
1238       * @return string attribute values
1239       */
1240      public function getAttribute($key)
1241      {
1242      	 // Sequence validation
1243      	 $this->ensureAuthenticationCallSuccessful();
1244  
1245          if ($this->_hasAttribute($key)) {
1246              return $this->_attributes[$key];
1247          }
1248      }
1249  
1250      /**
1251       * This method is called to renew the authentication of the user
1252       * If the user is authenticated, renew the connection
1253       * If not, redirect to CAS
1254       *
1255       * @return bool true when the user is authenticated; otherwise halt.
1256       */
1257      public function renewAuthentication()
1258      {
1259          phpCAS::traceBegin();
1260          // Either way, the user is authenticated by CAS
1261          if (isset( $_SESSION['phpCAS']['auth_checked'])) {
1262              unset($_SESSION['phpCAS']['auth_checked']);
1263          }
1264          if ( $this->isAuthenticated(true) ) {
1265              phpCAS::trace('user already authenticated');
1266              $res = true;
1267          } else {
1268              $this->redirectToCas(false, true);
1269              // never reached
1270              $res = false;
1271          }
1272          phpCAS::traceEnd();
1273          return $res;
1274      }
1275  
1276      /**
1277       * This method is called to be sure that the user is authenticated. When not
1278       * authenticated, halt by redirecting to the CAS server; otherwise return true.
1279       *
1280       * @return bool true when the user is authenticated; otherwise halt.
1281       */
1282      public function forceAuthentication()
1283      {
1284          phpCAS::traceBegin();
1285  
1286          if ( $this->isAuthenticated() ) {
1287              // the user is authenticated, nothing to be done.
1288              phpCAS::trace('no need to authenticate');
1289              $res = true;
1290          } else {
1291              // the user is not authenticated, redirect to the CAS server
1292              if (isset($_SESSION['phpCAS']['auth_checked'])) {
1293                  unset($_SESSION['phpCAS']['auth_checked']);
1294              }
1295              $this->redirectToCas(false/* no gateway */);
1296              // never reached
1297              $res = false;
1298          }
1299          phpCAS::traceEnd($res);
1300          return $res;
1301      }
1302  
1303      /**
1304       * An integer that gives the number of times authentication will be cached
1305       * before rechecked.
1306       *
1307       * @hideinitializer
1308       */
1309      private $_cache_times_for_auth_recheck = 0;
1310  
1311      /**
1312       * Set the number of times authentication will be cached before rechecked.
1313       *
1314       * @param int $n number of times to wait for a recheck
1315       *
1316       * @return void
1317       */
1318      public function setCacheTimesForAuthRecheck($n)
1319      {
1320      	 if (gettype($n) != 'integer')
1321          	 throw new CAS_TypeMismatchException($n, '$n', 'string');
1322  
1323          $this->_cache_times_for_auth_recheck = $n;
1324      }
1325  
1326      /**
1327       * This method is called to check whether the user is authenticated or not.
1328       *
1329       * @return bool true when the user is authenticated, false when a previous
1330       * gateway login failed or  the function will not return if the user is
1331       * redirected to the cas server for a gateway login attempt
1332       */
1333      public function checkAuthentication()
1334      {
1335          phpCAS::traceBegin();
1336          if ( $this->isAuthenticated() ) {
1337              phpCAS::trace('user is authenticated');
1338              /* The 'auth_checked' variable is removed just in case it's set. */
1339              unset($_SESSION['phpCAS']['auth_checked']);
1340              $res = true;
1341          } else if (isset($_SESSION['phpCAS']['auth_checked'])) {
1342              // the previous request has redirected the client to the CAS server
1343              // with gateway=true
1344              unset($_SESSION['phpCAS']['auth_checked']);
1345              $res = false;
1346          } else {
1347              // avoid a check against CAS on every request
1348              if (!isset($_SESSION['phpCAS']['unauth_count'])) {
1349                  $_SESSION['phpCAS']['unauth_count'] = -2; // uninitialized
1350              }
1351  
1352              if (($_SESSION['phpCAS']['unauth_count'] != -2
1353                  && $this->_cache_times_for_auth_recheck == -1)
1354                  || ($_SESSION['phpCAS']['unauth_count'] >= 0
1355                  && $_SESSION['phpCAS']['unauth_count'] < $this->_cache_times_for_auth_recheck)
1356              ) {
1357                  $res = false;
1358  
1359                  if ($this->_cache_times_for_auth_recheck != -1) {
1360                      $_SESSION['phpCAS']['unauth_count']++;
1361                      phpCAS::trace(
1362                          'user is not authenticated (cached for '
1363                          .$_SESSION['phpCAS']['unauth_count'].' times of '
1364                          .$this->_cache_times_for_auth_recheck.')'
1365                      );
1366                  } else {
1367                      phpCAS::trace(
1368                          'user is not authenticated (cached for until login pressed)'
1369                      );
1370                  }
1371              } else {
1372                  $_SESSION['phpCAS']['unauth_count'] = 0;
1373                  $_SESSION['phpCAS']['auth_checked'] = true;
1374                  phpCAS::trace('user is not authenticated (cache reset)');
1375                  $this->redirectToCas(true/* gateway */);
1376                  // never reached
1377                  $res = false;
1378              }
1379          }
1380          phpCAS::traceEnd($res);
1381          return $res;
1382      }
1383  
1384      /**
1385       * This method is called to check if the user is authenticated (previously or by
1386       * tickets given in the URL).
1387       *
1388       * @param bool $renew true to force the authentication with the CAS server
1389       *
1390       * @return bool true when the user is authenticated. Also may redirect to the
1391       * same URL without the ticket.
1392       */
1393      public function isAuthenticated($renew=false)
1394      {
1395          phpCAS::traceBegin();
1396          $res = false;
1397          $validate_url = '';
1398          if ( $this->_wasPreviouslyAuthenticated() ) {
1399              if ($this->hasTicket()) {
1400                  // User has a additional ticket but was already authenticated
1401                  phpCAS::trace(
1402                      'ticket was present and will be discarded, use renewAuthenticate()'
1403                  );
1404                  if ($this->_clearTicketsFromUrl) {
1405                      phpCAS::trace("Prepare redirect to : ".$this->getURL());
1406                      session_write_close();
1407                      header('Location: '.$this->getURL());
1408                      flush();
1409                      phpCAS::traceExit();
1410                      throw new CAS_GracefullTerminationException();
1411                  } else {
1412                      phpCAS::trace(
1413                          'Already authenticated, but skipping ticket clearing since setNoClearTicketsFromUrl() was used.'
1414                      );
1415                      $res = true;
1416                  }
1417              } else {
1418                  // the user has already (previously during the session) been
1419                  // authenticated, nothing to be done.
1420                  phpCAS::trace(
1421                      'user was already authenticated, no need to look for tickets'
1422                  );
1423                  $res = true;
1424              }
1425  
1426              // Mark the auth-check as complete to allow post-authentication
1427              // callbacks to make use of phpCAS::getUser() and similar methods
1428              $this->markAuthenticationCall($res);
1429          } else {
1430              if ($this->hasTicket()) {
1431                  switch ($this->getServerVersion()) {
1432                  case CAS_VERSION_1_0:
1433                      // if a Service Ticket was given, validate it
1434                      phpCAS::trace(
1435                          'CAS 1.0 ticket `'.$this->getTicket().'\' is present'
1436                      );
1437                      $this->validateCAS10(
1438                          $validate_url, $text_response, $tree_response, $renew
1439                      ); // if it fails, it halts
1440                      phpCAS::trace(
1441                          'CAS 1.0 ticket `'.$this->getTicket().'\' was validated'
1442                      );
1443                      $_SESSION['phpCAS']['user'] = $this->_getUser();
1444                      $res = true;
1445                      $logoutTicket = $this->getTicket();
1446                      break;
1447                  case CAS_VERSION_2_0:
1448                  case CAS_VERSION_3_0:
1449                      // if a Proxy Ticket was given, validate it
1450                      phpCAS::trace(
1451                          'CAS '.$this->getServerVersion().' ticket `'.$this->getTicket().'\' is present'
1452                      );
1453                      $this->validateCAS20(
1454                          $validate_url, $text_response, $tree_response, $renew
1455                      ); // note: if it fails, it halts
1456                      phpCAS::trace(
1457                          'CAS '.$this->getServerVersion().' ticket `'.$this->getTicket().'\' was validated'
1458                      );
1459                      if ( $this->isProxy() ) {
1460                          $this->_validatePGT(
1461                              $validate_url, $text_response, $tree_response
1462                          ); // idem
1463                          phpCAS::trace('PGT `'.$this->_getPGT().'\' was validated');
1464                          $_SESSION['phpCAS']['pgt'] = $this->_getPGT();
1465                      }
1466                      $_SESSION['phpCAS']['user'] = $this->_getUser();
1467                      if (!empty($this->_attributes)) {
1468                          $_SESSION['phpCAS']['attributes'] = $this->_attributes;
1469                      }
1470                      $proxies = $this->getProxies();
1471                      if (!empty($proxies)) {
1472                          $_SESSION['phpCAS']['proxies'] = $this->getProxies();
1473                      }
1474                      $res = true;
1475                      $logoutTicket = $this->getTicket();
1476                      break;
1477                  case SAML_VERSION_1_1:
1478                      // if we have a SAML ticket, validate it.
1479                      phpCAS::trace(
1480                          'SAML 1.1 ticket `'.$this->getTicket().'\' is present'
1481                      );
1482                      $this->validateSA(
1483                          $validate_url, $text_response, $tree_response, $renew
1484                      ); // if it fails, it halts
1485                      phpCAS::trace(
1486                          'SAML 1.1 ticket `'.$this->getTicket().'\' was validated'
1487                      );
1488                      $_SESSION['phpCAS']['user'] = $this->_getUser();
1489                      $_SESSION['phpCAS']['attributes'] = $this->_attributes;
1490                      $res = true;
1491                      $logoutTicket = $this->getTicket();
1492                      break;
1493                  default:
1494                      phpCAS::trace('Protocoll error');
1495                      break;
1496                  }
1497              } else {
1498                  // no ticket given, not authenticated
1499                  phpCAS::trace('no ticket found');
1500              }
1501  
1502              // Mark the auth-check as complete to allow post-authentication
1503              // callbacks to make use of phpCAS::getUser() and similar methods
1504              $this->markAuthenticationCall($res);
1505  
1506              if ($res) {
1507                  // call the post-authenticate callback if registered.
1508                  if ($this->_postAuthenticateCallbackFunction) {
1509                      $args = $this->_postAuthenticateCallbackArgs;
1510                      array_unshift($args, $logoutTicket);
1511                      call_user_func_array(
1512                          $this->_postAuthenticateCallbackFunction, $args
1513                      );
1514                  }
1515  
1516                  // if called with a ticket parameter, we need to redirect to the
1517                  // app without the ticket so that CAS-ification is transparent
1518                  // to the browser (for later POSTS) most of the checks and
1519                  // errors should have been made now, so we're safe for redirect
1520                  // without masking error messages. remove the ticket as a
1521                  // security precaution to prevent a ticket in the HTTP_REFERRER
1522                  if ($this->_clearTicketsFromUrl) {
1523                      phpCAS::trace("Prepare redirect to : ".$this->getURL());
1524                      session_write_close();
1525                      header('Location: '.$this->getURL());
1526                      flush();
1527                      phpCAS::traceExit();
1528                      throw new CAS_GracefullTerminationException();
1529                  }
1530              }
1531          }
1532          phpCAS::traceEnd($res);
1533          return $res;
1534      }
1535  
1536      /**
1537       * This method tells if the current session is authenticated.
1538       *
1539       * @return bool true if authenticated based soley on $_SESSION variable
1540       */
1541      public function isSessionAuthenticated ()
1542      {
1543          return !empty($_SESSION['phpCAS']['user']);
1544      }
1545  
1546      /**
1547       * This method tells if the user has already been (previously) authenticated
1548       * by looking into the session variables.
1549       *
1550       * @note This function switches to callback mode when needed.
1551       *
1552       * @return bool true when the user has already been authenticated; false otherwise.
1553       */
1554      private function _wasPreviouslyAuthenticated()
1555      {
1556          phpCAS::traceBegin();
1557  
1558          if ( $this->_isCallbackMode() ) {
1559              // Rebroadcast the pgtIou and pgtId to all nodes
1560              if ($this->_rebroadcast&&!isset($_POST['rebroadcast'])) {
1561                  $this->_rebroadcast(self::PGTIOU);
1562              }
1563              $this->_callback();
1564          }
1565  
1566          $auth = false;
1567  
1568          if ( $this->isProxy() ) {
1569              // CAS proxy: username and PGT must be present
1570              if ( $this->isSessionAuthenticated()
1571                  && !empty($_SESSION['phpCAS']['pgt'])
1572              ) {
1573                  // authentication already done
1574                  $this->_setUser($_SESSION['phpCAS']['user']);
1575                  if (isset($_SESSION['phpCAS']['attributes'])) {
1576                      $this->setAttributes($_SESSION['phpCAS']['attributes']);
1577                  }
1578                  $this->_setPGT($_SESSION['phpCAS']['pgt']);
1579                  phpCAS::trace(
1580                      'user = `'.$_SESSION['phpCAS']['user'].'\', PGT = `'
1581                      .$_SESSION['phpCAS']['pgt'].'\''
1582                  );
1583  
1584                  // Include the list of proxies
1585                  if (isset($_SESSION['phpCAS']['proxies'])) {
1586                      $this->_setProxies($_SESSION['phpCAS']['proxies']);
1587                      phpCAS::trace(
1588                          'proxies = "'
1589                          .implode('", "', $_SESSION['phpCAS']['proxies']).'"'
1590                      );
1591                  }
1592  
1593                  $auth = true;
1594              } elseif ( $this->isSessionAuthenticated()
1595                  && empty($_SESSION['phpCAS']['pgt'])
1596              ) {
1597                  // these two variables should be empty or not empty at the same time
1598                  phpCAS::trace(
1599                      'username found (`'.$_SESSION['phpCAS']['user']
1600                      .'\') but PGT is empty'
1601                  );
1602                  // unset all tickets to enforce authentication
1603                  unset($_SESSION['phpCAS']);
1604                  $this->setTicket('');
1605              } elseif ( !$this->isSessionAuthenticated()
1606                  && !empty($_SESSION['phpCAS']['pgt'])
1607              ) {
1608                  // these two variables should be empty or not empty at the same time
1609                  phpCAS::trace(
1610                      'PGT found (`'.$_SESSION['phpCAS']['pgt']
1611                      .'\') but username is empty'
1612                  );
1613                  // unset all tickets to enforce authentication
1614                  unset($_SESSION['phpCAS']);
1615                  $this->setTicket('');
1616              } else {
1617                  phpCAS::trace('neither user nor PGT found');
1618              }
1619          } else {
1620              // `simple' CAS client (not a proxy): username must be present
1621              if ( $this->isSessionAuthenticated() ) {
1622                  // authentication already done
1623                  $this->_setUser($_SESSION['phpCAS']['user']);
1624                  if (isset($_SESSION['phpCAS']['attributes'])) {
1625                      $this->setAttributes($_SESSION['phpCAS']['attributes']);
1626                  }
1627                  phpCAS::trace('user = `'.$_SESSION['phpCAS']['user'].'\'');
1628  
1629                  // Include the list of proxies
1630                  if (isset($_SESSION['phpCAS']['proxies'])) {
1631                      $this->_setProxies($_SESSION['phpCAS']['proxies']);
1632                      phpCAS::trace(
1633                          'proxies = "'
1634                          .implode('", "', $_SESSION['phpCAS']['proxies']).'"'
1635                      );
1636                  }
1637  
1638                  $auth = true;
1639              } else {
1640                  phpCAS::trace('no user found');
1641              }
1642          }
1643  
1644          phpCAS::traceEnd($auth);
1645          return $auth;
1646      }
1647  
1648      /**
1649       * This method is used to redirect the client to the CAS server.
1650       * It is used by CAS_Client::forceAuthentication() and
1651       * CAS_Client::checkAuthentication().
1652       *
1653       * @param bool $gateway true to check authentication, false to force it
1654       * @param bool $renew   true to force the authentication with the CAS server
1655       *
1656       * @return void
1657       */
1658      public function redirectToCas($gateway=false,$renew=false)
1659      {
1660          phpCAS::traceBegin();
1661          $cas_url = $this->getServerLoginURL($gateway, $renew);
1662          session_write_close();
1663          if (php_sapi_name() === 'cli') {
1664              @header('Location: '.$cas_url);
1665          } else {
1666              header('Location: '.$cas_url);
1667          }
1668          phpCAS::trace("Redirect to : ".$cas_url);
1669          $lang = $this->getLangObj();
1670          $this->printHTMLHeader($lang->getAuthenticationWanted());
1671          printf('<p>'. $lang->getShouldHaveBeenRedirected(). '</p>', $cas_url);
1672          $this->printHTMLFooter();
1673          phpCAS::traceExit();
1674          throw new CAS_GracefullTerminationException();
1675      }
1676  
1677  
1678      /**
1679       * This method is used to logout from CAS.
1680       *
1681       * @param array $params an array that contains the optional url and service
1682       * parameters that will be passed to the CAS server
1683       *
1684       * @return void
1685       */
1686      public function logout($params)
1687      {
1688          phpCAS::traceBegin();
1689          $cas_url = $this->getServerLogoutURL();
1690          $paramSeparator = '?';
1691          if (isset($params['url'])) {
1692              $cas_url = $cas_url . $paramSeparator . "url="
1693                  . urlencode($params['url']);
1694              $paramSeparator = '&';
1695          }
1696          if (isset($params['service'])) {
1697              $cas_url = $cas_url . $paramSeparator . "service="
1698                  . urlencode($params['service']);
1699          }
1700          header('Location: '.$cas_url);
1701          phpCAS::trace("Prepare redirect to : ".$cas_url);
1702  
1703          phpCAS::trace("Destroying session : ".session_id());
1704          session_unset();
1705          session_destroy();
1706          if (session_status() === PHP_SESSION_NONE) {
1707              phpCAS::trace("Session terminated");
1708          } else {
1709              phpCAS::error("Session was not terminated");
1710              phpCAS::trace("Session was not terminated");
1711          }
1712          $lang = $this->getLangObj();
1713          $this->printHTMLHeader($lang->getLogout());
1714          printf('<p>'.$lang->getShouldHaveBeenRedirected(). '</p>', $cas_url);
1715          $this->printHTMLFooter();
1716          phpCAS::traceExit();
1717          throw new CAS_GracefullTerminationException();
1718      }
1719  
1720      /**
1721       * Check of the current request is a logout request
1722       *
1723       * @return bool is logout request.
1724       */
1725      private function _isLogoutRequest()
1726      {
1727          return !empty($_POST['logoutRequest']);
1728      }
1729  
1730      /**
1731       * This method handles logout requests.
1732       *
1733       * @param bool $check_client    true to check the client bofore handling
1734       * the request, false not to perform any access control. True by default.
1735       * @param array $allowed_clients an array of host names allowed to send
1736       * logout requests.
1737       *
1738       * @return void
1739       */
1740      public function handleLogoutRequests($check_client=true, $allowed_clients=array())
1741      {
1742          phpCAS::traceBegin();
1743          if (!$this->_isLogoutRequest()) {
1744              phpCAS::trace("Not a logout request");
1745              phpCAS::traceEnd();
1746              return;
1747          }
1748          if (!$this->getChangeSessionID()
1749              && is_null($this->_signoutCallbackFunction)
1750          ) {
1751              phpCAS::trace(
1752                  "phpCAS can't handle logout requests if it is not allowed to change session_id."
1753              );
1754          }
1755          phpCAS::trace("Logout requested");
1756          $decoded_logout_rq = urldecode($_POST['logoutRequest']);
1757          phpCAS::trace("SAML REQUEST: ".$decoded_logout_rq);
1758          $allowed = false;
1759          if ($check_client) {
1760              if ($allowed_clients === array()) {
1761                  $allowed_clients = array( $this->_getServerHostname() );
1762              }
1763              $client_ip = $_SERVER['REMOTE_ADDR'];
1764              $client = gethostbyaddr($client_ip);
1765              phpCAS::trace("Client: ".$client."/".$client_ip);
1766              foreach ($allowed_clients as $allowed_client) {
1767                  if (($client == $allowed_client)
1768                      || ($client_ip == $allowed_client)
1769                  ) {
1770                      phpCAS::trace(
1771                          "Allowed client '".$allowed_client
1772                          ."' matches, logout request is allowed"
1773                      );
1774                      $allowed = true;
1775                      break;
1776                  } else {
1777                      phpCAS::trace(
1778                          "Allowed client '".$allowed_client."' does not match"
1779                      );
1780                  }
1781              }
1782          } else {
1783              phpCAS::trace("No access control set");
1784              $allowed = true;
1785          }
1786          // If Logout command is permitted proceed with the logout
1787          if ($allowed) {
1788              phpCAS::trace("Logout command allowed");
1789              // Rebroadcast the logout request
1790              if ($this->_rebroadcast && !isset($_POST['rebroadcast'])) {
1791                  $this->_rebroadcast(self::LOGOUT);
1792              }
1793              // Extract the ticket from the SAML Request
1794              preg_match(
1795                  "|<samlp:SessionIndex>(.*)</samlp:SessionIndex>|",
1796                  $decoded_logout_rq, $tick, PREG_OFFSET_CAPTURE, 3
1797              );
1798              $wrappedSamlSessionIndex = preg_replace(
1799                  '|<samlp:SessionIndex>|', '', $tick[0][0]
1800              );
1801              $ticket2logout = preg_replace(
1802                  '|</samlp:SessionIndex>|', '', $wrappedSamlSessionIndex
1803              );
1804              phpCAS::trace("Ticket to logout: ".$ticket2logout);
1805  
1806              // call the post-authenticate callback if registered.
1807              if ($this->_signoutCallbackFunction) {
1808                  $args = $this->_signoutCallbackArgs;
1809                  array_unshift($args, $ticket2logout);
1810                  call_user_func_array($this->_signoutCallbackFunction, $args);
1811              }
1812  
1813              // If phpCAS is managing the session_id, destroy session thanks to
1814              // session_id.
1815              if ($this->getChangeSessionID()) {
1816                  $session_id = $this->_sessionIdForTicket($ticket2logout);
1817                  phpCAS::trace("Session id: ".$session_id);
1818  
1819                  // destroy a possible application session created before phpcas
1820                  if (session_id() !== "") {
1821                      session_unset();
1822                      session_destroy();
1823                  }
1824                  // fix session ID
1825                  session_id($session_id);
1826                  $_COOKIE[session_name()]=$session_id;
1827                  $_GET[session_name()]=$session_id;
1828  
1829                  // Overwrite session
1830                  session_start();
1831                  session_unset();
1832                  session_destroy();
1833                  phpCAS::trace("Session ". $session_id . " destroyed");
1834              }
1835          } else {
1836              phpCAS::error("Unauthorized logout request from client '".$client."'");
1837              phpCAS::trace("Unauthorized logout request from client '".$client."'");
1838          }
1839          flush();
1840          phpCAS::traceExit();
1841          throw new CAS_GracefullTerminationException();
1842  
1843      }
1844  
1845      /** @} */
1846  
1847      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1848      // XX                                                                    XX
1849      // XX                  BASIC CLIENT FEATURES (CAS 1.0)                   XX
1850      // XX                                                                    XX
1851      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1852  
1853      // ########################################################################
1854      //  ST
1855      // ########################################################################
1856      /**
1857      * @addtogroup internalBasic
1858      * @{
1859      */
1860  
1861      /**
1862       * The Ticket provided in the URL of the request if present
1863       * (empty otherwise). Written by CAS_Client::CAS_Client(), read by
1864       * CAS_Client::getTicket() and CAS_Client::_hasPGT().
1865       *
1866       * @hideinitializer
1867       */
1868      private $_ticket = '';
1869  
1870      /**
1871       * This method returns the Service Ticket provided in the URL of the request.
1872       *
1873       * @return string service ticket.
1874       */
1875      public  function getTicket()
1876      {
1877          return $this->_ticket;
1878      }
1879  
1880      /**
1881       * This method stores the Service Ticket.
1882       *
1883       * @param string $st The Service Ticket.
1884       *
1885       * @return void
1886       */
1887      public function setTicket($st)
1888      {
1889          $this->_ticket = $st;
1890      }
1891  
1892      /**
1893       * This method tells if a Service Ticket was stored.
1894       *
1895       * @return bool if a Service Ticket has been stored.
1896       */
1897      public function hasTicket()
1898      {
1899          return !empty($this->_ticket);
1900      }
1901  
1902      /** @} */
1903  
1904      // ########################################################################
1905      //  ST VALIDATION
1906      // ########################################################################
1907      /**
1908      * @addtogroup internalBasic
1909      * @{
1910      */
1911  
1912      /**
1913       * @var  string the certificate of the CAS server CA.
1914       *
1915       * @hideinitializer
1916       */
1917      private $_cas_server_ca_cert = null;
1918  
1919  
1920      /**
1921  
1922       * validate CN of the CAS server certificate
1923  
1924       *
1925  
1926       * @hideinitializer
1927  
1928       */
1929  
1930      private $_cas_server_cn_validate = true;
1931  
1932      /**
1933       * Set to true not to validate the CAS server.
1934       *
1935       * @hideinitializer
1936       */
1937      private $_no_cas_server_validation = false;
1938  
1939  
1940      /**
1941       * Set the CA certificate of the CAS server.
1942       *
1943       * @param string $cert        the PEM certificate file name of the CA that emited
1944       * the cert of the server
1945       * @param bool   $validate_cn valiate CN of the CAS server certificate
1946       *
1947       * @return void
1948       */
1949      public function setCasServerCACert($cert, $validate_cn)
1950      {
1951      // Argument validation
1952          if (gettype($cert) != 'string') {
1953              throw new CAS_TypeMismatchException($cert, '$cert', 'string');
1954          }
1955          if (gettype($validate_cn) != 'boolean') {
1956              throw new CAS_TypeMismatchException($validate_cn, '$validate_cn', 'boolean');
1957          }
1958          if ( !file_exists($cert) && $this->_requestImplementation !== 'CAS_TestHarness_DummyRequest'){
1959              throw new CAS_InvalidArgumentException("Certificate file does not exist " . $this->_requestImplementation);
1960          }
1961          $this->_cas_server_ca_cert = $cert;
1962          $this->_cas_server_cn_validate = $validate_cn;
1963      }
1964  
1965      /**
1966       * Set no SSL validation for the CAS server.
1967       *
1968       * @return void
1969       */
1970      public function setNoCasServerValidation()
1971      {
1972          $this->_no_cas_server_validation = true;
1973      }
1974  
1975      /**
1976       * This method is used to validate a CAS 1,0 ticket; halt on failure, and
1977       * sets $validate_url, $text_reponse and $tree_response on success.
1978       *
1979       * @param string &$validate_url  reference to the the URL of the request to
1980       * the CAS server.
1981       * @param string &$text_response reference to the response of the CAS
1982       * server, as is (XML text).
1983       * @param string &$tree_response reference to the response of the CAS
1984       * server, as a DOM XML tree.
1985       * @param bool   $renew          true to force the authentication with the CAS server
1986       *
1987       * @return bool true when successfull and issue a CAS_AuthenticationException
1988       * and false on an error
1989       * @throws  CAS_AuthenticationException
1990       */
1991      public function validateCAS10(&$validate_url,&$text_response,&$tree_response,$renew=false)
1992      {
1993          phpCAS::traceBegin();
1994          // build the URL to validate the ticket
1995          $validate_url = $this->getServerServiceValidateURL()
1996              .'&ticket='.urlencode($this->getTicket());
1997  
1998          if ( $renew ) {
1999              // pass the renew
2000              $validate_url .= '&renew=true';
2001          }
2002  
2003          // open and read the URL
2004          if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) {
2005              phpCAS::trace(
2006                  'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'
2007              );
2008              throw new CAS_AuthenticationException(
2009                  $this, 'CAS 1.0 ticket not validated', $validate_url,
2010                  true/*$no_response*/
2011              );
2012          }
2013  
2014          if (preg_match('/^no\n/', $text_response)) {
2015              phpCAS::trace('Ticket has not been validated');
2016              throw new CAS_AuthenticationException(
2017                  $this, 'ST not validated', $validate_url, false/*$no_response*/,
2018                  false/*$bad_response*/, $text_response
2019              );
2020          } else if (!preg_match('/^yes\n/', $text_response)) {
2021              phpCAS::trace('ill-formed response');
2022              throw new CAS_AuthenticationException(
2023                  $this, 'Ticket not validated', $validate_url,
2024                  false/*$no_response*/, true/*$bad_response*/, $text_response
2025              );
2026          }
2027          // ticket has been validated, extract the user name
2028          $arr = preg_split('/\n/', $text_response);
2029          $this->_setUser(trim($arr[1]));
2030  
2031          $this->_renameSession($this->getTicket());
2032  
2033          // at this step, ticket has been validated and $this->_user has been set,
2034          phpCAS::traceEnd(true);
2035          return true;
2036      }
2037  
2038      /** @} */
2039  
2040  
2041      // ########################################################################
2042      //  SAML VALIDATION
2043      // ########################################################################
2044      /**
2045      * @addtogroup internalSAML
2046      * @{
2047      */
2048  
2049      /**
2050       * This method is used to validate a SAML TICKET; halt on failure, and sets
2051       * $validate_url, $text_reponse and $tree_response on success. These
2052       * parameters are used later by CAS_Client::_validatePGT() for CAS proxies.
2053       *
2054       * @param string &$validate_url  reference to the the URL of the request to
2055       * the CAS server.
2056       * @param string &$text_response reference to the response of the CAS
2057       * server, as is (XML text).
2058       * @param string &$tree_response reference to the response of the CAS
2059       * server, as a DOM XML tree.
2060       * @param bool   $renew          true to force the authentication with the CAS server
2061       *
2062       * @return bool true when successfull and issue a CAS_AuthenticationException
2063       * and false on an error
2064       *
2065       * @throws  CAS_AuthenticationException
2066       */
2067      public function validateSA(&$validate_url,&$text_response,&$tree_response,$renew=false)
2068      {
2069          phpCAS::traceBegin();
2070          $result = false;
2071          // build the URL to validate the ticket
2072          $validate_url = $this->getServerSamlValidateURL();
2073  
2074          if ( $renew ) {
2075              // pass the renew
2076              $validate_url .= '&renew=true';
2077          }
2078  
2079          // open and read the URL
2080          if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) {
2081              phpCAS::trace(
2082                  'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'
2083              );
2084              throw new CAS_AuthenticationException(
2085                  $this, 'SA not validated', $validate_url, true/*$no_response*/
2086              );
2087          }
2088  
2089          phpCAS::trace('server version: '.$this->getServerVersion());
2090  
2091          // analyze the result depending on the version
2092          switch ($this->getServerVersion()) {
2093          case SAML_VERSION_1_1:
2094              // create new DOMDocument Object
2095              $dom = new DOMDocument();
2096              // Fix possible whitspace problems
2097              $dom->preserveWhiteSpace = false;
2098              // read the response of the CAS server into a DOM object
2099              if (!($dom->loadXML($text_response))) {
2100                  phpCAS::trace('dom->loadXML() failed');
2101                  throw new CAS_AuthenticationException(
2102                      $this, 'SA not validated', $validate_url,
2103                      false/*$no_response*/, true/*$bad_response*/,
2104                      $text_response
2105                  );
2106              }
2107              // read the root node of the XML tree
2108              if (!($tree_response = $dom->documentElement)) {
2109                  phpCAS::trace('documentElement() failed');
2110                  throw new CAS_AuthenticationException(
2111                      $this, 'SA not validated', $validate_url,
2112                      false/*$no_response*/, true/*$bad_response*/,
2113                      $text_response
2114                  );
2115              } else if ( $tree_response->localName != 'Envelope' ) {
2116                  // insure that tag name is 'Envelope'
2117                  phpCAS::trace(
2118                      'bad XML root node (should be `Envelope\' instead of `'
2119                      .$tree_response->localName.'\''
2120                  );
2121                  throw new CAS_AuthenticationException(
2122                      $this, 'SA not validated', $validate_url,
2123                      false/*$no_response*/, true/*$bad_response*/,
2124                      $text_response
2125                  );
2126              } else if ($tree_response->getElementsByTagName("NameIdentifier")->length != 0) {
2127                  // check for the NameIdentifier tag in the SAML response
2128                  $success_elements = $tree_response->getElementsByTagName("NameIdentifier");
2129                  phpCAS::trace('NameIdentifier found');
2130                  $user = trim($success_elements->item(0)->nodeValue);
2131                  phpCAS::trace('user = `'.$user.'`');
2132                  $this->_setUser($user);
2133                  $this->_setSessionAttributes($text_response);
2134                  $result = true;
2135              } else {
2136                  phpCAS::trace('no <NameIdentifier> tag found in SAML payload');
2137                  throw new CAS_AuthenticationException(
2138                      $this, 'SA not validated', $validate_url,
2139                      false/*$no_response*/, true/*$bad_response*/,
2140                      $text_response
2141                  );
2142              }
2143          }
2144          if ($result) {
2145              $this->_renameSession($this->getTicket());
2146          }
2147          // at this step, ST has been validated and $this->_user has been set,
2148          phpCAS::traceEnd($result);
2149          return $result;
2150      }
2151  
2152      /**
2153       * This method will parse the DOM and pull out the attributes from the SAML
2154       * payload and put them into an array, then put the array into the session.
2155       *
2156       * @param string $text_response the SAML payload.
2157       *
2158       * @return bool true when successfull and false if no attributes a found
2159       */
2160      private function _setSessionAttributes($text_response)
2161      {
2162          phpCAS::traceBegin();
2163  
2164          $result = false;
2165  
2166          $attr_array = array();
2167  
2168          // create new DOMDocument Object
2169          $dom = new DOMDocument();
2170          // Fix possible whitspace problems
2171          $dom->preserveWhiteSpace = false;
2172          if (($dom->loadXML($text_response))) {
2173              $xPath = new DOMXPath($dom);
2174              $xPath->registerNamespace('samlp', 'urn:oasis:names:tc:SAML:1.0:protocol');
2175              $xPath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:1.0:assertion');
2176              $nodelist = $xPath->query("//saml:Attribute");
2177  
2178              if ($nodelist) {
2179                  foreach ($nodelist as $node) {
2180                      $xres = $xPath->query("saml:AttributeValue", $node);
2181                      $name = $node->getAttribute("AttributeName");
2182                      $value_array = array();
2183                      foreach ($xres as $node2) {
2184                          $value_array[] = $node2->nodeValue;
2185                      }
2186                      $attr_array[$name] = $value_array;
2187                  }
2188                  // UGent addition...
2189                  foreach ($attr_array as $attr_key => $attr_value) {
2190                      if (count($attr_value) > 1) {
2191                          $this->_attributes[$attr_key] = $attr_value;
2192                          phpCAS::trace("* " . $attr_key . "=" . print_r($attr_value, true));
2193                      } else {
2194                          $this->_attributes[$attr_key] = $attr_value[0];
2195                          phpCAS::trace("* " . $attr_key . "=" . $attr_value[0]);
2196                      }
2197                  }
2198                  $result = true;
2199              } else {
2200                  phpCAS::trace("SAML Attributes are empty");
2201                  $result = false;
2202              }
2203          }
2204          phpCAS::traceEnd($result);
2205          return $result;
2206      }
2207  
2208      /** @} */
2209  
2210      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2211      // XX                                                                    XX
2212      // XX                     PROXY FEATURES (CAS 2.0)                       XX
2213      // XX                                                                    XX
2214      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2215  
2216      // ########################################################################
2217      //  PROXYING
2218      // ########################################################################
2219      /**
2220      * @addtogroup internalProxy
2221      * @{
2222      */
2223  
2224      /**
2225       * @var  bool is the client a proxy
2226       * A boolean telling if the client is a CAS proxy or not. Written by
2227       * CAS_Client::CAS_Client(), read by CAS_Client::isProxy().
2228       */
2229      private $_proxy;
2230  
2231      /**
2232       * @var  CAS_CookieJar Handler for managing service cookies.
2233       */
2234      private $_serviceCookieJar;
2235  
2236      /**
2237       * Tells if a CAS client is a CAS proxy or not
2238       *
2239       * @return bool true when the CAS client is a CAS proxy, false otherwise
2240       */
2241      public function isProxy()
2242      {
2243          return $this->_proxy;
2244      }
2245  
2246  
2247      /** @} */
2248      // ########################################################################
2249      //  PGT
2250      // ########################################################################
2251      /**
2252      * @addtogroup internalProxy
2253      * @{
2254      */
2255  
2256      /**
2257       * the Proxy Grnting Ticket given by the CAS server (empty otherwise).
2258       * Written by CAS_Client::_setPGT(), read by CAS_Client::_getPGT() and
2259       * CAS_Client::_hasPGT().
2260       *
2261       * @hideinitializer
2262       */
2263      private $_pgt = '';
2264  
2265      /**
2266       * This method returns the Proxy Granting Ticket given by the CAS server.
2267       *
2268       * @return string the Proxy Granting Ticket.
2269       */
2270      private function _getPGT()
2271      {
2272          return $this->_pgt;
2273      }
2274  
2275      /**
2276       * This method stores the Proxy Granting Ticket.
2277       *
2278       * @param string $pgt The Proxy Granting Ticket.
2279       *
2280       * @return void
2281       */
2282      private function _setPGT($pgt)
2283      {
2284          $this->_pgt = $pgt;
2285      }
2286  
2287      /**
2288       * This method tells if a Proxy Granting Ticket was stored.
2289       *
2290       * @return bool true if a Proxy Granting Ticket has been stored.
2291       */
2292      private function _hasPGT()
2293      {
2294          return !empty($this->_pgt);
2295      }
2296  
2297      /** @} */
2298  
2299      // ########################################################################
2300      //  CALLBACK MODE
2301      // ########################################################################
2302      /**
2303      * @addtogroup internalCallback
2304      * @{
2305      */
2306      /**
2307       * each PHP script using phpCAS in proxy mode is its own callback to get the
2308       * PGT back from the CAS server. callback_mode is detected by the constructor
2309       * thanks to the GET parameters.
2310       */
2311  
2312      /**
2313       * @var bool a boolean to know if the CAS client is running in callback mode. Written by
2314       * CAS_Client::setCallBackMode(), read by CAS_Client::_isCallbackMode().
2315       *
2316       * @hideinitializer
2317       */
2318      private $_callback_mode = false;
2319  
2320      /**
2321       * This method sets/unsets callback mode.
2322       *
2323       * @param bool $callback_mode true to set callback mode, false otherwise.
2324       *
2325       * @return void
2326       */
2327      private function _setCallbackMode($callback_mode)
2328      {
2329          $this->_callback_mode = $callback_mode;
2330      }
2331  
2332      /**
2333       * This method returns true when the CAS client is running in callback mode,
2334       * false otherwise.
2335       *
2336       * @return bool A boolean.
2337       */
2338      private function _isCallbackMode()
2339      {
2340          return $this->_callback_mode;
2341      }
2342  
2343      /**
2344       * @var bool a boolean to know if the CAS client is using POST parameters when in callback mode.
2345       * Written by CAS_Client::_setCallbackModeUsingPost(), read by CAS_Client::_isCallbackModeUsingPost().
2346       *
2347       * @hideinitializer
2348       */
2349      private $_callback_mode_using_post = false;
2350  
2351      /**
2352       * This method sets/unsets usage of POST parameters in callback mode (default/false is GET parameters)
2353       *
2354       * @param bool $callback_mode_using_post true to use POST, false to use GET (default).
2355       *
2356       * @return void
2357       */
2358      private function _setCallbackModeUsingPost($callback_mode_using_post)
2359      {
2360          $this->_callback_mode_using_post = $callback_mode_using_post;
2361      }
2362  
2363      /**
2364       * This method returns true when the callback mode is using POST, false otherwise.
2365       *
2366       * @return bool A boolean.
2367       */
2368      private function _isCallbackModeUsingPost()
2369      {
2370          return $this->_callback_mode_using_post;
2371      }
2372  
2373      /**
2374       * the URL that should be used for the PGT callback (in fact the URL of the
2375       * current request without any CGI parameter). Written and read by
2376       * CAS_Client::_getCallbackURL().
2377       *
2378       * @hideinitializer
2379       */
2380      private $_callback_url = '';
2381  
2382      /**
2383       * This method returns the URL that should be used for the PGT callback (in
2384       * fact the URL of the current request without any CGI parameter, except if
2385       * phpCAS::setFixedCallbackURL() was used).
2386       *
2387       * @return string The callback URL
2388       */
2389      private function _getCallbackURL()
2390      {
2391          // the URL is built when needed only
2392          if ( empty($this->_callback_url) ) {
2393              // remove the ticket if present in the URL
2394              $final_uri = 'https://';
2395              $final_uri .= $this->_getClientUrl();
2396              $request_uri = $_SERVER['REQUEST_URI'];
2397              $request_uri = preg_replace('/\?.*$/', '', $request_uri);
2398              $final_uri .= $request_uri;
2399              $this->_callback_url = $final_uri;
2400          }
2401          return $this->_callback_url;
2402      }
2403  
2404      /**
2405       * This method sets the callback url.
2406       *
2407       * @param string $url url to set callback
2408       *
2409       * @return string the callback url
2410       */
2411      public function setCallbackURL($url)
2412      {
2413      	 // Sequence validation
2414          $this->ensureIsProxy();
2415      	 // Argument Validation
2416      	 if (gettype($url) != 'string')
2417          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
2418  
2419          return $this->_callback_url = $url;
2420      }
2421  
2422      /**
2423       * This method is called by CAS_Client::CAS_Client() when running in callback
2424       * mode. It stores the PGT and its PGT Iou, prints its output and halts.
2425       *
2426       * @return void
2427       */
2428      private function _callback()
2429      {
2430          phpCAS::traceBegin();
2431          if ($this->_isCallbackModeUsingPost()) {
2432              $pgtId = $_POST['pgtId'];
2433              $pgtIou = $_POST['pgtIou'];
2434          } else {
2435              $pgtId = $_GET['pgtId'];
2436              $pgtIou = $_GET['pgtIou'];
2437          }
2438          if (preg_match('/^PGTIOU-[\.\-\w]+$/', $pgtIou)) {
2439              if (preg_match('/^[PT]GT-[\.\-\w]+$/', $pgtId)) {
2440                  phpCAS::trace('Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\')');
2441                  $this->_storePGT($pgtId, $pgtIou);
2442                  if (array_key_exists('HTTP_ACCEPT', $_SERVER) &&
2443                      (   $_SERVER['HTTP_ACCEPT'] == 'application/xml' ||
2444                          $_SERVER['HTTP_ACCEPT'] == 'text/xml'
2445                      )
2446                  ) {
2447                      echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n";
2448                      echo '<proxySuccess xmlns="http://www.yale.edu/tp/cas" />';
2449                      phpCAS::traceExit("XML response sent");
2450                  } else {
2451                      $this->printHTMLHeader('phpCAS callback');
2452                      echo '<p>Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\').</p>';
2453                      $this->printHTMLFooter();
2454                      phpCAS::traceExit("HTML response sent");
2455                  }
2456                  phpCAS::traceExit("Successfull Callback");
2457              } else {
2458                  phpCAS::error('PGT format invalid' . $pgtId);
2459                  phpCAS::traceExit('PGT format invalid' . $pgtId);
2460              }
2461          } else {
2462              phpCAS::error('PGTiou format invalid' . $pgtIou);
2463              phpCAS::traceExit('PGTiou format invalid' . $pgtIou);
2464          }
2465  
2466          // Flush the buffer to prevent from sending anything other then a 200
2467          // Success Status back to the CAS Server. The Exception would normally
2468          // report as a 500 error.
2469          flush();
2470          throw new CAS_GracefullTerminationException();
2471      }
2472  
2473  
2474      /** @} */
2475  
2476      // ########################################################################
2477      //  PGT STORAGE
2478      // ########################################################################
2479      /**
2480      * @addtogroup internalPGTStorage
2481      * @{
2482      */
2483  
2484      /**
2485       * @var  CAS_PGTStorage_AbstractStorage
2486       * an instance of a class inheriting of PGTStorage, used to deal with PGT
2487       * storage. Created by CAS_Client::setPGTStorageFile(), used
2488       * by CAS_Client::setPGTStorageFile() and CAS_Client::_initPGTStorage().
2489       *
2490       * @hideinitializer
2491       */
2492      private $_pgt_storage = null;
2493  
2494      /**
2495       * This method is used to initialize the storage of PGT's.
2496       * Halts on error.
2497       *
2498       * @return void
2499       */
2500      private function _initPGTStorage()
2501      {
2502          // if no SetPGTStorageXxx() has been used, default to file
2503          if ( !is_object($this->_pgt_storage) ) {
2504              $this->setPGTStorageFile();
2505          }
2506  
2507          // initializes the storage
2508          $this->_pgt_storage->init();
2509      }
2510  
2511      /**
2512       * This method stores a PGT. Halts on error.
2513       *
2514       * @param string $pgt     the PGT to store
2515       * @param string $pgt_iou its corresponding Iou
2516       *
2517       * @return void
2518       */
2519      private function _storePGT($pgt,$pgt_iou)
2520      {
2521          // ensure that storage is initialized
2522          $this->_initPGTStorage();
2523          // writes the PGT
2524          $this->_pgt_storage->write($pgt, $pgt_iou);
2525      }
2526  
2527      /**
2528       * This method reads a PGT from its Iou and deletes the corresponding
2529       * storage entry.
2530       *
2531       * @param string $pgt_iou the PGT Iou
2532       *
2533       * @return string mul The PGT corresponding to the Iou, false when not found.
2534       */
2535      private function _loadPGT($pgt_iou)
2536      {
2537          // ensure that storage is initialized
2538          $this->_initPGTStorage();
2539          // read the PGT
2540          return $this->_pgt_storage->read($pgt_iou);
2541      }
2542  
2543      /**
2544       * This method can be used to set a custom PGT storage object.
2545       *
2546       * @param CAS_PGTStorage_AbstractStorage $storage a PGT storage object that
2547       * inherits from the CAS_PGTStorage_AbstractStorage class
2548       *
2549       * @return void
2550       */
2551      public function setPGTStorage($storage)
2552      {
2553      	 // Sequence validation
2554          $this->ensureIsProxy();
2555  
2556          // check that the storage has not already been set
2557          if ( is_object($this->_pgt_storage) ) {
2558              phpCAS::error('PGT storage already defined');
2559          }
2560  
2561          // check to make sure a valid storage object was specified
2562          if ( !($storage instanceof CAS_PGTStorage_AbstractStorage) )
2563              throw new CAS_TypeMismatchException($storage, '$storage', 'CAS_PGTStorage_AbstractStorage object');
2564  
2565          // store the PGTStorage object
2566          $this->_pgt_storage = $storage;
2567      }
2568  
2569      /**
2570       * This method is used to tell phpCAS to store the response of the
2571       * CAS server to PGT requests in a database.
2572       *
2573       * @param string|PDO $dsn_or_pdo     a dsn string to use for creating a PDO
2574       * object or a PDO object
2575       * @param string $username       the username to use when connecting to the
2576       * database
2577       * @param string $password       the password to use when connecting to the
2578       * database
2579       * @param string $table          the table to use for storing and retrieving
2580       * PGTs
2581       * @param string $driver_options any driver options to use when connecting
2582       * to the database
2583       *
2584       * @return void
2585       */
2586      public function setPGTStorageDb(
2587          $dsn_or_pdo, $username='', $password='', $table='', $driver_options=null
2588      ) {
2589      	 // Sequence validation
2590          $this->ensureIsProxy();
2591  
2592      	 // Argument validation
2593      	 if (!(is_object($dsn_or_pdo) && $dsn_or_pdo instanceof PDO) && !is_string($dsn_or_pdo))
2594  	 	 	 throw new CAS_TypeMismatchException($dsn_or_pdo, '$dsn_or_pdo', 'string or PDO object');
2595      	 if (gettype($username) != 'string')
2596          	 throw new CAS_TypeMismatchException($username, '$username', 'string');
2597          if (gettype($password) != 'string')
2598          	 throw new CAS_TypeMismatchException($password, '$password', 'string');
2599          if (gettype($table) != 'string')
2600          	 throw new CAS_TypeMismatchException($table, '$password', 'string');
2601  
2602          // create the storage object
2603          $this->setPGTStorage(
2604              new CAS_PGTStorage_Db(
2605                  $this, $dsn_or_pdo, $username, $password, $table, $driver_options
2606              )
2607          );
2608      }
2609  
2610      /**
2611       * This method is used to tell phpCAS to store the response of the
2612       * CAS server to PGT requests onto the filesystem.
2613       *
2614       * @param string $path the path where the PGT's should be stored
2615       *
2616       * @return void
2617       */
2618      public function setPGTStorageFile($path='')
2619      {
2620      	 // Sequence validation
2621          $this->ensureIsProxy();
2622  
2623      	 // Argument validation
2624      	 if (gettype($path) != 'string')
2625          	 throw new CAS_TypeMismatchException($path, '$path', 'string');
2626  
2627          // create the storage object
2628          $this->setPGTStorage(new CAS_PGTStorage_File($this, $path));
2629      }
2630  
2631  
2632      // ########################################################################
2633      //  PGT VALIDATION
2634      // ########################################################################
2635      /**
2636      * This method is used to validate a PGT; halt on failure.
2637      *
2638      * @param string &$validate_url the URL of the request to the CAS server.
2639      * @param string $text_response the response of the CAS server, as is
2640      *                              (XML text); result of
2641      *                              CAS_Client::validateCAS10() or
2642      *                              CAS_Client::validateCAS20().
2643      * @param DOMElement $tree_response the response of the CAS server, as a DOM XML
2644      * tree; result of CAS_Client::validateCAS10() or CAS_Client::validateCAS20().
2645      *
2646      * @return bool true when successfull and issue a CAS_AuthenticationException
2647      * and false on an error
2648      *
2649      * @throws CAS_AuthenticationException
2650      */
2651      private function _validatePGT(&$validate_url,$text_response,$tree_response)
2652      {
2653          phpCAS::traceBegin();
2654          if ( $tree_response->getElementsByTagName("proxyGrantingTicket")->length == 0) {
2655              phpCAS::trace('<proxyGrantingTicket> not found');
2656              // authentication succeded, but no PGT Iou was transmitted
2657              throw new CAS_AuthenticationException(
2658                  $this, 'Ticket validated but no PGT Iou transmitted',
2659                  $validate_url, false/*$no_response*/, false/*$bad_response*/,
2660                  $text_response
2661              );
2662          } else {
2663              // PGT Iou transmitted, extract it
2664              $pgt_iou = trim(
2665                  $tree_response->getElementsByTagName("proxyGrantingTicket")->item(0)->nodeValue
2666              );
2667              if (preg_match('/^PGTIOU-[\.\-\w]+$/', $pgt_iou)) {
2668                  $pgt = $this->_loadPGT($pgt_iou);
2669                  if ( $pgt == false ) {
2670                      phpCAS::trace('could not load PGT');
2671                      throw new CAS_AuthenticationException(
2672                          $this,
2673                          'PGT Iou was transmitted but PGT could not be retrieved',
2674                          $validate_url, false/*$no_response*/,
2675                          false/*$bad_response*/, $text_response
2676                      );
2677                  }
2678                  $this->_setPGT($pgt);
2679              } else {
2680                  phpCAS::trace('PGTiou format error');
2681                  throw new CAS_AuthenticationException(
2682                      $this, 'PGT Iou was transmitted but has wrong format',
2683                      $validate_url, false/*$no_response*/, false/*$bad_response*/,
2684                      $text_response
2685                  );
2686              }
2687          }
2688          phpCAS::traceEnd(true);
2689          return true;
2690      }
2691  
2692      // ########################################################################
2693      //  PGT VALIDATION
2694      // ########################################################################
2695  
2696      /**
2697       * This method is used to retrieve PT's from the CAS server thanks to a PGT.
2698       *
2699       * @param string $target_service the service to ask for with the PT.
2700       * @param int &$err_code      an error code (PHPCAS_SERVICE_OK on success).
2701       * @param string &$err_msg       an error message (empty on success).
2702       *
2703       * @return string|false a Proxy Ticket, or false on error.
2704       */
2705      public function retrievePT($target_service,&$err_code,&$err_msg)
2706      {
2707      	 // Argument validation
2708      	 if (gettype($target_service) != 'string')
2709          	 throw new CAS_TypeMismatchException($target_service, '$target_service', 'string');
2710  
2711          phpCAS::traceBegin();
2712  
2713          // by default, $err_msg is set empty and $pt to true. On error, $pt is
2714          // set to false and $err_msg to an error message. At the end, if $pt is false
2715          // and $error_msg is still empty, it is set to 'invalid response' (the most
2716          // commonly encountered error).
2717          $err_msg = '';
2718  
2719          // build the URL to retrieve the PT
2720          $cas_url = $this->getServerProxyURL().'?targetService='
2721              .urlencode($target_service).'&pgt='.$this->_getPGT();
2722  
2723          // open and read the URL
2724          if ( !$this->_readURL($cas_url, $headers, $cas_response, $err_msg) ) {
2725              phpCAS::trace(
2726                  'could not open URL \''.$cas_url.'\' to validate ('.$err_msg.')'
2727              );
2728              $err_code = PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE;
2729              $err_msg = 'could not retrieve PT (no response from the CAS server)';
2730              phpCAS::traceEnd(false);
2731              return false;
2732          }
2733  
2734          $bad_response = false;
2735  
2736          // create new DOMDocument object
2737          $dom = new DOMDocument();
2738          // Fix possible whitspace problems
2739          $dom->preserveWhiteSpace = false;
2740          // read the response of the CAS server into a DOM object
2741          if ( !($dom->loadXML($cas_response))) {
2742              phpCAS::trace('dom->loadXML() failed');
2743              // read failed
2744              $bad_response = true;
2745          }
2746  
2747          if ( !$bad_response ) {
2748              // read the root node of the XML tree
2749              if ( !($root = $dom->documentElement) ) {
2750                  phpCAS::trace('documentElement failed');
2751                  // read failed
2752                  $bad_response = true;
2753              }
2754          }
2755  
2756          if ( !$bad_response ) {
2757              // insure that tag name is 'serviceResponse'
2758              if ( $root->localName != 'serviceResponse' ) {
2759                  phpCAS::trace('localName failed');
2760                  // bad root node
2761                  $bad_response = true;
2762              }
2763          }
2764  
2765          if ( !$bad_response ) {
2766              // look for a proxySuccess tag
2767              if ( $root->getElementsByTagName("proxySuccess")->length != 0) {
2768                  $proxy_success_list = $root->getElementsByTagName("proxySuccess");
2769  
2770                  // authentication succeded, look for a proxyTicket tag
2771                  if ( $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->length != 0) {
2772                      $err_code = PHPCAS_SERVICE_OK;
2773                      $err_msg = '';
2774                      $pt = trim(
2775                          $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->item(0)->nodeValue
2776                      );
2777                      phpCAS::trace('original PT: '.trim($pt));
2778                      phpCAS::traceEnd($pt);
2779                      return $pt;
2780                  } else {
2781                      phpCAS::trace('<proxySuccess> was found, but not <proxyTicket>');
2782                  }
2783              } else if ($root->getElementsByTagName("proxyFailure")->length != 0) {
2784                  // look for a proxyFailure tag
2785                  $proxy_failure_list = $root->getElementsByTagName("proxyFailure");
2786  
2787                  // authentication failed, extract the error
2788                  $err_code = PHPCAS_SERVICE_PT_FAILURE;
2789                  $err_msg = 'PT retrieving failed (code=`'
2790                  .$proxy_failure_list->item(0)->getAttribute('code')
2791                  .'\', message=`'
2792                  .trim($proxy_failure_list->item(0)->nodeValue)
2793                  .'\')';
2794                  phpCAS::traceEnd(false);
2795                  return false;
2796              } else {
2797                  phpCAS::trace('neither <proxySuccess> nor <proxyFailure> found');
2798              }
2799          }
2800  
2801          // at this step, we are sure that the response of the CAS server was
2802          // illformed
2803          $err_code = PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE;
2804          $err_msg = 'Invalid response from the CAS server (response=`'
2805              .$cas_response.'\')';
2806  
2807          phpCAS::traceEnd(false);
2808          return false;
2809      }
2810  
2811      /** @} */
2812  
2813      // ########################################################################
2814      // READ CAS SERVER ANSWERS
2815      // ########################################################################
2816  
2817      /**
2818       * @addtogroup internalMisc
2819       * @{
2820       */
2821  
2822      /**
2823       * This method is used to acces a remote URL.
2824       *
2825       * @param string $url      the URL to access.
2826       * @param string &$headers an array containing the HTTP header lines of the
2827       * response (an empty array on failure).
2828       * @param string &$body    the body of the response, as a string (empty on
2829       * failure).
2830       * @param string &$err_msg an error message, filled on failure.
2831       *
2832       * @return bool true on success, false otherwise (in this later case, $err_msg
2833       * contains an error message).
2834       */
2835      private function _readURL($url, &$headers, &$body, &$err_msg)
2836      {
2837          phpCAS::traceBegin();
2838          $className = $this->_requestImplementation;
2839          $request = new $className();
2840  
2841          if (count($this->_curl_options)) {
2842              $request->setCurlOptions($this->_curl_options);
2843          }
2844  
2845          $request->setUrl($url);
2846  
2847          if (empty($this->_cas_server_ca_cert) && !$this->_no_cas_server_validation) {
2848              phpCAS::error(
2849                  'one of the methods phpCAS::setCasServerCACert() or phpCAS::setNoCasServerValidation() must be called.'
2850              );
2851          }
2852          if ($this->_cas_server_ca_cert != '') {
2853              $request->setSslCaCert(
2854                  $this->_cas_server_ca_cert, $this->_cas_server_cn_validate
2855              );
2856          }
2857  
2858          // add extra stuff if SAML
2859          if ($this->getServerVersion() == SAML_VERSION_1_1) {
2860              $request->addHeader("soapaction: http://www.oasis-open.org/committees/security");
2861              $request->addHeader("cache-control: no-cache");
2862              $request->addHeader("pragma: no-cache");
2863              $request->addHeader("accept: text/xml");
2864              $request->addHeader("connection: keep-alive");
2865              $request->addHeader("content-type: text/xml");
2866              $request->makePost();
2867              $request->setPostBody($this->_buildSAMLPayload());
2868          }
2869  
2870          if ($request->send()) {
2871              $headers = $request->getResponseHeaders();
2872              $body = $request->getResponseBody();
2873              $err_msg = '';
2874              phpCAS::traceEnd(true);
2875              return true;
2876          } else {
2877              $headers = '';
2878              $body = '';
2879              $err_msg = $request->getErrorMessage();
2880              phpCAS::traceEnd(false);
2881              return false;
2882          }
2883      }
2884  
2885      /**
2886       * This method is used to build the SAML POST body sent to /samlValidate URL.
2887       *
2888       * @return string the SOAP-encased SAMLP artifact (the ticket).
2889       */
2890      private function _buildSAMLPayload()
2891      {
2892          phpCAS::traceBegin();
2893  
2894          //get the ticket
2895          $sa = urlencode($this->getTicket());
2896  
2897          $body = SAML_SOAP_ENV.SAML_SOAP_BODY.SAMLP_REQUEST
2898              .SAML_ASSERTION_ARTIFACT.$sa.SAML_ASSERTION_ARTIFACT_CLOSE
2899              .SAMLP_REQUEST_CLOSE.SAML_SOAP_BODY_CLOSE.SAML_SOAP_ENV_CLOSE;
2900  
2901          phpCAS::traceEnd($body);
2902          return ($body);
2903      }
2904  
2905      /** @} **/
2906  
2907      // ########################################################################
2908      // ACCESS TO EXTERNAL SERVICES
2909      // ########################################################################
2910  
2911      /**
2912       * @addtogroup internalProxyServices
2913       * @{
2914       */
2915  
2916  
2917      /**
2918       * Answer a proxy-authenticated service handler.
2919       *
2920       * @param string $type The service type. One of:
2921       * PHPCAS_PROXIED_SERVICE_HTTP_GET, PHPCAS_PROXIED_SERVICE_HTTP_POST,
2922       * PHPCAS_PROXIED_SERVICE_IMAP
2923       *
2924       * @return CAS_ProxiedService
2925       * @throws InvalidArgumentException If the service type is unknown.
2926       */
2927      public function getProxiedService ($type)
2928      {
2929      	 // Sequence validation
2930          $this->ensureIsProxy();
2931      	 $this->ensureAuthenticationCallSuccessful();
2932  
2933      	 // Argument validation
2934      	 if (gettype($type) != 'string')
2935          	 throw new CAS_TypeMismatchException($type, '$type', 'string');
2936  
2937          switch ($type) {
2938          case PHPCAS_PROXIED_SERVICE_HTTP_GET:
2939          case PHPCAS_PROXIED_SERVICE_HTTP_POST:
2940              $requestClass = $this->_requestImplementation;
2941              $request = new $requestClass();
2942              if (count($this->_curl_options)) {
2943                  $request->setCurlOptions($this->_curl_options);
2944              }
2945              $proxiedService = new $type($request, $this->_serviceCookieJar);
2946              if ($proxiedService instanceof CAS_ProxiedService_Testable) {
2947                  $proxiedService->setCasClient($this);
2948              }
2949              return $proxiedService;
2950          case PHPCAS_PROXIED_SERVICE_IMAP;
2951              $proxiedService = new CAS_ProxiedService_Imap($this->_getUser());
2952              if ($proxiedService instanceof CAS_ProxiedService_Testable) {
2953                  $proxiedService->setCasClient($this);
2954              }
2955              return $proxiedService;
2956          default:
2957              throw new CAS_InvalidArgumentException(
2958                  "Unknown proxied-service type, $type."
2959              );
2960          }
2961      }
2962  
2963      /**
2964       * Initialize a proxied-service handler with the proxy-ticket it should use.
2965       *
2966       * @param CAS_ProxiedService $proxiedService service handler
2967       *
2968       * @return void
2969       *
2970       * @throws CAS_ProxyTicketException If there is a proxy-ticket failure.
2971       *	 	 The code of the Exception will be one of:
2972       *	 	 	 PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE
2973       *	 	 	 PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE
2974       *	 	 	 PHPCAS_SERVICE_PT_FAILURE
2975       * @throws CAS_ProxiedService_Exception If there is a failure getting the
2976       * url from the proxied service.
2977       */
2978      public function initializeProxiedService (CAS_ProxiedService $proxiedService)
2979      {
2980      	 // Sequence validation
2981          $this->ensureIsProxy();
2982      	 $this->ensureAuthenticationCallSuccessful();
2983  
2984          $url = $proxiedService->getServiceUrl();
2985          if (!is_string($url)) {
2986              throw new CAS_ProxiedService_Exception(
2987                  "Proxied Service ".get_class($proxiedService)
2988                  ."->getServiceUrl() should have returned a string, returned a "
2989                  .gettype($url)." instead."
2990              );
2991          }
2992          $pt = $this->retrievePT($url, $err_code, $err_msg);
2993          if (!$pt) {
2994              throw new CAS_ProxyTicketException($err_msg, $err_code);
2995          }
2996          $proxiedService->setProxyTicket($pt);
2997      }
2998  
2999      /**
3000       * This method is used to access an HTTP[S] service.
3001       *
3002       * @param string $url       the service to access.
3003       * @param int    &$err_code an error code Possible values are
3004       * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE,
3005       * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE,
3006       * PHPCAS_SERVICE_NOT_AVAILABLE.
3007       * @param string &$output   the output of the service (also used to give an error
3008       * message on failure).
3009       *
3010       * @return bool true on success, false otherwise (in this later case, $err_code
3011       * gives the reason why it failed and $output contains an error message).
3012       */
3013      public function serviceWeb($url,&$err_code,&$output)
3014      {
3015      	 // Sequence validation
3016          $this->ensureIsProxy();
3017      	 $this->ensureAuthenticationCallSuccessful();
3018  
3019      	 // Argument validation
3020      	 if (gettype($url) != 'string')
3021          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
3022  
3023          try {
3024              $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_HTTP_GET);
3025              $service->setUrl($url);
3026              $service->send();
3027              $output = $service->getResponseBody();
3028              $err_code = PHPCAS_SERVICE_OK;
3029              return true;
3030          } catch (CAS_ProxyTicketException $e) {
3031              $err_code = $e->getCode();
3032              $output = $e->getMessage();
3033              return false;
3034          } catch (CAS_ProxiedService_Exception $e) {
3035              $lang = $this->getLangObj();
3036              $output = sprintf(
3037                  $lang->getServiceUnavailable(), $url, $e->getMessage()
3038              );
3039              $err_code = PHPCAS_SERVICE_NOT_AVAILABLE;
3040              return false;
3041          }
3042      }
3043  
3044      /**
3045       * This method is used to access an IMAP/POP3/NNTP service.
3046       *
3047       * @param string $url        a string giving the URL of the service, including
3048       * the mailing box for IMAP URLs, as accepted by imap_open().
3049       * @param string $serviceUrl a string giving for CAS retrieve Proxy ticket
3050       * @param string $flags      options given to imap_open().
3051       * @param int    &$err_code  an error code Possible values are
3052       * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE,
3053       * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE,
3054       *  PHPCAS_SERVICE_NOT_AVAILABLE.
3055       * @param string &$err_msg   an error message on failure
3056       * @param string &$pt        the Proxy Ticket (PT) retrieved from the CAS
3057       * server to access the URL on success, false on error).
3058       *
3059       * @return object|false an IMAP stream on success, false otherwise (in this later
3060       *  case, $err_code gives the reason why it failed and $err_msg contains an
3061       *  error message).
3062       */
3063      public function serviceMail($url,$serviceUrl,$flags,&$err_code,&$err_msg,&$pt)
3064      {
3065      	 // Sequence validation
3066          $this->ensureIsProxy();
3067      	 $this->ensureAuthenticationCallSuccessful();
3068  
3069      	 // Argument validation
3070      	 if (gettype($url) != 'string')
3071          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
3072          if (gettype($serviceUrl) != 'string')
3073          	 throw new CAS_TypeMismatchException($serviceUrl, '$serviceUrl', 'string');
3074          if (gettype($flags) != 'integer')
3075          	 throw new CAS_TypeMismatchException($flags, '$flags', 'string');
3076  
3077          try {
3078              $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_IMAP);
3079              $service->setServiceUrl($serviceUrl);
3080              $service->setMailbox($url);
3081              $service->setOptions($flags);
3082  
3083              $stream = $service->open();
3084              $err_code = PHPCAS_SERVICE_OK;
3085              $pt = $service->getImapProxyTicket();
3086              return $stream;
3087          } catch (CAS_ProxyTicketException $e) {
3088              $err_msg = $e->getMessage();
3089              $err_code = $e->getCode();
3090              $pt = false;
3091              return false;
3092          } catch (CAS_ProxiedService_Exception $e) {
3093              $lang = $this->getLangObj();
3094              $err_msg = sprintf(
3095                  $lang->getServiceUnavailable(),
3096                  $url,
3097                  $e->getMessage()
3098              );
3099              $err_code = PHPCAS_SERVICE_NOT_AVAILABLE;
3100              $pt = false;
3101              return false;
3102          }
3103      }
3104  
3105      /** @} **/
3106  
3107      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3108      // XX                                                                    XX
3109      // XX                  PROXIED CLIENT FEATURES (CAS 2.0)                 XX
3110      // XX                                                                    XX
3111      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3112  
3113      // ########################################################################
3114      //  PT
3115      // ########################################################################
3116      /**
3117      * @addtogroup internalService
3118      * @{
3119      */
3120  
3121      /**
3122       * This array will store a list of proxies in front of this application. This
3123       * property will only be populated if this script is being proxied rather than
3124       * accessed directly.
3125       *
3126       * It is set in CAS_Client::validateCAS20() and can be read by
3127       * CAS_Client::getProxies()
3128       *
3129       * @access private
3130       */
3131      private $_proxies = array();
3132  
3133      /**
3134       * Answer an array of proxies that are sitting in front of this application.
3135       *
3136       * This method will only return a non-empty array if we have received and
3137       * validated a Proxy Ticket.
3138       *
3139       * @return array
3140       * @access public
3141       */
3142      public function getProxies()
3143      {
3144          return $this->_proxies;
3145      }
3146  
3147      /**
3148       * Set the Proxy array, probably from persistant storage.
3149       *
3150       * @param array $proxies An array of proxies
3151       *
3152       * @return void
3153       * @access private
3154       */
3155      private function _setProxies($proxies)
3156      {
3157          $this->_proxies = $proxies;
3158          if (!empty($proxies)) {
3159              // For proxy-authenticated requests people are not viewing the URL
3160              // directly since the client is another application making a
3161              // web-service call.
3162              // Because of this, stripping the ticket from the URL is unnecessary
3163              // and causes another web-service request to be performed. Additionally,
3164              // if session handling on either the client or the server malfunctions
3165              // then the subsequent request will not complete successfully.
3166              $this->setNoClearTicketsFromUrl();
3167          }
3168      }
3169  
3170      /**
3171       * A container of patterns to be allowed as proxies in front of the cas client.
3172       *
3173       * @var CAS_ProxyChain_AllowedList
3174       */
3175      private $_allowed_proxy_chains;
3176  
3177      /**
3178       * Answer the CAS_ProxyChain_AllowedList object for this client.
3179       *
3180       * @return CAS_ProxyChain_AllowedList
3181       */
3182      public function getAllowedProxyChains ()
3183      {
3184          if (empty($this->_allowed_proxy_chains)) {
3185              $this->_allowed_proxy_chains = new CAS_ProxyChain_AllowedList();
3186          }
3187          return $this->_allowed_proxy_chains;
3188      }
3189  
3190      /** @} */
3191      // ########################################################################
3192      //  PT VALIDATION
3193      // ########################################################################
3194      /**
3195      * @addtogroup internalProxied
3196      * @{
3197      */
3198  
3199      /**
3200       * This method is used to validate a cas 2.0 ST or PT; halt on failure
3201       * Used for all CAS 2.0 validations
3202       *
3203       * @param string &$validate_url  the url of the reponse
3204       * @param string &$text_response the text of the repsones
3205       * @param DOMElement &$tree_response the domxml tree of the respones
3206       * @param bool   $renew          true to force the authentication with the CAS server
3207       *
3208       * @return bool true when successfull and issue a CAS_AuthenticationException
3209       * and false on an error
3210       *
3211       * @throws  CAS_AuthenticationException
3212       */
3213      public function validateCAS20(&$validate_url,&$text_response,&$tree_response, $renew=false)
3214      {
3215          phpCAS::traceBegin();
3216          phpCAS::trace($text_response);
3217          // build the URL to validate the ticket
3218          if ($this->getAllowedProxyChains()->isProxyingAllowed()) {
3219              $validate_url = $this->getServerProxyValidateURL().'&ticket='
3220                  .urlencode($this->getTicket());
3221          } else {
3222              $validate_url = $this->getServerServiceValidateURL().'&ticket='
3223                  .urlencode($this->getTicket());
3224          }
3225  
3226          if ( $this->isProxy() ) {
3227              // pass the callback url for CAS proxies
3228              $validate_url .= '&pgtUrl='.urlencode($this->_getCallbackURL());
3229          }
3230  
3231          if ( $renew ) {
3232              // pass the renew
3233              $validate_url .= '&renew=true';
3234          }
3235  
3236          // open and read the URL
3237          if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) {
3238              phpCAS::trace(
3239                  'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'
3240              );
3241              throw new CAS_AuthenticationException(
3242                  $this, 'Ticket not validated', $validate_url,
3243                  true/*$no_response*/
3244              );
3245          }
3246  
3247          // create new DOMDocument object
3248          $dom = new DOMDocument();
3249          // Fix possible whitspace problems
3250          $dom->preserveWhiteSpace = false;
3251          // CAS servers should only return data in utf-8
3252          $dom->encoding = "utf-8";
3253          // read the response of the CAS server into a DOMDocument object
3254          if ( !($dom->loadXML($text_response))) {
3255              // read failed
3256              throw new CAS_AuthenticationException(
3257                  $this, 'Ticket not validated', $validate_url,
3258                  false/*$no_response*/, true/*$bad_response*/, $text_response
3259              );
3260          } else if ( !($tree_response = $dom->documentElement) ) {
3261              // read the root node of the XML tree
3262              // read failed
3263              throw new CAS_AuthenticationException(
3264                  $this, 'Ticket not validated', $validate_url,
3265                  false/*$no_response*/, true/*$bad_response*/, $text_response
3266              );
3267          } else if ($tree_response->localName != 'serviceResponse') {
3268              // insure that tag name is 'serviceResponse'
3269              // bad root node
3270              throw new CAS_AuthenticationException(
3271                  $this, 'Ticket not validated', $validate_url,
3272                  false/*$no_response*/, true/*$bad_response*/, $text_response
3273              );
3274          } else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) {
3275              // authentication failed, extract the error code and message and throw exception
3276              $auth_fail_list = $tree_response
3277                  ->getElementsByTagName("authenticationFailure");
3278              throw new CAS_AuthenticationException(
3279                  $this, 'Ticket not validated', $validate_url,
3280                  false/*$no_response*/, false/*$bad_response*/,
3281                  $text_response,
3282                  $auth_fail_list->item(0)->getAttribute('code')/*$err_code*/,
3283                  trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/
3284              );
3285          } else if ($tree_response->getElementsByTagName("authenticationSuccess")->length != 0) {
3286              // authentication succeded, extract the user name
3287              $success_elements = $tree_response
3288                  ->getElementsByTagName("authenticationSuccess");
3289              if ( $success_elements->item(0)->getElementsByTagName("user")->length == 0) {
3290                  // no user specified => error
3291                  throw new CAS_AuthenticationException(
3292                      $this, 'Ticket not validated', $validate_url,
3293                      false/*$no_response*/, true/*$bad_response*/, $text_response
3294                  );
3295              } else {
3296                  $this->_setUser(
3297                      trim(
3298                          $success_elements->item(0)->getElementsByTagName("user")->item(0)->nodeValue
3299                      )
3300                  );
3301                  $this->_readExtraAttributesCas20($success_elements);
3302                  // Store the proxies we are sitting behind for authorization checking
3303                  $proxyList = array();
3304                  if ( sizeof($arr = $success_elements->item(0)->getElementsByTagName("proxy")) > 0) {
3305                      foreach ($arr as $proxyElem) {
3306                          phpCAS::trace("Found Proxy: ".$proxyElem->nodeValue);
3307                          $proxyList[] = trim($proxyElem->nodeValue);
3308                      }
3309                      $this->_setProxies($proxyList);
3310                      phpCAS::trace("Storing Proxy List");
3311                  }
3312                  // Check if the proxies in front of us are allowed
3313                  if (!$this->getAllowedProxyChains()->isProxyListAllowed($proxyList)) {
3314                      throw new CAS_AuthenticationException(
3315                          $this, 'Proxy not allowed', $validate_url,
3316                          false/*$no_response*/, true/*$bad_response*/,
3317                          $text_response
3318                      );
3319                  } else {
3320                      $result = true;
3321                  }
3322              }
3323          } else {
3324              throw new CAS_AuthenticationException(
3325                  $this, 'Ticket not validated', $validate_url,
3326                  false/*$no_response*/, true/*$bad_response*/,
3327                  $text_response
3328              );
3329          }
3330  
3331          $this->_renameSession($this->getTicket());
3332  
3333          // at this step, Ticket has been validated and $this->_user has been set,
3334  
3335          phpCAS::traceEnd($result);
3336          return $result;
3337      }
3338  
3339  
3340      /**
3341       * This method will parse the DOM and pull out the attributes from the XML
3342       * payload and put them into an array, then put the array into the session.
3343       *
3344       * @param DOMNodeList $success_elements payload of the response
3345       *
3346       * @return bool true when successfull, halt otherwise by calling
3347       * CAS_Client::_authError().
3348       */
3349      private function _readExtraAttributesCas20($success_elements)
3350      {
3351          phpCAS::traceBegin();
3352  
3353          $extra_attributes = array();
3354  
3355          // "Jasig Style" Attributes:
3356          //
3357          // 	 <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
3358          // 	 	 <cas:authenticationSuccess>
3359          // 	 	 	 <cas:user>jsmith</cas:user>
3360          // 	 	 	 <cas:attributes>
3361          // 	 	 	 	 <cas:attraStyle>RubyCAS</cas:attraStyle>
3362          // 	 	 	 	 <cas:surname>Smith</cas:surname>
3363          // 	 	 	 	 <cas:givenName>John</cas:givenName>
3364          // 	 	 	 	 <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf>
3365          // 	 	 	 	 <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf>
3366          // 	 	 	 </cas:attributes>
3367          // 	 	 	 <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
3368          // 	 	 </cas:authenticationSuccess>
3369          // 	 </cas:serviceResponse>
3370          //
3371          if ($this->_casAttributeParserCallbackFunction !== null
3372              && is_callable($this->_casAttributeParserCallbackFunction)
3373          ) {
3374              array_unshift($this->_casAttributeParserCallbackArgs, $success_elements->item(0));
3375              phpCAS :: trace("Calling attritubeParser callback");
3376              $extra_attributes =  call_user_func_array(
3377                  $this->_casAttributeParserCallbackFunction,
3378                  $this->_casAttributeParserCallbackArgs
3379              );
3380          } elseif ( $success_elements->item(0)->getElementsByTagName("attributes")->length != 0) {
3381              $attr_nodes = $success_elements->item(0)
3382                  ->getElementsByTagName("attributes");
3383              phpCAS :: trace("Found nested jasig style attributes");
3384              if ($attr_nodes->item(0)->hasChildNodes()) {
3385                  // Nested Attributes
3386                  foreach ($attr_nodes->item(0)->childNodes as $attr_child) {
3387                      phpCAS :: trace(
3388                          "Attribute [".$attr_child->localName."] = "
3389                          .$attr_child->nodeValue
3390                      );
3391                      $this->_addAttributeToArray(
3392                          $extra_attributes, $attr_child->localName,
3393                          $attr_child->nodeValue
3394                      );
3395                  }
3396              }
3397          } else {
3398              // "RubyCAS Style" attributes
3399              //
3400              // 	 <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
3401              // 	 	 <cas:authenticationSuccess>
3402              // 	 	 	 <cas:user>jsmith</cas:user>
3403              //
3404              // 	 	 	 <cas:attraStyle>RubyCAS</cas:attraStyle>
3405              // 	 	 	 <cas:surname>Smith</cas:surname>
3406              // 	 	 	 <cas:givenName>John</cas:givenName>
3407              // 	 	 	 <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf>
3408              // 	 	 	 <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf>
3409              //
3410              // 	 	 	 <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
3411              // 	 	 </cas:authenticationSuccess>
3412              // 	 </cas:serviceResponse>
3413              //
3414              phpCAS :: trace("Testing for rubycas style attributes");
3415              $childnodes = $success_elements->item(0)->childNodes;
3416              foreach ($childnodes as $attr_node) {
3417                  switch ($attr_node->localName) {
3418                  case 'user':
3419                  case 'proxies':
3420                  case 'proxyGrantingTicket':
3421                      break;
3422                  default:
3423                      if (strlen(trim($attr_node->nodeValue))) {
3424                          phpCAS :: trace(
3425                              "Attribute [".$attr_node->localName."] = ".$attr_node->nodeValue
3426                          );
3427                          $this->_addAttributeToArray(
3428                              $extra_attributes, $attr_node->localName,
3429                              $attr_node->nodeValue
3430                          );
3431                      }
3432                  }
3433              }
3434          }
3435  
3436          // "Name-Value" attributes.
3437          //
3438          // Attribute format from these mailing list thread:
3439          // http://jasig.275507.n4.nabble.com/CAS-attributes-and-how-they-appear-in-the-CAS-response-td264272.html
3440          // Note: This is a less widely used format, but in use by at least two institutions.
3441          //
3442          // 	 <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
3443          // 	 	 <cas:authenticationSuccess>
3444          // 	 	 	 <cas:user>jsmith</cas:user>
3445          //
3446          // 	 	 	 <cas:attribute name='attraStyle' value='Name-Value' />
3447          // 	 	 	 <cas:attribute name='surname' value='Smith' />
3448          // 	 	 	 <cas:attribute name='givenName' value='John' />
3449          // 	 	 	 <cas:attribute name='memberOf' value='CN=Staff,OU=Groups,DC=example,DC=edu' />
3450          // 	 	 	 <cas:attribute name='memberOf' value='CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu' />
3451          //
3452          // 	 	 	 <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
3453          // 	 	 </cas:authenticationSuccess>
3454          // 	 </cas:serviceResponse>
3455          //
3456          if (!count($extra_attributes)
3457              && $success_elements->item(0)->getElementsByTagName("attribute")->length != 0
3458          ) {
3459              $attr_nodes = $success_elements->item(0)
3460                  ->getElementsByTagName("attribute");
3461              $firstAttr = $attr_nodes->item(0);
3462              if (!$firstAttr->hasChildNodes()
3463                  && $firstAttr->hasAttribute('name')
3464                  && $firstAttr->hasAttribute('value')
3465              ) {
3466                  phpCAS :: trace("Found Name-Value style attributes");
3467                  // Nested Attributes
3468                  foreach ($attr_nodes as $attr_node) {
3469                      if ($attr_node->hasAttribute('name')
3470                          && $attr_node->hasAttribute('value')
3471                      ) {
3472                          phpCAS :: trace(
3473                              "Attribute [".$attr_node->getAttribute('name')
3474                              ."] = ".$attr_node->getAttribute('value')
3475                          );
3476                          $this->_addAttributeToArray(
3477                              $extra_attributes, $attr_node->getAttribute('name'),
3478                              $attr_node->getAttribute('value')
3479                          );
3480                      }
3481                  }
3482              }
3483          }
3484  
3485          $this->setAttributes($extra_attributes);
3486          phpCAS::traceEnd();
3487          return true;
3488      }
3489  
3490      /**
3491       * Add an attribute value to an array of attributes.
3492       *
3493       * @param array  &$attributeArray reference to array
3494       * @param string $name            name of attribute
3495       * @param string $value           value of attribute
3496       *
3497       * @return void
3498       */
3499      private function _addAttributeToArray(array &$attributeArray, $name, $value)
3500      {
3501          // If multiple attributes exist, add as an array value
3502          if (isset($attributeArray[$name])) {
3503              // Initialize the array with the existing value
3504              if (!is_array($attributeArray[$name])) {
3505                  $existingValue = $attributeArray[$name];
3506                  $attributeArray[$name] = array($existingValue);
3507              }
3508  
3509              $attributeArray[$name][] = trim($value);
3510          } else {
3511              $attributeArray[$name] = trim($value);
3512          }
3513      }
3514  
3515      /** @} */
3516  
3517      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3518      // XX                                                                    XX
3519      // XX                               MISC                                 XX
3520      // XX                                                                    XX
3521      // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3522  
3523      /**
3524       * @addtogroup internalMisc
3525       * @{
3526       */
3527  
3528      // ########################################################################
3529      //  URL
3530      // ########################################################################
3531      /**
3532      * the URL of the current request (without any ticket CGI parameter). Written
3533      * and read by CAS_Client::getURL().
3534      *
3535      * @hideinitializer
3536      */
3537      private $_url = '';
3538  
3539  
3540      /**
3541       * This method sets the URL of the current request
3542       *
3543       * @param string $url url to set for service
3544       *
3545       * @return void
3546       */
3547      public function setURL($url)
3548      {
3549      	 // Argument Validation
3550      	 if (gettype($url) != 'string')
3551          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
3552  
3553          $this->_url = $url;
3554      }
3555  
3556      /**
3557       * This method returns the URL of the current request (without any ticket
3558       * CGI parameter).
3559       *
3560       * @return string The URL
3561       */
3562      public function getURL()
3563      {
3564          phpCAS::traceBegin();
3565          // the URL is built when needed only
3566          if ( empty($this->_url) ) {
3567              // remove the ticket if present in the URL
3568              $final_uri = ($this->_isHttps()) ? 'https' : 'http';
3569              $final_uri .= '://';
3570  
3571              $final_uri .= $this->_getClientUrl();
3572              $request_uri	 = explode('?', $_SERVER['REQUEST_URI'], 2);
3573              $final_uri	 	 .= $request_uri[0];
3574  
3575              if (isset($request_uri[1]) && $request_uri[1]) {
3576                  $query_string= $this->_removeParameterFromQueryString('ticket', $request_uri[1]);
3577  
3578                  // If the query string still has anything left,
3579                  // append it to the final URI
3580                  if ($query_string !== '') {
3581                      $final_uri	 .= "?$query_string";
3582                  }
3583              }
3584  
3585              phpCAS::trace("Final URI: $final_uri");
3586              $this->setURL($final_uri);
3587          }
3588          phpCAS::traceEnd($this->_url);
3589          return $this->_url;
3590      }
3591  
3592      /**
3593       * This method sets the base URL of the CAS server.
3594       *
3595       * @param string $url the base URL
3596       *
3597       * @return string base url
3598       */
3599      public function setBaseURL($url)
3600      {
3601      	 // Argument Validation
3602      	 if (gettype($url) != 'string')
3603          	 throw new CAS_TypeMismatchException($url, '$url', 'string');
3604  
3605          return $this->_server['base_url'] = $url;
3606      }
3607  
3608  
3609      /**
3610       * Try to figure out the phpCAS client URL with possible Proxys / Ports etc.
3611       *
3612       * @return string Server URL with domain:port
3613       */
3614      private function _getClientUrl()
3615      {
3616          if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) {
3617              // explode the host list separated by comma and use the first host
3618              $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
3619              // see rfc7239#5.3 and rfc7230#2.7.1: port is in HTTP_X_FORWARDED_HOST if non default
3620              return $hosts[0];
3621          } else if (!empty($_SERVER['HTTP_X_FORWARDED_SERVER'])) {
3622              $server_url = $_SERVER['HTTP_X_FORWARDED_SERVER'];
3623          } else {
3624              if (empty($_SERVER['SERVER_NAME'])) {
3625                  $server_url = $_SERVER['HTTP_HOST'];
3626              } else {
3627                  $server_url = $_SERVER['SERVER_NAME'];
3628              }
3629          }
3630          if (!strpos($server_url, ':')) {
3631              if (empty($_SERVER['HTTP_X_FORWARDED_PORT'])) {
3632                  $server_port = $_SERVER['SERVER_PORT'];
3633              } else {
3634                  $ports = explode(',', $_SERVER['HTTP_X_FORWARDED_PORT']);
3635                  $server_port = $ports[0];
3636              }
3637  
3638              if ( ($this->_isHttps() && $server_port!=443)
3639                  || (!$this->_isHttps() && $server_port!=80)
3640              ) {
3641                  $server_url .= ':';
3642                  $server_url .= $server_port;
3643              }
3644          }
3645          return $server_url;
3646      }
3647  
3648      /**
3649       * This method checks to see if the request is secured via HTTPS
3650       *
3651       * @return bool true if https, false otherwise
3652       */
3653      private function _isHttps()
3654      {
3655          if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
3656              return ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
3657          } elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) {
3658              return ($_SERVER['HTTP_X_FORWARDED_PROTOCOL'] === 'https');
3659          } elseif ( isset($_SERVER['HTTPS'])
3660              && !empty($_SERVER['HTTPS'])
3661              && strcasecmp($_SERVER['HTTPS'], 'off') !== 0
3662          ) {
3663              return true;
3664          }
3665          return false;
3666  
3667      }
3668  
3669      /**
3670       * Removes a parameter from a query string
3671       *
3672       * @param string $parameterName name of parameter
3673       * @param string $queryString   query string
3674       *
3675       * @return string new query string
3676       *
3677       * @link http://stackoverflow.com/questions/1842681/regular-expression-to-remove-one-parameter-from-query-string
3678       */
3679      private function _removeParameterFromQueryString($parameterName, $queryString)
3680      {
3681          $parameterName	 = preg_quote($parameterName);
3682          return preg_replace(
3683              "/&$parameterName(=[^&]*)?|^$parameterName(=[^&]*)?&?/",
3684              '', $queryString
3685          );
3686      }
3687  
3688      /**
3689       * This method is used to append query parameters to an url. Since the url
3690       * might already contain parameter it has to be detected and to build a proper
3691       * URL
3692       *
3693       * @param string $url   base url to add the query params to
3694       * @param string $query params in query form with & separated
3695       *
3696       * @return string url with query params
3697       */
3698      private function _buildQueryUrl($url, $query)
3699      {
3700          $url .= (strstr($url, '?') === false) ? '?' : '&';
3701          $url .= $query;
3702          return $url;
3703      }
3704  
3705      /**
3706       * Renaming the session
3707       *
3708       * @param string $ticket name of the ticket
3709       *
3710       * @return void
3711       */
3712      private function _renameSession($ticket)
3713      {
3714          phpCAS::traceBegin();
3715          if ($this->getChangeSessionID()) {
3716              if (!empty($this->_user)) {
3717                  $old_session = $_SESSION;
3718                  phpCAS :: trace("Killing session: ". session_id());
3719                  session_destroy();
3720                  // set up a new session, of name based on the ticket
3721                  $session_id = $this->_sessionIdForTicket($ticket);
3722                  phpCAS :: trace("Starting session: ". $session_id);
3723                  session_id($session_id);
3724                  session_start();
3725                  phpCAS :: trace("Restoring old session vars");
3726                  $_SESSION = $old_session;
3727              } else {
3728                  phpCAS :: trace (
3729                      'Session should only be renamed after successfull authentication'
3730                  );
3731              }
3732          } else {
3733              phpCAS :: trace(
3734                  "Skipping session rename since phpCAS is not handling the session."
3735              );
3736          }
3737          phpCAS::traceEnd();
3738      }
3739  
3740      /**
3741       * Answer a valid session-id given a CAS ticket.
3742       *
3743       * The output must be deterministic to allow single-log-out when presented with
3744       * the ticket to log-out.
3745       *
3746       *
3747       * @param string $ticket name of the ticket
3748       *
3749       * @return string
3750       */
3751      private function _sessionIdForTicket($ticket)
3752      {
3753        // Hash the ticket to ensure that the value meets the PHP 7.1 requirement
3754        // that session-ids have a length between 22 and 256 characters.
3755        return hash('sha256', $this->_sessionIdSalt . $ticket);
3756      }
3757  
3758      /**
3759       * Set a salt/seed for the session-id hash to make it harder to guess.
3760       *
3761       * @var string $_sessionIdSalt
3762       */
3763      private $_sessionIdSalt = '';
3764  
3765      /**
3766       * Set a salt/seed for the session-id hash to make it harder to guess.
3767       *
3768       * @param string $salt
3769       *
3770       * @return void
3771       */
3772      public function setSessionIdSalt($salt) {
3773        $this->_sessionIdSalt = (string)$salt;
3774      }
3775  
3776      // ########################################################################
3777      //  AUTHENTICATION ERROR HANDLING
3778      // ########################################################################
3779      /**
3780      * This method is used to print the HTML output when the user was not
3781      * authenticated.
3782      *
3783      * @param string $failure      the failure that occured
3784      * @param string $cas_url      the URL the CAS server was asked for
3785      * @param bool   $no_response  the response from the CAS server (other
3786      * parameters are ignored if true)
3787      * @param bool   $bad_response bad response from the CAS server ($err_code
3788      * and $err_msg ignored if true)
3789      * @param string $cas_response the response of the CAS server
3790      * @param int    $err_code     the error code given by the CAS server
3791      * @param string $err_msg      the error message given by the CAS server
3792      *
3793      * @return void
3794      */
3795      private function _authError(
3796          $failure,
3797          $cas_url,
3798          $no_response=false,
3799          $bad_response=false,
3800          $cas_response='',
3801          $err_code=-1,
3802          $err_msg=''
3803      ) {
3804          phpCAS::traceBegin();
3805          $lang = $this->getLangObj();
3806          $this->printHTMLHeader($lang->getAuthenticationFailed());
3807          printf(
3808              $lang->getYouWereNotAuthenticated(), htmlentities($this->getURL()),
3809              isset($_SERVER['SERVER_ADMIN']) ? $_SERVER['SERVER_ADMIN']:''
3810          );
3811          phpCAS::trace('CAS URL: '.$cas_url);
3812          phpCAS::trace('Authentication failure: '.$failure);
3813          if ( $no_response ) {
3814              phpCAS::trace('Reason: no response from the CAS server');
3815          } else {
3816              if ( $bad_response ) {
3817                  phpCAS::trace('Reason: bad response from the CAS server');
3818              } else {
3819                  switch ($this->getServerVersion()) {
3820                  case CAS_VERSION_1_0:
3821                      phpCAS::trace('Reason: CAS error');
3822                      break;
3823                  case CAS_VERSION_2_0:
3824                  case CAS_VERSION_3_0:
3825                      if ( $err_code === -1 ) {
3826                          phpCAS::trace('Reason: no CAS error');
3827                      } else {
3828                          phpCAS::trace(
3829                              'Reason: ['.$err_code.'] CAS error: '.$err_msg
3830                          );
3831                      }
3832                      break;
3833                  }
3834              }
3835              phpCAS::trace('CAS response: '.$cas_response);
3836          }
3837          $this->printHTMLFooter();
3838          phpCAS::traceExit();
3839          throw new CAS_GracefullTerminationException();
3840      }
3841  
3842      // ########################################################################
3843      //  PGTIOU/PGTID and logoutRequest rebroadcasting
3844      // ########################################################################
3845  
3846      /**
3847       * Boolean of whether to rebroadcast pgtIou/pgtId and logoutRequest, and
3848       * array of the nodes.
3849       */
3850      private $_rebroadcast = false;
3851      private $_rebroadcast_nodes = array();
3852  
3853      /**
3854       * Constants used for determining rebroadcast node type.
3855       */
3856      const HOSTNAME = 0;
3857      const IP = 1;
3858  
3859      /**
3860       * Determine the node type from the URL.
3861       *
3862       * @param String $nodeURL The node URL.
3863       *
3864       * @return int hostname
3865       *
3866       */
3867      private function _getNodeType($nodeURL)
3868      {
3869          phpCAS::traceBegin();
3870          if (preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $nodeURL)) {
3871              phpCAS::traceEnd(self::IP);
3872              return self::IP;
3873          } else {
3874              phpCAS::traceEnd(self::HOSTNAME);
3875              return self::HOSTNAME;
3876          }
3877      }
3878  
3879      /**
3880       * Store the rebroadcast node for pgtIou/pgtId and logout requests.
3881       *
3882       * @param string $rebroadcastNodeUrl The rebroadcast node URL.
3883       *
3884       * @return void
3885       */
3886      public function addRebroadcastNode($rebroadcastNodeUrl)
3887      {
3888      	 // Argument validation
3889      	 if ( !(bool)preg_match("/^(http|https):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?(\d+)?\/?/i", $rebroadcastNodeUrl))
3890          	 throw new CAS_TypeMismatchException($rebroadcastNodeUrl, '$rebroadcastNodeUrl', 'url');
3891  
3892          // Store the rebroadcast node and set flag
3893          $this->_rebroadcast = true;
3894          $this->_rebroadcast_nodes[] = $rebroadcastNodeUrl;
3895      }
3896  
3897      /**
3898       * An array to store extra rebroadcast curl options.
3899       */
3900      private $_rebroadcast_headers = array();
3901  
3902      /**
3903       * This method is used to add header parameters when rebroadcasting
3904       * pgtIou/pgtId or logoutRequest.
3905       *
3906       * @param string $header Header to send when rebroadcasting.
3907       *
3908       * @return void
3909       */
3910      public function addRebroadcastHeader($header)
3911      {
3912      	 if (gettype($header) != 'string')
3913          	 throw new CAS_TypeMismatchException($header, '$header', 'string');
3914  
3915          $this->_rebroadcast_headers[] = $header;
3916      }
3917  
3918      /**
3919       * Constants used for determining rebroadcast type (logout or pgtIou/pgtId).
3920       */
3921      const LOGOUT = 0;
3922      const PGTIOU = 1;
3923  
3924      /**
3925       * This method rebroadcasts logout/pgtIou requests. Can be LOGOUT,PGTIOU
3926       *
3927       * @param int $type type of rebroadcasting.
3928       *
3929       * @return void
3930       */
3931      private function _rebroadcast($type)
3932      {
3933          phpCAS::traceBegin();
3934  
3935          $rebroadcast_curl_options = array(
3936          CURLOPT_FAILONERROR => 1,
3937          CURLOPT_FOLLOWLOCATION => 1,
3938          CURLOPT_RETURNTRANSFER => 1,
3939          CURLOPT_CONNECTTIMEOUT => 1,
3940          CURLOPT_TIMEOUT => 4);
3941  
3942          // Try to determine the IP address of the server
3943          if (!empty($_SERVER['SERVER_ADDR'])) {
3944              $ip = $_SERVER['SERVER_ADDR'];
3945          } else if (!empty($_SERVER['LOCAL_ADDR'])) {
3946              // IIS 7
3947              $ip = $_SERVER['LOCAL_ADDR'];
3948          }
3949          // Try to determine the DNS name of the server
3950          if (!empty($ip)) {
3951              $dns = gethostbyaddr($ip);
3952          }
3953          $multiClassName = 'CAS_Request_CurlMultiRequest';
3954          $multiRequest = new $multiClassName();
3955  
3956          for ($i = 0; $i < sizeof($this->_rebroadcast_nodes); $i++) {
3957              if ((($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::HOSTNAME) && !empty($dns) && (stripos($this->_rebroadcast_nodes[$i], $dns) === false))
3958                  || (($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::IP) && !empty($ip) && (stripos($this->_rebroadcast_nodes[$i], $ip) === false))
3959              ) {
3960                  phpCAS::trace(
3961                      'Rebroadcast target URL: '.$this->_rebroadcast_nodes[$i]
3962                      .$_SERVER['REQUEST_URI']
3963                  );
3964                  $className = $this->_requestImplementation;
3965                  $request = new $className();
3966  
3967                  $url = $this->_rebroadcast_nodes[$i].$_SERVER['REQUEST_URI'];
3968                  $request->setUrl($url);
3969  
3970                  if (count($this->_rebroadcast_headers)) {
3971                      $request->addHeaders($this->_rebroadcast_headers);
3972                  }
3973  
3974                  $request->makePost();
3975                  if ($type == self::LOGOUT) {
3976                      // Logout request
3977                      $request->setPostBody(
3978                          'rebroadcast=false&logoutRequest='.$_POST['logoutRequest']
3979                      );
3980                  } else if ($type == self::PGTIOU) {
3981                      // pgtIou/pgtId rebroadcast
3982                      $request->setPostBody('rebroadcast=false');
3983                  }
3984  
3985                  $request->setCurlOptions($rebroadcast_curl_options);
3986  
3987                  $multiRequest->addRequest($request);
3988              } else {
3989                  phpCAS::trace(
3990                      'Rebroadcast not sent to self: '
3991                      .$this->_rebroadcast_nodes[$i].' == '.(!empty($ip)?$ip:'')
3992                      .'/'.(!empty($dns)?$dns:'')
3993                  );
3994              }
3995          }
3996          // We need at least 1 request
3997          if ($multiRequest->getNumRequests() > 0) {
3998              $multiRequest->send();
3999          }
4000          phpCAS::traceEnd();
4001      }
4002  
4003      /** @} */
4004  }