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