PharInvocationResolver.php 7.2 KB

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