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); } }