diff --git a/composer.json b/composer.json index e89865bfc7dff09b1c0b76487222ebf133671e4b..71e97b073b319fb04195efbeb5e76ed650ef84fa 100644 --- a/composer.json +++ b/composer.json @@ -57,6 +57,7 @@ "symfony/config": "^5.4", "symfony/console": "^5.4", "symfony/dependency-injection": "^5.4", + "symfony/filesystem": "^5.4", "symfony/finder": "^5.4", "symfony/framework-bundle": "^5.4", "symfony/http-foundation": "^5.4", @@ -94,8 +95,9 @@ }, "config": { "allow-plugins": { + "composer/package-versions-deprecated": true, "simplesamlphp/composer-module-installer": true, - "composer/package-versions-deprecated": true + "muglug/package-versions-56": true } } } diff --git a/composer.lock b/composer.lock index 431871eea52d22092f248d2b3ee9751cc73b2d67..72ed91148b8275c93486fa6b0d42ad56c13bb628 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9cedbdf88b9c6b9cf915d55ded93758d", + "content-hash": "b961c9acd0500cbabf669f97bea560ae", "packages": [ { "name": "gettext/gettext", @@ -3670,16 +3670,16 @@ }, { "name": "composer/semver", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "f79c90ad4e9b41ac4dfc5d77bf398cf61fbd718b" + "reference": "5d8e574bb0e69188786b8ef77d43341222a41a71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/f79c90ad4e9b41ac4dfc5d77bf398cf61fbd718b", - "reference": "f79c90ad4e9b41ac4dfc5d77bf398cf61fbd718b", + "url": "https://api.github.com/repos/composer/semver/zipball/5d8e574bb0e69188786b8ef77d43341222a41a71", + "reference": "5d8e574bb0e69188786b8ef77d43341222a41a71", "shasum": "" }, "require": { @@ -3731,7 +3731,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.3.0" + "source": "https://github.com/composer/semver/tree/3.3.1" }, "funding": [ { @@ -3747,7 +3747,7 @@ "type": "tidelift" } ], - "time": "2022-03-15T08:35:57+00:00" + "time": "2022-03-16T11:22:07+00:00" }, { "name": "composer/xdebug-handler", diff --git a/lib/SimpleSAML/Configuration.php b/lib/SimpleSAML/Configuration.php index 54a6d9edbd0002e4735a6c58cda9942476e1eb6f..1355d8b93dcedcefc23604d198b208de546e1ccc 100644 --- a/lib/SimpleSAML/Configuration.php +++ b/lib/SimpleSAML/Configuration.php @@ -4,11 +4,33 @@ declare(strict_types=1); namespace SimpleSAML; +use Exception; +use ParseError; use SAML2\Constants; use SimpleSAML\Assert\Assert; use SimpleSAML\Assert\AssertionFailedException; use SimpleSAML\Error; use SimpleSAML\Utils; +use Symfony\Component\Filesystem\Filesystem; + +use function array_key_exists; +use function array_keys; +use function dirname; +use function implode; +use function interface_exists; +use function in_array; +use function is_array; +use function is_bool; +use function is_int; +use function is_string; +use function ob_end_clean; +use function ob_get_length; +use function ob_start; +use function preg_match; +use function preg_replace; +use function rtrim; +use function substr; +use function var_export; /** * Configuration of SimpleSAMLphp @@ -106,7 +128,8 @@ class Configuration implements Utils\ClearableState return self::$loadedConfigs[$filename]; } - if (file_exists($filename)) { + $fileSystem = new Filesystem(); + if ($fileSystem->exists($filename)) { /** @psalm-var mixed $config */ $config = 'UNINITIALIZED'; @@ -115,7 +138,7 @@ class Configuration implements Utils\ClearableState if (interface_exists('Throwable', false)) { try { require($filename); - } catch (\ParseError $e) { + } catch (ParseError $e) { self::$loadedConfigs[$filename] = self::loadFromArray([], '[ARRAY]', 'simplesaml'); throw new Error\ConfigurationError($e->getMessage(), $filename, []); } @@ -204,7 +227,7 @@ class Configuration implements Utils\ClearableState ): void { if (!array_key_exists($configSet, self::$configDirs)) { if ($configSet !== 'simplesaml') { - throw new \Exception('Configuration set \'' . $configSet . '\' not initialized.'); + throw new Exception('Configuration set \'' . $configSet . '\' not initialized.'); } else { self::$configDirs['simplesaml'] = dirname(dirname(dirname(__FILE__))) . '/config'; } @@ -232,7 +255,7 @@ class Configuration implements Utils\ClearableState ): Configuration { if (!array_key_exists($configSet, self::$configDirs)) { if ($configSet !== 'simplesaml') { - throw new \Exception('Configuration set \'' . $configSet . '\' not initialized.'); + throw new Exception('Configuration set \'' . $configSet . '\' not initialized.'); } else { $configUtils = new Utils\Config(); self::$configDirs['simplesaml'] = $configUtils->getConfigDir(); @@ -262,7 +285,7 @@ class Configuration implements Utils\ClearableState ): Configuration { if (!array_key_exists($configSet, self::$configDirs)) { if ($configSet !== 'simplesaml') { - throw new \Exception('Configuration set \'' . $configSet . '\' not initialized.'); + throw new Exception('Configuration set \'' . $configSet . '\' not initialized.'); } else { $configUtils = new Utils\Config(); self::$configDirs['simplesaml'] = $configUtils->getConfigDir(); @@ -1113,7 +1136,7 @@ class Configuration implements Utils\ClearableState case 'saml20-idp-remote:ArtifactResolutionService': return Constants::BINDING_SOAP; default: - throw new \Exception('Missing default binding for ' . $endpointType . ' in ' . $set); + throw new Exception('Missing default binding for ' . $endpointType . ' in ' . $set); } } @@ -1142,7 +1165,7 @@ class Configuration implements Utils\ClearableState // for backwards-compatibility $eps = [$eps]; } elseif (!is_array($eps)) { - throw new \Exception($loc . ': Expected array or string.'); + throw new Exception($loc . ': Expected array or string.'); } @@ -1160,32 +1183,32 @@ class Configuration implements Utils\ClearableState $ep['ResponseLocation'] = $responseLocation; } } elseif (!is_array($ep)) { - throw new \Exception($iloc . ': Expected a string or an array.'); + throw new Exception($iloc . ': Expected a string or an array.'); } if (!array_key_exists('Location', $ep)) { - throw new \Exception($iloc . ': Missing Location.'); + throw new Exception($iloc . ': Missing Location.'); } if (!is_string($ep['Location'])) { - throw new \Exception($iloc . ': Location must be a string.'); + throw new Exception($iloc . ': Location must be a string.'); } if (!array_key_exists('Binding', $ep)) { - throw new \Exception($iloc . ': Missing Binding.'); + throw new Exception($iloc . ': Missing Binding.'); } if (!is_string($ep['Binding'])) { - throw new \Exception($iloc . ': Binding must be a string.'); + throw new Exception($iloc . ': Binding must be a string.'); } if (array_key_exists('ResponseLocation', $ep)) { if (!is_string($ep['ResponseLocation'])) { - throw new \Exception($iloc . ': ResponseLocation must be a string.'); + throw new Exception($iloc . ': ResponseLocation must be a string.'); } } if (array_key_exists('index', $ep)) { if (!is_int($ep['index'])) { - throw new \Exception($iloc . ': index must be an integer.'); + throw new Exception($iloc . ': index must be an integer.'); } } } @@ -1223,7 +1246,7 @@ class Configuration implements Utils\ClearableState if ($default === self::REQUIRED_OPTION) { $loc = $this->location . '[' . var_export($endpointType, true) . ']:'; - throw new \Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.'); + throw new Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.'); } return $default; @@ -1253,7 +1276,7 @@ class Configuration implements Utils\ClearableState if ($default === self::REQUIRED_OPTION) { $loc = $this->location . '[' . var_export($endpointType, true) . ']:'; - throw new \Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.'); + throw new Exception($loc . 'Could not find a supported ' . $endpointType . ' endpoint.'); } return $default; @@ -1368,7 +1391,7 @@ class Configuration implements Utils\ClearableState $data = @file_get_contents($file); if ($data === false) { - throw new \Exception( + throw new Exception( $this->location . ': Unable to load certificate/public key from file "' . $file . '".' ); } @@ -1376,7 +1399,7 @@ class Configuration implements Utils\ClearableState // extract certificate data (if this is a certificate) $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; if (!preg_match($pattern, $data, $matches)) { - throw new \SimpleSAML\Error\Exception( + throw new Error\Exception( $this->location . ': Could not find PEM encoded certificate in "' . $file . '".' ); } @@ -1393,7 +1416,7 @@ class Configuration implements Utils\ClearableState ], ]; } elseif ($required === true) { - throw new \SimpleSAML\Error\Exception($this->location . ': Missing certificate in metadata.'); + throw new Error\Exception($this->location . ': Missing certificate in metadata.'); } else { return []; } diff --git a/lib/SimpleSAML/Locale/Localization.php b/lib/SimpleSAML/Locale/Localization.php index 1dbb5c1f6ac6af03606ea878393755d3840308c8..0f446864fd4819e9af8e9436aa57130d60f92ea4 100644 --- a/lib/SimpleSAML/Locale/Localization.php +++ b/lib/SimpleSAML/Locale/Localization.php @@ -10,10 +10,13 @@ declare(strict_types=1); namespace SimpleSAML\Locale; +use Exception; use Gettext\Translations; use Gettext\Translator; use SimpleSAML\Configuration; use SimpleSAML\Logger; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\File; class Localization { @@ -66,6 +69,11 @@ class Localization */ private string $langcode; + /** + * @var \Symfony\Component\Filesystem\Filesystem; + */ + private Filesystem $fileSystem; + /** * Constructor @@ -74,6 +82,7 @@ class Localization */ public function __construct(Configuration $configuration) { + $this->fileSystem = new Filesystem(); $this->configuration = $configuration; /** @var string $locales */ $locales = $this->configuration->resolvePath('locales'); @@ -186,7 +195,7 @@ class Localization // Locale for default language missing even, error out $error = "Localization directory '$langPath' missing/broken for langcode '$langcode' and domain '$domain'"; Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error); - throw new \Exception($error); + throw new Exception($error); } @@ -217,7 +226,7 @@ class Localization ): void { try { $langPath = $this->getLangPath($domain); - } catch (\Exception $e) { + } catch (Exception $e) { $error = "Something went wrong when trying to get path to language file, cannot load domain '$domain'."; Logger::debug($_SERVER['PHP_SELF'] . ' - ' . $error); if ($catchException) { @@ -227,14 +236,18 @@ class Localization throw $e; } } - $poFile = $domain . '.po'; - $poPath = $langPath . $poFile; - if (file_exists($poPath) && is_readable($poPath)) { - $translations = Translations::fromPoFile($poPath); + + $file = new File($langPath . $domain . '.po'); + if ($this->fileSystem->exists($file->getRealPath()) && $file->isReadable()) { + $translations = Translations::fromPoFile($file->getRealPath()); $this->translator->loadTranslations($translations); } else { - $error = "Localization file '$poFile' not found in '$langPath', falling back to default"; - Logger::debug($_SERVER['PHP_SELF'] . ' - ' . $error); + Logger::debug(sprintf( + "%s - Localization file '%s' not found or not readable in '%s', falling back to default", + $_SERVER['PHP_SELF'], + $file->getfileName(), + $langPath, + )); } } diff --git a/lib/SimpleSAML/Logger/FileLoggingHandler.php b/lib/SimpleSAML/Logger/FileLoggingHandler.php index 179080bb2436ee1750e0c55f7a97a4ade2616aa2..a11eff283a85a2c59b204c69419a7bc5b42b3e1b 100644 --- a/lib/SimpleSAML/Logger/FileLoggingHandler.php +++ b/lib/SimpleSAML/Logger/FileLoggingHandler.php @@ -7,6 +7,9 @@ namespace SimpleSAML\Logger; use SimpleSAML\Configuration; use SimpleSAML\Logger; use SimpleSAML\Utils; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; +use Symfony\Component\HttpFoundation\File\File; /** * A logging handler that dumps logs to files. @@ -45,6 +48,11 @@ class FileLoggingHandler implements LoggingHandlerInterface /** @var string */ protected string $format = "%b %d %H:%M:%S"; + /** + * @var \Symfony\Component\Filesystem\Filesystem; + */ + protected Filesystem $fileSystem; + /** * Build a new logging handler based on files. @@ -52,6 +60,8 @@ class FileLoggingHandler implements LoggingHandlerInterface */ public function __construct(Configuration $config) { + $this->fileSystem = new Filesystem(); + // get the metadata handler option from the configuration $this->logFile = $config->getPathValue('loggingdir', 'log/') . $config->getOptionalString('logging.logfile', 'simplesamlphp.log'); @@ -63,17 +73,19 @@ class FileLoggingHandler implements LoggingHandlerInterface $config->getOptionalString('logging.processname', 'SimpleSAMLphp') ); - if (@file_exists($this->logFile)) { - if (!@is_writeable($this->logFile)) { - throw new \Exception("Could not write to logfile: " . $this->logFile); - } - } else { - if (!@touch($this->logFile)) { - throw new \Exception( - "Could not create logfile: " . $this->logFile . - " The logging directory is not writable for the web server user." + $file = new File($this->logFile); + // Suppress E_WARNING if not exists + if (@$this->fileSystem->exists($this->logFile)) { + if (!$file->isWritable()) { + throw new CannotWriteFileException( + sprintf("Could not write to logfile: %s", $this->logFile), ); } + } elseif (!$this->fileSystem->touch($this->logFile)) { + throw new CannotWriteFileException(sprintf( + "The logging directory is not writable for the web server user. Could not create logfile: %s", + $this->logFile, + )); } $timeUtils = new Utils\Time(); @@ -121,8 +133,20 @@ class FileLoggingHandler implements LoggingHandlerInterface array_push($replacements, date($format)); } - $string = str_replace($formats, $replacements, $string); - file_put_contents($this->logFile, $string . \PHP_EOL, FILE_APPEND); + if (preg_match('/^php:\/\//', $this->logFile)) { + // Dirty hack to get unit tests for Windows working.. Symfony doesn't deal well with them. + file_put_contents( + $this->logFile, + str_replace($formats, $replacements, $string) . \PHP_EOL, + FILE_APPEND, + ); + } else { + $this->fileSystem->appendToFile( + $this->logFile, + str_replace($formats, $replacements, $string) . \PHP_EOL, + false, + ); + } } } } diff --git a/lib/SimpleSAML/Logger/StandardErrorLoggingHandler.php b/lib/SimpleSAML/Logger/StandardErrorLoggingHandler.php index 1fd967955ee9bed379d36fc458829c19198b2d84..37bf8e14f0dbbb0d89f5d35dbdb5bef70c06813c 100644 --- a/lib/SimpleSAML/Logger/StandardErrorLoggingHandler.php +++ b/lib/SimpleSAML/Logger/StandardErrorLoggingHandler.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace SimpleSAML\Logger; use SimpleSAML\Configuration; +use Symfony\Component\Filesystem\Filesystem; /** * A logging handler that outputs all messages to standard error. @@ -22,12 +23,15 @@ class StandardErrorLoggingHandler extends FileLoggingHandler */ public function __construct(Configuration $config) { + $this->fileSystem = new Filesystem(); + // Remove any non-printable characters before storing $this->processname = preg_replace( '/[\x00-\x1F\x7F\xA0]/u', '', $config->getOptionalString('logging.processname', 'SimpleSAMLphp') ); + $this->logFile = 'php://stderr'; } } diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php index 8b1f39b0a256d01c8de964d1ca2852ef815ac1b8..45bae13548aff54c3352b1ed1f1b0a495b1e5990 100644 --- a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php +++ b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php @@ -4,9 +4,13 @@ declare(strict_types=1); namespace SimpleSAML\Metadata; +use Exception; use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; +use function array_key_exists; +use function is_array; + /** * This file defines a flat file metadata source. * Instantiation of session handler objects should be done through @@ -45,6 +49,8 @@ class MetaDataStorageHandlerFlatFile extends MetaDataStorageSource */ protected function __construct(array $config) { + parent::__construct(); + // get the configuration $globalConfig = Configuration::getInstance(); @@ -79,7 +85,7 @@ class MetaDataStorageHandlerFlatFile extends MetaDataStorageSource { $metadatasetfile = $this->directory . $set . '.php'; - if (!file_exists($metadatasetfile)) { + if (!$this->fileSystem->exists($metadatasetfile)) { return null; } @@ -89,7 +95,7 @@ class MetaDataStorageHandlerFlatFile extends MetaDataStorageSource include($metadatasetfile); if (!is_array($metadata)) { - throw new \Exception('Could not load metadata set [' . $set . '] from file: ' . $metadatasetfile); + throw new Exception('Could not load metadata set [' . $set . '] from file: ' . $metadatasetfile); } return $metadata; diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php index 1dcf7d1cf19265f523bbd52617e4dd0a64d6654c..07499485df25cbebbf19531ab0b5402801e21a32 100644 --- a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php +++ b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php @@ -8,6 +8,18 @@ use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Logger; use SimpleSAML\Utils; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\Finder; +use Symfony\Component\HttpFoundation\File\File; + +use function array_key_exists; +use function rawurlencode; +use function serialize; +use function strlen; +use function substr; +use function unserialize; +use function var_export; /** * Class for handling metadata files in serialized format. @@ -42,8 +54,9 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource */ public function __construct(array $config) { - $globalConfig = Configuration::getInstance(); + parent::__construct(); + $globalConfig = Configuration::getInstance(); $cfgHelp = Configuration::loadFromArray($config, 'serialize metadata source'); $this->directory = $cfgHelp->getString('directory'); @@ -78,35 +91,22 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource { $ret = []; - $dh = @opendir($this->directory); - if ($dh === false) { + $loc = new File($this->directory, false); + if (!$this->fileSystem->exists($this->directory) || !$loc->isReadable()) { Logger::warning( 'Serialize metadata handler: Unable to open directory: ' . var_export($this->directory, true) ); return $ret; } - while (($entry = readdir($dh)) !== false) { - if ($entry[0] === '.') { - // skip '..', '.' and hidden files - continue; - } - - $path = $this->directory . '/' . $entry; + $finder = new Finder(); + $finder->directories()->name(sprintf('/%s$/' . self::EXTENSION))->in($this->directory); - if (!is_dir($path)) { - Logger::warning( - 'Serialize metadata handler: Metadata directory contained a file where only directories should ' . - 'exist: ' . var_export($path, true) - ); - continue; - } - - $ret[] = rawurldecode($entry); + $ret = []; + foreach ($finder as $file) { + $ret[] = rawurlencode($file->getPathName()); } - closedir($dh); - return $ret; } @@ -122,32 +122,23 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource { $ret = []; - $dir = $this->directory . '/' . rawurlencode($set); - if (!is_dir($dir)) { - // probably some code asked for a metadata set which wasn't available - return $ret; - } - - $dh = @opendir($dir); - if ($dh === false) { - Logger::warning( - 'Serialize metadata handler: Unable to open directory: ' . var_export($dir, true) - ); + $loc = new File(Path::canonicalize($this->directory . '/' . rawurlencode($set)), false); + if (!$this->fileSystem->exists($loc) || !$loc->isReadable()) { + Logger::warning(sprintf( + 'Serialize metadata handler: Unable to open directory: %s', + var_export($loc->getPathName(), true), + )); return $ret; } $extLen = strlen(self::EXTENSION); - while (($file = readdir($dh)) !== false) { - if (strlen($file) <= $extLen) { - continue; - } - - if (substr($file, -$extLen) !== self::EXTENSION) { - continue; - } + $finder = new Finder(); + $finder->directories()->name(sprintf('/%s/$', self::EXTENSION))->in($this->directory); - $entityId = substr($file, 0, -$extLen); + $ret = []; + foreach ($finder as $file) { + $entityId = substr($file->getPathName(), 0, -$extLen); $entityId = rawurldecode($entityId); $md = $this->getMetaData($entityId, $set); @@ -156,8 +147,6 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource } } - closedir($dh); - return $ret; } @@ -175,17 +164,15 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource { $filePath = $this->getMetadataPath($entityId, $set); - if (!file_exists($filePath)) { + if (!$this->fileSystem->exists($filePath)) { return null; } - $data = @file_get_contents($filePath); - if ($data === false) { - /** @var array $error */ - $error = error_get_last(); - Logger::warning( - 'Error reading file ' . $filePath . ': ' . $error['message'] - ); + $file = new File($filePath); + try { + $data = $file->getContent(); + } catch (IOException $e) { + Logger::warning('Error reading file ' . $filePath . ': ' . $e->getMessage()); return null; } @@ -214,38 +201,37 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource */ public function saveMetadata(string $entityId, string $set, array $metadata): bool { - $filePath = $this->getMetadataPath($entityId, $set); - $newPath = $filePath . '.new'; - - $dir = dirname($filePath); - if (!is_dir($dir)) { - Logger::info('Creating directory: ' . $dir); - $res = @mkdir($dir, 0777, true); - if ($res === false) { - /** @var array $error */ - $error = error_get_last(); - Logger::error('Failed to create directory ' . $dir . ': ' . $error['message']); + $old = new File($this->getMetadataPath($entityId, $set), false); + $new = new File($old->getPathName() . '.new', false); + + $loc = new File($old->getPath(), false); + if (!$loc->isDir()) { + Logger::info('Creating directory: ' . $loc); + try { + $this->fileSystem->mkdir($loc, 0777); + } catch (IOException $e) { + Logger::error('Failed to create directory ' . $loc . ': ' . $e->getMessage()); return false; } } $data = serialize($metadata); - Logger::debug('Writing: ' . $newPath); + Logger::debug('Writing: ' . $new->getPathName()); - $res = file_put_contents($newPath, $data); - if ($res === false) { - /** @var array $error */ - $error = error_get_last(); - Logger::error('Error saving file ' . $newPath . ': ' . $error['message']); + try { + $this->fileSystem->appendToFile($new->getPathName(), $data); + } catch (IOException $e) { + Logger::error('Error saving file ' . $new->getPathName() . ': ' . $e->getMessage()); return false; } - $res = rename($newPath, $filePath); - if ($res === false) { - /** @var array $error */ - $error = error_get_last(); - Logger::error('Error renaming ' . $newPath . ' to ' . $filePath . ': ' . $error['message']); + try { + $this->fileSystem->rename($new->getPathName(), $old->getPathName(), true); + } catch (IOException $e) { + Logger::error( + sprintf('Error renaming %s to %s: %s', $new->getPathName(), $old->getPathName(), $e->getMessage()) + ); return false; } @@ -263,7 +249,7 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource { $filePath = $this->getMetadataPath($entityId, $set); - if (!file_exists($filePath)) { + if (!$this->fileSystem->exists($filePath)) { Logger::warning( 'Attempted to erase nonexistent metadata entry ' . var_export($entityId, true) . ' in set ' . var_export($set, true) . '.' @@ -271,14 +257,14 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource return; } - $res = unlink($filePath); - if ($res === false) { - /** @var array $error */ - $error = error_get_last(); - Logger::error( - 'Failed to delete file ' . $filePath . - ': ' . $error['message'] - ); + try { + $this->fileSystem->remove($filePath); + } catch (IOException $e) { + Logger::error(sprintf( + 'Failed to delete file %s: %s', + $filePath, + $e->getMessage(), + )); } } diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageSource.php b/lib/SimpleSAML/Metadata/MetaDataStorageSource.php index faef115c5ff069dce575b690464018afa936ff4c..9dac4f0c72a4250bcfb4149a9b4cda59f2ad06dd 100644 --- a/lib/SimpleSAML/Metadata/MetaDataStorageSource.php +++ b/lib/SimpleSAML/Metadata/MetaDataStorageSource.php @@ -8,6 +8,7 @@ use SimpleSAML\Assert\Assert; use SimpleSAML\Error; use SimpleSAML\Module; use SimpleSAML\Utils; +use Symfony\Component\Filesystem\Filesystem; /** * This abstract class defines an interface for metadata storage sources. @@ -21,6 +22,21 @@ use SimpleSAML\Utils; abstract class MetaDataStorageSource { + /** + * @var \Symfony\Component\Filesystem\Filesystem; + */ + protected Filesystem $fileSystem; + + + /** + * This function initializes an XML metadata source. + */ + protected function __construct() + { + $this->fileSystem = new Filesystem(); + } + + /** * Parse array with metadata sources. * diff --git a/lib/SimpleSAML/Metadata/SAMLParser.php b/lib/SimpleSAML/Metadata/SAMLParser.php index 3b79cd0e6835fbf41bf11c84df50996d93f8cd6d..46fa2b25dc4d1fe6418f41dbc54f96b91b0818a7 100644 --- a/lib/SimpleSAML/Metadata/SAMLParser.php +++ b/lib/SimpleSAML/Metadata/SAMLParser.php @@ -6,6 +6,7 @@ namespace SimpleSAML\Metadata; use DOMDocument; use DOMElement; +use Exception; use RobRichards\XMLSecLibs\XMLSecurityDSig; use RobRichards\XMLSecLibs\XMLSecurityKey; use SAML2\Constants; @@ -38,6 +39,15 @@ use SAML2\XML\shibmd\Scope; use SimpleSAML\Assert\Assert; use SimpleSAML\Logger; use SimpleSAML\Utils; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\File; + +use function array_diff; +use function array_intersect; +use function array_key_exists; +use function array_map; +use function array_merge; +use function count; /** * This is class for parsing of SAML 2.0 metadata. @@ -161,6 +171,11 @@ class SAMLParser */ private string $entityDescriptor; + /** + * @var \Symfony\Component\Filesystem\Filesystem; + */ + protected Filesystem $fileSystem; + /** * This is the constructor for the SAMLParser class. @@ -177,6 +192,7 @@ class SAMLParser array $validators = [], array $parentExtensions = [] ) { + $this->fileSystem = new Filesystem(); $this->spDescriptors = []; $this->idpDescriptors = []; @@ -236,8 +252,8 @@ class SAMLParser try { $doc = DOMDocumentFactory::fromString($data); - } catch (\Exception $e) { - throw new \Exception('Failed to read XML from file: ' . $file); + } catch (Exception $e) { + throw new Exception('Failed to read XML from file: ' . $file); } return self::parseDocument($doc); @@ -256,8 +272,8 @@ class SAMLParser { try { $doc = DOMDocumentFactory::fromString($metadata); - } catch (\Exception $e) { - throw new \Exception('Failed to parse XML string.'); + } catch (Exception $e) { + throw new Exception('Failed to parse XML string.'); } return self::parseDocument($doc); @@ -307,7 +323,7 @@ class SAMLParser public static function parseDescriptorsFile(string $file): array { if (empty($file)) { - throw new \Exception('Cannot open file; file name not specified.'); + throw new Exception('Cannot open file; file name not specified.'); } /** @var string $data */ @@ -316,8 +332,8 @@ class SAMLParser try { $doc = DOMDocumentFactory::fromString($data); - } catch (\Exception $e) { - throw new \Exception('Failed to read XML from file: ' . $file); + } catch (Exception $e) { + throw new Exception('Failed to read XML from file: ' . $file); } return self::parseDescriptorsElement($doc->documentElement); @@ -339,8 +355,8 @@ class SAMLParser { try { $doc = DOMDocumentFactory::fromString($string); - } catch (\Exception $e) { - throw new \Exception('Failed to parse XML string.'); + } catch (Exception $e) { + throw new Exception('Failed to parse XML string.'); } return self::parseDescriptorsElement($doc->documentElement); @@ -361,7 +377,7 @@ class SAMLParser public static function parseDescriptorsElement(DOMElement $element = null): array { if ($element === null) { - throw new \Exception('Document was empty.'); + throw new Exception('Document was empty.'); } $xmlUtils = new Utils\XML(); @@ -370,7 +386,7 @@ class SAMLParser } elseif ($xmlUtils->isDOMNodeOfType($element, 'EntitiesDescriptor', '@md') === true) { return self::processDescriptorsElement(new EntitiesDescriptor($element)); } else { - throw new \Exception('Unexpected root node: [' . $element->namespaceURI . ']:' . $element->localName); + throw new Exception('Unexpected root node: [' . $element->namespaceURI . ']:' . $element->localName); } } @@ -1247,7 +1263,7 @@ class SAMLParser $xmlUtils = new Utils\XML(); if ($xmlUtils->isDOMNodeOfType($ed, 'EntityDescriptor', '@md') === false) { - throw new \Exception('Expected first element in the metadata document to be an EntityDescriptor element.'); + throw new Exception('Expected first element in the metadata document to be an EntityDescriptor element.'); } return new EntityDescriptor($ed); @@ -1270,12 +1286,13 @@ class SAMLParser foreach ($certificates as $cert) { Assert::string($cert); $certFile = $configUtils->getCertPath($cert); - if (!file_exists($certFile)) { - throw new \Exception( + if (!$this->fileSystem->exists($certFile)) { + throw new Exception( 'Could not find certificate file [' . $certFile . '], which is needed to validate signature' ); } - $certData = file_get_contents($certFile); + $file = new File($certFile); + $certData = $file->getContent(); foreach ($this->validators as $validator) { $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'public']); @@ -1284,7 +1301,7 @@ class SAMLParser if ($validator->validate($key)) { return true; } - } catch (\Exception $e) { + } catch (Exception $e) { // this certificate did not sign this element, skip } } diff --git a/lib/SimpleSAML/Metadata/Signer.php b/lib/SimpleSAML/Metadata/Signer.php index 10dd1208a74805265e7be1ad8d3edcca5225be54..7a35a33dcefc24206c8f1ed2b690c755d45793c1 100644 --- a/lib/SimpleSAML/Metadata/Signer.php +++ b/lib/SimpleSAML/Metadata/Signer.php @@ -4,17 +4,26 @@ declare(strict_types=1); namespace SimpleSAML\Metadata; +use Exception; use RobRichards\XMLSecLibs\XMLSecurityKey; use RobRichards\XMLSecLibs\XMLSecurityDSig; use SAML2\DOMDocumentFactory; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\Utils; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\File; + +use function array_key_exists; +use function hash; +use function in_array; +use function is_bool; +use function is_string; /** * This class implements a helper function for signing of metadata. * - * @package SimpleSAMLphp + * @package simplesamlphp/simplesamlphp */ class Signer @@ -41,7 +50,7 @@ class Signer !array_key_exists('metadata.sign.privatekey', $entityMetadata) || !array_key_exists('metadata.sign.certificate', $entityMetadata) ) { - throw new \Exception( + throw new Exception( 'Missing either the "metadata.sign.privatekey" or the' . ' "metadata.sign.certificate" configuration option in the metadata for' . ' the ' . $type . ' "' . $entityMetadata['entityid'] . '". If one of' . @@ -66,7 +75,7 @@ class Signer $certificate = $config->getOptionalString('metadata.sign.certificate', null); if ($privatekey !== null || $certificate !== null) { if ($privatekey === null || $certificate === null) { - throw new \Exception( + throw new Exception( 'Missing either the "metadata.sign.privatekey" or the' . ' "metadata.sign.certificate" configuration option in the global' . ' configuration. If one of these options is specified, then the other' . @@ -92,7 +101,7 @@ class Signer !array_key_exists('privatekey', $entityMetadata) || !array_key_exists('certificate', $entityMetadata) ) { - throw new \Exception( + throw new Exception( 'Both the "privatekey" and the "certificate" option must' . ' be set in the metadata for the ' . $type . ' "' . $entityMetadata['entityid'] . '" before it is possible to sign metadata' . @@ -112,7 +121,7 @@ class Signer return $ret; } - throw new \Exception( + throw new Exception( 'Could not find what key & certificate should be used to sign the metadata' . ' for the ' . $type . ' "' . $entityMetadata['entityid'] . '".' ); @@ -134,7 +143,7 @@ class Signer // first check the metadata for the entity if (array_key_exists('metadata.sign.enable', $entityMetadata)) { if (!is_bool($entityMetadata['metadata.sign.enable'])) { - throw new \Exception( + throw new Exception( 'Invalid value for the "metadata.sign.enable" configuration option for' . ' the ' . $type . ' "' . $entityMetadata['entityid'] . '". This option' . ' should be a boolean.' @@ -237,27 +246,30 @@ class Signer $keyCertFiles = self::findKeyCert($config, $entityMetadata, $type); $keyFile = $configUtils->getCertPath($keyCertFiles['privatekey']); - if (!file_exists($keyFile)) { - throw new \Exception( + $fileSystem = new Filesystem(); + if (!$fileSystem->exists($keyFile)) { + throw new Exception( 'Could not find private key file [' . $keyFile . '], which is needed to sign the metadata' ); } - $keyData = file_get_contents($keyFile); + + $key = new File($keyFile); + $keyData = $key->getContent(); $certFile = $configUtils->getCertPath($keyCertFiles['certificate']); - if (!file_exists($certFile)) { - throw new \Exception( + $cert = new File($certFile); + if (!$fileSystem->exists($certFile)) { + throw new Exception( 'Could not find certificate file [' . $certFile . '], which is needed to sign the metadata' ); } - $certData = file_get_contents($certFile); - + $certData = $cert->getContent(); // convert the metadata to a DOM tree try { $xml = DOMDocumentFactory::fromString($metadataString); - } catch (\Exception $e) { - throw new \Exception('Error parsing self-generated metadata.'); + } catch (Exception $e) { + throw new Exception('Error parsing self-generated metadata.'); } $signature_cf = self::getMetadataSigningAlgorithm($config, $entityMetadata, $type); diff --git a/lib/SimpleSAML/Metadata/Sources/MDQ.php b/lib/SimpleSAML/Metadata/Sources/MDQ.php index 278cc57a29e10dcadb1560dbb7a5df88e7f29d21..7b99ced918bf906908e9200220747eed548cf6d0 100644 --- a/lib/SimpleSAML/Metadata/Sources/MDQ.php +++ b/lib/SimpleSAML/Metadata/Sources/MDQ.php @@ -4,21 +4,35 @@ declare(strict_types=1); namespace SimpleSAML\Metadata\Sources; +use Exception; use RobRichards\XMLSecLibs\XMLSecurityDSig; use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\Logger; +use SimpleSAML\Metadata\MetaDataStorageSource; use SimpleSAML\Metadata\SAMLParser; use SimpleSAML\Utils; +use Symfony\Component\HttpFoundation\File\File; + +use function array_key_exists; +use function error_get_last; +use function is_array; +use function serialize; +use function sha1; +use function sprintf; +use function strval; +use function time; +use function unserialize; +use function urlencode; /** * This class implements SAML Metadata Query Protocol * - * @package SimpleSAMLphp + * @package simplesamlphp/simplesamlphp */ -class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource +class MDQ extends MetaDataStorageSource { /** * The URL of MDQ server (url:port) @@ -34,7 +48,6 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource */ private ?string $cacheDir; - /** * The maximum cache length, in seconds. * @@ -42,7 +55,6 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource */ private int $cacheLength; - /** * This function initializes the dynamic XML metadata source. * @@ -60,8 +72,10 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource */ protected function __construct(array $config) { + parent::__construct($config); + if (!array_key_exists('server', $config)) { - throw new \Exception(__CLASS__ . ": the 'server' configuration option is not set."); + throw new Exception(__CLASS__ . ": the 'server' configuration option is not set."); } else { $this->server = $config['server']; } @@ -120,7 +134,7 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource * @param string $set The metadata set this entity belongs to. * @param string $entityId The entity id of this entity. * - * @return array|NULL The associative array with the metadata for this entity, or NULL + * @return array|null The associative array with the metadata for this entity, or NULL * if the entity could not be found. * @throws \Exception If an error occurs while loading metadata from cache. */ @@ -130,41 +144,47 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource return null; } - $cachefilename = $this->getCacheFilename($set, $entityId); - if (!file_exists($cachefilename)) { + $cacheFileName = $this->getCacheFilename($set, $entityId); + if (!$this->fileSystem->exists($cacheFileName)) { return null; } - if (!is_readable($cachefilename)) { - throw new \Exception(__CLASS__ . ': could not read cache file for entity [' . $cachefilename . ']'); + + $file = new File($cacheFileName); + if (!$file->isReadable()) { + throw new Exception(sprintf('%s: could not read cache file for entity [%s]', strval($file), __CLASS__)); } - Logger::debug(__CLASS__ . ': reading cache [' . $entityId . '] => [' . $cachefilename . ']'); + Logger::debug(sprintf('%s: reading cache [%s] => [%s]', __CLASS__, $entityId, strval($file))); /* Ensure that this metadata isn't older that the cachelength option allows. This * must be verified based on the file, since this option may be changed after the * file is written. */ - $stat = stat($cachefilename); - if ($stat['mtime'] + $this->cacheLength <= time()) { - Logger::debug(__CLASS__ . ': cache file older that the cachelength option allows.'); + if (($file->getMtime() + $this->cacheLength) <= time()) { + Logger::debug(sprintf('%s: cache file older that the cachelength option allows.', __CLASS__)); return null; } - $rawData = file_get_contents($cachefilename); + $rawData = $file->getContent(); if (empty($rawData)) { /** @var array $error */ $error = error_get_last(); - throw new \Exception( - __CLASS__ . ': error reading metadata from cache file "' . $cachefilename . '": ' . $error['message'] - ); + throw new Exception(sprintf( + '%s: error reading metadata from cache file "%s": %s', + __CLASS__, + strval($file), + $error['message'], + )); } $data = unserialize($rawData); if ($data === false) { - throw new \Exception(__CLASS__ . ': error unserializing cached data from file "' . $cachefilename . '".'); + throw new Exception( + sprintf('%s: error unserializing cached data from file "%s".', __CLASS__, strval($file)) + ); } if (!is_array($data)) { - throw new \Exception(__CLASS__ . ': Cached metadata from "' . $cachefilename . '" wasn\'t an array.'); + throw new Exception(sprintf('%s: Cached metadata from "%s" wasn\'t an array.', __CLASS__, strval($file))); } return $data; @@ -186,12 +206,11 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource return; } - $cachefilename = $this->getCacheFilename($set, $entityId); - if (!is_writable(dirname($cachefilename))) { - throw new \Exception(__CLASS__ . ': could not write cache file for entity [' . $cachefilename . ']'); - } - Logger::debug(__CLASS__ . ': Writing cache [' . $entityId . '] => [' . $cachefilename . ']'); - file_put_contents($cachefilename, serialize($data)); + $cacheFileName = $this->getCacheFilename($set, $entityId); + $file = new File($cacheFileName); + + Logger::debug(sprintf('%s: Writing cache [%s] => [%s]', __CLASS__, $entityId, strval($file))); + $this->fileSystem->appendToFile(strval($file), serialize($data), true); } @@ -201,7 +220,7 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource * @param \SimpleSAML\Metadata\SAMLParser $entity A SAML2Parser representing an entity. * @param string $set The metadata set we are looking for. * - * @return array|NULL The associative array with the metadata, or NULL if no metadata for + * @return array|null The associative array with the metadata, or NULL if no metadata for * the given set was found. */ private static function getParsedSet(SAMLParser $entity, string $set): ?array @@ -214,7 +233,7 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource case 'attributeauthority-remote': return $entity->getAttributeAuthorities(); default: - Logger::warning(__CLASS__ . ': unknown metadata set: \'' . $set . '\'.'); + Logger::warning(sprintf('%s: unknown metadata set: \'%s\'.', __CLASS__, $set)); } return null; @@ -241,12 +260,12 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource */ public function getMetaData(string $entityId, string $set): ?array { - Logger::info(__CLASS__ . ': loading metadata entity [' . $entityId . '] from [' . $set . ']'); + Logger::info(sprintf('%s: loading metadata entity [%s] from [%s]', __CLASS__, $entityId, $set)); // read from cache if possible try { $data = $this->getFromCache($set, $entityId); - } catch (\Exception $e) { + } catch (Exception $e) { Logger::error($e->getMessage()); // proceed with fetching metadata even if the cache is broken $data = null; @@ -259,45 +278,49 @@ class MDQ extends \SimpleSAML\Metadata\MetaDataStorageSource if (isset($data)) { // metadata found in cache and not expired - Logger::debug(__CLASS__ . ': using cached metadata for: ' . $entityId . '.'); + Logger::debug(sprintf('%s: using cached metadata for: %s.', __CLASS__, $entityId)); return $data; } // look at Metadata Query Protocol: https://github.com/iay/md-query/blob/master/draft-young-md-query.txt $mdq_url = $this->server . '/entities/' . urlencode($entityId); - Logger::debug(__CLASS__ . ': downloading metadata for "' . $entityId . '" from [' . $mdq_url . ']'); + Logger::debug(sprintf('%s: downloading metadata for "%s" from [%s]', __CLASS__, $entityId, $mdq_url)); $httpUtils = new Utils\HTTP(); try { $xmldata = $httpUtils->fetch($mdq_url); - } catch (\Exception $e) { + } catch (Exception $e) { // Avoid propagating the exception, make sure we can handle the error later $xmldata = false; } if (empty($xmldata)) { $error = error_get_last(); - Logger::info('Unable to fetch metadata for "' . $entityId . '" from ' . $mdq_url . ': ' . - (is_array($error) ? $error['message'] : 'no error available')); + Logger::info(sprintf( + 'Unable to fetch metadata for "%s" from %s: %s', + $entityId, + $mdq_url, + (is_array($error) ? $error['message'] : 'no error available') + )); return null; } /** @var string $xmldata */ $entity = SAMLParser::parseString($xmldata); - Logger::debug(__CLASS__ . ': completed parsing of [' . $mdq_url . ']'); + Logger::debug(sprintf('%s: completed parsing of [%s]', __CLASS__, $mdq_url)); $data = self::getParsedSet($entity, $set); if ($data === null) { - throw new \Exception( - __CLASS__ . ': no metadata for set "' . $set . '" available from "' . $entityId . '".' + throw new Exception( + sprintf('%s: no metadata for set "%s" available from "%s".', __CLASS__, $set, $entityId) ); } try { $this->writeToCache($set, $entityId, $data); - } catch (\Exception $e) { + } catch (Exception $e) { // Proceed without writing to cache - Logger::error('Error writing MDQ result to cache: ' . $e->getMessage()); + Logger::error(sprintf('Error writing MDQ result to cache: %s', $e->getMessage())); } return $data; diff --git a/lib/SimpleSAML/Module.php b/lib/SimpleSAML/Module.php index c23e4f6430a83e351564def48a4b84b86157a552..cb3d9be3bf55ca3d65254f17ba0630df9bb13e5a 100644 --- a/lib/SimpleSAML/Module.php +++ b/lib/SimpleSAML/Module.php @@ -9,6 +9,9 @@ use SimpleSAML\Assert\Assert; use SimpleSAML\Kernel; use SimpleSAML\Utils; use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\Finder; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -16,6 +19,31 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use function array_filter; +use function array_key_exists; +use function class_exists; +use function count; +use function dirname; +use function explode; +use function function_exists; +use function in_array; +use function is_bool; +use function is_callable; +use function is_dir; +use function is_file; +use function is_null; +use function is_subclass_of; +use function mb_strtolower; +use function mime_content_type; +use function preg_match; +use function rtrim; +use function str_replace; +use function strlen; +use function strpos; +use function strtolower; +use function strval; +use function substr; + /** * Helper class for accessing information about modules. * @@ -233,11 +261,12 @@ class Module } $path = $moduleDir . $url; + $fileSystem = new Filesystem(); if ($path[strlen($path) - 1] === '/') { // path ends with a slash - directory reference. Attempt to find index file in directory foreach (self::$indexFiles as $if) { - if (file_exists($path . $if)) { + if ($fileSystem->exists($path . $if)) { $path .= $if; break; } @@ -251,7 +280,7 @@ class Module throw new Error\NotFound('Directory listing not available.'); } - if (!file_exists($path)) { + if (!$fileSystem->exists($path)) { // file not found Logger::info('Could not find file \'' . $path . '\'.'); throw new Error\NotFound('The URL wasn\'t found in the module.'); @@ -373,21 +402,11 @@ class Module $path = self::getModuleDir('.'); - $dh = scandir($path); - if ($dh === false) { - throw new Exception('Unable to open module directory "' . $path . '".'); - } - - foreach ($dh as $f) { - if ($f[0] === '.') { - continue; - } - - if (!is_dir($path . '/' . $f)) { - continue; - } + $finder = new Finder(); + $finder->directories()->in($path)->depth(0); - self::$modules[] = $f; + foreach ($finder as $module) { + self::$modules[] = $module->getFileName(); } return self::$modules; @@ -485,25 +504,21 @@ class Module return self::$modules[$module]['hooks']; } - $hook_dir = self::getModuleDir($module) . '/hooks'; - if (!is_dir($hook_dir)) { - return []; - } - $hooks = []; - $files = scandir($hook_dir); - foreach ($files as $file) { - if ($file[0] === '.') { - continue; - } - - if (!preg_match('/^hook_(\w+)\.php$/', $file, $matches)) { - continue; + $hook_dir = Path::canonicalize(dirname(dirname(dirname(__FILE__))) . '/modules/' . $module . '/hooks'); + if ((new Filesystem())->exists($hook_dir)) { + $finder = new Finder(); + $finder->files()->in($hook_dir)->depth(0); + + foreach ($finder as $file) { + if (preg_match('/^hook_(\w+)\.php$/', $file->getFileName(), $matches)) { + $hook_name = $matches[1]; + $hook_func = $module . '_hook_' . $hook_name; + $hooks[$hook_name] = ['file' => Path::canonicalize(strval($file)), 'func' => $hook_func]; + } } - $hook_name = $matches[1]; - $hook_func = $module . '_hook_' . $hook_name; - $hooks[$hook_name] = ['file' => $hook_dir . '/' . $file, 'func' => $hook_func]; } + return $hooks; } diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php index 8b0c421aeea76ac00ea28c2859312987cf8b6c68..a0415a4a0bf5e4daac1a59f30ce3958cf012d193 100644 --- a/lib/SimpleSAML/XHTML/Template.php +++ b/lib/SimpleSAML/XHTML/Template.php @@ -10,8 +10,11 @@ declare(strict_types=1); namespace SimpleSAML\XHTML; +use Exception; +use InvalidArgumentException; use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; +use SimpleSAML\Error; use SimpleSAML\Locale\Language; use SimpleSAML\Locale\Localization; use SimpleSAML\Locale\Translate; @@ -20,12 +23,32 @@ use SimpleSAML\Logger; use SimpleSAML\Module; use SimpleSAML\Utils; use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\Finder; +use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\Response; use Twig\Environment; +use Twig\Error\RuntimeError; +use Twig\Extra\Intl\IntlExtension; use Twig\Loader\FilesystemLoader; use Twig\TwigFilter; use Twig\TwigFunction; +use function class_exists; +use function count; +use function date; +use function explode; +use function hash; +use function htmlspecialchars; +use function in_array; +use function is_null; +use function key; +use function ksort; +use function strripos; +use function strtolower; +use function strval; +use function substr; + /** * The content-property is set upstream, but this is not recognized by Psalm * @psalm-suppress PropertyNotSetInConstructor @@ -110,6 +133,11 @@ class Template extends Response */ private array $theme = ['module' => null, 'name' => 'default']; + /** + * @var \Symfony\Component\Filesystem\Filesystem; + */ + private Filesystem $fileSystem; + /** * Constructor @@ -150,6 +178,7 @@ class Template extends Response $this->twig = $this->setupTwig(); $this->charset = 'UTF-8'; + $this->fileSystem = new Filesystem(); parent::__construct(); } @@ -173,14 +202,15 @@ class Template extends Response $path = Module::getModuleUrl($module . '/assets/' . $asset); } - if (!file_exists($file)) { + if (!$this->fileSystem->exists($file)) { // don't be too harsh if an asset is missing, just pretend it's there... return $path; } + $file = new File($file); $tag = $this->configuration->getVersion(); if ($tag === 'master') { - $tag = strval(filemtime($file)); + $tag = strval($file->getMtime()); } $tag = substr(hash('md5', $tag), 0, 5); @@ -240,7 +270,7 @@ class Template extends Response $templateDirs[] = [ $this->theme['module'] => TemplateLoader::getModuleTemplateDir($this->theme['module']) ]; - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { // either the module is not enabled or it has no "templates" directory, ignore } } @@ -273,7 +303,7 @@ class Template extends Response // abort if twig template does not exist if (!$loader->exists($this->twig_template)) { - throw new \Exception('Template-file \"' . $this->getTemplateName() . '\" does not exist.'); + throw new Exception('Template-file \"' . $this->getTemplateName() . '\" does not exist.'); } // load extra i18n domains @@ -294,7 +324,7 @@ class Template extends Response $twig = new Environment($loader, $options); $twigTranslator = new TwigTranslator([Translate::class, 'translateSingularGettext']); $twig->addExtension(new TranslationExtension($twigTranslator)); - $twig->addExtension(new \Twig\Extra\Intl\IntlExtension()); + $twig->addExtension(new IntlExtension()); $twig->addFunction(new TwigFunction('moduleURL', [Module::class, 'getModuleURL'])); @@ -356,28 +386,30 @@ class Template extends Response // setup directories & namespaces $themeDir = Module::getModuleDir($this->theme['module']) . '/themes/' . $this->theme['name']; - $subdirs = @scandir($themeDir); - if (empty($subdirs)) { + + if (!$this->fileSystem->exists($themeDir)) { Logger::warning( - sprintf( - 'Theme directory for theme "%s" (%s) is not readable or is empty.', - $this->theme['name'], - $themeDir - ) + sprintf('Theme directory for theme "%s" (%s) does not exist.', $this->theme['name'], $themeDir), ); return []; } - $themeTemplateDirs = []; - foreach ($subdirs as $entry) { - // discard anything that's not a directory. Expression is negated to profit from lazy evaluation - if (!($entry !== '.' && $entry !== '..' && is_dir($themeDir . '/' . $entry))) { - continue; - } + $finder = new Finder(); + $finder->directories()->in($themeDir)->depth(0); + if (!$finder->hasResults()) { + Logger::warning(sprintf( + 'Theme directory for theme "%s" (%s) is not readable or is empty.', + $this->theme['name'], + $themeDir, + )); + return []; + } + $themeTemplateDirs = []; + foreach ($finder as $entry) { // set correct name for the default namespace - $ns = ($entry === 'default') ? FilesystemLoader::MAIN_NAMESPACE : $entry; - $themeTemplateDirs[] = [$ns => $themeDir . '/' . $entry]; + $ns = ($entry->getFileName() === 'default') ? FilesystemLoader::MAIN_NAMESPACE : $entry->getFileName(); + $themeTemplateDirs[] = [$ns => strval($entry)]; } return $themeTemplateDirs; } @@ -394,15 +426,16 @@ class Template extends Response private function getModuleTemplateDir(string $module): string { if (!Module::isModuleEnabled($module)) { - throw new \InvalidArgumentException('The module \'' . $module . '\' is not enabled.'); + throw new InvalidArgumentException('The module \'' . $module . '\' is not enabled.'); } $moduledir = Module::getModuleDir($module); // check if module has a /templates dir, if so, append - $templatedir = $moduledir . '/templates'; - if (!is_dir($templatedir)) { - throw new \InvalidArgumentException('The module \'' . $module . '\' has no templates directory.'); + $templateDir = $moduledir . '/templates'; + $file = new File($templateDir); + if (!$file->isDir()) { + throw new InvalidArgumentException('The module \'' . $module . '\' has no templates directory.'); } - return $templatedir; + return $templateDir; } @@ -515,8 +548,8 @@ class Template extends Response } try { return $this->twig->render($this->twig_template, $this->data); - } catch (\Twig\Error\RuntimeError $e) { - throw new \SimpleSAML\Error\Exception(substr($e->getMessage(), 0, -1) . ' in ' . $this->template, 0, $e); + } catch (RuntimeError $e) { + throw new Error\Exception(substr($e->getMessage(), 0, -1) . ' in ' . $this->template, 0, $e); } } diff --git a/lib/SimpleSAML/XML/Signer.php b/lib/SimpleSAML/XML/Signer.php index 24f62d7c3af2004e852f35b7922a5b3c452d8c40..2ba619b56428ca3694c178414f9b642d98b93959 100644 --- a/lib/SimpleSAML/XML/Signer.php +++ b/lib/SimpleSAML/XML/Signer.php @@ -5,7 +5,7 @@ * * This is a helper class for signing XML documents. * - * @package SimpleSAMLphp + * @package simplesamlphp/simplesamlphp */ declare(strict_types=1); @@ -15,10 +15,15 @@ namespace SimpleSAML\XML; use DOMComment; use DOMElement; use DOMText; +use Exception; use RobRichards\XMLSecLibs\XMLSecurityDSig; use RobRichards\XMLSecLibs\XMLSecurityKey; use SimpleSAML\Assert\Assert; use SimpleSAML\Utils; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\File; + +use function array_key_exists; class Signer { @@ -37,12 +42,16 @@ class Signer */ private string $certificate = ''; - /** * @var array Extra certificates which should be included in the response. */ private array $extraCertificates = []; + /** + * @var \Symfony\Component\Filesystem\Filesystem; + */ + private Filesystem $fileSystem; + /** * Constructor for the metadata signer. @@ -62,6 +71,8 @@ class Signer */ public function __construct(array $options = []) { + $this->fileSystem = new Filesystem(); + if (array_key_exists('privatekey', $options)) { $pass = null; if (array_key_exists('privatekey_pass', $options)) { @@ -131,12 +142,14 @@ class Signer $keyFile = $file; } - if (!file_exists($keyFile)) { - throw new \Exception('Could not find private key file "' . $keyFile . '".'); + if (!$this->fileSystem->exists($keyFile)) { + throw new Exception('Could not find private key file "' . $keyFile . '".'); } - $keyData = file_get_contents($keyFile); + + $file = new File($keyFile); + $keyData = $file->getContent(); if ($keyData === false) { - throw new \Exception('Unable to read private key file "' . $keyFile . '".'); + throw new Exception('Unable to read private key file "' . $keyFile . '".'); } $privatekey = ['PEM' => $keyData]; @@ -160,7 +173,7 @@ class Signer { if (!array_key_exists('PEM', $publickey)) { // We have a public key with only a fingerprint - throw new \Exception('Tried to add a certificate fingerprint in a signature.'); + throw new Exception('Tried to add a certificate fingerprint in a signature.'); } // For now, we only assume that the public key is an X509 certificate @@ -189,13 +202,14 @@ class Signer $certFile = $file; } - if (!file_exists($certFile)) { - throw new \Exception('Could not find certificate file "' . $certFile . '".'); + if (!$this->fileSystem->exists($certFile)) { + throw new Exception('Could not find certificate file "' . $certFile . '".'); } - $cert = file_get_contents($certFile); + $file = new File($certFile); + $cert = $file->getContent(); if ($cert === false) { - throw new \Exception('Unable to read certificate file "' . $certFile . '".'); + throw new Exception('Unable to read certificate file "' . $certFile . '".'); } $this->certificate = $cert; } @@ -232,13 +246,14 @@ class Signer $certFile = $file; } - if (!file_exists($certFile)) { - throw new \Exception('Could not find extra certificate file "' . $certFile . '".'); + if (!$this->fileSystem->exists($certFile)) { + throw new Exception('Could not find extra certificate file "' . $certFile . '".'); } - $certificate = file_get_contents($certFile); + $file = new File($certFile); + $certificate = $file->getContent(); if ($certificate === false) { - throw new \Exception('Unable to read extra certificate file "' . $certFile . '".'); + throw new Exception('Unable to read extra certificate file "' . $certFile . '".'); } $this->extraCertificates[] = $certificate; @@ -263,7 +278,7 @@ class Signer $privateKey = $this->privateKey; if ($privateKey === false) { - throw new \Exception('Private key not set.'); + throw new Exception('Private key not set.'); } diff --git a/modules/core/lib/Auth/Process/AttributeMap.php b/modules/core/lib/Auth/Process/AttributeMap.php index 16cf735ddbc1285e98f175df17d298ea75079ba1..23767bebdf927728387a23d790f7a1b2700a03b8 100644 --- a/modules/core/lib/Auth/Process/AttributeMap.php +++ b/modules/core/lib/Auth/Process/AttributeMap.php @@ -9,6 +9,7 @@ use SimpleSAML\Assert\Assert; use SimpleSAML\Auth; use SimpleSAML\Configuration; use SimpleSAML\Module; +use Symfony\Component\Filesystem\Filesystem; /** * Attribute filter for renaming attributes. @@ -93,7 +94,8 @@ class AttributeMap extends Auth\ProcessingFilter $filePath = $attributenamemapdir . $fileName . '.php'; } - if (!file_exists($filePath)) { + $fileSystem = new Filesystem(); + if (!$fileSystem->exists($filePath)) { throw new Exception('Could not find attribute map file: ' . $filePath); } diff --git a/tests/lib/SimpleSAML/ModuleTest.php b/tests/lib/SimpleSAML/ModuleTest.php index 966f9c75f74cddfee3555f8a0ad90b5ac922cbe6..c5bb51e3cf96dd47216477e8abf03edddedb9664 100644 --- a/tests/lib/SimpleSAML/ModuleTest.php +++ b/tests/lib/SimpleSAML/ModuleTest.php @@ -8,6 +8,7 @@ use Exception; use PHPUnit\Framework\TestCase; use SimpleSAML\Configuration; use SimpleSAML\Module; +use Symfony\Component\Filesystem\Path; /** * @covers \SimpleSAML\Module @@ -31,8 +32,8 @@ class ModuleTest extends TestCase { // test for the most basic functionality $this->assertEquals( - dirname(dirname(dirname(dirname(__FILE__)))) . '/modules/module', - Module::getModuleDir('module') + Path::canonicalize(dirname(dirname(dirname(dirname(__FILE__)))) . '/modules/module'), + Path::canonicalize(Module::getModuleDir('module')), ); } @@ -121,7 +122,7 @@ class ModuleTest extends TestCase $hooks = Module::getModuleHooks('cron'); $this->assertArrayHasKey('configpage', $hooks); $this->assertEquals('cron_hook_configpage', $hooks['configpage']['func']); - $expectedFile = dirname(__DIR__, 3) . '/modules/cron/hooks/hook_configpage.php'; + $expectedFile = Path::canonicalize(dirname(__DIR__, 3) . '/modules/cron/hooks/hook_configpage.php'); $this->assertEquals($expectedFile, $hooks['configpage']['file']); }