PharInvocationResolver.php 7.4 KB

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