vendor/twig/twig/src/Loader/FilesystemLoader.php line 136

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Twig.
  4. *
  5. * (c) Fabien Potencier
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Twig\Loader;
  11. use Twig\Error\LoaderError;
  12. use Twig\Source;
  13. /**
  14. * Loads template from the filesystem.
  15. *
  16. * @author Fabien Potencier <fabien@symfony.com>
  17. */
  18. class FilesystemLoader implements LoaderInterface
  19. {
  20. /** Identifier of the main namespace. */
  21. public const MAIN_NAMESPACE = '__main__';
  22. /**
  23. * @var array<string, list<string>>
  24. */
  25. protected $paths = [];
  26. protected $cache = [];
  27. protected $errorCache = [];
  28. private $rootPath;
  29. /**
  30. * @param string|string[] $paths A path or an array of paths where to look for templates
  31. * @param string|null $rootPath The root path common to all relative paths (null for getcwd())
  32. */
  33. public function __construct($paths = [], ?string $rootPath = null)
  34. {
  35. $this->rootPath = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR;
  36. if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) {
  37. $this->rootPath = $realPath.\DIRECTORY_SEPARATOR;
  38. }
  39. if ($paths) {
  40. $this->setPaths($paths);
  41. }
  42. }
  43. /**
  44. * Returns the paths to the templates.
  45. *
  46. * @return list<string>
  47. */
  48. public function getPaths(string $namespace = self::MAIN_NAMESPACE): array
  49. {
  50. return $this->paths[$namespace] ?? [];
  51. }
  52. /**
  53. * Returns the path namespaces.
  54. *
  55. * The main namespace is always defined.
  56. *
  57. * @return list<string>
  58. */
  59. public function getNamespaces(): array
  60. {
  61. return array_keys($this->paths);
  62. }
  63. /**
  64. * @param string|string[] $paths A path or an array of paths where to look for templates
  65. */
  66. public function setPaths($paths, string $namespace = self::MAIN_NAMESPACE): void
  67. {
  68. if (!\is_array($paths)) {
  69. $paths = [$paths];
  70. }
  71. $this->paths[$namespace] = [];
  72. foreach ($paths as $path) {
  73. $this->addPath($path, $namespace);
  74. }
  75. }
  76. /**
  77. * @throws LoaderError
  78. */
  79. public function addPath(string $path, string $namespace = self::MAIN_NAMESPACE): void
  80. {
  81. // invalidate the cache
  82. $this->cache = $this->errorCache = [];
  83. $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
  84. if (!is_dir($checkPath)) {
  85. throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
  86. }
  87. $this->paths[$namespace][] = rtrim($path, '/\\');
  88. }
  89. /**
  90. * @throws LoaderError
  91. */
  92. public function prependPath(string $path, string $namespace = self::MAIN_NAMESPACE): void
  93. {
  94. // invalidate the cache
  95. $this->cache = $this->errorCache = [];
  96. $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
  97. if (!is_dir($checkPath)) {
  98. throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
  99. }
  100. $path = rtrim($path, '/\\');
  101. if (!isset($this->paths[$namespace])) {
  102. $this->paths[$namespace][] = $path;
  103. } else {
  104. array_unshift($this->paths[$namespace], $path);
  105. }
  106. }
  107. public function getSourceContext(string $name): Source
  108. {
  109. if (null === $path = $this->findTemplate($name)) {
  110. return new Source('', $name, '');
  111. }
  112. return new Source(file_get_contents($path), $name, $path);
  113. }
  114. public function getCacheKey(string $name): string
  115. {
  116. if (null === $path = $this->findTemplate($name)) {
  117. return '';
  118. }
  119. $len = \strlen($this->rootPath);
  120. if (0 === strncmp($this->rootPath, $path, $len)) {
  121. return substr($path, $len);
  122. }
  123. return $path;
  124. }
  125. /**
  126. * @return bool
  127. */
  128. public function exists(string $name)
  129. {
  130. $name = $this->normalizeName($name);
  131. if (isset($this->cache[$name])) {
  132. return true;
  133. }
  134. return null !== $this->findTemplate($name, false);
  135. }
  136. public function isFresh(string $name, int $time): bool
  137. {
  138. // false support to be removed in 3.0
  139. if (null === $path = $this->findTemplate($name)) {
  140. return false;
  141. }
  142. return filemtime($path) < $time;
  143. }
  144. /**
  145. * @return string|null
  146. */
  147. protected function findTemplate(string $name, bool $throw = true)
  148. {
  149. $name = $this->normalizeName($name);
  150. if (isset($this->cache[$name])) {
  151. return $this->cache[$name];
  152. }
  153. if (isset($this->errorCache[$name])) {
  154. if (!$throw) {
  155. return null;
  156. }
  157. throw new LoaderError($this->errorCache[$name]);
  158. }
  159. try {
  160. [$namespace, $shortname] = $this->parseName($name);
  161. $this->validateName($shortname);
  162. } catch (LoaderError $e) {
  163. if (!$throw) {
  164. return null;
  165. }
  166. throw $e;
  167. }
  168. if (!isset($this->paths[$namespace])) {
  169. $this->errorCache[$name] = \sprintf('There are no registered paths for namespace "%s".', $namespace);
  170. if (!$throw) {
  171. return null;
  172. }
  173. throw new LoaderError($this->errorCache[$name]);
  174. }
  175. foreach ($this->paths[$namespace] as $path) {
  176. if (!$this->isAbsolutePath($path)) {
  177. $path = $this->rootPath.$path;
  178. }
  179. if (is_file($path.'/'.$shortname)) {
  180. if (false !== $realpath = realpath($path.'/'.$shortname)) {
  181. return $this->cache[$name] = $realpath;
  182. }
  183. return $this->cache[$name] = $path.'/'.$shortname;
  184. }
  185. }
  186. $this->errorCache[$name] = \sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]));
  187. if (!$throw) {
  188. return null;
  189. }
  190. throw new LoaderError($this->errorCache[$name]);
  191. }
  192. private function normalizeName(string $name): string
  193. {
  194. return preg_replace('#/{2,}#', '/', str_replace('\\', '/', $name));
  195. }
  196. private function parseName(string $name, string $default = self::MAIN_NAMESPACE): array
  197. {
  198. if (isset($name[0]) && '@' == $name[0]) {
  199. if (false === $pos = strpos($name, '/')) {
  200. throw new LoaderError(\sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
  201. }
  202. $namespace = substr($name, 1, $pos - 1);
  203. $shortname = substr($name, $pos + 1);
  204. return [$namespace, $shortname];
  205. }
  206. return [$default, $name];
  207. }
  208. private function validateName(string $name): void
  209. {
  210. if (str_contains($name, "\0")) {
  211. throw new LoaderError('A template name cannot contain NUL bytes.');
  212. }
  213. $name = ltrim($name, '/');
  214. $parts = explode('/', $name);
  215. $level = 0;
  216. foreach ($parts as $part) {
  217. if ('..' === $part) {
  218. --$level;
  219. } elseif ('.' !== $part) {
  220. ++$level;
  221. }
  222. if ($level < 0) {
  223. throw new LoaderError(\sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
  224. }
  225. }
  226. }
  227. private function isAbsolutePath(string $file): bool
  228. {
  229. return strspn($file, '/\\', 0, 1)
  230. || (\strlen($file) > 3 && ctype_alpha($file[0])
  231. && ':' === $file[1]
  232. && strspn($file, '/\\', 2, 1)
  233. )
  234. || null !== parse_url($file, \PHP_URL_SCHEME)
  235. ;
  236. }
  237. }