Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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