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