Search moodle.org's
Developer Documentation

See Release Notes

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