1<?php
2
3namespace SimpleSAML;
4
5use SAML2\Constants;
6use SimpleSAML\Error;
7use SimpleSAML\Utils;
8
9/**
10 * Configuration of SimpleSAMLphp
11 *
12 * @author Andreas Aakre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
13 * @package SimpleSAMLphp
14 */
15class Configuration implements Utils\ClearableState
16{
17    /**
18     * A default value which means that the given option is required.
19     *
20     * @var string
21     */
22    const REQUIRED_OPTION = '___REQUIRED_OPTION___';
23
24
25    /**
26     * Associative array with mappings from instance-names to configuration objects.
27     *
28     * @var array
29     */
30    private static $instance = [];
31
32
33    /**
34     * Configuration directories.
35     *
36     * This associative array contains the mappings from configuration sets to
37     * configuration directories.
38     *
39     * @var array
40     */
41    private static $configDirs = [];
42
43
44    /**
45     * Cache of loaded configuration files.
46     *
47     * The index in the array is the full path to the file.
48     *
49     * @var array
50     */
51    private static $loadedConfigs = [];
52
53
54    /**
55     * The configuration array.
56     *
57     * @var array
58     */
59    private $configuration;
60
61
62    /**
63     * The location which will be given when an error occurs.
64     *
65     * @var string
66     */
67    private $location;
68
69
70    /**
71     * The file this configuration was loaded from.
72     *
73     * @var string|null
74     */
75    private $filename = null;
76
77
78    /**
79     * Temporary property that tells if the deprecated getBaseURL() method has been called or not.
80     *
81     * @var bool
82     */
83    private $deprecated_base_url_used = false;
84
85
86    /**
87     * Initializes a configuration from the given array.
88     *
89     * @param array $config The configuration array.
90     * @param string $location The location which will be given when an error occurs.
91     */
92    public function __construct($config, $location)
93    {
94        assert(is_array($config));
95        assert(is_string($location));
96
97        $this->configuration = $config;
98        $this->location = $location;
99    }
100
101    /**
102     * Load the given configuration file.
103     *
104     * @param string $filename The full path of the configuration file.
105     * @param bool $required Whether the file is required.
106     *
107     * @return \SimpleSAML\Configuration The configuration file. An exception will be thrown if the
108     *                                   configuration file is missing.
109     *
110     * @throws \Exception If the configuration file is invalid or missing.
111     */
112    private static function loadFromFile($filename, $required)
113    {
114        assert(is_string($filename));
115        assert(is_bool($required));
116
117        if (array_key_exists($filename, self::$loadedConfigs)) {
118            return self::$loadedConfigs[$filename];
119        }
120
121        if (file_exists($filename)) {
122            $config = 'UNINITIALIZED';
123
124            // the file initializes a variable named '$config'
125            ob_start();
126            if (interface_exists('Throwable', false)) {
127                try {
128                    require($filename);
129                } catch (\ParseError $e) {
130                    self::$loadedConfigs[$filename] = self::loadFromArray([], '[ARRAY]', 'simplesaml');
131                    throw new Error\ConfigurationError($e->getMessage(), $filename, []);
132                }
133            } else {
134                require($filename);
135            }
136
137            $spurious_output = ob_get_length() > 0;
138            ob_end_clean();
139
140            // check that $config exists
141            if (!isset($config)) {
142                throw new Error\ConfigurationError(
143                    '$config is not defined in the configuration file.',
144                    $filename
145                );
146            }
147
148            // check that $config is initialized to an array
149            if (!is_array($config)) {
150                throw new Error\ConfigurationError(
151                    '$config is not an array.',
152                    $filename
153                );
154            }
155
156            // check that $config is not empty
157            if (empty($config)) {
158                throw new Error\ConfigurationError(
159                    '$config is empty.',
160                    $filename
161                );
162            }
163        } elseif ($required) {
164            // file does not exist, but is required
165            throw new Error\ConfigurationError('Missing configuration file', $filename);
166        } else {
167            // file does not exist, but is optional, so return an empty configuration object without saving it
168            $cfg = new Configuration([], $filename);
169            $cfg->filename = $filename;
170            return $cfg;
171        }
172
173        $cfg = new Configuration($config, $filename);
174        $cfg->filename = $filename;
175
176        self::$loadedConfigs[$filename] = $cfg;
177
178        if ($spurious_output) {
179            Logger::warning(
180                "The configuration file '$filename' generates output. Please review your configuration."
181            );
182        }
183
184        return $cfg;
185    }
186
187
188    /**
189     * Set the directory for configuration files for the given configuration set.
190     *
191     * @param string $path The directory which contains the configuration files.
192     * @param string $configSet The configuration set. Defaults to 'simplesaml'.
193     * @return void
194     */
195    public static function setConfigDir($path, $configSet = 'simplesaml')
196    {
197        assert(is_string($path));
198        assert(is_string($configSet));
199
200        self::$configDirs[$configSet] = $path;
201    }
202
203    /**
204     * Store a pre-initialized configuration.
205     *
206     * Allows consumers to create configuration objects without having them
207     * loaded from a file.
208     *
209     * @param \SimpleSAML\Configuration $config  The configuration object to store
210     * @param string $filename  The name of the configuration file.
211     * @param string $configSet  The configuration set. Optional, defaults to 'simplesaml'.
212     * @return void
213     * @throws \Exception
214     */
215    public static function setPreLoadedConfig(
216        Configuration $config,
217        $filename = 'config.php',
218        $configSet = 'simplesaml'
219    ) {
220        assert(is_string($filename));
221        assert(is_string($configSet));
222
223        if (!array_key_exists($configSet, self::$configDirs)) {
224            if ($configSet !== 'simplesaml') {
225                throw new \Exception('Configuration set \'' . $configSet . '\' not initialized.');
226            } else {
227                self::$configDirs['simplesaml'] = dirname(dirname(dirname(__FILE__))) . '/config';
228            }
229        }
230
231        $dir = self::$configDirs[$configSet];
232        $filePath = $dir . '/' . $filename;
233
234        self::$loadedConfigs[$filePath] = $config;
235    }
236
237
238    /**
239     * Load a configuration file from a configuration set.
240     *
241     * @param string $filename The name of the configuration file.
242     * @param string $configSet The configuration set. Optional, defaults to 'simplesaml'.
243     *
244     * @return \SimpleSAML\Configuration The Configuration object.
245     * @throws \Exception If the configuration set is not initialized.
246     */
247    public static function getConfig($filename = 'config.php', $configSet = 'simplesaml')
248    {
249        assert(is_string($filename));
250        assert(is_string($configSet));
251
252        if (!array_key_exists($configSet, self::$configDirs)) {
253            if ($configSet !== 'simplesaml') {
254                throw new \Exception('Configuration set \'' . $configSet . '\' not initialized.');
255            } else {
256                self::$configDirs['simplesaml'] = Utils\Config::getConfigDir();
257            }
258        }
259
260        $dir = self::$configDirs[$configSet];
261        $filePath = $dir . '/' . $filename;
262        return self::loadFromFile($filePath, true);
263    }
264
265
266    /**
267     * Load a configuration file from a configuration set.
268     *
269     * This function will return a configuration object even if the file does not exist.
270     *
271     * @param string $filename The name of the configuration file.
272     * @param string $configSet The configuration set. Optional, defaults to 'simplesaml'.
273     *
274     * @return \SimpleSAML\Configuration A configuration object.
275     * @throws \Exception If the configuration set is not initialized.
276     */
277    public static function getOptionalConfig($filename = 'config.php', $configSet = 'simplesaml')
278    {
279        assert(is_string($filename));
280        assert(is_string($configSet));
281
282        if (!array_key_exists($configSet, self::$configDirs)) {
283            if ($configSet !== 'simplesaml') {
284                throw new \Exception('Configuration set \'' . $configSet . '\' not initialized.');
285            } else {
286                self::$configDirs['simplesaml'] = Utils\Config::getConfigDir();
287            }
288        }
289
290        $dir = self::$configDirs[$configSet];
291        $filePath = $dir . '/' . $filename;
292        return self::loadFromFile($filePath, false);
293    }
294
295
296    /**
297     * Loads a configuration from the given array.
298     *
299     * @param array  $config The configuration array.
300     * @param string $location The location which will be given when an error occurs. Optional.
301     * @param string|null $instance The name of this instance. If specified, the configuration will be loaded and an
302     * instance with that name will be kept for it to be retrieved later with getInstance($instance). If null, the
303     * configuration will not be kept for later use. Defaults to null.
304     *
305     * @return \SimpleSAML\Configuration The configuration object.
306     */
307    public static function loadFromArray($config, $location = '[ARRAY]', $instance = null)
308    {
309        assert(is_array($config));
310        assert(is_string($location));
311
312        $c = new Configuration($config, $location);
313        if ($instance !== null) {
314            self::$instance[$instance] = $c;
315        }
316        return $c;
317    }
318
319
320    /**
321     * Get a configuration file by its instance name.
322     *
323     * This function retrieves a configuration file by its instance name. The instance
324     * name is initialized by the init function, or by copyFromBase function.
325     *
326     * If no configuration file with the given instance name is found, an exception will
327     * be thrown.
328     *
329     * @param string $instancename The instance name of the configuration file. Deprecated.
330     *
331     * @return \SimpleSAML\Configuration The configuration object.
332     *
333     * @throws \Exception If the configuration with $instancename name is not initialized.
334     */
335    public static function getInstance($instancename = 'simplesaml')
336    {
337        assert(is_string($instancename));
338
339        // check if the instance exists already
340        if (array_key_exists($instancename, self::$instance)) {
341            return self::$instance[$instancename];
342        }
343
344        if ($instancename === 'simplesaml') {
345            try {
346                return self::getConfig();
347            } catch (Error\ConfigurationError $e) {
348                throw Error\CriticalConfigurationError::fromException($e);
349            }
350        }
351
352        throw new Error\CriticalConfigurationError(
353            'Configuration with name ' . $instancename . ' is not initialized.'
354        );
355    }
356
357
358    /**
359     * Initialize a instance name with the given configuration file.
360     *
361     * TODO: remove.
362     *
363     * @param string $path
364     * @param string $instancename
365     * @param string $configfilename
366     * @return \SimpleSAML\Configuration
367     *
368     * @see setConfigDir()
369     * @deprecated This function is superseeded by the setConfigDir function.
370     */
371    public static function init($path, $instancename = 'simplesaml', $configfilename = 'config.php')
372    {
373        assert(is_string($path));
374        assert(is_string($instancename));
375        assert(is_string($configfilename));
376
377        if ($instancename === 'simplesaml') {
378            // for backwards compatibility
379            self::setConfigDir($path, 'simplesaml');
380        }
381
382        // check if we already have loaded the given config - return the existing instance if we have
383        if (array_key_exists($instancename, self::$instance)) {
384            return self::$instance[$instancename];
385        }
386
387        self::$instance[$instancename] = self::loadFromFile($path . '/' . $configfilename, true);
388        return self::$instance[$instancename];
389    }
390
391
392    /**
393     * Load a configuration file which is located in the same directory as this configuration file.
394     *
395     * TODO: remove.
396     *
397     * @param string $instancename
398     * @param string $filename
399     * @return \SimpleSAML\Configuration
400     *
401     * @see getConfig()
402     * @deprecated This function is superseeded by the getConfig() function.
403     */
404    public function copyFromBase($instancename, $filename)
405    {
406        assert(is_string($instancename));
407        assert(is_string($filename));
408        assert($this->filename !== null);
409
410        // check if we already have loaded the given config - return the existing instance if we have
411        if (array_key_exists($instancename, self::$instance)) {
412            return self::$instance[$instancename];
413        }
414
415        $dir = dirname($this->filename);
416
417        self::$instance[$instancename] = self::loadFromFile($dir . '/' . $filename, true);
418        return self::$instance[$instancename];
419    }
420
421
422    /**
423     * Retrieve the current version of SimpleSAMLphp.
424     *
425     * @return string
426     */
427    public function getVersion()
428    {
429        return '1.18.7';
430    }
431
432
433    /**
434     * Retrieve a configuration option set in config.php.
435     *
436     * @param string $name Name of the configuration option.
437     * @param mixed  $default Default value of the configuration option. This parameter will default to null if not
438     *                        specified. This can be set to \SimpleSAML\Configuration::REQUIRED_OPTION, which will
439     *                        cause an exception to be thrown if the option isn't found.
440     *
441     * @return mixed The configuration option with name $name, or $default if the option was not found.
442     *
443     * @throws \Exception If the required option cannot be retrieved.
444     */
445    public function getValue($name, $default = null)
446    {
447        // return the default value if the option is unset
448        if (!array_key_exists($name, $this->configuration)) {
449            if ($default === self::REQUIRED_OPTION) {
450                throw new \Exception(
451                    $this->location . ': Could not retrieve the required option ' .
452                    var_export($name, true)
453                );
454            }
455            return $default;
456        }
457
458        return $this->configuration[$name];
459    }
460
461
462    /**
463     * Check whether a key in the configuration exists or not.
464     *
465     * @param string $name The key in the configuration to look for.
466     *
467     * @return boolean If the value is set in this configuration.
468     */
469    public function hasValue($name)
470    {
471        return array_key_exists($name, $this->configuration);
472    }
473
474
475    /**
476     * Check whether any key of the set given exists in the configuration.
477     *
478     * @param array $names An array of options to look for.
479     *
480     * @return boolean If any of the keys in $names exist in the configuration
481     */
482    public function hasValueOneOf($names)
483    {
484        foreach ($names as $name) {
485            if ($this->hasValue($name)) {
486                return true;
487            }
488        }
489        return false;
490    }
491
492
493    /**
494     * Retrieve the absolute path of the SimpleSAMLphp installation, relative to the root of the website.
495     *
496     * For example: simplesaml/
497     *
498     * The path will always end with a '/' and never have a leading slash.
499     *
500     * @return string The absolute path relative to the root of the website.
501     *
502     * @throws \SimpleSAML\Error\CriticalConfigurationError If the format of 'baseurlpath' is incorrect.
503     *
504     * @deprecated This method will be removed in SimpleSAMLphp 2.0. Please use getBasePath() instead.
505     */
506    public function getBaseURL()
507    {
508        if (!$this->deprecated_base_url_used) {
509            $this->deprecated_base_url_used = true;
510            Logger::warning(
511                "\SimpleSAML\Configuration::getBaseURL() is deprecated, please use getBasePath() instead."
512            );
513        }
514        if (preg_match('/^\*(.*)$/D', $this->getString('baseurlpath', 'simplesaml/'), $matches)) {
515            // deprecated behaviour, will be removed in the future
516            return Utils\HTTP::getFirstPathElement(false) . $matches[1];
517        }
518        return ltrim($this->getBasePath(), '/');
519    }
520
521
522    /**
523     * Retrieve the absolute path pointing to the SimpleSAMLphp installation.
524     *
525     * The path is guaranteed to start and end with a slash ('/'). E.g.: /simplesaml/
526     *
527     * @return string The absolute path where SimpleSAMLphp can be reached in the web server.
528     *
529     * @throws \SimpleSAML\Error\CriticalConfigurationError If the format of 'baseurlpath' is incorrect.
530     */
531    public function getBasePath()
532    {
533        $baseURL = $this->getString('baseurlpath', 'simplesaml/');
534
535        if (preg_match('#^https?://[^/]*(?:/(.+/?)?)?$#', $baseURL, $matches)) {
536            // we have a full url, we need to strip the path
537            if (!array_key_exists(1, $matches)) {
538                // absolute URL without path
539                return '/';
540            }
541            return '/' . rtrim($matches[1], '/') . '/';
542        } elseif ($baseURL === '' || $baseURL === '/') {
543            // root directory of site
544            return '/';
545        } elseif (preg_match('#^/?((?:[^/\s]+/?)+)#', $baseURL, $matches)) {
546            // local path only
547            return '/' . rtrim($matches[1], '/') . '/';
548        } else {
549            /*
550             * Invalid 'baseurlpath'. We cannot recover from this, so throw a critical exception and try to be graceful
551             * with the configuration. Use a guessed base path instead of the one provided.
552             */
553            $c = $this->toArray();
554            $c['baseurlpath'] = Utils\HTTP::guessBasePath();
555            throw new Error\CriticalConfigurationError(
556                'Incorrect format for option \'baseurlpath\'. Value is: "' .
557                $this->getString('baseurlpath', 'simplesaml/') . '". Valid format is in the form' .
558                ' [(http|https)://(hostname|fqdn)[:port]]/[path/to/simplesaml/].',
559                $this->filename,
560                $c
561            );
562        }
563    }
564
565
566    /**
567     * This function resolves a path which may be relative to the SimpleSAMLphp base directory.
568     *
569     * The path will never end with a '/'.
570     *
571     * @param string|null $path The path we should resolve. This option may be null.
572     *
573     * @return string|null $path if $path is an absolute path, or $path prepended with the base directory of this
574     * SimpleSAMLphp installation. We will return NULL if $path is null.
575     */
576    public function resolvePath($path)
577    {
578        if ($path === null) {
579            return null;
580        }
581
582        assert(is_string($path));
583
584        return Utils\System::resolvePath($path, $this->getBaseDir());
585    }
586
587
588    /**
589     * Retrieve a path configuration option set in config.php.
590     *
591     * The function will always return an absolute path unless the option is not set. It will then return the default
592     * value.
593     *
594     * It checks if the value starts with a slash, and prefixes it with the value from getBaseDir if it doesn't.
595     *
596     * @param string $name Name of the configuration option.
597     * @param string|null $default Default value of the configuration option. This parameter will default to null if
598     * not specified.
599     *
600     * @return string|null The path configuration option with name $name, or $default if the option was not found.
601     */
602    public function getPathValue($name, $default = null)
603    {
604        // return the default value if the option is unset
605        if (!array_key_exists($name, $this->configuration)) {
606            $path = $default;
607        } else {
608            $path = $this->configuration[$name];
609        }
610
611        $path = $this->resolvePath($path);
612        if ($path === null) {
613            return null;
614        }
615
616        return $path . '/';
617    }
618
619
620    /**
621     * Retrieve the base directory for this SimpleSAMLphp installation.
622     *
623     * This function first checks the 'basedir' configuration option. If this option is undefined or null, then we
624     * fall back to looking at the current filename.
625     *
626     * @return string The absolute path to the base directory for this SimpleSAMLphp installation. This path will
627     * always end with a slash.
628     */
629    public function getBaseDir()
630    {
631        // check if a directory is configured in the configuration file
632        $dir = $this->getString('basedir', null);
633        if ($dir !== null) {
634            // add trailing slash if it is missing
635            if (substr($dir, -1) !== DIRECTORY_SEPARATOR) {
636                $dir .= DIRECTORY_SEPARATOR;
637            }
638
639            return $dir;
640        }
641
642        // the directory wasn't set in the configuration file, path is <base directory>/lib/SimpleSAML/Configuration.php
643        $dir = __FILE__;
644        assert(basename($dir) === 'Configuration.php');
645
646        $dir = dirname($dir);
647        assert(basename($dir) === 'SimpleSAML');
648
649        $dir = dirname($dir);
650        assert(basename($dir) === 'lib');
651
652        $dir = dirname($dir);
653
654        // Add trailing directory separator
655        $dir .= DIRECTORY_SEPARATOR;
656
657        return $dir;
658    }
659
660
661    /**
662     * This function retrieves a boolean configuration option.
663     *
664     * An exception will be thrown if this option isn't a boolean, or if this option isn't found, and no default value
665     * is given.
666     *
667     * @param string $name The name of the option.
668     * @param mixed  $default A default value which will be returned if the option isn't found. The option will be
669     *                  required if this parameter isn't given. The default value can be any value, including
670     *                  null.
671     *
672     * @return boolean|mixed The option with the given name, or $default if the option isn't found and $default is
673     *     specified.
674     *
675     * @throws \Exception If the option is not boolean.
676     */
677    public function getBoolean($name, $default = self::REQUIRED_OPTION)
678    {
679        assert(is_string($name));
680
681        $ret = $this->getValue($name, $default);
682
683        if ($ret === $default) {
684            // the option wasn't found, or it matches the default value. In any case, return this value
685            return $ret;
686        }
687
688        if (!is_bool($ret)) {
689            throw new \Exception(
690                $this->location . ': The option ' . var_export($name, true) .
691                ' is not a valid boolean value.'
692            );
693        }
694
695        return $ret;
696    }
697
698
699    /**
700     * This function retrieves a string configuration option.
701     *
702     * An exception will be thrown if this option isn't a string, or if this option isn't found, and no default value
703     * is given.
704     *
705     * @param string $name The name of the option.
706     * @param mixed  $default A default value which will be returned if the option isn't found. The option will be
707     *                  required if this parameter isn't given. The default value can be any value, including
708     *                  null.
709     *
710     * @return string|mixed The option with the given name, or $default if the option isn't found and $default is
711     *     specified.
712     *
713     * @throws \Exception If the option is not a string.
714     */
715    public function getString($name, $default = self::REQUIRED_OPTION)
716    {
717        assert(is_string($name));
718
719        $ret = $this->getValue($name, $default);
720
721        if ($ret === $default) {
722            // the option wasn't found, or it matches the default value. In any case, return this value
723            return $ret;
724        }
725
726        if (!is_string($ret)) {
727            throw new \Exception(
728                $this->location . ': The option ' . var_export($name, true) .
729                ' is not a valid string value.'
730            );
731        }
732
733        return $ret;
734    }
735
736
737    /**
738     * This function retrieves an integer configuration option.
739     *
740     * An exception will be thrown if this option isn't an integer, or if this option isn't found, and no default value
741     * is given.
742     *
743     * @param string $name The name of the option.
744     * @param mixed  $default A default value which will be returned if the option isn't found. The option will be
745     *                  required if this parameter isn't given. The default value can be any value, including
746     *                  null.
747     *
748     * @return int|mixed The option with the given name, or $default if the option isn't found and $default is
749     * specified.
750     *
751     * @throws \Exception If the option is not an integer.
752     */
753    public function getInteger($name, $default = self::REQUIRED_OPTION)
754    {
755        assert(is_string($name));
756
757        $ret = $this->getValue($name, $default);
758
759        if ($ret === $default) {
760            // the option wasn't found, or it matches the default value. In any case, return this value
761            return $ret;
762        }
763
764        if (!is_int($ret)) {
765            throw new \Exception(
766                $this->location . ': The option ' . var_export($name, true) .
767                ' is not a valid integer value.'
768            );
769        }
770
771        return $ret;
772    }
773
774
775    /**
776     * This function retrieves an integer configuration option where the value must be in the specified range.
777     *
778     * An exception will be thrown if:
779     * - the option isn't an integer
780     * - the option isn't found, and no default value is given
781     * - the value is outside of the allowed range
782     *
783     * @param string $name The name of the option.
784     * @param int    $minimum The smallest value which is allowed.
785     * @param int    $maximum The largest value which is allowed.
786     * @param mixed  $default A default value which will be returned if the option isn't found. The option will be
787     *                  required if this parameter isn't given. The default value can be any value, including
788     *                  null.
789     *
790     * @return int|mixed The option with the given name, or $default if the option isn't found and $default is
791     *     specified.
792     *
793     * @throws \Exception If the option is not in the range specified.
794     */
795    public function getIntegerRange($name, $minimum, $maximum, $default = self::REQUIRED_OPTION)
796    {
797        assert(is_string($name));
798        assert(is_int($minimum));
799        assert(is_int($maximum));
800
801        $ret = $this->getInteger($name, $default);
802
803        if ($ret === $default) {
804            // the option wasn't found, or it matches the default value. In any case, return this value
805            return $ret;
806        }
807
808        if ($ret < $minimum || $ret > $maximum) {
809            throw new \Exception(
810                $this->location . ': Value of option ' . var_export($name, true) .
811                ' is out of range. Value is ' . $ret . ', allowed range is ['
812                . $minimum . ' - ' . $maximum . ']'
813            );
814        }
815
816        return $ret;
817    }
818
819
820    /**
821     * Retrieve a configuration option with one of the given values.
822     *
823     * This will check that the configuration option matches one of the given values. The match will use
824     * strict comparison. An exception will be thrown if it does not match.
825     *
826     * The option can be mandatory or optional. If no default value is given, it will be considered to be
827     * mandatory, and an exception will be thrown if it isn't provided. If a default value is given, it
828     * is considered to be optional, and the default value is returned. The default value is automatically
829     * included in the list of allowed values.
830     *
831     * @param string $name The name of the option.
832     * @param array  $allowedValues The values the option is allowed to take, as an array.
833     * @param mixed  $default The default value which will be returned if the option isn't found. If this parameter
834     *                  isn't given, the option will be considered to be mandatory. The default value can be
835     *                  any value, including null.
836     *
837     * @return mixed The option with the given name, or $default if the option isn't found and $default is given.
838     *
839     * @throws \Exception If the option does not have any of the allowed values.
840     */
841    public function getValueValidate($name, $allowedValues, $default = self::REQUIRED_OPTION)
842    {
843        assert(is_string($name));
844        assert(is_array($allowedValues));
845
846        $ret = $this->getValue($name, $default);
847        if ($ret === $default) {
848            // the option wasn't found, or it matches the default value. In any case, return this value
849            return $ret;
850        }
851
852        if (!in_array($ret, $allowedValues, true)) {
853            $strValues = [];
854            foreach ($allowedValues as $av) {
855                $strValues[] = var_export($av, true);
856            }
857            $strValues = implode(', ', $strValues);
858
859            throw new \Exception(
860                $this->location . ': Invalid value given for the option ' .
861                var_export($name, true) . '. It should have one of the following values: ' .
862                $strValues . '; but it had the following value: ' . var_export($ret, true)
863            );
864        }
865
866        return $ret;
867    }
868
869
870    /**
871     * This function retrieves an array configuration option.
872     *
873     * An exception will be thrown if this option isn't an array, or if this option isn't found, and no
874     * default value is given.
875     *
876     * @param string $name The name of the option.
877     * @param mixed  $default A default value which will be returned if the option isn't found. The option will be
878     *                       required if this parameter isn't given. The default value can be any value, including
879     *                       null.
880     *
881     * @return array|mixed The option with the given name, or $default if the option isn't found and $default is
882     * specified.
883     *
884     * @throws \Exception If the option is not an array.
885     */
886    public function getArray($name, $default = self::REQUIRED_OPTION)
887    {
888        assert(is_string($name));
889
890        $ret = $this->getValue($name, $default);
891
892        if ($ret === $default) {
893            // the option wasn't found, or it matches the default value. In any case, return this value
894            return $ret;
895        }
896
897        if (!is_array($ret)) {
898            throw new \Exception($this->location . ': The option ' . var_export($name, true) . ' is not an array.');
899        }
900
901        return $ret;
902    }
903
904
905    /**
906     * This function retrieves an array configuration option.
907     *
908     * If the configuration option isn't an array, it will be converted to an array.
909     *
910     * @param string $name The name of the option.
911     * @param mixed  $default A default value which will be returned if the option isn't found. The option will be
912     *                       required if this parameter isn't given. The default value can be any value, including
913     *                       null.
914     *
915     * @return array The option with the given name, or $default if the option isn't found and $default is specified.
916     */
917    public function getArrayize($name, $default = self::REQUIRED_OPTION)
918    {
919        assert(is_string($name));
920
921        $ret = $this->getValue($name, $default);
922
923        if ($ret === $default) {
924            // the option wasn't found, or it matches the default value. In any case, return this value
925            return $ret;
926        }
927
928        if (!is_array($ret)) {
929            $ret = [$ret];
930        }
931
932        return $ret;
933    }
934
935
936    /**
937     * This function retrieves a configuration option with a string or an array of strings.
938     *
939     * If the configuration option is a string, it will be converted to an array with a single string
940     *
941     * @param string $name The name of the option.
942     * @param mixed  $default A default value which will be returned if the option isn't found. The option will be
943     *                       required if this parameter isn't given. The default value can be any value, including
944     *                       null.
945     *
946     * @return array The option with the given name, or $default if the option isn't found and $default is specified.
947     *
948     * @throws \Exception If the option is not a string or an array of strings.
949     */
950    public function getArrayizeString($name, $default = self::REQUIRED_OPTION)
951    {
952        assert(is_string($name));
953
954        $ret = $this->getArrayize($name, $default);
955
956        if ($ret === $default) {
957            // the option wasn't found, or it matches the default value. In any case, return this value
958            return $ret;
959        }
960
961        foreach ($ret as $value) {
962            if (!is_string($value)) {
963                throw new \Exception(
964                    $this->location . ': The option ' . var_export($name, true) .
965                    ' must be a string or an array of strings.'
966                );
967            }
968        }
969
970        return $ret;
971    }
972
973
974    /**
975     * Retrieve an array as a \SimpleSAML\Configuration object.
976     *
977     * This function will load the value of an option into a \SimpleSAML\Configuration object. The option must contain
978     * an array.
979     *
980     * An exception will be thrown if this option isn't an array, or if this option isn't found, and no default value
981     * is given.
982     *
983     * @param string $name The name of the option.
984     * @param array|null $default A default value which will be used if the option isn't found. An empty Configuration
985     *                        object will be returned if this parameter isn't given and the option doesn't exist.
986     *                        This function will only return null if $default is set to null and the option
987     *                        doesn't exist.
988     *
989     * @return mixed The option with the given name, or $default if the option isn't found and $default is specified.
990     *
991     * @throws \Exception If the option is not an array.
992     */
993    public function getConfigItem($name, $default = [])
994    {
995        assert(is_string($name));
996
997        $ret = $this->getValue($name, $default);
998
999        if ($ret === null) {
1000            // the option wasn't found, or it is explicitly null
1001            // do not instantiate a new Configuration instance, but just return null
1002            return null;
1003        }
1004
1005        if (!is_array($ret)) {
1006            throw new \Exception(
1007                $this->location . ': The option ' . var_export($name, true) .
1008                ' is not an array.'
1009            );
1010        }
1011
1012        return self::loadFromArray($ret, $this->location . '[' . var_export($name, true) . ']');
1013    }
1014
1015
1016    /**
1017     * Retrieve an array of arrays as an array of \SimpleSAML\Configuration objects.
1018     *
1019     * This function will retrieve an option containing an array of arrays, and create an array of
1020     * \SimpleSAML\Configuration objects from that array. The indexes in the new array will be the same as the original
1021     * indexes, but the values will be \SimpleSAML\Configuration objects.
1022     *
1023     * An exception will be thrown if this option isn't an array of arrays, or if this option isn't found, and no
1024     * default value is given.
1025     *
1026     * @param string $name The name of the option.
1027     *
1028     * @return array The array of \SimpleSAML\Configuration objects.
1029     *
1030     * @throws \Exception If the value of this element is not an array.
1031     *
1032     * @deprecated Very specific function, will be removed in a future release; use getConfigItem or getArray instead
1033     */
1034    public function getConfigList($name)
1035    {
1036        assert(is_string($name));
1037
1038        $ret = $this->getValue($name, []);
1039
1040        if (!is_array($ret)) {
1041            throw new \Exception(
1042                $this->location . ': The option ' . var_export($name, true) .
1043                ' is not an array.'
1044            );
1045        }
1046
1047        $out = [];
1048        foreach ($ret as $index => $config) {
1049            $newLoc = $this->location . '[' . var_export($name, true) . '][' .
1050                var_export($index, true) . ']';
1051            if (!is_array($config)) {
1052                throw new \Exception($newLoc . ': The value of this element was expected to be an array.');
1053            }
1054            $out[$index] = self::loadFromArray($config, $newLoc);
1055        }
1056
1057        return $out;
1058    }
1059
1060
1061    /**
1062     * Retrieve list of options.
1063     *
1064     * This function returns the name of all options which are defined in this
1065     * configuration file, as an array of strings.
1066     *
1067     * @return array Name of all options defined in this configuration file.
1068     */
1069    public function getOptions()
1070    {
1071        return array_keys($this->configuration);
1072    }
1073
1074
1075    /**
1076     * Convert this configuration object back to an array.
1077     *
1078     * @return array An associative array with all configuration options and values.
1079     */
1080    public function toArray()
1081    {
1082        return $this->configuration;
1083    }
1084
1085
1086    /**
1087     * Retrieve the default binding for the given endpoint type.
1088     *
1089     * This function combines the current metadata type (SAML 2 / SAML 1.1)
1090     * with the endpoint type to determine which binding is the default.
1091     *
1092     * @param string $endpointType The endpoint type.
1093     *
1094     * @return string The default binding.
1095     *
1096     * @throws \Exception If the default binding is missing for this endpoint type.
1097     */
1098    private function getDefaultBinding($endpointType)
1099    {
1100        assert(is_string($endpointType));
1101
1102        $set = $this->getString('metadata-set');
1103        switch ($set . ':' . $endpointType) {
1104            case 'saml20-idp-remote:SingleSignOnService':
1105            case 'saml20-idp-remote:SingleLogoutService':
1106            case 'saml20-sp-remote:SingleLogoutService':
1107                return Constants::BINDING_HTTP_REDIRECT;
1108            case 'saml20-sp-remote:AssertionConsumerService':
1109                return Constants::BINDING_HTTP_POST;
1110            case 'saml20-idp-remote:ArtifactResolutionService':
1111                return Constants::BINDING_SOAP;
1112            case 'shib13-idp-remote:SingleSignOnService':
1113                return 'urn:mace:shibboleth:1.0:profiles:AuthnRequest';
1114            case 'shib13-sp-remote:AssertionConsumerService':
1115                return 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post';
1116            default:
1117                throw new \Exception('Missing default binding for ' . $endpointType . ' in ' . $set);
1118        }
1119    }
1120
1121
1122    /**
1123     * Helper function for dealing with metadata endpoints.
1124     *
1125     * @param string $endpointType The endpoint type.
1126     *
1127     * @return array Array of endpoints of the given type.
1128     *
1129     * @throws \Exception If any element of the configuration options for this endpoint type is incorrect.
1130     */
1131    public function getEndpoints($endpointType)
1132    {
1133        assert(is_string($endpointType));
1134
1135        $loc = $this->location . '[' . var_export($endpointType, true) . ']:';
1136
1137        if (!array_key_exists($endpointType, $this->configuration)) {
1138            // no endpoints of the given type
1139            return [];
1140        }
1141
1142
1143        $eps = $this->configuration[$endpointType];
1144        if (is_string($eps)) {
1145            // for backwards-compatibility
1146            $eps = [$eps];
1147        } elseif (!is_array($eps)) {
1148            throw new \Exception($loc . ': Expected array or string.');
1149        }
1150
1151
1152        foreach ($eps as $i => &$ep) {
1153            $iloc = $loc . '[' . var_export($i, true) . ']';
1154
1155            if (is_string($ep)) {
1156                // for backwards-compatibility
1157                $ep = [
1158                    'Location' => $ep,
1159                    'Binding'  => $this->getDefaultBinding($endpointType),
1160                ];
1161                $responseLocation = $this->getString($endpointType . 'Response', null);
1162                if ($responseLocation !== null) {
1163                    $ep['ResponseLocation'] = $responseLocation;
1164                }
1165            } elseif (!is_array($ep)) {
1166                throw new \Exception($iloc . ': Expected a string or an array.');
1167            }
1168
1169            if (!array_key_exists('Location', $ep)) {
1170                throw new \Exception($iloc . ': Missing Location.');
1171            }
1172            if (!is_string($ep['Location'])) {
1173                throw new \Exception($iloc . ': Location must be a string.');
1174            }
1175
1176            if (!array_key_exists('Binding', $ep)) {
1177                throw new \Exception($iloc . ': Missing Binding.');
1178            }
1179            if (!is_string($ep['Binding'])) {
1180                throw new \Exception($iloc . ': Binding must be a string.');
1181            }
1182
1183            if (array_key_exists('ResponseLocation', $ep)) {
1184                if (!is_string($ep['ResponseLocation'])) {
1185                    throw new \Exception($iloc . ': ResponseLocation must be a string.');
1186                }
1187            }
1188
1189            if (array_key_exists('index', $ep)) {
1190                if (!is_int($ep['index'])) {
1191                    throw new \Exception($iloc . ': index must be an integer.');
1192                }
1193            }
1194        }
1195
1196        return $eps;
1197    }
1198
1199
1200    /**
1201     * Find an endpoint of the given type, using a list of supported bindings as a way to prioritize.
1202     *
1203     * @param string $endpointType The endpoint type.
1204     * @param array  $bindings Sorted array of acceptable bindings.
1205     * @param mixed  $default The default value to return if no matching endpoint is found. If no default is provided,
1206     *     an exception will be thrown.
1207     *
1208     * @return array|null The default endpoint, or null if no acceptable endpoints are used.
1209     *
1210     * @throws \Exception If no supported endpoint is found.
1211     */
1212    public function getEndpointPrioritizedByBinding($endpointType, array $bindings, $default = self::REQUIRED_OPTION)
1213    {
1214        assert(is_string($endpointType));
1215
1216        $endpoints = $this->getEndpoints($endpointType);
1217
1218        foreach ($bindings as $binding) {
1219            foreach ($endpoints as $ep) {
1220                if ($ep['Binding'] === $binding) {
1221                    return $ep;
1222                }
1223            }
1224        }
1225
1226        if ($default === self::REQUIRED_OPTION) {
1227            $loc = $this->location . '[' . var_export($endpointType, true) . ']:';
1228            throw new \Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.');
1229        }
1230
1231        return $default;
1232    }
1233
1234
1235    /**
1236     * Find the default endpoint of the given type.
1237     *
1238     * @param string $endpointType The endpoint type.
1239     * @param array  $bindings Array with acceptable bindings. Can be null if any binding is allowed.
1240     * @param mixed  $default The default value to return if no matching endpoint is found. If no default is provided,
1241     *     an exception will be thrown.
1242     *
1243     * @return mixed The default endpoint, or the $default parameter if no acceptable endpoints are used.
1244     *
1245     * @throws \Exception If no supported endpoint is found and no $default parameter is specified.
1246     */
1247    public function getDefaultEndpoint($endpointType, array $bindings = null, $default = self::REQUIRED_OPTION)
1248    {
1249        assert(is_string($endpointType));
1250
1251        $endpoints = $this->getEndpoints($endpointType);
1252
1253        $defaultEndpoint = Utils\Config\Metadata::getDefaultEndpoint($endpoints, $bindings);
1254        if ($defaultEndpoint !== null) {
1255            return $defaultEndpoint;
1256        }
1257
1258        if ($default === self::REQUIRED_OPTION) {
1259            $loc = $this->location . '[' . var_export($endpointType, true) . ']:';
1260            throw new \Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.');
1261        }
1262
1263        return $default;
1264    }
1265
1266
1267    /**
1268     * Retrieve a string which may be localized into many languages.
1269     *
1270     * The default language returned is always 'en'.
1271     *
1272     * @param string $name The name of the option.
1273     * @param mixed  $default The default value. If no default is given, and the option isn't found, an exception will
1274     *     be thrown.
1275     *
1276     * @return mixed Associative array with language => string pairs, or the provided default value.
1277     *
1278     * @throws \Exception If the translation is not an array or a string, or its index or value are not strings.
1279     */
1280    public function getLocalizedString($name, $default = self::REQUIRED_OPTION)
1281    {
1282        assert(is_string($name));
1283
1284        $ret = $this->getValue($name, $default);
1285        if ($ret === $default) {
1286            // the option wasn't found, or it matches the default value. In any case, return this value
1287            return $ret;
1288        }
1289
1290        $loc = $this->location . '[' . var_export($name, true) . ']';
1291
1292        if (is_string($ret)) {
1293            $ret = ['en' => $ret];
1294        }
1295
1296        if (!is_array($ret)) {
1297            throw new \Exception($loc . ': Must be an array or a string.');
1298        }
1299
1300        foreach ($ret as $k => $v) {
1301            if (!is_string($k)) {
1302                throw new \Exception($loc . ': Invalid language code: ' . var_export($k, true));
1303            }
1304            if (!is_string($v)) {
1305                throw new \Exception($loc . '[' . var_export($v, true) . ']: Must be a string.');
1306            }
1307        }
1308
1309        return $ret;
1310    }
1311
1312
1313    /**
1314     * Get public key from metadata.
1315     *
1316     * @param string|null $use The purpose this key can be used for. (encryption or signing).
1317     * @param bool $required Whether the public key is required. If this is true, a
1318     *                       missing key will cause an exception. Default is false.
1319     * @param string $prefix The prefix which should be used when reading from the metadata
1320     *                       array. Defaults to ''.
1321     *
1322     * @return array Public key data, or empty array if no public key or was found.
1323     *
1324     * @throws \Exception If the certificate or public key cannot be loaded from a file.
1325     * @throws \SimpleSAML\Error\Exception If the file does not contain a valid PEM-encoded certificate, or there is no
1326     * certificate in the metadata.
1327     */
1328    public function getPublicKeys($use = null, $required = false, $prefix = '')
1329    {
1330        assert(is_bool($required));
1331        assert(is_string($prefix));
1332
1333        if ($this->hasValue($prefix . 'keys')) {
1334            $ret = [];
1335            foreach ($this->getArray($prefix . 'keys') as $key) {
1336                if ($use !== null && isset($key[$use]) && !$key[$use]) {
1337                    continue;
1338                }
1339                if (isset($key['X509Certificate'])) {
1340                    // Strip whitespace from key
1341                    $key['X509Certificate'] = preg_replace('/\s+/', '', $key['X509Certificate']);
1342                }
1343                $ret[] = $key;
1344            }
1345            return $ret;
1346        } elseif ($this->hasValue($prefix . 'certData')) {
1347            $certData = $this->getString($prefix . 'certData');
1348            $certData = preg_replace('/\s+/', '', $certData);
1349            return [
1350                [
1351                    'encryption'      => true,
1352                    'signing'         => true,
1353                    'type'            => 'X509Certificate',
1354                    'X509Certificate' => $certData,
1355                ],
1356            ];
1357        } elseif ($this->hasValue($prefix . 'certificate')) {
1358            $file = $this->getString($prefix . 'certificate');
1359            $file = Utils\Config::getCertPath($file);
1360            $data = @file_get_contents($file);
1361
1362            if ($data === false) {
1363                throw new \Exception(
1364                    $this->location . ': Unable to load certificate/public key from file "' . $file . '".'
1365                );
1366            }
1367
1368            // extract certificate data (if this is a certificate)
1369            $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m';
1370            if (!preg_match($pattern, $data, $matches)) {
1371                throw new \SimpleSAML\Error\Exception(
1372                    $this->location . ': Could not find PEM encoded certificate in "' . $file . '".'
1373                );
1374            }
1375            $certData = preg_replace('/\s+/', '', $matches[1]);
1376
1377            return [
1378                [
1379                    'encryption'      => true,
1380                    'signing'         => true,
1381                    'type'            => 'X509Certificate',
1382                    'X509Certificate' => $certData,
1383                ],
1384            ];
1385        } elseif ($required === true) {
1386            throw new \SimpleSAML\Error\Exception($this->location . ': Missing certificate in metadata.');
1387        } else {
1388            return [];
1389        }
1390    }
1391
1392    /**
1393     * Clear any configuration information cached.
1394     * Allows for configuration files to be changed and reloaded during a given request. Most useful
1395     * when running phpunit tests and needing to alter config.php between test cases
1396     *
1397     * @return void
1398     */
1399    public static function clearInternalState()
1400    {
1401        self::$configDirs = [];
1402        self::$instance = [];
1403        self::$loadedConfigs = [];
1404    }
1405}
1406