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