*/ class TemplatePathStack implements ResolverInterface { public const FAILURE_NO_PATHS = 'TemplatePathStack_Failure_No_Paths'; public const FAILURE_NOT_FOUND = 'TemplatePathStack_Failure_Not_Found'; /** * Default suffix to use * * Appends this suffix if the template requested does not use it. * * @var string */ protected $defaultSuffix = 'phtml'; /** @var PathStack */ protected $paths; /** * Reason for last lookup failure * * @var false|string */ protected $lastLookupFailure = false; /** * Flag indicating whether or not LFI protection for rendering view scripts is enabled * * @var bool */ protected $lfiProtectionOn = true; /**@+ * Flags used to determine if a stream wrapper should be used for enabling short tags */ /** @var bool */ protected $useViewStream = false; /** @var bool */ protected $useStreamWrapper = false; /**@-*/ /** * Constructor * * @param null|array|Traversable $options */ public function __construct($options = null) { $this->useViewStream = (bool) ini_get('short_open_tag'); if ($this->useViewStream) { if (! in_array('laminas.view', stream_get_wrappers())) { /** @psalm-suppress DeprecatedClass */ stream_wrapper_register('laminas.view', Stream::class); } } /** @psalm-var PathStack $paths */ $paths = new SplStack(); $this->paths = $paths; if (null !== $options) { $this->setOptions($options); } } /** * Configure object * * @param array|Traversable $options * @return void * @throws Exception\InvalidArgumentException */ public function setOptions($options) { /** @psalm-suppress DocblockTypeContradiction */ if (! is_array($options) && ! $options instanceof Traversable) { throw new Exception\InvalidArgumentException(sprintf( 'Expected array or Traversable object; received "%s"', is_object($options) ? $options::class : gettype($options) )); } foreach ($options as $key => $value) { switch (strtolower($key)) { case 'lfi_protection': $this->setLfiProtection($value); break; case 'script_paths': $this->addPaths($value); break; case 'use_stream_wrapper': /** @psalm-suppress DeprecatedMethod */ $this->setUseStreamWrapper($value); break; case 'default_suffix': $this->setDefaultSuffix($value); break; default: break; } } } /** * Set default file suffix * * @param string $defaultSuffix * @return TemplatePathStack */ public function setDefaultSuffix($defaultSuffix) { $this->defaultSuffix = (string) $defaultSuffix; $this->defaultSuffix = ltrim($this->defaultSuffix, '.'); return $this; } /** * Get default file suffix * * @return string */ public function getDefaultSuffix() { return $this->defaultSuffix; } /** * Add many paths to the stack at once * * @param list $paths * @return TemplatePathStack */ public function addPaths(array $paths) { foreach ($paths as $path) { $this->addPath($path); } return $this; } /** * Rest the path stack to the paths provided * * @param PathStack|list $paths * @return TemplatePathStack * @throws Exception\InvalidArgumentException */ public function setPaths($paths) { if ($paths instanceof SplStack) { $this->paths = $paths; return $this; } /** @psalm-suppress RedundantConditionGivenDocblockType */ if (is_array($paths)) { $this->clearPaths(); $this->addPaths($paths); return $this; } throw new Exception\InvalidArgumentException( "Invalid argument provided for \$paths, expecting either an array or SplStack object" ); } /** * Normalize a path for insertion in the stack * * @param string $path * @return string */ public static function normalizePath($path) { $path = rtrim($path, '/'); $path = rtrim($path, '\\'); $path .= DIRECTORY_SEPARATOR; return $path; } /** * Add a single path to the stack * * @param string $path * @return TemplatePathStack * @throws Exception\InvalidArgumentException */ public function addPath($path) { if (! is_string($path)) { throw new Exception\InvalidArgumentException(sprintf( 'Invalid path provided; must be a string, received %s', gettype($path) )); } $this->paths[] = static::normalizePath($path); return $this; } /** * Clear all paths * * @return void */ public function clearPaths() { /** @psalm-var PathStack $paths */ $paths = new SplStack(); $this->paths = $paths; } /** * Returns stack of paths * * @return PathStack */ public function getPaths() { return $this->paths; } /** * Set LFI protection flag * * @param bool $flag * @return TemplatePathStack */ public function setLfiProtection($flag) { $this->lfiProtectionOn = (bool) $flag; return $this; } /** * Return status of LFI protection flag * * @return bool */ public function isLfiProtectionOn() { return $this->lfiProtectionOn; } /** * Set flag indicating if stream wrapper should be used if short_open_tag is off * * @deprecated will be removed in version 3 * * @param bool $flag * @return TemplatePathStack */ public function setUseStreamWrapper($flag) { $this->useStreamWrapper = (bool) $flag; return $this; } /** * Should the stream wrapper be used if short_open_tag is off? * * Returns true if the use_stream_wrapper flag is set, and if short_open_tag * is disabled. * * @deprecated will be removed in version 3 * * @return bool */ public function useStreamWrapper() { return $this->useViewStream && $this->useStreamWrapper; } /** * Retrieve the filesystem path to a view script * * @param string $name * @return string * @throws Exception\DomainException */ public function resolve($name, ?Renderer $renderer = null) { $this->lastLookupFailure = false; if ($this->isLfiProtectionOn() && preg_match('#\.\.[\\\/]#', $name)) { throw new Exception\DomainException( 'Requested scripts may not include parent directory traversal ("../", "..\\" notation)' ); } if (! count($this->paths)) { $this->lastLookupFailure = static::FAILURE_NO_PATHS; return false; } // Ensure we have the expected file extension $defaultSuffix = $this->getDefaultSuffix(); if (pathinfo($name, PATHINFO_EXTENSION) === '') { $name .= '.' . $defaultSuffix; } foreach ($this->paths as $path) { $file = new SplFileInfo($path . $name); if ($file->isReadable()) { // Found! Return it. if (($filePath = $file->getRealPath()) === false && 0 === strpos($path, 'phar://')) { // Do not try to expand phar paths (realpath + phars == fail) $filePath = $path . $name; if (! file_exists($filePath)) { break; } } /** @psalm-suppress DeprecatedMethod */ if ($this->useStreamWrapper()) { // If using a stream wrapper, prepend the spec to the path $filePath = 'laminas.view://' . $filePath; } return $filePath; } } $this->lastLookupFailure = static::FAILURE_NOT_FOUND; return false; } /** * Get the last lookup failure message, if any * * @return false|string */ public function getLastLookupFailure() { return $this->lastLookupFailure; } }