1<?php 2 3/* 4 * This file is part of Twig. 5 * 6 * (c) Fabien Potencier 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Twig\Loader; 13 14use Twig\Error\LoaderError; 15use Twig\Source; 16 17/** 18 * Loads template from the filesystem. 19 * 20 * @author Fabien Potencier <fabien@symfony.com> 21 */ 22class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, SourceContextLoaderInterface 23{ 24 /** Identifier of the main namespace. */ 25 const MAIN_NAMESPACE = '__main__'; 26 27 protected $paths = []; 28 protected $cache = []; 29 protected $errorCache = []; 30 31 private $rootPath; 32 33 /** 34 * @param string|array $paths A path or an array of paths where to look for templates 35 * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) 36 */ 37 public function __construct($paths = [], $rootPath = null) 38 { 39 $this->rootPath = (null === $rootPath ? getcwd() : $rootPath).\DIRECTORY_SEPARATOR; 40 if (false !== $realPath = realpath($rootPath)) { 41 $this->rootPath = $realPath.\DIRECTORY_SEPARATOR; 42 } 43 44 if ($paths) { 45 $this->setPaths($paths); 46 } 47 } 48 49 /** 50 * Returns the paths to the templates. 51 * 52 * @param string $namespace A path namespace 53 * 54 * @return array The array of paths where to look for templates 55 */ 56 public function getPaths($namespace = self::MAIN_NAMESPACE) 57 { 58 return isset($this->paths[$namespace]) ? $this->paths[$namespace] : []; 59 } 60 61 /** 62 * Returns the path namespaces. 63 * 64 * The main namespace is always defined. 65 * 66 * @return array The array of defined namespaces 67 */ 68 public function getNamespaces() 69 { 70 return array_keys($this->paths); 71 } 72 73 /** 74 * Sets the paths where templates are stored. 75 * 76 * @param string|array $paths A path or an array of paths where to look for templates 77 * @param string $namespace A path namespace 78 */ 79 public function setPaths($paths, $namespace = self::MAIN_NAMESPACE) 80 { 81 if (!\is_array($paths)) { 82 $paths = [$paths]; 83 } 84 85 $this->paths[$namespace] = []; 86 foreach ($paths as $path) { 87 $this->addPath($path, $namespace); 88 } 89 } 90 91 /** 92 * Adds a path where templates are stored. 93 * 94 * @param string $path A path where to look for templates 95 * @param string $namespace A path namespace 96 * 97 * @throws LoaderError 98 */ 99 public function addPath($path, $namespace = self::MAIN_NAMESPACE) 100 { 101 // invalidate the cache 102 $this->cache = $this->errorCache = []; 103 104 $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; 105 if (!is_dir($checkPath)) { 106 throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); 107 } 108 109 $this->paths[$namespace][] = rtrim($path, '/\\'); 110 } 111 112 /** 113 * Prepends a path where templates are stored. 114 * 115 * @param string $path A path where to look for templates 116 * @param string $namespace A path namespace 117 * 118 * @throws LoaderError 119 */ 120 public function prependPath($path, $namespace = self::MAIN_NAMESPACE) 121 { 122 // invalidate the cache 123 $this->cache = $this->errorCache = []; 124 125 $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; 126 if (!is_dir($checkPath)) { 127 throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); 128 } 129 130 $path = rtrim($path, '/\\'); 131 132 if (!isset($this->paths[$namespace])) { 133 $this->paths[$namespace][] = $path; 134 } else { 135 array_unshift($this->paths[$namespace], $path); 136 } 137 } 138 139 public function getSource($name) 140 { 141 @trigger_error(sprintf('Calling "getSource" on "%s" is deprecated since 1.27. Use getSourceContext() instead.', \get_class($this)), E_USER_DEPRECATED); 142 143 return file_get_contents($this->findTemplate($name)); 144 } 145 146 public function getSourceContext($name) 147 { 148 $path = $this->findTemplate($name); 149 150 return new Source(file_get_contents($path), $name, $path); 151 } 152 153 public function getCacheKey($name) 154 { 155 $path = $this->findTemplate($name); 156 $len = \strlen($this->rootPath); 157 if (0 === strncmp($this->rootPath, $path, $len)) { 158 return substr($path, $len); 159 } 160 161 return $path; 162 } 163 164 public function exists($name) 165 { 166 $name = $this->normalizeName($name); 167 168 if (isset($this->cache[$name])) { 169 return true; 170 } 171 172 try { 173 return false !== $this->findTemplate($name, false); 174 } catch (LoaderError $exception) { 175 @trigger_error(sprintf('In %s::findTemplate(), you must accept a second argument that when set to "false" returns "false" instead of throwing an exception. Not supporting this argument is deprecated since version 1.27.', \get_class($this)), E_USER_DEPRECATED); 176 177 return false; 178 } 179 } 180 181 public function isFresh($name, $time) 182 { 183 return filemtime($this->findTemplate($name)) < $time; 184 } 185 186 protected function findTemplate($name) 187 { 188 $throw = \func_num_args() > 1 ? func_get_arg(1) : true; 189 $name = $this->normalizeName($name); 190 191 if (isset($this->cache[$name])) { 192 return $this->cache[$name]; 193 } 194 195 if (isset($this->errorCache[$name])) { 196 if (!$throw) { 197 return false; 198 } 199 200 throw new LoaderError($this->errorCache[$name]); 201 } 202 203 try { 204 $this->validateName($name); 205 206 list($namespace, $shortname) = $this->parseName($name); 207 } catch (LoaderError $e) { 208 if (!$throw) { 209 return false; 210 } 211 212 throw $e; 213 } 214 215 if (!isset($this->paths[$namespace])) { 216 $this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace); 217 218 if (!$throw) { 219 return false; 220 } 221 222 throw new LoaderError($this->errorCache[$name]); 223 } 224 225 foreach ($this->paths[$namespace] as $path) { 226 if (!$this->isAbsolutePath($path)) { 227 $path = $this->rootPath.$path; 228 } 229 230 if (is_file($path.'/'.$shortname)) { 231 if (false !== $realpath = realpath($path.'/'.$shortname)) { 232 return $this->cache[$name] = $realpath; 233 } 234 235 return $this->cache[$name] = $path.'/'.$shortname; 236 } 237 } 238 239 $this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])); 240 241 if (!$throw) { 242 return false; 243 } 244 245 throw new LoaderError($this->errorCache[$name]); 246 } 247 248 protected function parseName($name, $default = self::MAIN_NAMESPACE) 249 { 250 if (isset($name[0]) && '@' == $name[0]) { 251 if (false === $pos = strpos($name, '/')) { 252 throw new LoaderError(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); 253 } 254 255 $namespace = substr($name, 1, $pos - 1); 256 $shortname = substr($name, $pos + 1); 257 258 return [$namespace, $shortname]; 259 } 260 261 return [$default, $name]; 262 } 263 264 protected function normalizeName($name) 265 { 266 return preg_replace('#/{2,}#', '/', str_replace('\\', '/', (string) $name)); 267 } 268 269 protected function validateName($name) 270 { 271 if (false !== strpos($name, "\0")) { 272 throw new LoaderError('A template name cannot contain NUL bytes.'); 273 } 274 275 $name = ltrim($name, '/'); 276 $parts = explode('/', $name); 277 $level = 0; 278 foreach ($parts as $part) { 279 if ('..' === $part) { 280 --$level; 281 } elseif ('.' !== $part) { 282 ++$level; 283 } 284 285 if ($level < 0) { 286 throw new LoaderError(sprintf('Looks like you try to load a template outside configured directories (%s).', $name)); 287 } 288 } 289 } 290 291 private function isAbsolutePath($file) 292 { 293 return strspn($file, '/\\', 0, 1) 294 || (\strlen($file) > 3 && ctype_alpha($file[0]) 295 && ':' === substr($file, 1, 1) 296 && strspn($file, '/\\', 2, 1) 297 ) 298 || null !== parse_url($file, PHP_URL_SCHEME) 299 ; 300 } 301} 302 303class_alias('Twig\Loader\FilesystemLoader', 'Twig_Loader_Filesystem'); 304