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