+<?php
+
+/**
+ * Licensed to Jasig under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for
+ * additional information regarding copyright ownership.
+ *
+ * Jasig licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * PHP Version 5
+ *
+ * @file CAS/Client.php
+ * @category Authentication
+ * @package PhpCAS
+ * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr>
+ * @author Olivier Berger <olivier.berger@it-sudparis.eu>
+ * @author Brett Bieber <brett.bieber@gmail.com>
+ * @author Joachim Fritschi <jfritschi@freenet.de>
+ * @author Adam Franco <afranco@middlebury.edu>
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
+ * @link https://wiki.jasig.org/display/CASC/phpCAS
+ */
+
+/**
+ * The CAS_Client class is a client interface that provides CAS authentication
+ * to PHP applications.
+ *
+ * @class CAS_Client
+ * @category Authentication
+ * @package PhpCAS
+ * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr>
+ * @author Olivier Berger <olivier.berger@it-sudparis.eu>
+ * @author Brett Bieber <brett.bieber@gmail.com>
+ * @author Joachim Fritschi <jfritschi@freenet.de>
+ * @author Adam Franco <afranco@middlebury.edu>
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
+ * @link https://wiki.jasig.org/display/CASC/phpCAS
+ *
+ */
+
+class CAS_Client
+{
+
+ // ########################################################################
+ // HTML OUTPUT
+ // ########################################################################
+ /**
+ * @addtogroup internalOutput
+ * @{
+ */
+
+ /**
+ * This method filters a string by replacing special tokens by appropriate values
+ * and prints it. The corresponding tokens are taken into account:
+ * - __CAS_VERSION__
+ * - __PHPCAS_VERSION__
+ * - __SERVER_BASE_URL__
+ *
+ * Used by CAS_Client::PrintHTMLHeader() and CAS_Client::printHTMLFooter().
+ *
+ * @param string $str the string to filter and output
+ *
+ * @return void
+ */
+ private function _htmlFilterOutput($str)
+ {
+ $str = str_replace('__CAS_VERSION__', $this->getServerVersion(), $str);
+ $str = str_replace('__PHPCAS_VERSION__', phpCAS::getVersion(), $str);
+ $str = str_replace('__SERVER_BASE_URL__', $this->_getServerBaseURL(), $str);
+ echo $str;
+ }
+
+ /**
+ * A string used to print the header of HTML pages. Written by
+ * CAS_Client::setHTMLHeader(), read by CAS_Client::printHTMLHeader().
+ *
+ * @hideinitializer
+ * @see CAS_Client::setHTMLHeader, CAS_Client::printHTMLHeader()
+ */
+ private $_output_header = '';
+
+ /**
+ * This method prints the header of the HTML output (after filtering). If
+ * CAS_Client::setHTMLHeader() was not used, a default header is output.
+ *
+ * @param string $title the title of the page
+ *
+ * @return void
+ * @see _htmlFilterOutput()
+ */
+ public function printHTMLHeader($title)
+ {
+ $this->_htmlFilterOutput(
+ str_replace(
+ '__TITLE__', $title,
+ (empty($this->_output_header)
+ ? '<html><head><title>__TITLE__</title></head><body><h1>__TITLE__</h1>'
+ : $this->_output_header)
+ )
+ );
+ }
+
+ /**
+ * A string used to print the footer of HTML pages. Written by
+ * CAS_Client::setHTMLFooter(), read by printHTMLFooter().
+ *
+ * @hideinitializer
+ * @see CAS_Client::setHTMLFooter, CAS_Client::printHTMLFooter()
+ */
+ private $_output_footer = '';
+
+ /**
+ * This method prints the footer of the HTML output (after filtering). If
+ * CAS_Client::setHTMLFooter() was not used, a default footer is output.
+ *
+ * @return void
+ * @see _htmlFilterOutput()
+ */
+ public function printHTMLFooter()
+ {
+ $lang = $this->getLangObj();
+ $this->_htmlFilterOutput(
+ empty($this->_output_footer)?
+ ('<hr><address>phpCAS __PHPCAS_VERSION__ '
+ .$lang->getUsingServer()
+ .' <a href="__SERVER_BASE_URL__">__SERVER_BASE_URL__</a> (CAS __CAS_VERSION__)</a></address></body></html>')
+ :$this->_output_footer
+ );
+ }
+
+ /**
+ * This method set the HTML header used for all outputs.
+ *
+ * @param string $header the HTML header.
+ *
+ * @return void
+ */
+ public function setHTMLHeader($header)
+ {
+ $this->_output_header = $header;
+ }
+
+ /**
+ * This method set the HTML footer used for all outputs.
+ *
+ * @param string $footer the HTML footer.
+ *
+ * @return void
+ */
+ public function setHTMLFooter($footer)
+ {
+ $this->_output_footer = $footer;
+ }
+
+
+ /** @} */
+
+
+ // ########################################################################
+ // INTERNATIONALIZATION
+ // ########################################################################
+ /**
+ * @addtogroup internalLang
+ * @{
+ */
+ /**
+ * A string corresponding to the language used by phpCAS. Written by
+ * CAS_Client::setLang(), read by CAS_Client::getLang().
+
+ * @note debugging information is always in english (debug purposes only).
+ */
+ private $_lang = PHPCAS_LANG_DEFAULT;
+
+ /**
+ * This method is used to set the language used by phpCAS.
+ *
+ * @param string $lang representing the language.
+ *
+ * @return void
+ */
+ public function setLang($lang)
+ {
+ phpCAS::traceBegin();
+ $obj = new $lang();
+ if (!($obj instanceof CAS_Languages_LanguageInterface)) {
+ throw new CAS_InvalidArgumentException('$className must implement the CAS_Languages_LanguageInterface');
+ }
+ $this->_lang = $lang;
+ phpCAS::traceEnd();
+ }
+ /**
+ * Create the language
+ *
+ * @return CAS_Languages_LanguageInterface object implementing the class
+ */
+ public function getLangObj()
+ {
+ $classname = $this->_lang;
+ return new $classname();
+ }
+
+ /** @} */
+ // ########################################################################
+ // CAS SERVER CONFIG
+ // ########################################################################
+ /**
+ * @addtogroup internalConfig
+ * @{
+ */
+
+ /**
+ * a record to store information about the CAS server.
+ * - $_server['version']: the version of the CAS server
+ * - $_server['hostname']: the hostname of the CAS server
+ * - $_server['port']: the port the CAS server is running on
+ * - $_server['uri']: the base URI the CAS server is responding on
+ * - $_server['base_url']: the base URL of the CAS server
+ * - $_server['login_url']: the login URL of the CAS server
+ * - $_server['service_validate_url']: the service validating URL of the
+ * CAS server
+ * - $_server['proxy_url']: the proxy URL of the CAS server
+ * - $_server['proxy_validate_url']: the proxy validating URL of the CAS server
+ * - $_server['logout_url']: the logout URL of the CAS server
+ *
+ * $_server['version'], $_server['hostname'], $_server['port'] and
+ * $_server['uri'] are written by CAS_Client::CAS_Client(), read by
+ * CAS_Client::getServerVersion(), CAS_Client::_getServerHostname(),
+ * CAS_Client::_getServerPort() and CAS_Client::_getServerURI().
+ *
+ * The other fields are written and read by CAS_Client::_getServerBaseURL(),
+ * CAS_Client::getServerLoginURL(), CAS_Client::getServerServiceValidateURL(),
+ * CAS_Client::getServerProxyValidateURL() and CAS_Client::getServerLogoutURL().
+ *
+ * @hideinitializer
+ */
+ private $_server = array(
+ 'version' => -1,
+ 'hostname' => 'none',
+ 'port' => -1,
+ 'uri' => 'none');
+
+ /**
+ * This method is used to retrieve the version of the CAS server.
+ *
+ * @return string the version of the CAS server.
+ */
+ public function getServerVersion()
+ {
+ return $this->_server['version'];
+ }
+
+ /**
+ * This method is used to retrieve the hostname of the CAS server.
+ *
+ * @return string the hostname of the CAS server.
+ */
+ private function _getServerHostname()
+ {
+ return $this->_server['hostname'];
+ }
+
+ /**
+ * This method is used to retrieve the port of the CAS server.
+ *
+ * @return string the port of the CAS server.
+ */
+ private function _getServerPort()
+ {
+ return $this->_server['port'];
+ }
+
+ /**
+ * This method is used to retrieve the URI of the CAS server.
+ *
+ * @return string a URI.
+ */
+ private function _getServerURI()
+ {
+ return $this->_server['uri'];
+ }
+
+ /**
+ * This method is used to retrieve the base URL of the CAS server.
+ *
+ * @return string a URL.
+ */
+ private function _getServerBaseURL()
+ {
+ // the URL is build only when needed
+ if ( empty($this->_server['base_url']) ) {
+ $this->_server['base_url'] = 'https://' . $this->_getServerHostname();
+ if ($this->_getServerPort()!=443) {
+ $this->_server['base_url'] .= ':'
+ .$this->_getServerPort();
+ }
+ $this->_server['base_url'] .= $this->_getServerURI();
+ }
+ return $this->_server['base_url'];
+ }
+
+ /**
+ * This method is used to retrieve the login URL of the CAS server.
+ *
+ * @param bool $gateway true to check authentication, false to force it
+ * @param bool $renew true to force the authentication with the CAS server
+ *
+ * @return a URL.
+ * @note It is recommended that CAS implementations ignore the "gateway"
+ * parameter if "renew" is set
+ */
+ public function getServerLoginURL($gateway=false,$renew=false)
+ {
+ phpCAS::traceBegin();
+ // the URL is build only when needed
+ if ( empty($this->_server['login_url']) ) {
+ $this->_server['login_url'] = $this->_getServerBaseURL();
+ $this->_server['login_url'] .= 'login?service=';
+ $this->_server['login_url'] .= urlencode($this->getURL());
+ }
+ $url = $this->_server['login_url'];
+ if ($renew) {
+ // It is recommended that when the "renew" parameter is set, its
+ // value be "true"
+ $url = $this->_buildQueryUrl($url, 'renew=true');
+ } elseif ($gateway) {
+ // It is recommended that when the "gateway" parameter is set, its
+ // value be "true"
+ $url = $this->_buildQueryUrl($url, 'gateway=true');
+ }
+ phpCAS::traceEnd($url);
+ return $url;
+ }
+
+ /**
+ * This method sets the login URL of the CAS server.
+ *
+ * @param string $url the login URL
+ *
+ * @return string login url
+ */
+ public function setServerLoginURL($url)
+ {
+ return $this->_server['login_url'] = $url;
+ }
+
+
+ /**
+ * This method sets the serviceValidate URL of the CAS server.
+ *
+ * @param string $url the serviceValidate URL
+ *
+ * @return string serviceValidate URL
+ */
+ public function setServerServiceValidateURL($url)
+ {
+ return $this->_server['service_validate_url'] = $url;
+ }
+
+
+ /**
+ * This method sets the proxyValidate URL of the CAS server.
+ *
+ * @param string $url the proxyValidate URL
+ *
+ * @return string proxyValidate URL
+ */
+ public function setServerProxyValidateURL($url)
+ {
+ return $this->_server['proxy_validate_url'] = $url;
+ }
+
+
+ /**
+ * This method sets the samlValidate URL of the CAS server.
+ *
+ * @param string $url the samlValidate URL
+ *
+ * @return string samlValidate URL
+ */
+ public function setServerSamlValidateURL($url)
+ {
+ return $this->_server['saml_validate_url'] = $url;
+ }
+
+
+ /**
+ * This method is used to retrieve the service validating URL of the CAS server.
+ *
+ * @return string serviceValidate URL.
+ */
+ public function getServerServiceValidateURL()
+ {
+ phpCAS::traceBegin();
+ // the URL is build only when needed
+ if ( empty($this->_server['service_validate_url']) ) {
+ switch ($this->getServerVersion()) {
+ case CAS_VERSION_1_0:
+ $this->_server['service_validate_url'] = $this->_getServerBaseURL()
+ .'validate';
+ break;
+ case CAS_VERSION_2_0:
+ $this->_server['service_validate_url'] = $this->_getServerBaseURL()
+ .'serviceValidate';
+ break;
+ }
+ }
+ $url = $this->_buildQueryUrl($this->_server['service_validate_url'], 'service='.urlencode($this->getURL()));
+ phpCAS::traceEnd($url);
+ return $url;
+ }
+ /**
+ * This method is used to retrieve the SAML validating URL of the CAS server.
+ *
+ * @return string samlValidate URL.
+ */
+ public function getServerSamlValidateURL()
+ {
+ phpCAS::traceBegin();
+ // the URL is build only when needed
+ if ( empty($this->_server['saml_validate_url']) ) {
+ switch ($this->getServerVersion()) {
+ case SAML_VERSION_1_1:
+ $this->_server['saml_validate_url'] = $this->_getServerBaseURL().'samlValidate';
+ break;
+ }
+ }
+
+ $url = $this->_buildQueryUrl($this->_server['saml_validate_url'], 'TARGET='.urlencode($this->getURL()));
+ phpCAS::traceEnd($url);
+ return $url;
+ }
+
+ /**
+ * This method is used to retrieve the proxy validating URL of the CAS server.
+ *
+ * @return string proxyValidate URL.
+ */
+ public function getServerProxyValidateURL()
+ {
+ phpCAS::traceBegin();
+ // the URL is build only when needed
+ if ( empty($this->_server['proxy_validate_url']) ) {
+ switch ($this->getServerVersion()) {
+ case CAS_VERSION_1_0:
+ $this->_server['proxy_validate_url'] = '';
+ break;
+ case CAS_VERSION_2_0:
+ $this->_server['proxy_validate_url'] = $this->_getServerBaseURL().'proxyValidate';
+ break;
+ }
+ }
+ $url = $this->_buildQueryUrl($this->_server['proxy_validate_url'], 'service='.urlencode($this->getURL()));
+ phpCAS::traceEnd($url);
+ return $url;
+ }
+
+
+ /**
+ * This method is used to retrieve the proxy URL of the CAS server.
+ *
+ * @return string proxy URL.
+ */
+ public function getServerProxyURL()
+ {
+ // the URL is build only when needed
+ if ( empty($this->_server['proxy_url']) ) {
+ switch ($this->getServerVersion()) {
+ case CAS_VERSION_1_0:
+ $this->_server['proxy_url'] = '';
+ break;
+ case CAS_VERSION_2_0:
+ $this->_server['proxy_url'] = $this->_getServerBaseURL().'proxy';
+ break;
+ }
+ }
+ return $this->_server['proxy_url'];
+ }
+
+ /**
+ * This method is used to retrieve the logout URL of the CAS server.
+ *
+ * @return string logout URL.
+ */
+ public function getServerLogoutURL()
+ {
+ // the URL is build only when needed
+ if ( empty($this->_server['logout_url']) ) {
+ $this->_server['logout_url'] = $this->_getServerBaseURL().'logout';
+ }
+ return $this->_server['logout_url'];
+ }
+
+ /**
+ * This method sets the logout URL of the CAS server.
+ *
+ * @param string $url the logout URL
+ *
+ * @return string logout url
+ */
+ public function setServerLogoutURL($url)
+ {
+ return $this->_server['logout_url'] = $url;
+ }
+
+ /**
+ * An array to store extra curl options.
+ */
+ private $_curl_options = array();
+
+ /**
+ * This method is used to set additional user curl options.
+ *
+ * @param string $key name of the curl option
+ * @param string $value value of the curl option
+ *
+ * @return void
+ */
+ public function setExtraCurlOption($key, $value)
+ {
+ $this->_curl_options[$key] = $value;
+ }
+
+ /** @} */
+
+ // ########################################################################
+ // Change the internal behaviour of phpcas
+ // ########################################################################
+
+ /**
+ * @addtogroup internalBehave
+ * @{
+ */
+
+ /**
+ * The class to instantiate for making web requests in readUrl().
+ * The class specified must implement the CAS_Request_RequestInterface.
+ * By default CAS_Request_CurlRequest is used, but this may be overridden to
+ * supply alternate request mechanisms for testing.
+ */
+ private $_requestImplementation = 'CAS_Request_CurlRequest';
+
+ /**
+ * Override the default implementation used to make web requests in readUrl().
+ * This class must implement the CAS_Request_RequestInterface.
+ *
+ * @param string $className name of the RequestImplementation class
+ *
+ * @return void
+ */
+ public function setRequestImplementation ($className)
+ {
+ $obj = new $className;
+ if (!($obj instanceof CAS_Request_RequestInterface)) {
+ throw new CAS_InvalidArgumentException('$className must implement the CAS_Request_RequestInterface');
+ }
+ $this->_requestImplementation = $className;
+ }
+
+ /**
+ * @var boolean $_clearTicketsFromUrl; If true, phpCAS will clear session
+ * tickets from the URL after a successful authentication.
+ */
+ private $_clearTicketsFromUrl = true;
+
+ /**
+ * Configure the client to not send redirect headers and call exit() on
+ * authentication success. The normal redirect is used to remove the service
+ * ticket from the client's URL, but for running unit tests we need to
+ * continue without exiting.
+ *
+ * Needed for testing authentication
+ *
+ * @return void
+ */
+ public function setNoClearTicketsFromUrl ()
+ {
+ $this->_clearTicketsFromUrl = false;
+ }
+
+ /**
+ * @var callback $_postAuthenticateCallbackFunction;
+ */
+ private $_postAuthenticateCallbackFunction = null;
+
+ /**
+ * @var array $_postAuthenticateCallbackArgs;
+ */
+ private $_postAuthenticateCallbackArgs = array();
+
+ /**
+ * Set a callback function to be run when a user authenticates.
+ *
+ * The callback function will be passed a $logoutTicket as its first parameter,
+ * followed by any $additionalArgs you pass. The $logoutTicket parameter is an
+ * opaque string that can be used to map a session-id to the logout request
+ * in order to support single-signout in applications that manage their own
+ * sessions (rather than letting phpCAS start the session).
+ *
+ * phpCAS::forceAuthentication() will always exit and forward client unless
+ * they are already authenticated. To perform an action at the moment the user
+ * logs in (such as registering an account, performing logging, etc), register
+ * a callback function here.
+ *
+ * @param string $function callback function to call
+ * @param array $additionalArgs optional array of arguments
+ *
+ * @return void
+ */
+ public function setPostAuthenticateCallback ($function, array $additionalArgs = array())
+ {
+ $this->_postAuthenticateCallbackFunction = $function;
+ $this->_postAuthenticateCallbackArgs = $additionalArgs;
+ }
+
+ /**
+ * @var callback $_signoutCallbackFunction;
+ */
+ private $_signoutCallbackFunction = null;
+
+ /**
+ * @var array $_signoutCallbackArgs;
+ */
+ private $_signoutCallbackArgs = array();
+
+ /**
+ * Set a callback function to be run when a single-signout request is received.
+ *
+ * The callback function will be passed a $logoutTicket as its first parameter,
+ * followed by any $additionalArgs you pass. The $logoutTicket parameter is an
+ * opaque string that can be used to map a session-id to the logout request in
+ * order to support single-signout in applications that manage their own sessions
+ * (rather than letting phpCAS start and destroy the session).
+ *
+ * @param string $function callback function to call
+ * @param array $additionalArgs optional array of arguments
+ *
+ * @return void
+ */
+ public function setSingleSignoutCallback ($function, array $additionalArgs = array())
+ {
+ $this->_signoutCallbackFunction = $function;
+ $this->_signoutCallbackArgs = $additionalArgs;
+ }
+
+ // ########################################################################
+ // Methods for supplying code-flow feedback to integrators.
+ // ########################################################################
+
+ /**
+ * Mark the caller of authentication. This will help client integraters determine
+ * problems with their code flow if they call a function such as getUser() before
+ * authentication has occurred.
+ *
+ * @param bool $auth True if authentication was successful, false otherwise.
+ *
+ * @return null
+ */
+ public function markAuthenticationCall ($auth)
+ {
+ // store where the authentication has been checked and the result
+ $dbg = debug_backtrace();
+ $this->_authentication_caller = array (
+ 'file' => $dbg[1]['file'],
+ 'line' => $dbg[1]['line'],
+ 'method' => $dbg[1]['class'] . '::' . $dbg[1]['function'],
+ 'result' => (boolean)$auth
+ );
+ }
+ private $_authentication_caller;
+
+ /**
+ * Answer true if authentication has been checked.
+ *
+ * @return bool
+ */
+ public function wasAuthenticationCalled ()
+ {
+ return !empty($this->_authentication_caller);
+ }
+
+ /**
+ * Answer the result of the authentication call.
+ *
+ * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
+ * and markAuthenticationCall() didn't happen.
+ *
+ * @return bool
+ */
+ public function wasAuthenticationCallSuccessful ()
+ {
+ if (empty($this->_authentication_caller)) {
+ throw new CAS_OutOfSequenceException('markAuthenticationCall() hasn\'t happened.');
+ }
+ return $this->_authentication_caller['result'];
+ }
+
+ /**
+ * Answer information about the authentication caller.
+ *
+ * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
+ * and markAuthenticationCall() didn't happen.
+ *
+ * @return array Keys are 'file', 'line', and 'method'
+ */
+ public function getAuthenticationCallerFile ()
+ {
+ if (empty($this->_authentication_caller)) {
+ throw new CAS_OutOfSequenceException('markAuthenticationCall() hasn\'t happened.');
+ }
+ return $this->_authentication_caller['file'];
+ }
+
+ /**
+ * Answer information about the authentication caller.
+ *
+ * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
+ * and markAuthenticationCall() didn't happen.
+ *
+ * @return array Keys are 'file', 'line', and 'method'
+ */
+ public function getAuthenticationCallerLine ()
+ {
+ if (empty($this->_authentication_caller)) {
+ throw new CAS_OutOfSequenceException('markAuthenticationCall() hasn\'t happened.');
+ }
+ return $this->_authentication_caller['line'];
+ }
+
+ /**
+ * Answer information about the authentication caller.
+ *
+ * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
+ * and markAuthenticationCall() didn't happen.
+ *
+ * @return array Keys are 'file', 'line', and 'method'
+ */
+ public function getAuthenticationCallerMethod ()
+ {
+ if (empty($this->_authentication_caller)) {
+ throw new CAS_OutOfSequenceException('markAuthenticationCall() hasn\'t happened.');
+ }
+ return $this->_authentication_caller['method'];
+ }
+
+ /** @} */
+
+ // ########################################################################
+ // CONSTRUCTOR
+ // ########################################################################
+ /**
+ * @addtogroup internalConfig
+ * @{
+ */
+
+ /**
+ * CAS_Client constructor.
+ *
+ * @param string $server_version the version of the CAS server
+ * @param bool $proxy true if the CAS client is a CAS proxy
+ * @param string $server_hostname the hostname of the CAS server
+ * @param int $server_port the port the CAS server is running on
+ * @param string $server_uri the URI the CAS server is responding on
+ * @param bool $changeSessionID Allow phpCAS to change the session_id (Single Sign Out/handleLogoutRequests is based on that change)
+ *
+ * @return a newly created CAS_Client object
+ */
+ public function __construct(
+ $server_version,
+ $proxy,
+ $server_hostname,
+ $server_port,
+ $server_uri,
+ $changeSessionID = true
+ ) {
+
+ phpCAS::traceBegin();
+
+ $this->_setChangeSessionID($changeSessionID); // true : allow to change the session_id(), false session_id won't be change and logout won't be handle because of that
+
+ // skip Session Handling for logout requests and if don't want it'
+ if (session_id()=="" && !$this->_isLogoutRequest()) {
+ phpCAS :: trace("Starting a new session");
+ session_start();
+ }
+
+ // are we in proxy mode ?
+ $this->_proxy = $proxy;
+
+ // Make cookie handling available.
+ if ($this->isProxy()) {
+ if (!isset($_SESSION['phpCAS'])) {
+ $_SESSION['phpCAS'] = array();
+ }
+ if (!isset($_SESSION['phpCAS']['service_cookies'])) {
+ $_SESSION['phpCAS']['service_cookies'] = array();
+ }
+ $this->_serviceCookieJar = new CAS_CookieJar($_SESSION['phpCAS']['service_cookies']);
+ }
+
+ //check version
+ switch ($server_version) {
+ case CAS_VERSION_1_0:
+ if ( $this->isProxy() ) {
+ phpCAS::error(
+ 'CAS proxies are not supported in CAS '.$server_version
+ );
+ }
+ break;
+ case CAS_VERSION_2_0:
+ break;
+ case SAML_VERSION_1_1:
+ break;
+ default:
+ phpCAS::error(
+ 'this version of CAS (`'.$server_version
+ .'\') is not supported by phpCAS '.phpCAS::getVersion()
+ );
+ }
+ $this->_server['version'] = $server_version;
+
+ // check hostname
+ if ( empty($server_hostname)
+ || !preg_match('/[\.\d\-abcdefghijklmnopqrstuvwxyz]*/', $server_hostname)
+ ) {
+ phpCAS::error('bad CAS server hostname (`'.$server_hostname.'\')');
+ }
+ $this->_server['hostname'] = $server_hostname;
+
+ // check port
+ if ( $server_port == 0
+ || !is_int($server_port)
+ ) {
+ phpCAS::error('bad CAS server port (`'.$server_hostname.'\')');
+ }
+ $this->_server['port'] = $server_port;
+
+ // check URI
+ if ( !preg_match('/[\.\d\-_abcdefghijklmnopqrstuvwxyz\/]*/', $server_uri) ) {
+ phpCAS::error('bad CAS server URI (`'.$server_uri.'\')');
+ }
+ // add leading and trailing `/' and remove doubles
+ $server_uri = preg_replace('/\/\//', '/', '/'.$server_uri.'/');
+ $this->_server['uri'] = $server_uri;
+
+ // set to callback mode if PgtIou and PgtId CGI GET parameters are provided
+ if ( $this->isProxy() ) {
+ $this->_setCallbackMode(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId']));
+ }
+
+ if ( $this->_isCallbackMode() ) {
+ //callback mode: check that phpCAS is secured
+ if ( !$this->_isHttps() ) {
+ phpCAS::error('CAS proxies must be secured to use phpCAS; PGT\'s will not be received from the CAS server');
+ }
+ } else {
+ //normal mode: get ticket and remove it from CGI parameters for
+ // developers
+ $ticket = (isset($_GET['ticket']) ? $_GET['ticket'] : null);
+ if (preg_match('/^[SP]T-/', $ticket) ) {
+ phpCAS::trace('Ticket \''.$ticket.'\' found');
+ $this->setTicket($ticket);
+ unset($_GET['ticket']);
+ } else if ( !empty($ticket) ) {
+ //ill-formed ticket, halt
+ phpCAS::error('ill-formed ticket found in the URL (ticket=`'.htmlentities($ticket).'\')');
+ }
+
+ }
+ phpCAS::traceEnd();
+ }
+
+ /** @} */
+
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ // XX XX
+ // XX Session Handling XX
+ // XX XX
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+ /**
+ * @addtogroup internalConfig
+ * @{
+ */
+
+
+ /**
+ * A variable to whether phpcas will use its own session handling. Default = true
+ * @hideinitializer
+ */
+ private $_change_session_id = true;
+
+ /**
+ * Set a parameter whether to allow phpCas to change session_id
+ *
+ * @param bool $allowed allow phpCas to change session_id
+ *
+ * @return void
+ */
+ private function _setChangeSessionID($allowed)
+ {
+ $this->_change_session_id = $allowed;
+ }
+
+ /**
+ * Get whether phpCas is allowed to change session_id
+ *
+ * @return bool
+ */
+ public function getChangeSessionID()
+ {
+ return $this->_change_session_id;
+ }
+
+ /** @} */
+
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ // XX XX
+ // XX AUTHENTICATION XX
+ // XX XX
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+ /**
+ * @addtogroup internalAuthentication
+ * @{
+ */
+
+ /**
+ * The Authenticated user. Written by CAS_Client::_setUser(), read by
+ * CAS_Client::getUser().
+ *
+ * @hideinitializer
+ */
+ private $_user = '';
+
+ /**
+ * This method sets the CAS user's login name.
+ *
+ * @param string $user the login name of the authenticated user.
+ *
+ * @return void
+ */
+ private function _setUser($user)
+ {
+ $this->_user = $user;
+ }
+
+ /**
+ * This method returns the CAS user's login name.
+ *
+ * @return string the login name of the authenticated user
+ *
+ * @warning should be called only after CAS_Client::forceAuthentication() or
+ * CAS_Client::isAuthenticated(), otherwise halt with an error.
+ */
+ public function getUser()
+ {
+ if ( empty($this->_user) ) {
+ phpCAS::error(
+ 'this method should be used only after '.__CLASS__
+ .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()'
+ );
+ }
+ return $this->_user;
+ }
+
+ /**
+ * The Authenticated users attributes. Written by
+ * CAS_Client::setAttributes(), read by CAS_Client::getAttributes().
+ * @attention client applications should use phpCAS::getAttributes().
+ *
+ * @hideinitializer
+ */
+ private $_attributes = array();
+
+ /**
+ * Set an array of attributes
+ *
+ * @param array $attributes a key value array of attributes
+ *
+ * @return void
+ */
+ public function setAttributes($attributes)
+ {
+ $this->_attributes = $attributes;
+ }
+
+ /**
+ * Get an key values arry of attributes
+ *
+ * @return arry of attributes
+ */
+ public function getAttributes()
+ {
+ if ( empty($this->_user) ) {
+ // if no user is set, there shouldn't be any attributes also...
+ phpCAS::error(
+ 'this method should be used only after '.__CLASS__
+ .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()'
+ );
+ }
+ return $this->_attributes;
+ }
+
+ /**
+ * Check whether attributes are available
+ *
+ * @return bool attributes available
+ */
+ public function hasAttributes()
+ {
+ return !empty($this->_attributes);
+ }
+ /**
+ * Check whether a specific attribute with a name is available
+ *
+ * @param string $key name of attribute
+ *
+ * @return bool is attribute available
+ */
+ public function hasAttribute($key)
+ {
+ return (is_array($this->_attributes)
+ && array_key_exists($key, $this->_attributes));
+ }
+
+ /**
+ * Get a specific attribute by name
+ *
+ * @param string $key name of attribute
+ *
+ * @return string attribute values
+ */
+ public function getAttribute($key)
+ {
+ if ($this->hasAttribute($key)) {
+ return $this->_attributes[$key];
+ }
+ }
+
+ /**
+ * This method is called to renew the authentication of the user
+ * If the user is authenticated, renew the connection
+ * If not, redirect to CAS
+ *
+ * @return void
+ */
+ public function renewAuthentication()
+ {
+ phpCAS::traceBegin();
+ // Either way, the user is authenticated by CAS
+ if (isset( $_SESSION['phpCAS']['auth_checked'])) {
+ unset($_SESSION['phpCAS']['auth_checked']);
+ }
+ if ( $this->isAuthenticated() ) {
+ phpCAS::trace('user already authenticated; renew');
+ $this->redirectToCas(false, true);
+ } else {
+ $this->redirectToCas();
+ }
+ phpCAS::traceEnd();
+ }
+
+ /**
+ * This method is called to be sure that the user is authenticated. When not
+ * authenticated, halt by redirecting to the CAS server; otherwise return true.
+ *
+ * @return true when the user is authenticated; otherwise halt.
+ */
+ public function forceAuthentication()
+ {
+ phpCAS::traceBegin();
+
+ if ( $this->isAuthenticated() ) {
+ // the user is authenticated, nothing to be done.
+ phpCAS::trace('no need to authenticate');
+ $res = true;
+ } else {
+ // the user is not authenticated, redirect to the CAS server
+ if (isset($_SESSION['phpCAS']['auth_checked'])) {
+ unset($_SESSION['phpCAS']['auth_checked']);
+ }
+ $this->redirectToCas(false/* no gateway */);
+ // never reached
+ $res = false;
+ }
+ phpCAS::traceEnd($res);
+ return $res;
+ }
+
+ /**
+ * An integer that gives the number of times authentication will be cached
+ * before rechecked.
+ *
+ * @hideinitializer
+ */
+ private $_cache_times_for_auth_recheck = 0;
+
+ /**
+ * Set the number of times authentication will be cached before rechecked.
+ *
+ * @param int $n number of times to wait for a recheck
+ *
+ * @return void
+ */
+ public function setCacheTimesForAuthRecheck($n)
+ {
+ $this->_cache_times_for_auth_recheck = $n;
+ }
+
+ /**
+ * This method is called to check whether the user is authenticated or not.
+ *
+ * @return true when the user is authenticated, false when a previous
+ * gateway login failed or the function will not return if the user is
+ * redirected to the cas server for a gateway login attempt
+ */
+ public function checkAuthentication()
+ {
+ phpCAS::traceBegin();
+ $res = false;
+ if ( $this->isAuthenticated() ) {
+ phpCAS::trace('user is authenticated');
+ /* The 'auth_checked' variable is removed just in case it's set. */
+ unset($_SESSION['phpCAS']['auth_checked']);
+ $res = true;
+ } else if (isset($_SESSION['phpCAS']['auth_checked'])) {
+ // the previous request has redirected the client to the CAS server
+ // with gateway=true
+ unset($_SESSION['phpCAS']['auth_checked']);
+ $res = false;
+ } else {
+ // avoid a check against CAS on every request
+ if (!isset($_SESSION['phpCAS']['unauth_count'])) {
+ $_SESSION['phpCAS']['unauth_count'] = -2; // uninitialized
+ }
+
+ if (($_SESSION['phpCAS']['unauth_count'] != -2
+ && $this->_cache_times_for_auth_recheck == -1)
+ || ($_SESSION['phpCAS']['unauth_count'] >= 0
+ && $_SESSION['phpCAS']['unauth_count'] < $this->_cache_times_for_auth_recheck)
+ ) {
+ $res = false;
+
+ if ($this->_cache_times_for_auth_recheck != -1) {
+ $_SESSION['phpCAS']['unauth_count']++;
+ phpCAS::trace(
+ 'user is not authenticated (cached for '
+ .$_SESSION['phpCAS']['unauth_count'].' times of '
+ .$this->_cache_times_for_auth_recheck.')'
+ );
+ } else {
+ phpCAS::trace('user is not authenticated (cached for until login pressed)');
+ }
+ } else {
+ $_SESSION['phpCAS']['unauth_count'] = 0;
+ $_SESSION['phpCAS']['auth_checked'] = true;
+ phpCAS::trace('user is not authenticated (cache reset)');
+ $this->redirectToCas(true/* gateway */);
+ // never reached
+ $res = false;
+ }
+ }
+ phpCAS::traceEnd($res);
+ return $res;
+ }
+
+ /**
+ * This method is called to check if the user is authenticated (previously or by
+ * tickets given in the URL).
+ *
+ * @return true when the user is authenticated. Also may redirect to the
+ * same URL without the ticket.
+ */
+ public function isAuthenticated()
+ {
+ phpCAS::traceBegin();
+ $res = false;
+ $validate_url = '';
+ if ( $this->_wasPreviouslyAuthenticated() ) {
+ if ($this->hasTicket()) {
+ // User has a additional ticket but was already authenticated
+ phpCAS::trace('ticket was present and will be discarded, use renewAuthenticate()');
+ if ($this->_clearTicketsFromUrl) {
+ phpCAS::trace("Prepare redirect to : ".$this->getURL());
+ header('Location: '.$this->getURL());
+ flush();
+ phpCAS::traceExit();
+ throw new CAS_GracefullTerminationException();
+ } else {
+ phpCAS::trace('Already authenticated, but skipping ticket clearing since setNoClearTicketsFromUrl() was used.');
+ $res = true;
+ }
+ } else {
+ // the user has already (previously during the session) been
+ // authenticated, nothing to be done.
+ phpCAS::trace('user was already authenticated, no need to look for tickets');
+ $res = true;
+ }
+ } else {
+ if ($this->hasTicket()) {
+ switch ($this->getServerVersion()) {
+ case CAS_VERSION_1_0:
+ // if a Service Ticket was given, validate it
+ phpCAS::trace('CAS 1.0 ticket `'.$this->getTicket().'\' is present');
+ $this->validateCAS10($validate_url, $text_response, $tree_response); // if it fails, it halts
+ phpCAS::trace('CAS 1.0 ticket `'.$this->getTicket().'\' was validated');
+ $_SESSION['phpCAS']['user'] = $this->getUser();
+ $res = true;
+ $logoutTicket = $this->getTicket();
+ break;
+ case CAS_VERSION_2_0:
+ // if a Proxy Ticket was given, validate it
+ phpCAS::trace('CAS 2.0 ticket `'.$this->getTicket().'\' is present');
+ $this->validateCAS20($validate_url, $text_response, $tree_response); // note: if it fails, it halts
+ phpCAS::trace('CAS 2.0 ticket `'.$this->getTicket().'\' was validated');
+ if ( $this->isProxy() ) {
+ $this->_validatePGT($validate_url, $text_response, $tree_response); // idem
+ phpCAS::trace('PGT `'.$this->_getPGT().'\' was validated');
+ $_SESSION['phpCAS']['pgt'] = $this->_getPGT();
+ }
+ $_SESSION['phpCAS']['user'] = $this->getUser();
+ if ($this->hasAttributes()) {
+ $_SESSION['phpCAS']['attributes'] = $this->getAttributes();
+ }
+ $proxies = $this->getProxies();
+ if (!empty($proxies)) {
+ $_SESSION['phpCAS']['proxies'] = $this->getProxies();
+ }
+ $res = true;
+ $logoutTicket = $this->getTicket();
+ break;
+ case SAML_VERSION_1_1:
+ // if we have a SAML ticket, validate it.
+ phpCAS::trace('SAML 1.1 ticket `'.$this->getTicket().'\' is present');
+ $this->validateSA($validate_url, $text_response, $tree_response); // if it fails, it halts
+ phpCAS::trace('SAML 1.1 ticket `'.$this->getTicket().'\' was validated');
+ $_SESSION['phpCAS']['user'] = $this->getUser();
+ $_SESSION['phpCAS']['attributes'] = $this->getAttributes();
+ $res = true;
+ $logoutTicket = $this->getTicket();
+ break;
+ default:
+ phpCAS::trace('Protocoll error');
+ break;
+ }
+ } else {
+ // no ticket given, not authenticated
+ phpCAS::trace('no ticket found');
+ }
+ if ($res) {
+ // Mark the auth-check as complete to allow post-authentication
+ // callbacks to make use of phpCAS::getUser() and similar methods
+ $this->markAuthenticationCall($res);
+
+ // call the post-authenticate callback if registered.
+ if ($this->_postAuthenticateCallbackFunction) {
+ $args = $this->_postAuthenticateCallbackArgs;
+ array_unshift($args, $logoutTicket);
+ call_user_func_array($this->_postAuthenticateCallbackFunction, $args);
+ }
+
+ // if called with a ticket parameter, we need to redirect to the
+ // app without the ticket so that CAS-ification is transparent
+ // to the browser (for later POSTS) most of the checks and
+ // errors should have been made now, so we're safe for redirect
+ // without masking error messages. remove the ticket as a
+ // security precaution to prevent a ticket in the HTTP_REFERRER
+ if ($this->_clearTicketsFromUrl) {
+ phpCAS::trace("Prepare redirect to : ".$this->getURL());
+ header('Location: '.$this->getURL());
+ flush();
+ phpCAS::traceExit();
+ throw new CAS_GracefullTerminationException();
+ }
+ }
+ }
+
+ phpCAS::traceEnd($res);
+ return $res;
+ }
+
+ /**
+ * This method tells if the current session is authenticated.
+ *
+ * @return true if authenticated based soley on $_SESSION variable
+ */
+ public function isSessionAuthenticated ()
+ {
+ return !empty($_SESSION['phpCAS']['user']);
+ }
+
+ /**
+ * This method tells if the user has already been (previously) authenticated
+ * by looking into the session variables.
+ *
+ * @note This function switches to callback mode when needed.
+ *
+ * @return true when the user has already been authenticated; false otherwise.
+ */
+ private function _wasPreviouslyAuthenticated()
+ {
+ phpCAS::traceBegin();
+
+ if ( $this->_isCallbackMode() ) {
+ // Rebroadcast the pgtIou and pgtId to all nodes
+ if ($this->_rebroadcast&&!isset($_POST['rebroadcast'])) {
+ $this->_rebroadcast(self::PGTIOU);
+ }
+ $this->_callback();
+ }
+
+ $auth = false;
+
+ if ( $this->isProxy() ) {
+ // CAS proxy: username and PGT must be present
+ if ( $this->isSessionAuthenticated() && !empty($_SESSION['phpCAS']['pgt']) ) {
+ // authentication already done
+ $this->_setUser($_SESSION['phpCAS']['user']);
+ if (isset($_SESSION['phpCAS']['attributes'])) {
+ $this->setAttributes($_SESSION['phpCAS']['attributes']);
+ }
+ $this->_setPGT($_SESSION['phpCAS']['pgt']);
+ phpCAS::trace('user = `'.$_SESSION['phpCAS']['user'].'\', PGT = `'.$_SESSION['phpCAS']['pgt'].'\'');
+
+ // Include the list of proxies
+ if (isset($_SESSION['phpCAS']['proxies'])) {
+ $this->_setProxies($_SESSION['phpCAS']['proxies']);
+ phpCAS::trace('proxies = "'.implode('", "', $_SESSION['phpCAS']['proxies']).'"');
+ }
+
+ $auth = true;
+ } elseif ( $this->isSessionAuthenticated() && empty($_SESSION['phpCAS']['pgt']) ) {
+ // these two variables should be empty or not empty at the same time
+ phpCAS::trace('username found (`'.$_SESSION['phpCAS']['user'].'\') but PGT is empty');
+ // unset all tickets to enforce authentication
+ unset($_SESSION['phpCAS']);
+ $this->setTicket('');
+ } elseif ( !$this->isSessionAuthenticated() && !empty($_SESSION['phpCAS']['pgt']) ) {
+ // these two variables should be empty or not empty at the same time
+ phpCAS::trace('PGT found (`'.$_SESSION['phpCAS']['pgt'].'\') but username is empty');
+ // unset all tickets to enforce authentication
+ unset($_SESSION['phpCAS']);
+ $this->setTicket('');
+ } else {
+ phpCAS::trace('neither user nor PGT found');
+ }
+ } else {
+ // `simple' CAS client (not a proxy): username must be present
+ if ( $this->isSessionAuthenticated() ) {
+ // authentication already done
+ $this->_setUser($_SESSION['phpCAS']['user']);
+ if (isset($_SESSION['phpCAS']['attributes'])) {
+ $this->setAttributes($_SESSION['phpCAS']['attributes']);
+ }
+ phpCAS::trace('user = `'.$_SESSION['phpCAS']['user'].'\'');
+
+ // Include the list of proxies
+ if (isset($_SESSION['phpCAS']['proxies'])) {
+ $this->_setProxies($_SESSION['phpCAS']['proxies']);
+ phpCAS::trace('proxies = "'.implode('", "', $_SESSION['phpCAS']['proxies']).'"');
+ }
+
+ $auth = true;
+ } else {
+ phpCAS::trace('no user found');
+ }
+ }
+
+ phpCAS::traceEnd($auth);
+ return $auth;
+ }
+
+ /**
+ * This method is used to redirect the client to the CAS server.
+ * It is used by CAS_Client::forceAuthentication() and
+ * CAS_Client::checkAuthentication().
+ *
+ * @param bool $gateway true to check authentication, false to force it
+ * @param bool $renew true to force the authentication with the CAS server
+ *
+ * @return void
+ */
+ public function redirectToCas($gateway=false,$renew=false)
+ {
+ phpCAS::traceBegin();
+ $cas_url = $this->getServerLoginURL($gateway, $renew);
+ if (php_sapi_name() === 'cli') {
+ @header('Location: '.$cas_url);
+ } else {
+ header('Location: '.$cas_url);
+ }
+ phpCAS::trace("Redirect to : ".$cas_url);
+ $lang = $this->getLangObj();
+ $this->printHTMLHeader($lang->getAuthenticationWanted());
+ printf('<p>'. $lang->getShouldHaveBeenRedirected(). '</p>', $cas_url);
+ $this->printHTMLFooter();
+ phpCAS::traceExit();
+ throw new CAS_GracefullTerminationException();
+ }
+
+
+ /**
+ * This method is used to logout from CAS.
+ *
+ * @param array $params an array that contains the optional url and service
+ * parameters that will be passed to the CAS server
+ *
+ * @return void
+ */
+ public function logout($params)
+ {
+ phpCAS::traceBegin();
+ $cas_url = $this->getServerLogoutURL();
+ $paramSeparator = '?';
+ if (isset($params['url'])) {
+ $cas_url = $cas_url . $paramSeparator . "url=" . urlencode($params['url']);
+ $paramSeparator = '&';
+ }
+ if (isset($params['service'])) {
+ $cas_url = $cas_url . $paramSeparator . "service=" . urlencode($params['service']);
+ }
+ header('Location: '.$cas_url);
+ phpCAS::trace("Prepare redirect to : ".$cas_url);
+
+ session_unset();
+ session_destroy();
+ $lang = $this->getLangObj();
+ $this->printHTMLHeader($lang->getLogout());
+ printf('<p>'.$lang->getShouldHaveBeenRedirected(). '</p>', $cas_url);
+ $this->printHTMLFooter();
+ phpCAS::traceExit();
+ throw new CAS_GracefullTerminationException();
+ }
+
+ /**
+ * Check of the current request is a logout request
+ *
+ * @return bool is logout request.
+ */
+ private function _isLogoutRequest()
+ {
+ return !empty($_POST['logoutRequest']);
+ }
+
+ /**
+ * This method handles logout requests.
+ *
+ * @param bool $check_client true to check the client bofore handling
+ * the request, false not to perform any access control. True by default.
+ * @param bool $allowed_clients an array of host names allowed to send
+ * logout requests.
+ *
+ * @return void
+ */
+ public function handleLogoutRequests($check_client=true, $allowed_clients=false)
+ {
+ phpCAS::traceBegin();
+ if (!$this->_isLogoutRequest()) {
+ phpCAS::trace("Not a logout request");
+ phpCAS::traceEnd();
+ return;
+ }
+ if (!$this->getChangeSessionID() && is_null($this->_signoutCallbackFunction)) {
+ phpCAS::trace("phpCAS can't handle logout requests if it is not allowed to change session_id.");
+ }
+ phpCAS::trace("Logout requested");
+ $decoded_logout_rq = urldecode($_POST['logoutRequest']);
+ phpCAS::trace("SAML REQUEST: ".$decoded_logout_rq);
+ $allowed = false;
+ if ($check_client) {
+ if (!$allowed_clients) {
+ $allowed_clients = array( $this->_getServerHostname() );
+ }
+ $client_ip = $_SERVER['REMOTE_ADDR'];
+ $client = gethostbyaddr($client_ip);
+ phpCAS::trace("Client: ".$client."/".$client_ip);
+ foreach ($allowed_clients as $allowed_client) {
+ if (($client == $allowed_client) or ($client_ip == $allowed_client)) {
+ phpCAS::trace("Allowed client '".$allowed_client."' matches, logout request is allowed");
+ $allowed = true;
+ break;
+ } else {
+ phpCAS::trace("Allowed client '".$allowed_client."' does not match");
+ }
+ }
+ } else {
+ phpCAS::trace("No access control set");
+ $allowed = true;
+ }
+ // If Logout command is permitted proceed with the logout
+ if ($allowed) {
+ phpCAS::trace("Logout command allowed");
+ // Rebroadcast the logout request
+ if ($this->_rebroadcast && !isset($_POST['rebroadcast'])) {
+ $this->_rebroadcast(self::LOGOUT);
+ }
+ // Extract the ticket from the SAML Request
+ preg_match("|<samlp:SessionIndex>(.*)</samlp:SessionIndex>|", $decoded_logout_rq, $tick, PREG_OFFSET_CAPTURE, 3);
+ $wrappedSamlSessionIndex = preg_replace('|<samlp:SessionIndex>|', '', $tick[0][0]);
+ $ticket2logout = preg_replace('|</samlp:SessionIndex>|', '', $wrappedSamlSessionIndex);
+ phpCAS::trace("Ticket to logout: ".$ticket2logout);
+
+ // call the post-authenticate callback if registered.
+ if ($this->_signoutCallbackFunction) {
+ $args = $this->_signoutCallbackArgs;
+ array_unshift($args, $ticket2logout);
+ call_user_func_array($this->_signoutCallbackFunction, $args);
+ }
+
+ // If phpCAS is managing the session_id, destroy session thanks to session_id.
+ if ($this->getChangeSessionID()) {
+ $session_id = preg_replace('/[^a-zA-Z0-9\-]/', '', $ticket2logout);
+ phpCAS::trace("Session id: ".$session_id);
+
+ // destroy a possible application session created before phpcas
+ if (session_id() !== "") {
+ session_unset();
+ session_destroy();
+ }
+ // fix session ID
+ session_id($session_id);
+ $_COOKIE[session_name()]=$session_id;
+ $_GET[session_name()]=$session_id;
+
+ // Overwrite session
+ session_start();
+ session_unset();
+ session_destroy();
+ phpCAS::trace("Session ". $session_id . " destroyed");
+ }
+ } else {
+ phpCAS::error("Unauthorized logout request from client '".$client."'");
+ phpCAS::trace("Unauthorized logout request from client '".$client."'");
+ }
+ flush();
+ phpCAS::traceExit();
+ throw new CAS_GracefullTerminationException();
+
+ }
+
+ /** @} */
+
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ // XX XX
+ // XX BASIC CLIENT FEATURES (CAS 1.0) XX
+ // XX XX
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+ // ########################################################################
+ // ST
+ // ########################################################################
+ /**
+ * @addtogroup internalBasic
+ * @{
+ */
+
+ /**
+ * The Ticket provided in the URL of the request if present
+ * (empty otherwise). Written by CAS_Client::CAS_Client(), read by
+ * CAS_Client::getTicket() and CAS_Client::_hasPGT().
+ *
+ * @hideinitializer
+ */
+ private $_ticket = '';
+
+ /**
+ * This method returns the Service Ticket provided in the URL of the request.
+ *
+ * @return string service ticket.
+ */
+ public function getTicket()
+ {
+ return $this->_ticket;
+ }
+
+ /**
+ * This method stores the Service Ticket.
+ *
+ * @param string $st The Service Ticket.
+ *
+ * @return void
+ */
+ public function setTicket($st)
+ {
+ $this->_ticket = $st;
+ }
+
+ /**
+ * This method tells if a Service Ticket was stored.
+ *
+ * @return bool if a Service Ticket has been stored.
+ */
+ public function hasTicket()
+ {
+ return !empty($this->_ticket);
+ }
+
+ /** @} */
+
+ // ########################################################################
+ // ST VALIDATION
+ // ########################################################################
+ /**
+ * @addtogroup internalBasic
+ * @{
+ */
+
+ /**
+ * the certificate of the CAS server CA.
+ *
+ * @hideinitializer
+ */
+ private $_cas_server_ca_cert = null;
+
+
+ /**\r
+ * validate CN of the CAS server certificate\r
+ *\r
+ * @hideinitializer\r
+ */\r
+ private $_cas_server_cn_validate = true;
+
+ /**
+ * Set to true not to validate the CAS server.
+ *
+ * @hideinitializer
+ */
+ private $_no_cas_server_validation = false;
+
+
+ /**
+ * Set the CA certificate of the CAS server.
+ *
+ * @param string $cert the PEM certificate file name of the CA that emited
+ * the cert of the server
+ * @param bool $validate_cn valiate CN of the CAS server certificate
+ *
+ * @return void
+ */
+ public function setCasServerCACert($cert, $validate_cn)
+ {
+ $this->_cas_server_ca_cert = $cert;
+ $this->_cas_server_cn_validate = $validate_cn;
+ }
+
+ /**
+ * Set no SSL validation for the CAS server.
+ *
+ * @return void
+ */
+ public function setNoCasServerValidation()
+ {
+ $this->_no_cas_server_validation = true;
+ }
+
+ /**
+ * This method is used to validate a CAS 1,0 ticket; halt on failure, and
+ * sets $validate_url, $text_reponse and $tree_response on success.
+ *
+ * @param string &$validate_url reference to the the URL of the request to
+ * the CAS server.
+ * @param string &$text_response reference to the response of the CAS
+ * server, as is (XML text).
+ * @param string &$tree_response reference to the response of the CAS
+ * server, as a DOM XML tree.
+ *
+ * @return bool true when successfull and issue a CAS_AuthenticationException
+ * and false on an error
+ */
+ public function validateCAS10(&$validate_url,&$text_response,&$tree_response)
+ {
+ phpCAS::traceBegin();
+ $result = false;
+ // build the URL to validate the ticket
+ $validate_url = $this->getServerServiceValidateURL().'&ticket='.$this->getTicket();
+
+ // open and read the URL
+ if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) {
+ phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')');
+ throw new CAS_AuthenticationException(
+ $this, 'CAS 1.0 ticket not validated', $validate_url,
+ true/*$no_response*/
+ );
+ $result = false;
+ }
+
+ if (preg_match('/^no\n/', $text_response)) {
+ phpCAS::trace('Ticket has not been validated');
+ throw new CAS_AuthenticationException(
+ $this, 'ST not validated', $validate_url, false/*$no_response*/,
+ false/*$bad_response*/, $text_response
+ );
+ $result = false;
+ } else if (!preg_match('/^yes\n/', $text_response)) {
+ phpCAS::trace('ill-formed response');
+ throw new CAS_AuthenticationException(
+ $this, 'Ticket not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/, $text_response
+ );
+ $result = false;
+ }
+ // ticket has been validated, extract the user name
+ $arr = preg_split('/\n/', $text_response);
+ $this->_setUser(trim($arr[1]));
+ $result = true;
+
+ if ($result) {
+ $this->_renameSession($this->getTicket());
+ }
+ // at this step, ticket has been validated and $this->_user has been set,
+ phpCAS::traceEnd(true);
+ return true;
+ }
+
+ /** @} */
+
+
+ // ########################################################################
+ // SAML VALIDATION
+ // ########################################################################
+ /**
+ * @addtogroup internalSAML
+ * @{
+ */
+
+ /**
+ * This method is used to validate a SAML TICKET; halt on failure, and sets
+ * $validate_url, $text_reponse and $tree_response on success. These
+ * parameters are used later by CAS_Client::_validatePGT() for CAS proxies.
+ *
+ * @param string &$validate_url reference to the the URL of the request to
+ * the CAS server.
+ * @param string &$text_response reference to the response of the CAS
+ * server, as is (XML text).
+ * @param string &$tree_response reference to the response of the CAS
+ * server, as a DOM XML tree.
+ *
+ * @return bool true when successfull and issue a CAS_AuthenticationException
+ * and false on an error
+ */
+ public function validateSA(&$validate_url,&$text_response,&$tree_response)
+ {
+ phpCAS::traceBegin();
+ $result = false;
+ // build the URL to validate the ticket
+ $validate_url = $this->getServerSamlValidateURL();
+
+ // open and read the URL
+ if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) {
+ phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')');
+ throw new CAS_AuthenticationException($this, 'SA not validated', $validate_url, true/*$no_response*/);
+ }
+
+ phpCAS::trace('server version: '.$this->getServerVersion());
+
+ // analyze the result depending on the version
+ switch ($this->getServerVersion()) {
+ case SAML_VERSION_1_1:
+ // create new DOMDocument Object
+ $dom = new DOMDocument();
+ // Fix possible whitspace problems
+ $dom->preserveWhiteSpace = false;
+ // read the response of the CAS server into a DOM object
+ if (!($dom->loadXML($text_response))) {
+ phpCAS::trace('dom->loadXML() failed');
+ throw new CAS_AuthenticationException(
+ $this, 'SA not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/,
+ $text_response
+ );
+ $result = false;
+ }
+ // read the root node of the XML tree
+ if (!($tree_response = $dom->documentElement)) {
+ phpCAS::trace('documentElement() failed');
+ throw new CAS_AuthenticationException(
+ $this, 'SA not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/,
+ $text_response
+ );
+ $result = false;
+ } else if ( $tree_response->localName != 'Envelope' ) {
+ // insure that tag name is 'Envelope'
+ phpCAS::trace('bad XML root node (should be `Envelope\' instead of `'.$tree_response->localName.'\'');
+ throw new CAS_AuthenticationException(
+ $this, 'SA not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/,
+ $text_response
+ );
+ $result = false;
+ } else if ($tree_response->getElementsByTagName("NameIdentifier")->length != 0) {
+ // check for the NameIdentifier tag in the SAML response
+ $success_elements = $tree_response->getElementsByTagName("NameIdentifier");
+ phpCAS::trace('NameIdentifier found');
+ $user = trim($success_elements->item(0)->nodeValue);
+ phpCAS::trace('user = `'.$user.'`');
+ $this->_setUser($user);
+ $this->_setSessionAttributes($text_response);
+ $result = true;
+ } else {
+ phpCAS::trace('no <NameIdentifier> tag found in SAML payload');
+ throw new CAS_AuthenticationException(
+ $this, 'SA not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/,
+ $text_response
+ );
+ $result = false;
+ }
+ }
+ if ($result) {
+ $this->_renameSession($this->getTicket());
+ }
+ // at this step, ST has been validated and $this->_user has been set,
+ phpCAS::traceEnd($result);
+ return $result;
+ }
+
+ /**
+ * This method will parse the DOM and pull out the attributes from the SAML
+ * payload and put them into an array, then put the array into the session.
+ *
+ * @param string $text_response the SAML payload.
+ *
+ * @return bool true when successfull and false if no attributes a found
+ */
+ private function _setSessionAttributes($text_response)
+ {
+ phpCAS::traceBegin();
+
+ $result = false;
+
+ $attr_array = array();
+
+ // create new DOMDocument Object
+ $dom = new DOMDocument();
+ // Fix possible whitspace problems
+ $dom->preserveWhiteSpace = false;
+ if (($dom->loadXML($text_response))) {
+ $xPath = new DOMXpath($dom);
+ $xPath->registerNamespace('samlp', 'urn:oasis:names:tc:SAML:1.0:protocol');
+ $xPath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:1.0:assertion');
+ $nodelist = $xPath->query("//saml:Attribute");
+
+ if ($nodelist) {
+ foreach ($nodelist as $node) {
+ $xres = $xPath->query("saml:AttributeValue", $node);
+ $name = $node->getAttribute("AttributeName");
+ $value_array = array();
+ foreach ($xres as $node2) {
+ $value_array[] = $node2->nodeValue;
+ }
+ $attr_array[$name] = $value_array;
+ }
+ // UGent addition...
+ foreach ($attr_array as $attr_key => $attr_value) {
+ if (count($attr_value) > 1) {
+ $this->_attributes[$attr_key] = $attr_value;
+ phpCAS::trace("* " . $attr_key . "=" . $attr_value);
+ } else {
+ $this->_attributes[$attr_key] = $attr_value[0];
+ phpCAS::trace("* " . $attr_key . "=" . $attr_value[0]);
+ }
+ }
+ $result = true;
+ } else {
+ phpCAS::trace("SAML Attributes are empty");
+ $result = false;
+ }
+ }
+ phpCAS::traceEnd($result);
+ return $result;
+ }
+
+ /** @} */
+
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ // XX XX
+ // XX PROXY FEATURES (CAS 2.0) XX
+ // XX XX
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+ // ########################################################################
+ // PROXYING
+ // ########################################################################
+ /**
+ * @addtogroup internalProxy
+ * @{
+ */
+
+ /**
+ * A boolean telling if the client is a CAS proxy or not. Written by
+ * CAS_Client::CAS_Client(), read by CAS_Client::isProxy().
+ */
+ private $_proxy;
+
+ /**
+ * Handler for managing service cookies.
+ */
+ private $_serviceCookieJar;
+
+ /**
+ * Tells if a CAS client is a CAS proxy or not
+ *
+ * @return true when the CAS client is a CAs proxy, false otherwise
+ */
+ public function isProxy()
+ {
+ return $this->_proxy;
+ }
+
+ /** @} */
+ // ########################################################################
+ // PGT
+ // ########################################################################
+ /**
+ * @addtogroup internalProxy
+ * @{
+ */
+
+ /**
+ * the Proxy Grnting Ticket given by the CAS server (empty otherwise).
+ * Written by CAS_Client::_setPGT(), read by CAS_Client::_getPGT() and
+ * CAS_Client::_hasPGT().
+ *
+ * @hideinitializer
+ */
+ private $_pgt = '';
+
+ /**
+ * This method returns the Proxy Granting Ticket given by the CAS server.
+ *
+ * @return string the Proxy Granting Ticket.
+ */
+ private function _getPGT()
+ {
+ return $this->_pgt;
+ }
+
+ /**
+ * This method stores the Proxy Granting Ticket.
+ *
+ * @param string $pgt The Proxy Granting Ticket.
+ *
+ * @return void
+ */
+ private function _setPGT($pgt)
+ {
+ $this->_pgt = $pgt;
+ }
+
+ /**
+ * This method tells if a Proxy Granting Ticket was stored.
+ *
+ * @return true if a Proxy Granting Ticket has been stored.
+ */
+ private function _hasPGT()
+ {
+ return !empty($this->_pgt);
+ }
+
+ /** @} */
+
+ // ########################################################################
+ // CALLBACK MODE
+ // ########################################################################
+ /**
+ * @addtogroup internalCallback
+ * @{
+ */
+ /**
+ * each PHP script using phpCAS in proxy mode is its own callback to get the
+ * PGT back from the CAS server. callback_mode is detected by the constructor
+ * thanks to the GET parameters.
+ */
+
+ /**
+ * a boolean to know if the CAS client is running in callback mode. Written by
+ * CAS_Client::setCallBackMode(), read by CAS_Client::_isCallbackMode().
+ *
+ * @hideinitializer
+ */
+ private $_callback_mode = false;
+
+ /**
+ * This method sets/unsets callback mode.
+ *
+ * @param bool $callback_mode true to set callback mode, false otherwise.
+ *
+ * @return void
+ */
+ private function _setCallbackMode($callback_mode)
+ {
+ $this->_callback_mode = $callback_mode;
+ }
+
+ /**
+ * This method returns true when the CAs client is running i callback mode,
+ * false otherwise.
+ *
+ * @return A boolean.
+ */
+ private function _isCallbackMode()
+ {
+ return $this->_callback_mode;
+ }
+
+ /**
+ * the URL that should be used for the PGT callback (in fact the URL of the
+ * current request without any CGI parameter). Written and read by
+ * CAS_Client::_getCallbackURL().
+ *
+ * @hideinitializer
+ */
+ private $_callback_url = '';
+
+ /**
+ * This method returns the URL that should be used for the PGT callback (in
+ * fact the URL of the current request without any CGI parameter, except if
+ * phpCAS::setFixedCallbackURL() was used).
+ *
+ * @return The callback URL
+ */
+ private function _getCallbackURL()
+ {
+ // the URL is built when needed only
+ if ( empty($this->_callback_url) ) {
+ $final_uri = '';
+ // remove the ticket if present in the URL
+ $final_uri = 'https://';
+ $final_uri .= $this->_getServerUrl();
+ $request_uri = $_SERVER['REQUEST_URI'];
+ $request_uri = preg_replace('/\?.*$/', '', $request_uri);
+ $final_uri .= $request_uri;
+ $this->setCallbackURL($final_uri);
+ }
+ return $this->_callback_url;
+ }
+
+ /**
+ * This method sets the callback url.
+ *
+ * @param string $url url to set callback
+ *
+ * @return void
+ */
+ public function setCallbackURL($url)
+ {
+ return $this->_callback_url = $url;
+ }
+
+ /**
+ * This method is called by CAS_Client::CAS_Client() when running in callback
+ * mode. It stores the PGT and its PGT Iou, prints its output and halts.
+ *
+ * @return void
+ */
+ private function _callback()
+ {
+ phpCAS::traceBegin();
+ if (preg_match('/PGTIOU-[\.\-\w]/', $_GET['pgtIou'])) {
+ if (preg_match('/[PT]GT-[\.\-\w]/', $_GET['pgtId'])) {
+ $this->printHTMLHeader('phpCAS callback');
+ $pgt_iou = $_GET['pgtIou'];
+ $pgt = $_GET['pgtId'];
+ phpCAS::trace('Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\')');
+ echo '<p>Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\').</p>';
+ $this->_storePGT($pgt, $pgt_iou);
+ $this->printHTMLFooter();
+ phpCAS::traceExit("Successfull Callback");
+ } else {
+ phpCAS::error('PGT format invalid' . $_GET['pgtId']);
+ phpCAS::traceExit('PGT format invalid' . $_GET['pgtId']);
+ }
+ } else {
+ phpCAS::error('PGTiou format invalid' . $_GET['pgtIou']);
+ phpCAS::traceExit('PGTiou format invalid' . $_GET['pgtIou']);
+ }
+
+ // Flush the buffer to prevent from sending anything other then a 200
+ // Success Status back to the CAS Server. The Exception would normally
+ // report as a 500 error.
+ flush();
+ throw new CAS_GracefullTerminationException();
+ }
+
+
+ /** @} */
+
+ // ########################################################################
+ // PGT STORAGE
+ // ########################################################################
+ /**
+ * @addtogroup internalPGTStorage
+ * @{
+ */
+
+ /**
+ * an instance of a class inheriting of PGTStorage, used to deal with PGT
+ * storage. Created by CAS_Client::setPGTStorageFile(), used
+ * by CAS_Client::setPGTStorageFile() and CAS_Client::_initPGTStorage().
+ *
+ * @hideinitializer
+ */
+ private $_pgt_storage = null;
+
+ /**
+ * This method is used to initialize the storage of PGT's.
+ * Halts on error.
+ *
+ * @return void
+ */
+ private function _initPGTStorage()
+ {
+ // if no SetPGTStorageXxx() has been used, default to file
+ if ( !is_object($this->_pgt_storage) ) {
+ $this->setPGTStorageFile();
+ }
+
+ // initializes the storage
+ $this->_pgt_storage->init();
+ }
+
+ /**
+ * This method stores a PGT. Halts on error.
+ *
+ * @param string $pgt the PGT to store
+ * @param string $pgt_iou its corresponding Iou
+ *
+ * @return void
+ */
+ private function _storePGT($pgt,$pgt_iou)
+ {
+ // ensure that storage is initialized
+ $this->_initPGTStorage();
+ // writes the PGT
+ $this->_pgt_storage->write($pgt, $pgt_iou);
+ }
+
+ /**
+ * This method reads a PGT from its Iou and deletes the corresponding
+ * storage entry.
+ *
+ * @param string $pgt_iou the PGT Iou
+ *
+ * @return mul The PGT corresponding to the Iou, false when not found.
+ */
+ private function _loadPGT($pgt_iou)
+ {
+ // ensure that storage is initialized
+ $this->_initPGTStorage();
+ // read the PGT
+ return $this->_pgt_storage->read($pgt_iou);
+ }
+
+ /**
+ * This method can be used to set a custom PGT storage object.
+ *
+ * @param CAS_PGTStorage_AbstractStorage $storage a PGT storage object that
+ * inherits from the CAS_PGTStorage_AbstractStorage class
+ *
+ * @return void
+ */
+ public function setPGTStorage($storage)
+ {
+ // check that the storage has not already been set
+ if ( is_object($this->_pgt_storage) ) {
+ phpCAS::error('PGT storage already defined');
+ }
+
+ // check to make sure a valid storage object was specified
+ if ( !($storage instanceof CAS_PGTStorage_AbstractStorage) ) {
+ phpCAS::error('Invalid PGT storage object');
+ }
+
+ // store the PGTStorage object
+ $this->_pgt_storage = $storage;
+ }
+
+ /**
+ * This method is used to tell phpCAS to store the response of the
+ * CAS server to PGT requests in a database.
+ *
+ * @param string $dsn_or_pdo a dsn string to use for creating a PDO
+ * object or a PDO object
+ * @param string $username the username to use when connecting to the
+ * database
+ * @param string $password the password to use when connecting to the
+ * database
+ * @param string $table the table to use for storing and retrieving
+ * PGTs
+ * @param string $driver_options any driver options to use when connecting
+ * to the database
+ *
+ * @return void
+ */
+ public function setPGTStorageDb($dsn_or_pdo, $username='', $password='', $table='', $driver_options=null)
+ {
+ // create the storage object
+ $this->setPGTStorage(new CAS_PGTStorage_Db($this, $dsn_or_pdo, $username, $password, $table, $driver_options));
+ }
+
+ /**
+ * This method is used to tell phpCAS to store the response of the
+ * CAS server to PGT requests onto the filesystem.
+ *
+ * @param string $path the path where the PGT's should be stored
+ *
+ * @return void
+ */
+ public function setPGTStorageFile($path='')
+ {
+ // create the storage object
+ $this->setPGTStorage(new CAS_PGTStorage_File($this, $path));
+ }
+
+
+ // ########################################################################
+ // PGT VALIDATION
+ // ########################################################################
+ /**
+ * This method is used to validate a PGT; halt on failure.
+ *
+ * @param string &$validate_url the URL of the request to the CAS server.
+ * @param string $text_response the response of the CAS server, as is
+ * (XML text); result of CAS_Client::validateCAS10() or CAS_Client::validateCAS20().
+ * @param string $tree_response the response of the CAS server, as a DOM XML
+ * tree; result of CAS_Client::validateCAS10() or CAS_Client::validateCAS20().
+ *
+ * @return bool true when successfull and issue a CAS_AuthenticationException
+ * and false on an error
+ */
+ private function _validatePGT(&$validate_url,$text_response,$tree_response)
+ {
+ phpCAS::traceBegin();
+ if ( $tree_response->getElementsByTagName("proxyGrantingTicket")->length == 0) {
+ phpCAS::trace('<proxyGrantingTicket> not found');
+ // authentication succeded, but no PGT Iou was transmitted
+ throw new CAS_AuthenticationException(
+ $this, 'Ticket validated but no PGT Iou transmitted',
+ $validate_url, false/*$no_response*/, false/*$bad_response*/,
+ $text_response
+ );
+ } else {
+ // PGT Iou transmitted, extract it
+ $pgt_iou = trim($tree_response->getElementsByTagName("proxyGrantingTicket")->item(0)->nodeValue);
+ if (preg_match('/PGTIOU-[\.\-\w]/', $pgt_iou)) {
+ $pgt = $this->_loadPGT($pgt_iou);
+ if ( $pgt == false ) {
+ phpCAS::trace('could not load PGT');
+ throw new CAS_AuthenticationException(
+ $this, 'PGT Iou was transmitted but PGT could not be retrieved',
+ $validate_url, false/*$no_response*/,
+ false/*$bad_response*/, $text_response
+ );
+ }
+ $this->_setPGT($pgt);
+ } else {
+ phpCAS::trace('PGTiou format error');
+ throw new CAS_AuthenticationException(
+ $this, 'PGT Iou was transmitted but has wrong format',
+ $validate_url, false/*$no_response*/, false/*$bad_response*/,
+ $text_response
+ );
+ }
+ }
+ phpCAS::traceEnd(true);
+ return true;
+ }
+
+ // ########################################################################
+ // PGT VALIDATION
+ // ########################################################################
+
+ /**
+ * This method is used to retrieve PT's from the CAS server thanks to a PGT.
+ *
+ * @param string $target_service the service to ask for with the PT.
+ * @param string &$err_code an error code (PHPCAS_SERVICE_OK on success).
+ * @param string &$err_msg an error message (empty on success).
+ *
+ * @return a Proxy Ticket, or false on error.
+ */
+ public function retrievePT($target_service,&$err_code,&$err_msg)
+ {
+ phpCAS::traceBegin();
+
+ // by default, $err_msg is set empty and $pt to true. On error, $pt is
+ // set to false and $err_msg to an error message. At the end, if $pt is false
+ // and $error_msg is still empty, it is set to 'invalid response' (the most
+ // commonly encountered error).
+ $err_msg = '';
+
+ // build the URL to retrieve the PT
+ $cas_url = $this->getServerProxyURL().'?targetService='.urlencode($target_service).'&pgt='.$this->_getPGT();
+
+ // open and read the URL
+ if ( !$this->_readURL($cas_url, $headers, $cas_response, $err_msg) ) {
+ phpCAS::trace('could not open URL \''.$cas_url.'\' to validate ('.$err_msg.')');
+ $err_code = PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE;
+ $err_msg = 'could not retrieve PT (no response from the CAS server)';
+ phpCAS::traceEnd(false);
+ return false;
+ }
+
+ $bad_response = false;
+
+ if ( !$bad_response ) {
+ // create new DOMDocument object
+ $dom = new DOMDocument();
+ // Fix possible whitspace problems
+ $dom->preserveWhiteSpace = false;
+ // read the response of the CAS server into a DOM object
+ if ( !($dom->loadXML($cas_response))) {
+ phpCAS::trace('dom->loadXML() failed');
+ // read failed
+ $bad_response = true;
+ }
+ }
+
+ if ( !$bad_response ) {
+ // read the root node of the XML tree
+ if ( !($root = $dom->documentElement) ) {
+ phpCAS::trace('documentElement failed');
+ // read failed
+ $bad_response = true;
+ }
+ }
+
+ if ( !$bad_response ) {
+ // insure that tag name is 'serviceResponse'
+ if ( $root->localName != 'serviceResponse' ) {
+ phpCAS::trace('localName failed');
+ // bad root node
+ $bad_response = true;
+ }
+ }
+
+ if ( !$bad_response ) {
+ // look for a proxySuccess tag
+ if ( $root->getElementsByTagName("proxySuccess")->length != 0) {
+ $proxy_success_list = $root->getElementsByTagName("proxySuccess");
+
+ // authentication succeded, look for a proxyTicket tag
+ if ( $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->length != 0) {
+ $err_code = PHPCAS_SERVICE_OK;
+ $err_msg = '';
+ $pt = trim($proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->item(0)->nodeValue);
+ phpCAS::trace('original PT: '.trim($pt));
+ phpCAS::traceEnd($pt);
+ return $pt;
+ } else {
+ phpCAS::trace('<proxySuccess> was found, but not <proxyTicket>');
+ }
+ } else if ($root->getElementsByTagName("proxyFailure")->length != 0) {
+ // look for a proxyFailure tag
+ $proxy_failure_list = $root->getElementsByTagName("proxyFailure");
+
+ // authentication failed, extract the error
+ $err_code = PHPCAS_SERVICE_PT_FAILURE;
+ $err_msg = 'PT retrieving failed (code=`'
+ .$proxy_failure_list->item(0)->getAttribute('code')
+ .'\', message=`'
+ .trim($proxy_failure_list->item(0)->nodeValue)
+ .'\')';
+ phpCAS::traceEnd(false);
+ return false;
+ } else {
+ phpCAS::trace('neither <proxySuccess> nor <proxyFailure> found');
+ }
+ }
+
+ // at this step, we are sure that the response of the CAS server was
+ // illformed
+ $err_code = PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE;
+ $err_msg = 'Invalid response from the CAS server (response=`'.$cas_response.'\')';
+
+ phpCAS::traceEnd(false);
+ return false;
+ }
+
+ /** @} */
+
+ // ########################################################################
+ // READ CAS SERVER ANSWERS
+ // ########################################################################
+
+ /**
+ * @addtogroup internalMisc
+ * @{
+ */
+
+ /**
+ * This method is used to acces a remote URL.
+ *
+ * @param string $url the URL to access.
+ * @param string &$headers an array containing the HTTP header lines of the
+ * response (an empty array on failure).
+ * @param string &$body the body of the response, as a string (empty on
+ * failure).
+ * @param string &$err_msg an error message, filled on failure.
+ *
+ * @return true on success, false otherwise (in this later case, $err_msg
+ * contains an error message).
+ */
+ private function _readURL($url, &$headers, &$body, &$err_msg)
+ {
+ phpCAS::traceBegin();
+ $className = $this->_requestImplementation;
+ $request = new $className();
+
+ if (count($this->_curl_options)) {
+ $request->setCurlOptions($this->_curl_options);
+ }
+
+ $request->setUrl($url);
+
+ if (empty($this->_cas_server_ca_cert) && !$this->_no_cas_server_validation) {
+ phpCAS::error('one of the methods phpCAS::setCasServerCACert() or phpCAS::setNoCasServerValidation() must be called.');
+ }
+ if ($this->_cas_server_ca_cert != '') {
+ $request->setSslCaCert($this->_cas_server_ca_cert, $this->_cas_server_cn_validate);
+ }
+
+ // add extra stuff if SAML
+ if ($this->getServerVersion() == SAML_VERSION_1_1) {
+ $request->addHeader("soapaction: http://www.oasis-open.org/committees/security");
+ $request->addHeader("cache-control: no-cache");
+ $request->addHeader("pragma: no-cache");
+ $request->addHeader("accept: text/xml");
+ $request->addHeader("connection: keep-alive");
+ $request->addHeader("content-type: text/xml");
+ $request->makePost();
+ $request->setPostBody($this->_buildSAMLPayload());
+ }
+
+ if ($request->send()) {
+ $headers = $request->getResponseHeaders();
+ $body = $request->getResponseBody();
+ $err_msg = '';
+ phpCAS::traceEnd(true);
+ return true;
+ } else {
+ $headers = '';
+ $body = '';
+ $err_msg = $request->getErrorMessage();
+ phpCAS::traceEnd(false);
+ return false;
+ }
+ }
+
+ /**
+ * This method is used to build the SAML POST body sent to /samlValidate URL.
+ *
+ * @return the SOAP-encased SAMLP artifact (the ticket).
+ */
+ private function _buildSAMLPayload()
+ {
+ phpCAS::traceBegin();
+
+ //get the ticket
+ $sa = $this->getTicket();
+
+ $body=SAML_SOAP_ENV.SAML_SOAP_BODY.SAMLP_REQUEST.SAML_ASSERTION_ARTIFACT.$sa.SAML_ASSERTION_ARTIFACT_CLOSE.SAMLP_REQUEST_CLOSE.SAML_SOAP_BODY_CLOSE.SAML_SOAP_ENV_CLOSE;
+
+ phpCAS::traceEnd($body);
+ return ($body);
+ }
+
+ /** @} **/
+
+ // ########################################################################
+ // ACCESS TO EXTERNAL SERVICES
+ // ########################################################################
+
+ /**
+ * @addtogroup internalProxyServices
+ * @{
+ */
+
+
+ /**
+ * Answer a proxy-authenticated service handler.
+ *
+ * @param string $type The service type. One of:
+ * PHPCAS_PROXIED_SERVICE_HTTP_GET, PHPCAS_PROXIED_SERVICE_HTTP_POST,
+ * PHPCAS_PROXIED_SERVICE_IMAP
+ *
+ * @return CAS_ProxiedService
+ * @throws InvalidArgumentException If the service type is unknown.
+ */
+ public function getProxiedService ($type)
+ {
+ switch ($type) {
+ case PHPCAS_PROXIED_SERVICE_HTTP_GET:
+ case PHPCAS_PROXIED_SERVICE_HTTP_POST:
+ $requestClass = $this->_requestImplementation;
+ $request = new $requestClass();
+ if (count($this->_curl_options)) {
+ $request->setCurlOptions($this->_curl_options);
+ }
+ $proxiedService = new $type($request, $this->_serviceCookieJar);
+ if ($proxiedService instanceof CAS_ProxiedService_Testable) {
+ $proxiedService->setCasClient($this);
+ }
+ return $proxiedService;
+ case PHPCAS_PROXIED_SERVICE_IMAP;
+ $proxiedService = new CAS_ProxiedService_Imap($this->getUser());
+ if ($proxiedService instanceof CAS_ProxiedService_Testable) {
+ $proxiedService->setCasClient($this);
+ }
+ return $proxiedService;
+ default:
+ throw new CAS_InvalidArgumentException("Unknown proxied-service type, $type.");
+ }
+ }
+
+ /**
+ * Initialize a proxied-service handler with the proxy-ticket it should use.
+ *
+ * @param CAS_ProxiedService $proxiedService service handler
+ *
+ * @return void
+ *
+ * @throws CAS_ProxyTicketException If there is a proxy-ticket failure.
+ * The code of the Exception will be one of:
+ * PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE
+ * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE
+ * PHPCAS_SERVICE_PT_FAILURE
+ * @throws CAS_ProxiedService_Exception If there is a failure getting the
+ * url from the proxied service.
+ */
+ public function initializeProxiedService (CAS_ProxiedService $proxiedService)
+ {
+ $url = $proxiedService->getServiceUrl();
+ if (!is_string($url)) {
+ throw new CAS_ProxiedService_Exception("Proxied Service ".get_class($proxiedService)."->getServiceUrl() should have returned a string, returned a ".gettype($url)." instead.");
+ }
+ $pt = $this->retrievePT($url, $err_code, $err_msg);
+ if (!$pt) {
+ throw new CAS_ProxyTicketException($err_msg, $err_code);
+ }
+ $proxiedService->setProxyTicket($pt);
+ }
+
+ /**
+ * This method is used to access an HTTP[S] service.
+ *
+ * @param string $url the service to access.
+ * @param int &$err_code an error code Possible values are
+ * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE,
+ * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE,
+ * PHPCAS_SERVICE_NOT_AVAILABLE.
+ * @param string &$output the output of the service (also used to give an error
+ * message on failure).
+ *
+ * @return true on success, false otherwise (in this later case, $err_code
+ * gives the reason why it failed and $output contains an error message).
+ */
+ public function serviceWeb($url,&$err_code,&$output)
+ {
+ try {
+ $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_HTTP_GET);
+ $service->setUrl($url);
+ $service->send();
+ $output = $service->getResponseBody();
+ $err_code = PHPCAS_SERVICE_OK;
+ return true;
+ } catch (CAS_ProxyTicketException $e) {
+ $err_code = $e->getCode();
+ $output = $e->getMessage();
+ return false;
+ } catch (CAS_ProxiedService_Exception $e) {
+ $lang = $this->getLangObj();
+ $output = sprintf($lang->getServiceUnavailable(), $url, $e->getMessage());
+ $err_code = PHPCAS_SERVICE_NOT_AVAILABLE;
+ return false;
+ }
+ }
+
+ /**
+ * This method is used to access an IMAP/POP3/NNTP service.
+ *
+ * @param string $url a string giving the URL of the service, including
+ * the mailing box for IMAP URLs, as accepted by imap_open().
+ * @param string $serviceUrl a string giving for CAS retrieve Proxy ticket
+ * @param string $flags options given to imap_open().
+ * @param int &$err_code an error code Possible values are
+ * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE,
+ * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE,
+ * PHPCAS_SERVICE_NOT_AVAILABLE.
+ * @param string &$err_msg an error message on failure
+ * @param string &$pt the Proxy Ticket (PT) retrieved from the CAS
+ * server to access the URL on success, false on error).
+ *
+ * @return object an IMAP stream on success, false otherwise (in this later
+ * case, $err_code gives the reason why it failed and $err_msg contains an
+ * error message).
+ */
+ public function serviceMail($url,$serviceUrl,$flags,&$err_code,&$err_msg,&$pt)
+ {
+ try {
+ $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_IMAP);
+ $service->setServiceUrl($serviceUrl);
+ $service->setMailbox($url);
+ $service->setOptions($flags);
+
+ $stream = $service->open();
+ $err_code = PHPCAS_SERVICE_OK;
+ $pt = $service->getImapProxyTicket();
+ return $stream;
+ } catch (CAS_ProxyTicketException $e) {
+ $err_msg = $e->getMessage();
+ $err_code = $e->getCode();
+ $pt = false;
+ return false;
+ } catch (CAS_ProxiedService_Exception $e) {
+ $lang = $this->getLangObj();
+ $err_msg = sprintf(
+ $lang->getServiceUnavailable(),
+ $url,
+ $e->getMessage()
+ );
+ $err_code = PHPCAS_SERVICE_NOT_AVAILABLE;
+ $pt = false;
+ return false;
+ }
+ }
+
+ /** @} **/
+
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ // XX XX
+ // XX PROXIED CLIENT FEATURES (CAS 2.0) XX
+ // XX XX
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+ // ########################################################################
+ // PT
+ // ########################################################################
+ /**
+ * @addtogroup internalService
+ * @{
+ */
+
+ /**
+ * This array will store a list of proxies in front of this application. This
+ * property will only be populated if this script is being proxied rather than
+ * accessed directly.
+ *
+ * It is set in CAS_Client::validateCAS20() and can be read by
+ * CAS_Client::getProxies()
+ *
+ * @access private
+ */
+ private $_proxies = array();
+
+ /**
+ * Answer an array of proxies that are sitting in front of this application.
+ *
+ * This method will only return a non-empty array if we have received and
+ * validated a Proxy Ticket.
+ *
+ * @return array
+ * @access public
+ */
+ public function getProxies()
+ {
+ return $this->_proxies;
+ }
+
+ /**
+ * Set the Proxy array, probably from persistant storage.
+ *
+ * @param array $proxies An array of proxies
+ *
+ * @return void
+ * @access private
+ */
+ private function _setProxies($proxies)
+ {
+ $this->_proxies = $proxies;
+ if (!empty($proxies)) {
+ // For proxy-authenticated requests people are not viewing the URL
+ // directly since the client is another application making a
+ // web-service call.
+ // Because of this, stripping the ticket from the URL is unnecessary
+ // and causes another web-service request to be performed. Additionally,
+ // if session handling on either the client or the server malfunctions
+ // then the subsequent request will not complete successfully.
+ $this->setNoClearTicketsFromUrl();
+ }
+ }
+
+ /**
+ * A container of patterns to be allowed as proxies in front of the cas client.
+ *
+ * @var CAS_ProxyChain_AllowedList
+ */
+ private $_allowed_proxy_chains;
+
+ /**
+ * Answer the CAS_ProxyChain_AllowedList object for this client.
+ *
+ * @return CAS_ProxyChain_AllowedList
+ */
+ public function getAllowedProxyChains ()
+ {
+ if (empty($this->_allowed_proxy_chains)) {
+ $this->_allowed_proxy_chains = new CAS_ProxyChain_AllowedList();
+ }
+ return $this->_allowed_proxy_chains;
+ }
+
+ /** @} */
+ // ########################################################################
+ // PT VALIDATION
+ // ########################################################################
+ /**
+ * @addtogroup internalProxied
+ * @{
+ */
+
+ /**
+ * This method is used to validate a cas 2.0 ST or PT; halt on failure
+ * Used for all CAS 2.0 validations
+ *
+ * @param string &$validate_url the url of the reponse
+ * @param string &$text_response the text of the repsones
+ * @param string &$tree_response the domxml tree of the respones
+ *
+ * @return bool true when successfull and issue a CAS_AuthenticationException
+ * and false on an error
+ */
+ public function validateCAS20(&$validate_url,&$text_response,&$tree_response)
+ {
+ phpCAS::traceBegin();
+ phpCAS::trace($text_response);
+ $result = false;
+ // build the URL to validate the ticket
+ if ($this->getAllowedProxyChains()->isProxyingAllowed()) {
+ $validate_url = $this->getServerProxyValidateURL().'&ticket='.$this->getTicket();
+ } else {
+ $validate_url = $this->getServerServiceValidateURL().'&ticket='.$this->getTicket();
+ }
+
+ if ( $this->isProxy() ) {
+ // pass the callback url for CAS proxies
+ $validate_url .= '&pgtUrl='.urlencode($this->_getCallbackURL());
+ }
+
+ // open and read the URL
+ if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) {
+ phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')');
+ throw new CAS_AuthenticationException(
+ $this, 'Ticket not validated', $validate_url,
+ true/*$no_response*/
+ );
+ $result = false;
+ }
+
+ // create new DOMDocument object
+ $dom = new DOMDocument();
+ // Fix possible whitspace problems
+ $dom->preserveWhiteSpace = false;
+ // CAS servers should only return data in utf-8
+ $dom->encoding = "utf-8";
+ // read the response of the CAS server into a DOMDocument object
+ if ( !($dom->loadXML($text_response))) {
+ // read failed
+ throw new CAS_AuthenticationException(
+ $this, 'Ticket not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/, $text_response
+ );
+ $result = false;
+ } else if ( !($tree_response = $dom->documentElement) ) {
+ // read the root node of the XML tree
+ // read failed
+ throw new CAS_AuthenticationException(
+ $this, 'Ticket not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/, $text_response
+ );
+ $result = false;
+ } else if ($tree_response->localName != 'serviceResponse') {
+ // insure that tag name is 'serviceResponse'
+ // bad root node
+ throw new CAS_AuthenticationException(
+ $this, 'Ticket not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/, $text_response
+ );
+ $result = false;
+ } else if ($tree_response->getElementsByTagName("authenticationSuccess")->length != 0) {
+ // authentication succeded, extract the user name
+ $success_elements = $tree_response->getElementsByTagName("authenticationSuccess");
+ if ( $success_elements->item(0)->getElementsByTagName("user")->length == 0) {
+ // no user specified => error
+ throw new CAS_AuthenticationException(
+ $this, 'Ticket not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/, $text_response
+ );
+ $result = false;
+ } else {
+ $this->_setUser(trim($success_elements->item(0)->getElementsByTagName("user")->item(0)->nodeValue));
+ $this->_readExtraAttributesCas20($success_elements);
+ // Store the proxies we are sitting behind for authorization checking
+ $proxyList = array();
+ if ( sizeof($arr = $success_elements->item(0)->getElementsByTagName("proxy")) > 0) {
+ foreach ($arr as $proxyElem) {
+ phpCAS::trace("Found Proxy: ".$proxyElem->nodeValue);
+ $proxyList[] = trim($proxyElem->nodeValue);
+ }
+ $this->_setProxies($proxyList);
+ phpCAS::trace("Storing Proxy List");
+ }
+ // Check if the proxies in front of us are allowed
+ if (!$this->getAllowedProxyChains()->isProxyListAllowed($proxyList)) {
+ throw new CAS_AuthenticationException(
+ $this, 'Proxy not allowed', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/,
+ $text_response
+ );
+ $result = false;
+ } else {
+ $result = true;
+ }
+ }
+ } else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) {
+ // authentication succeded, extract the error code and message
+ $auth_fail_list = $tree_response->getElementsByTagName("authenticationFailure");
+ throw new CAS_AuthenticationException(
+ $this, 'Ticket not validated', $validate_url,
+ false/*$no_response*/, false/*$bad_response*/,
+ $text_response,
+ $auth_fail_list->item(0)->getAttribute('code')/*$err_code*/,
+ trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/
+ );
+ $result = false;
+ } else {
+ throw new CAS_AuthenticationException(
+ $this, 'Ticket not validated', $validate_url,
+ false/*$no_response*/, true/*$bad_response*/,
+ $text_response
+ );
+ $result = false;
+ }
+ if ($result) {
+ $this->_renameSession($this->getTicket());
+ }
+ // at this step, Ticket has been validated and $this->_user has been set,
+
+ phpCAS::traceEnd($result);
+ return $result;
+ }
+
+
+ /**
+ * This method will parse the DOM and pull out the attributes from the XML
+ * payload and put them into an array, then put the array into the session.
+ *
+ * @param string $success_elements payload of the response
+ *
+ * @return bool true when successfull, halt otherwise by calling
+ * CAS_Client::_authError().
+ */
+ private function _readExtraAttributesCas20($success_elements)
+ {
+ phpCAS::traceBegin();
+
+ $extra_attributes = array();
+
+ // "Jasig Style" Attributes:
+ //
+ // <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
+ // <cas:authenticationSuccess>
+ // <cas:user>jsmith</cas:user>
+ // <cas:attributes>
+ // <cas:attraStyle>RubyCAS</cas:attraStyle>
+ // <cas:surname>Smith</cas:surname>
+ // <cas:givenName>John</cas:givenName>
+ // <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf>
+ // <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf>
+ // </cas:attributes>
+ // <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
+ // </cas:authenticationSuccess>
+ // </cas:serviceResponse>
+ //
+ if ( $success_elements->item(0)->getElementsByTagName("attributes")->length != 0) {
+ $attr_nodes = $success_elements->item(0)->getElementsByTagName("attributes");
+ phpCas :: trace("Found nested jasig style attributes");
+ if ($attr_nodes->item(0)->hasChildNodes()) {
+ // Nested Attributes
+ foreach ($attr_nodes->item(0)->childNodes as $attr_child) {
+ phpCas :: trace("Attribute [".$attr_child->localName."] = ".$attr_child->nodeValue);
+ $this->_addAttributeToArray($extra_attributes, $attr_child->localName, $attr_child->nodeValue);
+ }
+ }
+ } else {
+ // "RubyCAS Style" attributes
+ //
+ // <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
+ // <cas:authenticationSuccess>
+ // <cas:user>jsmith</cas:user>
+ //
+ // <cas:attraStyle>RubyCAS</cas:attraStyle>
+ // <cas:surname>Smith</cas:surname>
+ // <cas:givenName>John</cas:givenName>
+ // <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf>
+ // <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf>
+ //
+ // <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
+ // </cas:authenticationSuccess>
+ // </cas:serviceResponse>
+ //
+ phpCas :: trace("Testing for rubycas style attributes");
+ $childnodes = $success_elements->item(0)->childNodes;
+ foreach ($childnodes as $attr_node) {
+ switch ($attr_node->localName) {
+ case 'user':
+ case 'proxies':
+ case 'proxyGrantingTicket':
+ continue;
+ default:
+ if (strlen(trim($attr_node->nodeValue))) {
+ phpCas :: trace("Attribute [".$attr_node->localName."] = ".$attr_node->nodeValue);
+ $this->_addAttributeToArray($extra_attributes, $attr_node->localName, $attr_node->nodeValue);
+ }
+ }
+ }
+ }
+
+ // "Name-Value" attributes.
+ //
+ // Attribute format from these mailing list thread:
+ // http://jasig.275507.n4.nabble.com/CAS-attributes-and-how-they-appear-in-the-CAS-response-td264272.html
+ // Note: This is a less widely used format, but in use by at least two institutions.
+ //
+ // <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
+ // <cas:authenticationSuccess>
+ // <cas:user>jsmith</cas:user>
+ //
+ // <cas:attribute name='attraStyle' value='Name-Value' />
+ // <cas:attribute name='surname' value='Smith' />
+ // <cas:attribute name='givenName' value='John' />
+ // <cas:attribute name='memberOf' value='CN=Staff,OU=Groups,DC=example,DC=edu' />
+ // <cas:attribute name='memberOf' value='CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu' />
+ //
+ // <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
+ // </cas:authenticationSuccess>
+ // </cas:serviceResponse>
+ //
+ if (!count($extra_attributes) && $success_elements->item(0)->getElementsByTagName("attribute")->length != 0) {
+ $attr_nodes = $success_elements->item(0)->getElementsByTagName("attribute");
+ $firstAttr = $attr_nodes->item(0);
+ if (!$firstAttr->hasChildNodes() && $firstAttr->hasAttribute('name') && $firstAttr->hasAttribute('value')) {
+ phpCas :: trace("Found Name-Value style attributes");
+ // Nested Attributes
+ foreach ($attr_nodes as $attr_node) {
+ if ($attr_node->hasAttribute('name') && $attr_node->hasAttribute('value')) {
+ phpCas :: trace("Attribute [".$attr_node->getAttribute('name')."] = ".$attr_node->getAttribute('value'));
+ $this->_addAttributeToArray($extra_attributes, $attr_node->getAttribute('name'), $attr_node->getAttribute('value'));
+ }
+ }
+ }
+ }
+
+ $this->setAttributes($extra_attributes);
+ phpCAS::traceEnd();
+ return true;
+ }
+
+ /**
+ * Add an attribute value to an array of attributes.
+ *
+ * @param array &$attributeArray reference to array
+ * @param string $name name of attribute
+ * @param string $value value of attribute
+ *
+ * @return void
+ */
+ private function _addAttributeToArray(array &$attributeArray, $name, $value)
+ {
+ // If multiple attributes exist, add as an array value
+ if (isset($attributeArray[$name])) {
+ // Initialize the array with the existing value
+ if (!is_array($attributeArray[$name])) {
+ $existingValue = $attributeArray[$name];
+ $attributeArray[$name] = array($existingValue);
+ }
+
+ $attributeArray[$name][] = trim($value);
+ } else {
+ $attributeArray[$name] = trim($value);
+ }
+ }
+
+ /** @} */
+
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ // XX XX
+ // XX MISC XX
+ // XX XX
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+ /**
+ * @addtogroup internalMisc
+ * @{
+ */
+
+ // ########################################################################
+ // URL
+ // ########################################################################
+ /**
+ * the URL of the current request (without any ticket CGI parameter). Written
+ * and read by CAS_Client::getURL().
+ *
+ * @hideinitializer
+ */
+ private $_url = '';
+
+
+ /**
+ * This method sets the URL of the current request
+ *
+ * @param string $url url to set for service
+ *
+ * @return void
+ */
+ public function setURL($url)
+ {
+ $this->_url = $url;
+ }
+
+ /**
+ * This method returns the URL of the current request (without any ticket
+ * CGI parameter).
+ *
+ * @return The URL
+ */
+ public function getURL()
+ {
+ phpCAS::traceBegin();
+ // the URL is built when needed only
+ if ( empty($this->_url) ) {
+ $final_uri = '';
+ // remove the ticket if present in the URL
+ $final_uri = ($this->_isHttps()) ? 'https' : 'http';
+ $final_uri .= '://';
+
+ $final_uri .= $this->_getServerUrl();
+ $request_uri = explode('?', $_SERVER['REQUEST_URI'], 2);
+ $final_uri .= $request_uri[0];
+
+ if (isset($request_uri[1]) && $request_uri[1]) {
+ $query_string= $this->_removeParameterFromQueryString('ticket', $request_uri[1]);
+
+ // If the query string still has anything left, append it to the final URI
+ if ($query_string !== '') {
+ $final_uri .= "?$query_string";
+ }
+ }
+
+ phpCAS::trace("Final URI: $final_uri");
+ $this->setURL($final_uri);
+ }
+ phpCAS::traceEnd($this->_url);
+ return $this->_url;
+ }
+
+
+ /**
+ * Try to figure out the server URL with possible Proxys / Ports etc.
+ *
+ * @return string Server URL with domain:port
+ */
+ private function _getServerUrl()
+ {
+ $server_url = '';
+ if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) {
+ // explode the host list separated by comma and use the first host
+ $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
+ $server_url = $hosts[0];
+ } else if (!empty($_SERVER['HTTP_X_FORWARDED_SERVER'])) {
+ $server_url = $_SERVER['HTTP_X_FORWARDED_SERVER'];
+ } else {
+ if (empty($_SERVER['SERVER_NAME'])) {
+ $server_url = $_SERVER['HTTP_HOST'];
+ } else {
+ $server_url = $_SERVER['SERVER_NAME'];
+ }
+ }
+ if (!strpos($server_url, ':')) {
+ if (empty($_SERVER['HTTP_X_FORWARDED_PORT'])) {
+ $server_port = $_SERVER['SERVER_PORT'];
+ } else {
+ $server_port = $_SERVER['HTTP_X_FORWARDED_PORT'];
+ }
+
+ if ( ($this->_isHttps() && $server_port!=443)
+ || (!$this->_isHttps() && $server_port!=80)
+ ) {
+ $server_url .= ':';
+ $server_url .= $server_port;
+ }
+ }
+ return $server_url;
+ }
+
+ /**
+ * This method checks to see if the request is secured via HTTPS
+ *
+ * @return bool true if https, false otherwise
+ */
+ private function _isHttps()
+ {
+ if ( isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Removes a parameter from a query string
+ *
+ * @param string $parameterName name of parameter
+ * @param string $queryString query string
+ *
+ * @return string new query string
+ *
+ * @link http://stackoverflow.com/questions/1842681/regular-expression-to-remove-one-parameter-from-query-string
+ */
+ private function _removeParameterFromQueryString($parameterName, $queryString)
+ {
+ $parameterName = preg_quote($parameterName);
+ return preg_replace("/&$parameterName(=[^&]*)?|^$parameterName(=[^&]*)?&?/", '', $queryString);
+ }
+
+ /**
+ * This method is used to append query parameters to an url. Since the url
+ * might already contain parameter it has to be detected and to build a proper
+ * URL
+ *
+ * @param string $url base url to add the query params to
+ * @param string $query params in query form with & separated
+ *
+ * @return url with query params
+ */
+ private function _buildQueryUrl($url, $query)
+ {
+ $url .= (strstr($url, '?') === false) ? '?' : '&';
+ $url .= $query;
+ return $url;
+ }
+
+ /**
+ * Renaming the session
+ *
+ * @param string $ticket name of the ticket
+ *
+ * @return void
+ */
+ private function _renameSession($ticket)
+ {
+ phpCAS::traceBegin();
+ if ($this->getChangeSessionID()) {
+ if (!empty($this->_user)) {
+ $old_session = $_SESSION;
+ session_destroy();
+ // set up a new session, of name based on the ticket
+ $session_id = preg_replace('/[^a-zA-Z0-9\-]/', '', $ticket);
+ phpCAS :: trace("Session ID: ".$session_id);
+ session_id($session_id);
+ session_start();
+ phpCAS :: trace("Restoring old session vars");
+ $_SESSION = $old_session;
+ } else {
+ phpCAS :: error('Session should only be renamed after successfull authentication');
+ }
+ } else {
+ phpCAS :: trace("Skipping session rename since phpCAS is not handling the session.");
+ }
+ phpCAS::traceEnd();
+ }
+
+
+ // ########################################################################
+ // AUTHENTICATION ERROR HANDLING
+ // ########################################################################
+ /**
+ * This method is used to print the HTML output when the user was not
+ * authenticated.
+ *
+ * @param string $failure the failure that occured
+ * @param string $cas_url the URL the CAS server was asked for
+ * @param bool $no_response the response from the CAS server (other
+ * parameters are ignored if true)
+ * @param bool $bad_response bad response from the CAS server ($err_code
+ * and $err_msg ignored if true)
+ * @param string $cas_response the response of the CAS server
+ * @param int $err_code the error code given by the CAS server
+ * @param string $err_msg the error message given by the CAS server
+ *
+ * @return void
+ */
+ private function _authError(
+ $failure,
+ $cas_url,
+ $no_response,
+ $bad_response='',
+ $cas_response='',
+ $err_code='',
+ $err_msg=''
+ ) {
+ phpCAS::traceBegin();
+ $lang = $this->getLangObj();
+ $this->printHTMLHeader($lang->getAuthenticationFailed());
+ printf($lang->getYouWereNotAuthenticated(), htmlentities($this->getURL()), $_SERVER['SERVER_ADMIN']);
+ phpCAS::trace('CAS URL: '.$cas_url);
+ phpCAS::trace('Authentication failure: '.$failure);
+ if ( $no_response ) {
+ phpCAS::trace('Reason: no response from the CAS server');
+ } else {
+ if ( $bad_response ) {
+ phpCAS::trace('Reason: bad response from the CAS server');
+ } else {
+ switch ($this->getServerVersion()) {
+ case CAS_VERSION_1_0:
+ phpCAS::trace('Reason: CAS error');
+ break;
+ case CAS_VERSION_2_0:
+ if ( empty($err_code) ) {
+ phpCAS::trace('Reason: no CAS error');
+ } else {
+ phpCAS::trace('Reason: ['.$err_code.'] CAS error: '.$err_msg);
+ }
+ break;
+ }
+ }
+ phpCAS::trace('CAS response: '.$cas_response);
+ }
+ $this->printHTMLFooter();
+ phpCAS::traceExit();
+ throw new CAS_GracefullTerminationException();
+ }
+
+ // ########################################################################
+ // PGTIOU/PGTID and logoutRequest rebroadcasting
+ // ########################################################################
+
+ /**
+ * Boolean of whether to rebroadcast pgtIou/pgtId and logoutRequest, and
+ * array of the nodes.
+ */
+ private $_rebroadcast = false;
+ private $_rebroadcast_nodes = array();
+
+ /**
+ * Constants used for determining rebroadcast node type.
+ */
+ const HOSTNAME = 0;
+ const IP = 1;
+
+ /**
+ * Determine the node type from the URL.
+ *
+ * @param String $nodeURL The node URL.
+ *
+ * @return string hostname
+ *
+ */
+ private function _getNodeType($nodeURL)
+ {
+ phpCAS::traceBegin();
+ if (preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $nodeURL)) {
+ phpCAS::traceEnd(self::IP);
+ return self::IP;
+ } else {
+ phpCAS::traceEnd(self::HOSTNAME);
+ return self::HOSTNAME;
+ }
+ }
+
+ /**
+ * Store the rebroadcast node for pgtIou/pgtId and logout requests.
+ *
+ * @param string $rebroadcastNodeUrl The rebroadcast node URL.
+ *
+ * @return void
+ */
+ public function addRebroadcastNode($rebroadcastNodeUrl)
+ {
+ // Store the rebroadcast node and set flag
+ $this->_rebroadcast = true;
+ $this->_rebroadcast_nodes[] = $rebroadcastNodeUrl;
+ }
+
+ /**
+ * An array to store extra rebroadcast curl options.
+ */
+ private $_rebroadcast_headers = array();
+
+ /**
+ * This method is used to add header parameters when rebroadcasting
+ * pgtIou/pgtId or logoutRequest.
+ *
+ * @param string $header Header to send when rebroadcasting.
+ *
+ * @return void
+ */
+ public function addRebroadcastHeader($header)
+ {
+ $this->_rebroadcast_headers[] = $header;
+ }
+
+ /**
+ * Constants used for determining rebroadcast type (logout or pgtIou/pgtId).
+ */
+ const LOGOUT = 0;
+ const PGTIOU = 1;
+
+ /**
+ * This method rebroadcasts logout/pgtIou requests. Can be LOGOUT,PGTIOU
+ *
+ * @param int $type type of rebroadcasting.
+ *
+ * @return void
+ */
+ private function _rebroadcast($type)
+ {
+ phpCAS::traceBegin();
+
+ $rebroadcast_curl_options = array(
+ CURLOPT_FAILONERROR => 1,
+ CURLOPT_FOLLOWLOCATION => 1,
+ CURLOPT_RETURNTRANSFER => 1,
+ CURLOPT_CONNECTTIMEOUT => 1,
+ CURLOPT_TIMEOUT => 4);
+
+ // Try to determine the IP address of the server
+ if (!empty($_SERVER['SERVER_ADDR'])) {
+ $ip = $_SERVER['SERVER_ADDR'];
+ } else if (!empty($_SERVER['LOCAL_ADDR'])) {
+ // IIS 7
+ $ip = $_SERVER['LOCAL_ADDR'];
+ }
+ // Try to determine the DNS name of the server
+ if (!empty($ip)) {
+ $dns = gethostbyaddr($ip);
+ }
+ $multiClassName = 'CAS_Request_CurlMultiRequest';
+ $multiRequest = new $multiClassName();
+
+ for ($i = 0; $i < sizeof($this->_rebroadcast_nodes); $i++) {
+ if ((($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::HOSTNAME) && !empty($dns) && (stripos($this->_rebroadcast_nodes[$i], $dns) === false)) || (($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::IP) && !empty($ip) && (stripos($this->_rebroadcast_nodes[$i], $ip) === false))) {
+ phpCAS::trace('Rebroadcast target URL: '.$this->_rebroadcast_nodes[$i].$_SERVER['REQUEST_URI']);
+ $className = $this->_requestImplementation;
+ $request = new $className();
+
+ $url = $this->_rebroadcast_nodes[$i].$_SERVER['REQUEST_URI'];
+ $request->setUrl($url);
+
+ if (count($this->_rebroadcast_headers)) {
+ $request->addHeaders($this->_rebroadcast_headers);
+ }
+
+ $request->makePost();
+ if ($type == self::LOGOUT) {
+ // Logout request
+ $request->setPostBody('rebroadcast=false&logoutRequest='.$_POST['logoutRequest']);
+ } else if ($type == self::PGTIOU) {
+ // pgtIou/pgtId rebroadcast
+ $request->setPostBody('rebroadcast=false');
+ }
+
+ $request->setCurlOptions($rebroadcast_curl_options);
+
+ $multiRequest->addRequest($request);
+ } else {
+ phpCAS::trace('Rebroadcast not sent to self: '.$this->_rebroadcast_nodes[$i].' == '.(!empty($ip)?$ip:'').'/'.(!empty($dns)?$dns:''));
+ }
+ }
+ // We need at least 1 request
+ if ($multiRequest->getNumRequests() > 0) {
+ $multiRequest->send();
+ }
+ phpCAS::traceEnd();
+ }
+
+ /** @} */
+}
+
+?>