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