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