1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\View\Resolver; 11 12use SplFileInfo; 13use Traversable; 14use Zend\Stdlib\SplStack; 15use Zend\View\Exception; 16use Zend\View\Renderer\RendererInterface as Renderer; 17 18/** 19 * Resolves view scripts based on a stack of paths 20 */ 21class TemplatePathStack implements ResolverInterface 22{ 23 const FAILURE_NO_PATHS = 'TemplatePathStack_Failure_No_Paths'; 24 const FAILURE_NOT_FOUND = 'TemplatePathStack_Failure_Not_Found'; 25 26 /** 27 * Default suffix to use 28 * 29 * Appends this suffix if the template requested does not use it. 30 * 31 * @var string 32 */ 33 protected $defaultSuffix = 'phtml'; 34 35 /** 36 * @var SplStack 37 */ 38 protected $paths; 39 40 /** 41 * Reason for last lookup failure 42 * 43 * @var false|string 44 */ 45 protected $lastLookupFailure = false; 46 47 /** 48 * Flag indicating whether or not LFI protection for rendering view scripts is enabled 49 * @var bool 50 */ 51 protected $lfiProtectionOn = true; 52 53 /**@+ 54 * Flags used to determine if a stream wrapper should be used for enabling short tags 55 * @var bool 56 */ 57 protected $useViewStream = false; 58 protected $useStreamWrapper = false; 59 /**@-*/ 60 61 /** 62 * Constructor 63 * 64 * @param null|array|Traversable $options 65 */ 66 public function __construct($options = null) 67 { 68 $this->useViewStream = (bool) ini_get('short_open_tag'); 69 if ($this->useViewStream) { 70 if (!in_array('zend.view', stream_get_wrappers())) { 71 stream_wrapper_register('zend.view', 'Zend\View\Stream'); 72 } 73 } 74 75 $this->paths = new SplStack; 76 if (null !== $options) { 77 $this->setOptions($options); 78 } 79 } 80 81 /** 82 * Configure object 83 * 84 * @param array|Traversable $options 85 * @return void 86 * @throws Exception\InvalidArgumentException 87 */ 88 public function setOptions($options) 89 { 90 if (!is_array($options) && !$options instanceof Traversable) { 91 throw new Exception\InvalidArgumentException(sprintf( 92 'Expected array or Traversable object; received "%s"', 93 (is_object($options) ? get_class($options) : gettype($options)) 94 )); 95 } 96 97 foreach ($options as $key => $value) { 98 switch (strtolower($key)) { 99 case 'lfi_protection': 100 $this->setLfiProtection($value); 101 break; 102 case 'script_paths': 103 $this->addPaths($value); 104 break; 105 case 'use_stream_wrapper': 106 $this->setUseStreamWrapper($value); 107 break; 108 case 'default_suffix': 109 $this->setDefaultSuffix($value); 110 break; 111 default: 112 break; 113 } 114 } 115 } 116 117 /** 118 * Set default file suffix 119 * 120 * @param string $defaultSuffix 121 * @return TemplatePathStack 122 */ 123 public function setDefaultSuffix($defaultSuffix) 124 { 125 $this->defaultSuffix = (string) $defaultSuffix; 126 $this->defaultSuffix = ltrim($this->defaultSuffix, '.'); 127 return $this; 128 } 129 130 /** 131 * Get default file suffix 132 * 133 * @return string 134 */ 135 public function getDefaultSuffix() 136 { 137 return $this->defaultSuffix; 138 } 139 140 /** 141 * Add many paths to the stack at once 142 * 143 * @param array $paths 144 * @return TemplatePathStack 145 */ 146 public function addPaths(array $paths) 147 { 148 foreach ($paths as $path) { 149 $this->addPath($path); 150 } 151 return $this; 152 } 153 154 /** 155 * Rest the path stack to the paths provided 156 * 157 * @param SplStack|array $paths 158 * @return TemplatePathStack 159 * @throws Exception\InvalidArgumentException 160 */ 161 public function setPaths($paths) 162 { 163 if ($paths instanceof SplStack) { 164 $this->paths = $paths; 165 } elseif (is_array($paths)) { 166 $this->clearPaths(); 167 $this->addPaths($paths); 168 } else { 169 throw new Exception\InvalidArgumentException( 170 "Invalid argument provided for \$paths, expecting either an array or SplStack object" 171 ); 172 } 173 174 return $this; 175 } 176 177 /** 178 * Normalize a path for insertion in the stack 179 * 180 * @param string $path 181 * @return string 182 */ 183 public static function normalizePath($path) 184 { 185 $path = rtrim($path, '/'); 186 $path = rtrim($path, '\\'); 187 $path .= DIRECTORY_SEPARATOR; 188 return $path; 189 } 190 191 /** 192 * Add a single path to the stack 193 * 194 * @param string $path 195 * @return TemplatePathStack 196 * @throws Exception\InvalidArgumentException 197 */ 198 public function addPath($path) 199 { 200 if (!is_string($path)) { 201 throw new Exception\InvalidArgumentException(sprintf( 202 'Invalid path provided; must be a string, received %s', 203 gettype($path) 204 )); 205 } 206 $this->paths[] = static::normalizePath($path); 207 return $this; 208 } 209 210 /** 211 * Clear all paths 212 * 213 * @return void 214 */ 215 public function clearPaths() 216 { 217 $this->paths = new SplStack; 218 } 219 220 /** 221 * Returns stack of paths 222 * 223 * @return SplStack 224 */ 225 public function getPaths() 226 { 227 return $this->paths; 228 } 229 230 /** 231 * Set LFI protection flag 232 * 233 * @param bool $flag 234 * @return TemplatePathStack 235 */ 236 public function setLfiProtection($flag) 237 { 238 $this->lfiProtectionOn = (bool) $flag; 239 return $this; 240 } 241 242 /** 243 * Return status of LFI protection flag 244 * 245 * @return bool 246 */ 247 public function isLfiProtectionOn() 248 { 249 return $this->lfiProtectionOn; 250 } 251 252 /** 253 * Set flag indicating if stream wrapper should be used if short_open_tag is off 254 * 255 * @param bool $flag 256 * @return TemplatePathStack 257 */ 258 public function setUseStreamWrapper($flag) 259 { 260 $this->useStreamWrapper = (bool) $flag; 261 return $this; 262 } 263 264 /** 265 * Should the stream wrapper be used if short_open_tag is off? 266 * 267 * Returns true if the use_stream_wrapper flag is set, and if short_open_tag 268 * is disabled. 269 * 270 * @return bool 271 */ 272 public function useStreamWrapper() 273 { 274 return ($this->useViewStream && $this->useStreamWrapper); 275 } 276 277 /** 278 * Retrieve the filesystem path to a view script 279 * 280 * @param string $name 281 * @param null|Renderer $renderer 282 * @return string 283 * @throws Exception\DomainException 284 */ 285 public function resolve($name, Renderer $renderer = null) 286 { 287 $this->lastLookupFailure = false; 288 289 if ($this->isLfiProtectionOn() && preg_match('#\.\.[\\\/]#', $name)) { 290 throw new Exception\DomainException( 291 'Requested scripts may not include parent directory traversal ("../", "..\\" notation)' 292 ); 293 } 294 295 if (!count($this->paths)) { 296 $this->lastLookupFailure = static::FAILURE_NO_PATHS; 297 return false; 298 } 299 300 // Ensure we have the expected file extension 301 $defaultSuffix = $this->getDefaultSuffix(); 302 if (pathinfo($name, PATHINFO_EXTENSION) == '') { 303 $name .= '.' . $defaultSuffix; 304 } 305 306 foreach ($this->paths as $path) { 307 $file = new SplFileInfo($path . $name); 308 if ($file->isReadable()) { 309 // Found! Return it. 310 if (($filePath = $file->getRealPath()) === false && substr($path, 0, 7) === 'phar://') { 311 // Do not try to expand phar paths (realpath + phars == fail) 312 $filePath = $path . $name; 313 if (!file_exists($filePath)) { 314 break; 315 } 316 } 317 if ($this->useStreamWrapper()) { 318 // If using a stream wrapper, prepend the spec to the path 319 $filePath = 'zend.view://' . $filePath; 320 } 321 return $filePath; 322 } 323 } 324 325 $this->lastLookupFailure = static::FAILURE_NOT_FOUND; 326 return false; 327 } 328 329 /** 330 * Get the last lookup failure message, if any 331 * 332 * @return false|string 333 */ 334 public function getLastLookupFailure() 335 { 336 return $this->lastLookupFailure; 337 } 338} 339