Passed
Pull Request — master (#1264)
by Tim
05:33
created

lib/SimpleSAML/Utils/HTTP.php (1 issue)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Utils;
6
7
use SimpleSAML\Configuration;
8
use SimpleSAML\Error;
9
use SimpleSAML\Logger;
10
use SimpleSAML\Module;
11
use SimpleSAML\Session;
12
use SimpleSAML\XHTML\Template;
13
14
/**
15
 * HTTP-related utility methods.
16
 *
17
 * @package SimpleSAMLphp
18
 */
19
class HTTP
20
{
21
    /**
22
     * Obtain a URL where we can redirect to securely post a form with the given data to a specific destination.
23
     *
24
     * @param string $destination The destination URL.
25
     * @param array  $data An associative array containing the data to be posted to $destination.
26
     *
27
     * @throws Error\Exception If the current session is transient.
28
     * @return string  A URL which allows to securely post a form to $destination.
29
     *
30
     * @author Jaime Perez, UNINETT AS <[email protected]>
31
     */
32
    private static function getSecurePOSTRedirectURL(string $destination, array $data): string
33
    {
34
        $session = Session::getSessionFromRequest();
35
        $id = self::savePOSTData($session, $destination, $data);
36
37
        if ($session->isTransient()) {
38
            // this is a transient session, it is pointless to continue
39
            throw new Error\Exception('Cannot save POST data to a transient session.');
40
        }
41
42
        /** @var string $session_id */
43
        $session_id = $session->getSessionId();
44
45
        // encrypt the session ID and the random ID
46
        $info = base64_encode(Crypto::aesEncrypt($session_id . ':' . $id));
47
48
        $url = Module::getModuleURL('core/postredirect.php', ['RedirInfo' => $info]);
49
        return preg_replace('#^https:#', 'http:', $url);
50
    }
51
52
53
    /**
54
     * Retrieve Host value from $_SERVER environment variables.
55
     *
56
     * @return string The current host name, including the port if needed. It will use localhost when unable to
57
     *     determine the current host.
58
     *
59
     * @author Olav Morken, UNINETT AS <[email protected]>
60
     */
61
    private static function getServerHost(): string
62
    {
63
        if (array_key_exists('HTTP_HOST', $_SERVER)) {
64
            $current = $_SERVER['HTTP_HOST'];
65
        } elseif (array_key_exists('SERVER_NAME', $_SERVER)) {
66
            $current = $_SERVER['SERVER_NAME'];
67
        } else {
68
            // almost certainly not what you want, but...
69
            $current = 'localhost';
70
        }
71
72
        if (strstr($current, ":")) {
73
            $decomposed = explode(":", $current);
74
            $port = array_pop($decomposed);
75
            if (!is_numeric($port)) {
76
                array_push($decomposed, $port);
77
            }
78
            $current = implode(":", $decomposed);
79
        }
80
        return $current;
81
    }
82
83
84
    /**
85
     * Retrieve HTTPS status from $_SERVER environment variables.
86
     *
87
     * @return boolean True if the request was performed through HTTPS, false otherwise.
88
     *
89
     * @author Olav Morken, UNINETT AS <[email protected]>
90
     */
91
    public static function getServerHTTPS()
92
    {
93
        if (!array_key_exists('HTTPS', $_SERVER)) {
94
            // not an https-request
95
            return false;
96
        }
97
98
        if ($_SERVER['HTTPS'] === 'off') {
99
            // IIS with HTTPS off
100
            return false;
101
        }
102
103
        // otherwise, HTTPS will be non-empty
104
        return !empty($_SERVER['HTTPS']);
105
    }
106
107
108
    /**
109
     * Retrieve the port number from $_SERVER environment variables.
110
     *
111
     * @return string The port number prepended by a colon, if it is different than the default port for the protocol
112
     *     (80 for HTTP, 443 for HTTPS), or an empty string otherwise.
113
     *
114
     * @author Olav Morken, UNINETT AS <[email protected]>
115
     */
116
    public static function getServerPort()
117
    {
118
        $default_port = self::getServerHTTPS() ? '443' : '80';
119
        $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : $default_port;
120
121
        // Take care of edge-case where SERVER_PORT is an integer
122
        $port = strval($port);
123
124
        if ($port !== $default_port) {
125
            return ':' . $port;
126
        }
127
        return '';
128
    }
129
130
131
    /**
132
     * Verify that a given URL is valid.
133
     *
134
     * @param string $url The URL we want to verify.
135
     *
136
     * @return boolean True if the given URL is valid, false otherwise.
137
     */
138
    public static function isValidURL($url)
139
    {
140
        $url = filter_var($url, FILTER_VALIDATE_URL);
141
        if ($url === false) {
142
            return false;
143
        }
144
        $scheme = parse_url($url, PHP_URL_SCHEME);
145
        if (is_string($scheme) && in_array(strtolower($scheme), ['http', 'https'], true)) {
146
            return true;
147
        }
148
        return false;
149
    }
150
151
152
    /**
153
     * This function redirects the user to the specified address.
154
     *
155
     * This function will use the "HTTP 303 See Other" redirection if the current request used the POST method and the
156
     * HTTP version is 1.1. Otherwise, a "HTTP 302 Found" redirection will be used.
157
     *
158
     * The function will also generate a simple web page with a clickable link to the target page.
159
     *
160
     * @param string   $url The URL we should redirect to. This URL may include query parameters. If this URL is a
161
     *     relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the
162
     *     absolute URL to the root of the website.
163
     * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The
164
     *     name of the parameter is the array index. The value of the parameter is the value stored in the index. Both
165
     *     the name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just
166
     *     the name, without a value.
167
     *
168
     * @return void This function never returns.
169
     * @throws \InvalidArgumentException If $url is not a string or is empty, or $parameters is not an array.
170
     * @throws \SimpleSAML\Error\Exception If $url is not a valid HTTP URL.
171
     *
172
     * @author Olav Morken, UNINETT AS <[email protected]>
173
     * @author Mads Freek Petersen
174
     * @author Jaime Perez, UNINETT AS <[email protected]>
175
     */
176
    private static function redirect(string $url, array $parameters = [])
177
    {
178
        if (empty($url)) {
179
            throw new \InvalidArgumentException('Invalid input parameters.');
180
        }
181
        if (!self::isValidURL($url)) {
182
            throw new Error\Exception('Invalid destination URL.');
183
        }
184
185
        if (!empty($parameters)) {
186
            $url = self::addURLParameters($url, $parameters);
187
        }
188
189
        /* Set the HTTP result code. This is either 303 See Other or
190
         * 302 Found. HTTP 303 See Other is sent if the HTTP version
191
         * is HTTP/1.1 and the request type was a POST request.
192
         */
193
        if (
194
            $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1'
195
            && $_SERVER['REQUEST_METHOD'] === 'POST'
196
        ) {
197
            $code = 303;
198
        } else {
199
            $code = 302;
200
        }
201
202
        if (strlen($url) > 2048) {
203
            Logger::warning('Redirecting to a URL longer than 2048 bytes.');
204
        }
205
206
        if (!headers_sent()) {
207
            // set the location header
208
            header('Location: ' . $url, true, $code);
0 ignored issues
show
Security Response Splitting introduced by
'Location: ' . $url can contain request data and is used in response header context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and HTTP::redirectUntrustedURL() is called
    in modules/core/www/login-admin.php on line 12
  2. Enters via parameter $url
    in lib/SimpleSAML/Utils/HTTP.php on line 1040
  3. Data is passed through checkURLAllowed(), and self::checkURLAllowed($url) is assigned to $url
    in lib/SimpleSAML/Utils/HTTP.php on line 1046
  4. HTTP::redirect() is called
    in lib/SimpleSAML/Utils/HTTP.php on line 1047
  5. Enters via parameter $url
    in lib/SimpleSAML/Utils/HTTP.php on line 176

Response Splitting Attacks

Allowing an attacker to set a response header, opens your application to response splitting attacks; effectively allowing an attacker to send any response, he would like.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
209
210
            // disable caching of this response
211
            header('Pragma: no-cache');
212
            header('Cache-Control: no-cache, no-store, must-revalidate');
213
        }
214
215
        // show a minimal web page with a clickable link to the URL
216
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
217
        echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"';
218
        echo ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n";
219
        echo '<html xmlns="http://www.w3.org/1999/xhtml">' . "\n";
220
        echo "  <head>\n";
221
        echo '    <meta http-equiv="content-type" content="text/html; charset=utf-8">' . "\n";
222
        echo '    <meta http-equiv="refresh" content="0;URL=\'' . htmlspecialchars($url) . '\'">' . "\n";
223
        echo "    <title>Redirect</title>\n";
224
        echo "  </head>\n";
225
        echo "  <body>\n";
226
        echo "    <h1>Redirect</h1>\n";
227
        echo '      <p>You were redirected to: <a id="redirlink" href="' . htmlspecialchars($url) . '">';
228
        echo htmlspecialchars($url) . "</a>\n";
229
        echo '        <script type="text/javascript">document.getElementById("redirlink").focus();</script>' . "\n";
230
        echo "      </p>\n";
231
        echo "  </body>\n";
232
        echo '</html>';
233
234
        // end script execution
235
        exit;
236
    }
237
238
239
    /**
240
     * Save the given HTTP POST data and the destination where it should be posted to a given session.
241
     *
242
     * @param \SimpleSAML\Session $session The session where to temporarily store the data.
243
     * @param string              $destination The destination URL where the form should be posted.
244
     * @param array               $data An associative array with the data to be posted to $destination.
245
     *
246
     * @return string A random identifier that can be used to retrieve the data from the current session.
247
     *
248
     * @author Andjelko Horvat
249
     * @author Jaime Perez, UNINETT AS <[email protected]>
250
     */
251
    private static function savePOSTData(Session $session, string $destination, array $data): string
252
    {
253
        // generate a random ID to avoid replay attacks
254
        $id = Random::generateID();
255
        $postData = [
256
            'post' => $data,
257
            'url'  => $destination,
258
        ];
259
260
        // save the post data to the session, tied to the random ID
261
        $session->setData('core_postdatalink', $id, $postData);
262
263
        return $id;
264
    }
265
266
267
    /**
268
     * Add one or more query parameters to the given URL.
269
     *
270
     * @param string $url The URL the query parameters should be added to.
271
     * @param array  $parameters The query parameters which should be added to the url. This should be an associative
272
     *     array.
273
     *
274
     * @return string The URL with the new query parameters.
275
     * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array.
276
     *
277
     * @author Andreas Solberg, UNINETT AS <[email protected]>
278
     * @author Olav Morken, UNINETT AS <[email protected]>
279
     */
280
    public static function addURLParameters($url, $parameters)
281
    {
282
        if (!is_string($url) || !is_array($parameters)) {
283
            throw new \InvalidArgumentException('Invalid input parameters.');
284
        }
285
286
        $queryStart = strpos($url, '?');
287
        if ($queryStart === false) {
288
            $oldQuery = [];
289
            $url .= '?';
290
        } else {
291
            /** @var string|false $oldQuery */
292
            $oldQuery = substr($url, $queryStart + 1);
293
            if ($oldQuery === false) {
294
                $oldQuery = [];
295
            } else {
296
                $oldQuery = self::parseQueryString($oldQuery);
297
            }
298
            $url = substr($url, 0, $queryStart + 1);
299
        }
300
301
        /** @var array $oldQuery */
302
        $query = array_merge($oldQuery, $parameters);
303
        $url .= http_build_query($query, '', '&');
304
305
        return $url;
306
    }
307
308
309
    /**
310
     * Check for session cookie, and show missing-cookie page if it is missing.
311
     *
312
     * @param string|null $retryURL The URL the user should access to retry the operation. Defaults to null.
313
     *
314
     * @return void If there is a session cookie, nothing will be returned. Otherwise, the user will be redirected to a
315
     *     page telling about the missing cookie.
316
     * @throws \InvalidArgumentException If $retryURL is neither a string nor null.
317
     *
318
     * @author Olav Morken, UNINETT AS <[email protected]>
319
     */
320
    public static function checkSessionCookie($retryURL = null)
321
    {
322
        if (!is_null($retryURL) && !is_string($retryURL)) {
323
            throw new \InvalidArgumentException('Invalid input parameters.');
324
        }
325
326
        $session = Session::getSessionFromRequest();
327
        if ($session->hasSessionCookie()) {
328
            return;
329
        }
330
331
        // we didn't have a session cookie. Redirect to the no-cookie page
332
333
        $url = Module::getModuleURL('core/no_cookie.php');
334
        if ($retryURL !== null) {
335
            $url = self::addURLParameters($url, ['retryURL' => $retryURL]);
336
        }
337
        self::redirectTrustedURL($url);
338
    }
339
340
341
    /**
342
     * Check if a URL is valid and is in our list of allowed URLs.
343
     *
344
     * @param string $url The URL to check.
345
     * @param array  $trustedSites An optional white list of domains. If none specified, the 'trusted.url.domains'
346
     * configuration directive will be used.
347
     *
348
     * @return string The normalized URL itself if it is allowed. An empty string if the $url parameter is empty as
349
     * defined by the empty() function.
350
     * @throws \InvalidArgumentException If the URL is malformed.
351
     * @throws Error\Exception If the URL is not allowed by configuration.
352
     *
353
     * @author Jaime Perez, UNINETT AS <[email protected]>
354
     */
355
    public static function checkURLAllowed($url, array $trustedSites = null)
356
    {
357
        if (empty($url)) {
358
            return '';
359
        }
360
        $url = self::normalizeURL($url);
361
362
        if (!self::isValidURL($url)) {
363
            throw new Error\Exception('Invalid URL: ' . $url);
364
        }
365
366
        // get the white list of domains
367
        if ($trustedSites === null) {
368
            $trustedSites = Configuration::getInstance()->getValue('trusted.url.domains', []);
369
        }
370
371
        // validates the URL's host is among those allowed
372
        if (is_array($trustedSites)) {
373
            $components = parse_url($url);
374
            $hostname = $components['host'];
375
376
            // check for userinfo
377
            if (
378
                (isset($components['user'])
379
                && strpos($components['user'], '\\') !== false)
380
                || (isset($components['pass'])
381
                && strpos($components['pass'], '\\') !== false)
382
            ) {
383
                throw new Error\Exception('Invalid URL: ' . $url);
384
            }
385
386
            // allow URLs with standard ports specified (non-standard ports must then be allowed explicitly)
387
            if (
388
                isset($components['port'])
389
                && (($components['scheme'] === 'http'
390
                && $components['port'] !== 80)
391
                || ($components['scheme'] === 'https'
392
                && $components['port'] !== 443))
393
            ) {
394
                $hostname = $hostname . ':' . $components['port'];
395
            }
396
397
            $self_host = self::getSelfHostWithNonStandardPort();
398
399
            $trustedRegex = Configuration::getInstance()->getValue('trusted.url.regex', false);
400
401
            $trusted = false;
402
            if ($trustedRegex) {
403
                // add self host to the white list
404
                $trustedSites[] = preg_quote($self_host);
405
                foreach ($trustedSites as $regex) {
406
                    // Add start and end delimiters.
407
                    $regex = "@^{$regex}$@";
408
                    if (preg_match($regex, $hostname)) {
409
                        $trusted = true;
410
                        break;
411
                    }
412
                }
413
            } else {
414
                // add self host to the white list
415
                $trustedSites[] = $self_host;
416
                $trusted = in_array($hostname, $trustedSites, true);
417
            }
418
419
            // throw exception due to redirection to untrusted site
420
            if (!$trusted) {
421
                throw new Error\Exception('URL not allowed: ' . $url);
422
            }
423
        }
424
        return $url;
425
    }
426
427
428
    /**
429
     * Helper function to retrieve a file or URL with proxy support, also
430
     * supporting proxy basic authorization..
431
     *
432
     * An exception will be thrown if we are unable to retrieve the data.
433
     *
434
     * @param string  $url The path or URL we should fetch.
435
     * @param array   $context Extra context options. This parameter is optional.
436
     * @param boolean $getHeaders Whether to also return response headers. Optional.
437
     *
438
     * @return string|array An array if $getHeaders is set, containing the data and the headers respectively; string
439
     *  otherwise.
440
     * @throws \InvalidArgumentException If the input parameters are invalid.
441
     * @throws Error\Exception If the file or URL cannot be retrieved.
442
     *
443
     * @author Andjelko Horvat
444
     * @author Olav Morken, UNINETT AS <[email protected]>
445
     * @author Marco Ferrante, University of Genova <[email protected]>
446
     */
447
    public static function fetch($url, $context = [], $getHeaders = false)
448
    {
449
        if (!is_string($url)) {
450
            throw new \InvalidArgumentException('Invalid input parameters.');
451
        }
452
453
        $config = Configuration::getInstance();
454
455
        $proxy = $config->getString('proxy', null);
456
        if ($proxy !== null) {
457
            if (!isset($context['http']['proxy'])) {
458
                $context['http']['proxy'] = $proxy;
459
            }
460
            $proxy_auth = $config->getString('proxy.auth', false);
461
            if ($proxy_auth !== false) {
462
                $context['http']['header'] = "Proxy-Authorization: Basic " . base64_encode($proxy_auth);
463
            }
464
            if (!isset($context['http']['request_fulluri'])) {
465
                $context['http']['request_fulluri'] = true;
466
            }
467
            /*
468
             * If the remote endpoint over HTTPS uses the SNI extension (Server Name Indication RFC 4366), the proxy
469
             * could introduce a mismatch between the names in the Host: HTTP header and the SNI_server_name in TLS
470
             * negotiation (thanks to Cristiano Valli @ GARR-IDEM to have pointed this problem).
471
             * See: https://bugs.php.net/bug.php?id=63519
472
             * These controls will force the same value for both fields.
473
             * Marco Ferrante ([email protected]), Nov 2012
474
             */
475
            if (
476
                preg_match('#^https#i', $url)
477
                && defined('OPENSSL_TLSEXT_SERVER_NAME')
478
                && OPENSSL_TLSEXT_SERVER_NAME
479
            ) {
480
                // extract the hostname
481
                $hostname = parse_url($url, PHP_URL_HOST);
482
                if (!empty($hostname)) {
483
                    $context['ssl'] = [
484
                        'SNI_server_name' => $hostname,
485
                        'SNI_enabled'     => true,
486
                    ];
487
                } else {
488
                    Logger::warning('Invalid URL format or local URL used through a proxy');
489
                }
490
            }
491
        }
492
493
        $context = stream_context_create($context);
494
        $data = @file_get_contents($url, false, $context);
495
        if ($data === false) {
496
            $error = error_get_last();
497
            throw new Error\Exception('Error fetching ' . var_export($url, true) . ':' .
498
                (is_array($error) ? $error['message'] : 'no error available'));
499
        }
500
501
        // data and headers
502
        if ($getHeaders) {
503
            /**
504
             * @psalm-suppress UndefinedVariable    Remove when Psalm >= 3.0.17
505
             */
506
            if (!empty($http_response_header)) {
507
                $headers = [];
508
                /**
509
                 * @psalm-suppress UndefinedVariable    Remove when Psalm >= 3.0.17
510
                 */
511
                foreach ($http_response_header as $h) {
512
                    if (preg_match('@^HTTP/1\.[01]\s+\d{3}\s+@', $h)) {
513
                        $headers = []; // reset
514
                        $headers[0] = $h;
515
                        continue;
516
                    }
517
                    $bits = explode(':', $h, 2);
518
                    if (count($bits) === 2) {
519
                        $headers[strtolower($bits[0])] = trim($bits[1]);
520
                    }
521
                }
522
            } else {
523
                // no HTTP headers, probably a different protocol, e.g. file
524
                $headers = null;
525
            }
526
            return [$data, $headers];
527
        }
528
529
        return $data;
530
    }
531
532
533
    /**
534
     * This function parses the Accept-Language HTTP header and returns an associative array with each language and the
535
     * score for that language. If a language includes a region, then the result will include both the language with
536
     * the region and the language without the region.
537
     *
538
     * The returned array will be in the same order as the input.
539
     *
540
     * @return array An associative array with each language and the score for that language.
541
     *
542
     * @author Olav Morken, UNINETT AS <[email protected]>
543
     */
544
    public static function getAcceptLanguage()
545
    {
546
        if (!array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) {
547
            // no Accept-Language header, return an empty set
548
            return [];
549
        }
550
551
        $languages = explode(',', strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']));
552
553
        $ret = [];
554
555
        foreach ($languages as $l) {
556
            $opts = explode(';', $l);
557
558
            $l = trim(array_shift($opts)); // the language is the first element
559
560
            $q = 1.0;
561
562
            // iterate over all options, and check for the quality option
563
            foreach ($opts as $o) {
564
                $o = explode('=', $o);
565
                if (count($o) < 2) {
566
                    // skip option with no value
567
                    continue;
568
                }
569
570
                $name = trim($o[0]);
571
                $value = trim($o[1]);
572
573
                if ($name === 'q') {
574
                    $q = (float) $value;
575
                }
576
            }
577
578
            // remove the old key to ensure that the element is added to the end
579
            unset($ret[$l]);
580
581
            // set the quality in the result
582
            $ret[$l] = $q;
583
584
            if (strpos($l, '-')) {
585
                // the language includes a region part
586
587
                // extract the language without the region
588
                $l = explode('-', $l);
589
                $l = $l[0];
590
591
                // add this language to the result (unless it is defined already)
592
                if (!array_key_exists($l, $ret)) {
593
                    $ret[$l] = $q;
594
                }
595
            }
596
        }
597
        return $ret;
598
    }
599
600
601
    /**
602
     * Try to guess the base SimpleSAMLphp path from the current request.
603
     *
604
     * This method offers just a guess, so don't rely on it.
605
     *
606
     * @return string The guessed base path that should correspond to the root installation of SimpleSAMLphp.
607
     */
608
    public static function guessBasePath()
609
    {
610
        if (!array_key_exists('REQUEST_URI', $_SERVER) || !array_key_exists('SCRIPT_FILENAME', $_SERVER)) {
611
            return '/';
612
        }
613
        // get the name of the current script
614
        $path = explode('/', $_SERVER['SCRIPT_FILENAME']);
615
        $script = array_pop($path);
616
617
        // get the portion of the URI up to the script, i.e.: /simplesaml/some/directory/script.php
618
        if (!preg_match('#^/(?:[^/]+/)*' . $script . '#', $_SERVER['REQUEST_URI'], $matches)) {
619
            return '/';
620
        }
621
        $uri_s = explode('/', $matches[0]);
622
        $file_s = explode('/', $_SERVER['SCRIPT_FILENAME']);
623
624
        // compare both arrays from the end, popping elements matching out of them
625
        while ($uri_s[count($uri_s) - 1] === $file_s[count($file_s) - 1]) {
626
            array_pop($uri_s);
627
            array_pop($file_s);
628
        }
629
        // we are now left with the minimum part of the URI that does not match anything in the file system, use it
630
        return join('/', $uri_s) . '/';
631
    }
632
633
634
    /**
635
     * Retrieve the base URL of the SimpleSAMLphp installation. The URL will always end with a '/'. For example:
636
     *      https://idp.example.org/simplesaml/
637
     *
638
     * @return string The absolute base URL for the SimpleSAMLphp installation.
639
     * @throws \SimpleSAML\Error\CriticalConfigurationError If 'baseurlpath' has an invalid format.
640
     *
641
     * @author Olav Morken, UNINETT AS <[email protected]>
642
     */
643
    public static function getBaseURL()
644
    {
645
        $globalConfig = Configuration::getInstance();
646
        $baseURL = $globalConfig->getString('baseurlpath', 'simplesaml/');
647
648
        if (preg_match('#^https?://.*/?$#D', $baseURL, $matches)) {
649
            // full URL in baseurlpath, override local server values
650
            return rtrim($baseURL, '/') . '/';
651
        } elseif (
652
            (preg_match('#^/?([^/]?.*/)$#D', $baseURL, $matches))
653
            || (preg_match('#^\*(.*)/$#D', $baseURL, $matches))
654
            || ($baseURL === '')
655
        ) {
656
            // get server values
657
            $protocol = 'http';
658
            $protocol .= (self::getServerHTTPS()) ? 's' : '';
659
            $protocol .= '://';
660
661
            $hostname = self::getServerHost();
662
            $port = self::getServerPort();
663
            $path = $globalConfig->getBasePath();
664
665
            return $protocol . $hostname . $port . $path;
666
        } else {
667
            /*
668
             * Invalid 'baseurlpath'. We cannot recover from this, so throw a critical exception and try to be graceful
669
             * with the configuration. Use a guessed base path instead of the one provided.
670
             */
671
            $c = $globalConfig->toArray();
672
            $c['baseurlpath'] = self::guessBasePath();
673
            throw new Error\CriticalConfigurationError(
674
                'Invalid value for \'baseurlpath\' in config.php. Valid format is in the form: ' .
675
                '[(http|https)://(hostname|fqdn)[:port]]/[path/to/simplesaml/]. It must end with a \'/\'.',
676
                null,
677
                $c
678
            );
679
        }
680
    }
681
682
683
    /**
684
     * Retrieve the first element of the URL path.
685
     *
686
     * @param boolean $leadingSlash Whether to add a leading slash to the element or not. Defaults to true.
687
     *
688
     * @return string The first element of the URL path, with an optional, leading slash.
689
     *
690
     * @author Andreas Solberg, UNINETT AS <[email protected]>
691
     */
692
    public static function getFirstPathElement($leadingSlash = true)
693
    {
694
        if (preg_match('|^/(.*?)/|', $_SERVER['SCRIPT_NAME'], $matches)) {
695
            return ($leadingSlash ? '/' : '') . $matches[1];
696
        }
697
        return '';
698
    }
699
700
701
    /**
702
     * Create a link which will POST data.
703
     *
704
     * @param string $destination The destination URL.
705
     * @param array  $data The name-value pairs which will be posted to the destination.
706
     *
707
     * @return string  A URL which can be accessed to post the data.
708
     * @throws \InvalidArgumentException If $destination is not a string or $data is not an array.
709
     *
710
     * @author Andjelko Horvat
711
     * @author Jaime Perez, UNINETT AS <[email protected]>
712
     */
713
    public static function getPOSTRedirectURL($destination, $data)
714
    {
715
        if (!is_string($destination) || !is_array($data)) {
716
            throw new \InvalidArgumentException('Invalid input parameters.');
717
        }
718
719
        $config = Configuration::getInstance();
720
        $allowed = $config->getBoolean('enable.http_post', false);
721
722
        if ($allowed && preg_match("#^http:#", $destination) && self::isHTTPS()) {
723
            // we need to post the data to HTTP
724
            $url = self::getSecurePOSTRedirectURL($destination, $data);
725
        } else {
726
            // post the data directly
727
            $session = Session::getSessionFromRequest();
728
            $id = self::savePOSTData($session, $destination, $data);
729
            $url = Module::getModuleURL('core/postredirect.php', ['RedirId' => $id]);
730
        }
731
732
        return $url;
733
    }
734
735
736
    /**
737
     * Retrieve our own host.
738
     *
739
     * E.g. www.example.com
740
     *
741
     * @return string The current host.
742
     *
743
     * @author Jaime Perez, UNINETT AS <[email protected]>
744
     */
745
    public static function getSelfHost()
746
    {
747
        $decomposed = explode(':', self::getSelfHostWithNonStandardPort());
748
        return array_shift($decomposed);
749
    }
750
751
    /**
752
     * Retrieve our own host, including the port in case the it is not standard for the protocol in use. That is port
753
     * 80 for HTTP and port 443 for HTTPS.
754
     *
755
     * E.g. www.example.com:8080
756
     *
757
     * @return string The current host, followed by a colon and the port number, in case the port is not standard for
758
     * the protocol.
759
     *
760
     * @author Andreas Solberg, UNINETT AS <[email protected]>
761
     * @author Olav Morken, UNINETT AS <[email protected]>
762
     */
763
    public static function getSelfHostWithNonStandardPort()
764
    {
765
        $url = self::getBaseURL();
766
767
        /** @var int $colon getBaseURL() will allways return a valid URL */
768
        $colon = strpos($url, '://');
769
        $start = $colon + 3;
770
        $length = strcspn($url, '/', $start);
771
772
        return substr($url, $start, $length);
773
    }
774
775
776
    /**
777
     * Retrieve our own host together with the URL path. Please note this function will return the base URL for the
778
     * current SP, as defined in the global configuration.
779
     *
780
     * @return string The current host (with non-default ports included) plus the URL path.
781
     *
782
     * @author Andreas Solberg, UNINETT AS <[email protected]>
783
     * @author Olav Morken, UNINETT AS <[email protected]>
784
     */
785
    public static function getSelfHostWithPath()
786
    {
787
        $baseurl = explode("/", self::getBaseURL());
788
        $elements = array_slice($baseurl, 3 - count($baseurl), count($baseurl) - 4);
789
        $path = implode("/", $elements);
790
        return self::getSelfHostWithNonStandardPort() . "/" . $path;
791
    }
792
793
794
    /**
795
     * Retrieve the current URL using the base URL in the configuration, if possible.
796
     *
797
     * This method will try to see if the current script is part of SimpleSAMLphp. In that case, it will use the
798
     * 'baseurlpath' configuration option to rebuild the current URL based on that. If the current script is NOT
799
     * part of SimpleSAMLphp, it will just return the current URL.
800
     *
801
     * Note that this method does NOT make use of the HTTP X-Forwarded-* set of headers.
802
     *
803
     * @return string The current URL, including query parameters.
804
     *
805
     * @author Andreas Solberg, UNINETT AS <[email protected]>
806
     * @author Olav Morken, UNINETT AS <[email protected]>
807
     * @author Jaime Perez, UNINETT AS <[email protected]>
808
     */
809
    public static function getSelfURL()
810
    {
811
        $cfg = Configuration::getInstance();
812
        $baseDir = $cfg->getBaseDir();
813
        $cur_path = realpath($_SERVER['SCRIPT_FILENAME']);
814
        // make sure we got a string from realpath()
815
        $cur_path = is_string($cur_path) ? $cur_path : '';
816
        // find the path to the current script relative to the www/ directory of SimpleSAMLphp
817
        $rel_path = str_replace($baseDir . 'www' . DIRECTORY_SEPARATOR, '', $cur_path);
818
        // convert that relative path to an HTTP query
819
        $url_path = str_replace(DIRECTORY_SEPARATOR, '/', $rel_path);
820
        // find where the relative path starts in the current request URI
821
        $uri_pos = (!empty($url_path)) ? strpos($_SERVER['REQUEST_URI'] ?? '', $url_path) : false;
822
823
        if ($cur_path == $rel_path || $uri_pos === false) {
824
            /*
825
             * We were accessed from an external script. This can happen in the following cases:
826
             *
827
             * - $_SERVER['SCRIPT_FILENAME'] points to a script that doesn't exist. E.g. functional testing. In this
828
             *   case, realpath() returns false and str_replace an empty string, so we compare them loosely.
829
             *
830
             * - The URI requested does not belong to a script in the www/ directory of SimpleSAMLphp. In that case,
831
             *   removing SimpleSAMLphp's base dir from the current path yields the same path, so $cur_path and
832
             *   $rel_path are equal.
833
             *
834
             * - The request URI does not match the current script. Even if the current script is located in the www/
835
             *   directory of SimpleSAMLphp, the URI does not contain its relative path, and $uri_pos is false.
836
             *
837
             * It doesn't matter which one of those cases we have. We just know we can't apply our base URL to the
838
             * current URI, so we need to build it back from the PHP environment, unless we have a base URL specified
839
             * for this case in the configuration. First, check if that's the case.
840
             */
841
842
            /** @var \SimpleSAML\Configuration $appcfg */
843
            $appcfg = $cfg->getConfigItem('application');
844
            $appurl = $appcfg->getString('baseURL', '');
845
            if (!empty($appurl)) {
846
                $protocol = parse_url($appurl, PHP_URL_SCHEME);
847
                $hostname = parse_url($appurl, PHP_URL_HOST);
848
                $port = parse_url($appurl, PHP_URL_PORT);
849
                $port = !empty($port) ? ':' . $port : '';
850
            } else {
851
                // no base URL specified for app, just use the current URL
852
                $protocol = self::getServerHTTPS() ? 'https' : 'http';
853
                $hostname = self::getServerHost();
854
                $port = self::getServerPort();
855
            }
856
            return $protocol . '://' . $hostname . $port . $_SERVER['REQUEST_URI'];
857
        }
858
859
        return self::getBaseURL() . $url_path . substr($_SERVER['REQUEST_URI'], $uri_pos + strlen($url_path));
860
    }
861
862
863
    /**
864
     * Retrieve the current URL using the base URL in the configuration, containing the protocol, the host and
865
     * optionally, the port number.
866
     *
867
     * @return string The current URL without path or query parameters.
868
     *
869
     * @author Andreas Solberg, UNINETT AS <[email protected]>
870
     * @author Olav Morken, UNINETT AS <[email protected]>
871
     */
872
    public static function getSelfURLHost()
873
    {
874
        $url = self::getSelfURL();
875
876
        /** @var int $colon getBaseURL() will allways return a valid URL */
877
        $colon = strpos($url, '://');
878
        $start = $colon + 3;
879
        $length = strcspn($url, '/', $start) + $start;
880
        return substr($url, 0, $length);
881
    }
882
883
884
    /**
885
     * Retrieve the current URL using the base URL in the configuration, without the query parameters.
886
     *
887
     * @return string The current URL, not including query parameters.
888
     *
889
     * @author Andreas Solberg, UNINETT AS <[email protected]>
890
     * @author Jaime Perez, UNINETT AS <[email protected]>
891
     */
892
    public static function getSelfURLNoQuery()
893
    {
894
        $url = self::getSelfURL();
895
        $pos = strpos($url, '?');
896
        if (!$pos) {
897
            return $url;
898
        }
899
        return substr($url, 0, $pos);
900
    }
901
902
903
    /**
904
     * This function checks if we are using HTTPS as protocol.
905
     *
906
     * @return boolean True if the HTTPS is used, false otherwise.
907
     *
908
     * @author Olav Morken, UNINETT AS <[email protected]>
909
     * @author Jaime Perez, UNINETT AS <[email protected]>
910
     */
911
    public static function isHTTPS()
912
    {
913
        return strpos(self::getSelfURL(), 'https://') === 0;
914
    }
915
916
917
    /**
918
     * Normalizes a URL to an absolute URL and validate it. In addition to resolving the URL, this function makes sure
919
     * that it is a link to an http or https site.
920
     *
921
     * @param string $url The relative URL.
922
     *
923
     * @return string An absolute URL for the given relative URL.
924
     * @throws \InvalidArgumentException If $url is not a string or a valid URL.
925
     *
926
     * @author Olav Morken, UNINETT AS <[email protected]>
927
     * @author Jaime Perez, UNINETT AS <[email protected]>
928
     */
929
    public static function normalizeURL($url)
930
    {
931
        if (!is_string($url)) {
932
            throw new \InvalidArgumentException('Invalid input parameters.');
933
        }
934
935
        $url = self::resolveURL($url, self::getSelfURL());
936
937
        // verify that the URL is to a http or https site
938
        if (!preg_match('@^https?://@i', $url)) {
939
            throw new \InvalidArgumentException('Invalid URL: ' . $url);
940
        }
941
942
        return $url;
943
    }
944
945
946
    /**
947
     * Parse a query string into an array.
948
     *
949
     * This function parses a query string into an array, similar to the way the builtin 'parse_str' works, except it
950
     * doesn't handle arrays, and it doesn't do "magic quotes".
951
     *
952
     * Query parameters without values will be set to an empty string.
953
     *
954
     * @param string $query_string The query string which should be parsed.
955
     *
956
     * @return array The query string as an associative array.
957
     * @throws \InvalidArgumentException If $query_string is not a string.
958
     *
959
     * @author Olav Morken, UNINETT AS <[email protected]>
960
     */
961
    public static function parseQueryString($query_string)
962
    {
963
        if (!is_string($query_string)) {
964
            throw new \InvalidArgumentException('Invalid input parameters.');
965
        }
966
967
        $res = [];
968
        if (empty($query_string)) {
969
            return $res;
970
        }
971
972
        foreach (explode('&', $query_string) as $param) {
973
            $param = explode('=', $param);
974
            $name = urldecode($param[0]);
975
            if (count($param) === 1) {
976
                $value = '';
977
            } else {
978
                $value = urldecode($param[1]);
979
            }
980
            $res[$name] = $value;
981
        }
982
        return $res;
983
    }
984
985
986
    /**
987
     * This function redirects to the specified URL without performing any security checks. Please, do NOT use this
988
     * function with user supplied URLs.
989
     *
990
     * This function will use the "HTTP 303 See Other" redirection if the current request used the POST method and the
991
     * HTTP version is 1.1. Otherwise, a "HTTP 302 Found" redirection will be used.
992
     *
993
     * The function will also generate a simple web page with a clickable  link to the target URL.
994
     *
995
     * @param string   $url The URL we should redirect to. This URL may include query parameters. If this URL is a
996
     * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the absolute
997
     * URL to the root of the website.
998
     * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The
999
     * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both the
1000
     * name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just the
1001
     * name, without a value.
1002
     *
1003
     * @return void This function never returns.
1004
     * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array.
1005
     *
1006
     * @author Jaime Perez, UNINETT AS <[email protected]>
1007
     */
1008
    public static function redirectTrustedURL($url, $parameters = [])
1009
    {
1010
        if (!is_string($url) || !is_array($parameters)) {
1011
            throw new \InvalidArgumentException('Invalid input parameters.');
1012
        }
1013
1014
        $url = self::normalizeURL($url);
1015
        self::redirect($url, $parameters);
1016
    }
1017
1018
1019
    /**
1020
     * This function redirects to the specified URL after performing the appropriate security checks on it.
1021
     * Particularly, it will make sure that the provided URL is allowed by the 'trusted.url.domains' directive in the
1022
     * configuration.
1023
     *
1024
     * If the aforementioned option is not set or the URL does correspond to a trusted site, it performs a redirection
1025
     * to it. If the site is not trusted, an exception will be thrown.
1026
     *
1027
     * @param string   $url The URL we should redirect to. This URL may include query parameters. If this URL is a
1028
     * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the absolute
1029
     * URL to the root of the website.
1030
     * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The
1031
     * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both the
1032
     * name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just the
1033
     * name, without a value.
1034
     *
1035
     * @return void This function never returns.
1036
     * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array.
1037
     *
1038
     * @author Jaime Perez, UNINETT AS <[email protected]>
1039
     */
1040
    public static function redirectUntrustedURL($url, $parameters = [])
1041
    {
1042
        if (!is_string($url) || !is_array($parameters)) {
1043
            throw new \InvalidArgumentException('Invalid input parameters.');
1044
        }
1045
1046
        $url = self::checkURLAllowed($url);
1047
        self::redirect($url, $parameters);
1048
    }
1049
1050
1051
    /**
1052
     * Resolve a (possibly relative) URL relative to a given base URL.
1053
     *
1054
     * This function supports these forms of relative URLs:
1055
     * - ^\w+: Absolute URL. E.g. "http://www.example.com:port/path?query#fragment".
1056
     * - ^// Same protocol. E.g. "//www.example.com:port/path?query#fragment"
1057
     * - ^/ Same protocol and host. E.g. "/path?query#fragment".
1058
     * - ^? Same protocol, host and path, replace query string & fragment. E.g. "?query#fragment".
1059
     * - ^# Same protocol, host, path and query, replace fragment. E.g. "#fragment".
1060
     * - The rest: Relative to the base path.
1061
     *
1062
     * @param string $url The relative URL.
1063
     * @param string $base The base URL. Defaults to the base URL of this installation of SimpleSAMLphp.
1064
     *
1065
     * @return string An absolute URL for the given relative URL.
1066
     * @throws \InvalidArgumentException If the base URL cannot be parsed into a valid URL, or the given parameters
1067
     *     are not strings.
1068
     *
1069
     * @author Olav Morken, UNINETT AS <[email protected]>
1070
     * @author Jaime Perez, UNINETT AS <[email protected]>
1071
     */
1072
    public static function resolveURL($url, $base = null)
1073
    {
1074
        if ($base === null) {
1075
            $base = self::getBaseURL();
1076
        }
1077
1078
        if (!is_string($url) || !is_string($base)) {
1079
            throw new \InvalidArgumentException('Invalid input parameters.');
1080
        }
1081
1082
        if (!preg_match('/^((((\w+:)\/\/[^\/]+)(\/[^?#]*))(?:\?[^#]*)?)(?:#.*)?/', $base, $baseParsed)) {
1083
            throw new \InvalidArgumentException('Unable to parse base url: ' . $base);
1084
        }
1085
1086
        $baseDir = dirname($baseParsed[5] . 'filename');
1087
        $baseScheme = $baseParsed[4];
1088
        $baseHost = $baseParsed[3];
1089
        $basePath = $baseParsed[2];
1090
        $baseQuery = $baseParsed[1];
1091
1092
        if (preg_match('$^\w+:$', $url)) {
1093
            return $url;
1094
        }
1095
1096
        if (substr($url, 0, 2) === '//') {
1097
            return $baseScheme . $url;
1098
        }
1099
1100
        if ($url[0] === '/') {
1101
            return $baseHost . $url;
1102
        }
1103
        if ($url[0] === '?') {
1104
            return $basePath . $url;
1105
        }
1106
        if ($url[0] === '#') {
1107
            return $baseQuery . $url;
1108
        }
1109
1110
        // we have a relative path. Remove query string/fragment and save it as $tail
1111
        $queryPos = strpos($url, '?');
1112
        $fragmentPos = strpos($url, '#');
1113
        if ($queryPos !== false || $fragmentPos !== false) {
1114
            if ($queryPos === false) {
1115
                $tailPos = $fragmentPos;
1116
            } elseif ($fragmentPos === false) {
1117
                $tailPos = $queryPos;
1118
            } elseif ($queryPos < $fragmentPos) {
1119
                $tailPos = $queryPos;
1120
            } else {
1121
                $tailPos = $fragmentPos;
1122
            }
1123
1124
            $tail = substr($url, $tailPos);
1125
            $dir = substr($url, 0, $tailPos);
1126
        } else {
1127
            $dir = $url;
1128
            $tail = '';
1129
        }
1130
1131
        $dir = System::resolvePath($dir, $baseDir);
1132
1133
        return $baseHost . $dir . $tail;
1134
    }
1135
1136
1137
    /**
1138
     * Set a cookie.
1139
     *
1140
     * @param string      $name The name of the cookie.
1141
     * @param string|NULL $value The value of the cookie. Set to NULL to delete the cookie.
1142
     * @param array|NULL  $params Cookie parameters.
1143
     * @param bool        $throw Whether to throw exception if setcookie() fails.
1144
     *
1145
     * @throws \InvalidArgumentException If any parameter has an incorrect type.
1146
     * @throws \SimpleSAML\Error\CannotSetCookie If the headers were already sent and the cookie cannot be set.
1147
     *
1148
     * @return void
1149
     *
1150
     * @author Andjelko Horvat
1151
     * @author Jaime Perez, UNINETT AS <[email protected]>
1152
     */
1153
    public static function setCookie($name, $value, $params = null, $throw = true)
1154
    {
1155
        if (
1156
            !(is_string($name) // $name must be a string
1157
            && (is_string($value)
1158
            || is_null($value)) // $value can be a string or null
1159
            && (is_array($params)
1160
            || is_null($params)) // $params can be an array or null
1161
            && is_bool($throw)) // $throw must be boolean
1162
        ) {
1163
            throw new \InvalidArgumentException('Invalid input parameters.');
1164
        }
1165
1166
        $default_params = [
1167
            'lifetime' => 0,
1168
            'expire'   => null,
1169
            'path'     => '/',
1170
            'domain'   => '',
1171
            'secure'   => false,
1172
            'httponly' => true,
1173
            'raw'      => false,
1174
            'samesite' => null,
1175
        ];
1176
1177
        if ($params !== null) {
1178
            $params = array_merge($default_params, $params);
1179
        } else {
1180
            $params = $default_params;
1181
        }
1182
1183
        // Do not set secure cookie if not on HTTPS
1184
        if ($params['secure'] && !self::isHTTPS()) {
1185
            if ($throw) {
1186
                throw new Error\CannotSetCookie(
1187
                    'Setting secure cookie on plain HTTP is not allowed.',
1188
                    Error\CannotSetCookie::SECURE_COOKIE
1189
                );
1190
            }
1191
            Logger::warning('Error setting cookie: setting secure cookie on plain HTTP is not allowed.');
1192
            return;
1193
        }
1194
1195
        if ($value === null) {
1196
            $expire = time() - 365 * 24 * 60 * 60;
1197
            $value = strval($value);
1198
        } elseif (isset($params['expire'])) {
1199
            $expire = intval($params['expire']);
1200
        } elseif ($params['lifetime'] === 0) {
1201
            $expire = 0;
1202
        } else {
1203
            $expire = time() + intval($params['lifetime']);
1204
        }
1205
1206
        if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
1207
            /* use the new options array for PHP >= 7.3 */
1208
            if ($params['raw']) {
1209
                /** @psalm-suppress InvalidArgument  Remove when Psalm >= 3.4.10 */
1210
                $success = @setrawcookie(
1211
                    $name,
1212
                    $value,
1213
                    [
1214
                        'expires' => $expire,
1215
                        'path' => $params['path'],
1216
                        'domain' => $params['domain'],
1217
                        'secure' => $params['secure'],
1218
                        'httponly' => $params['httponly'],
1219
                        'samesite' => $params['samesite'],
1220
                    ]
1221
                );
1222
            } else {
1223
                /** @psalm-suppress InvalidArgument  Remove when Psalm >= 3.4.10 */
1224
                $success = @setcookie(
1225
                    $name,
1226
                    $value,
1227
                    [
1228
                        'expires' => $expire,
1229
                        'path' => $params['path'],
1230
                        'domain' => $params['domain'],
1231
                        'secure' => $params['secure'],
1232
                        'httponly' => $params['httponly'],
1233
                        'samesite' => $params['samesite'],
1234
                    ]
1235
                );
1236
            }
1237
        } else {
1238
            /* in older versions of PHP we need a nasty hack to set RFC6265bis SameSite attribute */
1239
            if ($params['samesite'] !== null && !preg_match('/;\s+samesite/i', $params['path'])) {
1240
                $params['path'] .= '; SameSite=' . $params['samesite'];
1241
            }
1242
            if ($params['raw']) {
1243
                $success = @setrawcookie(
1244
                    $name,
1245
                    $value,
1246
                    $expire,
1247
                    $params['path'],
1248
                    $params['domain'],
1249
                    $params['secure'],
1250
                    $params['httponly']
1251
                );
1252
            } else {
1253
                $success = @setcookie(
1254
                    $name,
1255
                    $value,
1256
                    $expire,
1257
                    $params['path'],
1258
                    $params['domain'],
1259
                    $params['secure'],
1260
                    $params['httponly']
1261
                );
1262
            }
1263
        }
1264
1265
        if (!$success) {
1266
            if ($throw) {
1267
                throw new Error\CannotSetCookie(
1268
                    'Headers already sent.',
1269
                    Error\CannotSetCookie::HEADERS_SENT
1270
                );
1271
            }
1272
            Logger::warning('Error setting cookie: headers already sent.');
1273
        }
1274
    }
1275
1276
1277
    /**
1278
     * Submit a POST form to a specific destination.
1279
     *
1280
     * This function never returns.
1281
     *
1282
     * @param string $destination The destination URL.
1283
     * @param array  $data An associative array with the data to be posted to $destination.
1284
     *
1285
     * @throws \InvalidArgumentException If $destination is not a string or $data is not an array.
1286
     * @throws \SimpleSAML\Error\Exception If $destination is not a valid HTTP URL.
1287
     *
1288
     * @return void
1289
     *
1290
     * @author Olav Morken, UNINETT AS <[email protected]>
1291
     * @author Andjelko Horvat
1292
     * @author Jaime Perez, UNINETT AS <[email protected]>
1293
     */
1294
    public static function submitPOSTData($destination, $data)
1295
    {
1296
        if (!is_string($destination) || !is_array($data)) {
1297
            throw new \InvalidArgumentException('Invalid input parameters.');
1298
        }
1299
        if (!self::isValidURL($destination)) {
1300
            throw new Error\Exception('Invalid destination URL.');
1301
        }
1302
1303
        $config = Configuration::getInstance();
1304
        $allowed = $config->getBoolean('enable.http_post', false);
1305
1306
        if ($allowed && preg_match("#^http:#", $destination) && self::isHTTPS()) {
1307
            // we need to post the data to HTTP
1308
            self::redirect(self::getSecurePOSTRedirectURL($destination, $data));
1309
        }
1310
1311
        $p = new Template($config, 'post.php');
1312
        $p->data['destination'] = $destination;
1313
        $p->data['post'] = $data;
1314
        $p->show();
1315
        exit(0);
1316
    }
1317
}
1318