Chris@18
|
1 <?php
|
Chris@18
|
2 namespace TYPO3\PharStreamWrapper\Resolver;
|
Chris@18
|
3
|
Chris@18
|
4 /*
|
Chris@18
|
5 * This file is part of the TYPO3 project.
|
Chris@18
|
6 *
|
Chris@18
|
7 * It is free software; you can redistribute it and/or modify it under the terms
|
Chris@18
|
8 * of the MIT License (MIT). For the full copyright and license information,
|
Chris@18
|
9 * please read the LICENSE file that was distributed with this source code.
|
Chris@18
|
10 *
|
Chris@18
|
11 * The TYPO3 project - inspiring people to share!
|
Chris@18
|
12 */
|
Chris@18
|
13
|
Chris@18
|
14 use TYPO3\PharStreamWrapper\Helper;
|
Chris@18
|
15 use TYPO3\PharStreamWrapper\Manager;
|
Chris@18
|
16 use TYPO3\PharStreamWrapper\Phar\Reader;
|
Chris@18
|
17 use TYPO3\PharStreamWrapper\Resolvable;
|
Chris@18
|
18
|
Chris@18
|
19 class PharInvocationResolver implements Resolvable
|
Chris@18
|
20 {
|
Chris@18
|
21 const RESOLVE_REALPATH = 1;
|
Chris@18
|
22 const RESOLVE_ALIAS = 2;
|
Chris@18
|
23 const ASSERT_INTERNAL_INVOCATION = 32;
|
Chris@18
|
24
|
Chris@18
|
25 /**
|
Chris@18
|
26 * @var string[]
|
Chris@18
|
27 */
|
Chris@18
|
28 private $invocationFunctionNames = array(
|
Chris@18
|
29 'include',
|
Chris@18
|
30 'include_once',
|
Chris@18
|
31 'require',
|
Chris@18
|
32 'require_once'
|
Chris@18
|
33 );
|
Chris@18
|
34
|
Chris@18
|
35 /**
|
Chris@18
|
36 * Contains resolved base names in order to reduce file IO.
|
Chris@18
|
37 *
|
Chris@18
|
38 * @var string[]
|
Chris@18
|
39 */
|
Chris@18
|
40 private $baseNames = array();
|
Chris@18
|
41
|
Chris@18
|
42 /**
|
Chris@18
|
43 * Resolves PharInvocation value object (baseName and optional alias).
|
Chris@18
|
44 *
|
Chris@18
|
45 * Phar aliases are intended to be used only inside Phar archives, however
|
Chris@18
|
46 * PharStreamWrapper needs this information exposed outside of Phar as well
|
Chris@18
|
47 * It is possible that same alias is used for different $baseName values.
|
Chris@18
|
48 * That's why PharInvocationCollection behaves like a stack when resolving
|
Chris@18
|
49 * base-name for a given alias. On the other hand it is not possible that
|
Chris@18
|
50 * one $baseName is referring to multiple aliases.
|
Chris@18
|
51 * @see https://secure.php.net/manual/en/phar.setalias.php
|
Chris@18
|
52 * @see https://secure.php.net/manual/en/phar.mapphar.php
|
Chris@18
|
53 *
|
Chris@18
|
54 * @param string $path
|
Chris@18
|
55 * @param int|null $flags
|
Chris@18
|
56 * @return null|PharInvocation
|
Chris@18
|
57 */
|
Chris@18
|
58 public function resolve($path, $flags = null)
|
Chris@18
|
59 {
|
Chris@18
|
60 $hasPharPrefix = Helper::hasPharPrefix($path);
|
Chris@18
|
61 if ($flags === null) {
|
Chris@18
|
62 $flags = static::RESOLVE_REALPATH | static::RESOLVE_ALIAS | static::ASSERT_INTERNAL_INVOCATION;
|
Chris@18
|
63 }
|
Chris@18
|
64
|
Chris@18
|
65 if ($hasPharPrefix && $flags & static::RESOLVE_ALIAS) {
|
Chris@18
|
66 $invocation = $this->findByAlias($path);
|
Chris@18
|
67 if ($invocation !== null) {
|
Chris@18
|
68 return $invocation;
|
Chris@18
|
69 }
|
Chris@18
|
70 }
|
Chris@18
|
71
|
Chris@18
|
72 $baseName = $this->resolveBaseName($path, $flags);
|
Chris@18
|
73 if ($baseName === null) {
|
Chris@18
|
74 return null;
|
Chris@18
|
75 }
|
Chris@18
|
76
|
Chris@18
|
77 if ($flags & static::RESOLVE_REALPATH) {
|
Chris@18
|
78 $baseName = $this->baseNames[$baseName];
|
Chris@18
|
79 }
|
Chris@18
|
80
|
Chris@18
|
81 return $this->retrieveInvocation($baseName, $flags);
|
Chris@18
|
82 }
|
Chris@18
|
83
|
Chris@18
|
84 /**
|
Chris@18
|
85 * Retrieves PharInvocation, either existing in collection or created on demand
|
Chris@18
|
86 * with resolving a potential alias name used in the according Phar archive.
|
Chris@18
|
87 *
|
Chris@18
|
88 * @param string $baseName
|
Chris@18
|
89 * @param int $flags
|
Chris@18
|
90 * @return PharInvocation
|
Chris@18
|
91 */
|
Chris@18
|
92 private function retrieveInvocation($baseName, $flags)
|
Chris@18
|
93 {
|
Chris@18
|
94 $invocation = $this->findByBaseName($baseName);
|
Chris@18
|
95 if ($invocation !== null) {
|
Chris@18
|
96 return $invocation;
|
Chris@18
|
97 }
|
Chris@18
|
98
|
Chris@18
|
99 if ($flags & static::RESOLVE_ALIAS) {
|
Chris@18
|
100 $reader = new Reader($baseName);
|
Chris@18
|
101 $alias = $reader->resolveContainer()->getAlias();
|
Chris@18
|
102 } else {
|
Chris@18
|
103 $alias = '';
|
Chris@18
|
104 }
|
Chris@18
|
105 // add unconfirmed(!) new invocation to collection
|
Chris@18
|
106 $invocation = new PharInvocation($baseName, $alias);
|
Chris@18
|
107 Manager::instance()->getCollection()->collect($invocation);
|
Chris@18
|
108 return $invocation;
|
Chris@18
|
109 }
|
Chris@18
|
110
|
Chris@18
|
111 /**
|
Chris@18
|
112 * @param string $path
|
Chris@18
|
113 * @param int $flags
|
Chris@18
|
114 * @return null|string
|
Chris@18
|
115 */
|
Chris@18
|
116 private function resolveBaseName($path, $flags)
|
Chris@18
|
117 {
|
Chris@18
|
118 $baseName = $this->findInBaseNames($path);
|
Chris@18
|
119 if ($baseName !== null) {
|
Chris@18
|
120 return $baseName;
|
Chris@18
|
121 }
|
Chris@18
|
122
|
Chris@18
|
123 $baseName = Helper::determineBaseFile($path);
|
Chris@18
|
124 if ($baseName !== null) {
|
Chris@18
|
125 $this->addBaseName($baseName);
|
Chris@18
|
126 return $baseName;
|
Chris@18
|
127 }
|
Chris@18
|
128
|
Chris@18
|
129 $possibleAlias = $this->resolvePossibleAlias($path);
|
Chris@18
|
130 if (!($flags & static::RESOLVE_ALIAS) || $possibleAlias === null) {
|
Chris@18
|
131 return null;
|
Chris@18
|
132 }
|
Chris@18
|
133
|
Chris@18
|
134 $trace = debug_backtrace();
|
Chris@18
|
135 foreach ($trace as $item) {
|
Chris@18
|
136 if (!isset($item['function']) || !isset($item['args'][0])
|
Chris@18
|
137 || !in_array($item['function'], $this->invocationFunctionNames, true)) {
|
Chris@18
|
138 continue;
|
Chris@18
|
139 }
|
Chris@18
|
140 $currentPath = $item['args'][0];
|
Chris@18
|
141 if (Helper::hasPharPrefix($currentPath)) {
|
Chris@18
|
142 continue;
|
Chris@18
|
143 }
|
Chris@18
|
144 $currentBaseName = Helper::determineBaseFile($currentPath);
|
Chris@18
|
145 if ($currentBaseName === null) {
|
Chris@18
|
146 continue;
|
Chris@18
|
147 }
|
Chris@18
|
148 // ensure the possible alias name (how we have been called initially) matches
|
Chris@18
|
149 // the resolved alias name that was retrieved by the current possible base name
|
Chris@18
|
150 $reader = new Reader($currentBaseName);
|
Chris@18
|
151 $currentAlias = $reader->resolveContainer()->getAlias();
|
Chris@18
|
152 if ($currentAlias !== $possibleAlias) {
|
Chris@18
|
153 continue;
|
Chris@18
|
154 }
|
Chris@18
|
155 $this->addBaseName($currentBaseName);
|
Chris@18
|
156 return $currentBaseName;
|
Chris@18
|
157 }
|
Chris@18
|
158
|
Chris@18
|
159 return null;
|
Chris@18
|
160 }
|
Chris@18
|
161
|
Chris@18
|
162 /**
|
Chris@18
|
163 * @param string $path
|
Chris@18
|
164 * @return null|string
|
Chris@18
|
165 */
|
Chris@18
|
166 private function resolvePossibleAlias($path)
|
Chris@18
|
167 {
|
Chris@18
|
168 $normalizedPath = Helper::normalizePath($path);
|
Chris@18
|
169 return strstr($normalizedPath, '/', true) ?: null;
|
Chris@18
|
170 }
|
Chris@18
|
171
|
Chris@18
|
172 /**
|
Chris@18
|
173 * @param string $baseName
|
Chris@18
|
174 * @return null|PharInvocation
|
Chris@18
|
175 */
|
Chris@18
|
176 private function findByBaseName($baseName)
|
Chris@18
|
177 {
|
Chris@18
|
178 return Manager::instance()->getCollection()->findByCallback(
|
Chris@18
|
179 function (PharInvocation $candidate) use ($baseName) {
|
Chris@18
|
180 return $candidate->getBaseName() === $baseName;
|
Chris@18
|
181 },
|
Chris@18
|
182 true
|
Chris@18
|
183 );
|
Chris@18
|
184 }
|
Chris@18
|
185
|
Chris@18
|
186 /**
|
Chris@18
|
187 * @param string $path
|
Chris@18
|
188 * @return null|string
|
Chris@18
|
189 */
|
Chris@18
|
190 private function findInBaseNames($path)
|
Chris@18
|
191 {
|
Chris@18
|
192 // return directly if the resolved base name was submitted
|
Chris@18
|
193 if (in_array($path, $this->baseNames, true)) {
|
Chris@18
|
194 return $path;
|
Chris@18
|
195 }
|
Chris@18
|
196
|
Chris@18
|
197 $parts = explode('/', Helper::normalizePath($path));
|
Chris@18
|
198
|
Chris@18
|
199 while (count($parts)) {
|
Chris@18
|
200 $currentPath = implode('/', $parts);
|
Chris@18
|
201 if (isset($this->baseNames[$currentPath])) {
|
Chris@18
|
202 return $currentPath;
|
Chris@18
|
203 }
|
Chris@18
|
204 array_pop($parts);
|
Chris@18
|
205 }
|
Chris@18
|
206
|
Chris@18
|
207 return null;
|
Chris@18
|
208 }
|
Chris@18
|
209
|
Chris@18
|
210 /**
|
Chris@18
|
211 * @param string $baseName
|
Chris@18
|
212 */
|
Chris@18
|
213 private function addBaseName($baseName)
|
Chris@18
|
214 {
|
Chris@18
|
215 if (isset($this->baseNames[$baseName])) {
|
Chris@18
|
216 return;
|
Chris@18
|
217 }
|
Chris@18
|
218 $this->baseNames[$baseName] = realpath($baseName);
|
Chris@18
|
219 }
|
Chris@18
|
220
|
Chris@18
|
221 /**
|
Chris@18
|
222 * Finds confirmed(!) invocations by alias.
|
Chris@18
|
223 *
|
Chris@18
|
224 * @param string $path
|
Chris@18
|
225 * @return null|PharInvocation
|
Chris@18
|
226 * @see \TYPO3\PharStreamWrapper\PharStreamWrapper::collectInvocation()
|
Chris@18
|
227 */
|
Chris@18
|
228 private function findByAlias($path)
|
Chris@18
|
229 {
|
Chris@18
|
230 $possibleAlias = $this->resolvePossibleAlias($path);
|
Chris@18
|
231 if ($possibleAlias === null) {
|
Chris@18
|
232 return null;
|
Chris@18
|
233 }
|
Chris@18
|
234 return Manager::instance()->getCollection()->findByCallback(
|
Chris@18
|
235 function (PharInvocation $candidate) use ($possibleAlias) {
|
Chris@18
|
236 return $candidate->isConfirmed() && $candidate->getAlias() === $possibleAlias;
|
Chris@18
|
237 },
|
Chris@18
|
238 true
|
Chris@18
|
239 );
|
Chris@18
|
240 }
|
Chris@18
|
241 }
|