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