|
@@ -0,0 +1,241 @@
|
|
|
+<?php
|
|
|
+namespace TYPO3\PharStreamWrapper\Resolver;
|
|
|
+
|
|
|
+/*
|
|
|
+ * This file is part of the TYPO3 project.
|
|
|
+ *
|
|
|
+ * It is free software; you can redistribute it and/or modify it under the terms
|
|
|
+ * of the MIT License (MIT). For the full copyright and license information,
|
|
|
+ * please read the LICENSE file that was distributed with this source code.
|
|
|
+ *
|
|
|
+ * The TYPO3 project - inspiring people to share!
|
|
|
+ */
|
|
|
+
|
|
|
+use TYPO3\PharStreamWrapper\Helper;
|
|
|
+use TYPO3\PharStreamWrapper\Manager;
|
|
|
+use TYPO3\PharStreamWrapper\Phar\Reader;
|
|
|
+use TYPO3\PharStreamWrapper\Resolvable;
|
|
|
+
|
|
|
+class PharInvocationResolver implements Resolvable
|
|
|
+{
|
|
|
+ const RESOLVE_REALPATH = 1;
|
|
|
+ const RESOLVE_ALIAS = 2;
|
|
|
+ const ASSERT_INTERNAL_INVOCATION = 32;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @var string[]
|
|
|
+ */
|
|
|
+ private $invocationFunctionNames = array(
|
|
|
+ 'include',
|
|
|
+ 'include_once',
|
|
|
+ 'require',
|
|
|
+ 'require_once'
|
|
|
+ );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Contains resolved base names in order to reduce file IO.
|
|
|
+ *
|
|
|
+ * @var string[]
|
|
|
+ */
|
|
|
+ private $baseNames = array();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Resolves PharInvocation value object (baseName and optional alias).
|
|
|
+ *
|
|
|
+ * Phar aliases are intended to be used only inside Phar archives, however
|
|
|
+ * PharStreamWrapper needs this information exposed outside of Phar as well
|
|
|
+ * It is possible that same alias is used for different $baseName values.
|
|
|
+ * That's why PharInvocationCollection behaves like a stack when resolving
|
|
|
+ * base-name for a given alias. On the other hand it is not possible that
|
|
|
+ * one $baseName is referring to multiple aliases.
|
|
|
+ * @see https://secure.php.net/manual/en/phar.setalias.php
|
|
|
+ * @see https://secure.php.net/manual/en/phar.mapphar.php
|
|
|
+ *
|
|
|
+ * @param string $path
|
|
|
+ * @param int|null $flags
|
|
|
+ * @return null|PharInvocation
|
|
|
+ */
|
|
|
+ public function resolve($path, $flags = null)
|
|
|
+ {
|
|
|
+ $hasPharPrefix = Helper::hasPharPrefix($path);
|
|
|
+ if ($flags === null) {
|
|
|
+ $flags = static::RESOLVE_REALPATH | static::RESOLVE_ALIAS | static::ASSERT_INTERNAL_INVOCATION;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($hasPharPrefix && $flags & static::RESOLVE_ALIAS) {
|
|
|
+ $invocation = $this->findByAlias($path);
|
|
|
+ if ($invocation !== null) {
|
|
|
+ return $invocation;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $baseName = $this->resolveBaseName($path, $flags);
|
|
|
+ if ($baseName === null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($flags & static::RESOLVE_REALPATH) {
|
|
|
+ $baseName = $this->baseNames[$baseName];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->retrieveInvocation($baseName, $flags);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Retrieves PharInvocation, either existing in collection or created on demand
|
|
|
+ * with resolving a potential alias name used in the according Phar archive.
|
|
|
+ *
|
|
|
+ * @param string $baseName
|
|
|
+ * @param int $flags
|
|
|
+ * @return PharInvocation
|
|
|
+ */
|
|
|
+ private function retrieveInvocation($baseName, $flags)
|
|
|
+ {
|
|
|
+ $invocation = $this->findByBaseName($baseName);
|
|
|
+ if ($invocation !== null) {
|
|
|
+ return $invocation;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($flags & static::RESOLVE_ALIAS) {
|
|
|
+ $reader = new Reader($baseName);
|
|
|
+ $alias = $reader->resolveContainer()->getAlias();
|
|
|
+ } else {
|
|
|
+ $alias = '';
|
|
|
+ }
|
|
|
+ // add unconfirmed(!) new invocation to collection
|
|
|
+ $invocation = new PharInvocation($baseName, $alias);
|
|
|
+ Manager::instance()->getCollection()->collect($invocation);
|
|
|
+ return $invocation;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param string $path
|
|
|
+ * @param int $flags
|
|
|
+ * @return null|string
|
|
|
+ */
|
|
|
+ private function resolveBaseName($path, $flags)
|
|
|
+ {
|
|
|
+ $baseName = $this->findInBaseNames($path);
|
|
|
+ if ($baseName !== null) {
|
|
|
+ return $baseName;
|
|
|
+ }
|
|
|
+
|
|
|
+ $baseName = Helper::determineBaseFile($path);
|
|
|
+ if ($baseName !== null) {
|
|
|
+ $this->addBaseName($baseName);
|
|
|
+ return $baseName;
|
|
|
+ }
|
|
|
+
|
|
|
+ $possibleAlias = $this->resolvePossibleAlias($path);
|
|
|
+ if (!($flags & static::RESOLVE_ALIAS) || $possibleAlias === null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $trace = debug_backtrace();
|
|
|
+ foreach ($trace as $item) {
|
|
|
+ if (!isset($item['function']) || !isset($item['args'][0])
|
|
|
+ || !in_array($item['function'], $this->invocationFunctionNames, true)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $currentPath = $item['args'][0];
|
|
|
+ if (Helper::hasPharPrefix($currentPath)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $currentBaseName = Helper::determineBaseFile($currentPath);
|
|
|
+ if ($currentBaseName === null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // ensure the possible alias name (how we have been called initially) matches
|
|
|
+ // the resolved alias name that was retrieved by the current possible base name
|
|
|
+ $reader = new Reader($currentBaseName);
|
|
|
+ $currentAlias = $reader->resolveContainer()->getAlias();
|
|
|
+ if ($currentAlias !== $possibleAlias) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $this->addBaseName($currentBaseName);
|
|
|
+ return $currentBaseName;
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param string $path
|
|
|
+ * @return null|string
|
|
|
+ */
|
|
|
+ private function resolvePossibleAlias($path)
|
|
|
+ {
|
|
|
+ $normalizedPath = Helper::normalizePath($path);
|
|
|
+ return strstr($normalizedPath, '/', true) ?: null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param string $baseName
|
|
|
+ * @return null|PharInvocation
|
|
|
+ */
|
|
|
+ private function findByBaseName($baseName)
|
|
|
+ {
|
|
|
+ return Manager::instance()->getCollection()->findByCallback(
|
|
|
+ function (PharInvocation $candidate) use ($baseName) {
|
|
|
+ return $candidate->getBaseName() === $baseName;
|
|
|
+ },
|
|
|
+ true
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param string $path
|
|
|
+ * @return null|string
|
|
|
+ */
|
|
|
+ private function findInBaseNames($path)
|
|
|
+ {
|
|
|
+ // return directly if the resolved base name was submitted
|
|
|
+ if (in_array($path, $this->baseNames, true)) {
|
|
|
+ return $path;
|
|
|
+ }
|
|
|
+
|
|
|
+ $parts = explode('/', Helper::normalizePath($path));
|
|
|
+
|
|
|
+ while (count($parts)) {
|
|
|
+ $currentPath = implode('/', $parts);
|
|
|
+ if (isset($this->baseNames[$currentPath])) {
|
|
|
+ return $currentPath;
|
|
|
+ }
|
|
|
+ array_pop($parts);
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param string $baseName
|
|
|
+ */
|
|
|
+ private function addBaseName($baseName)
|
|
|
+ {
|
|
|
+ if (isset($this->baseNames[$baseName])) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ $this->baseNames[$baseName] = realpath($baseName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Finds confirmed(!) invocations by alias.
|
|
|
+ *
|
|
|
+ * @param string $path
|
|
|
+ * @return null|PharInvocation
|
|
|
+ * @see \TYPO3\PharStreamWrapper\PharStreamWrapper::collectInvocation()
|
|
|
+ */
|
|
|
+ private function findByAlias($path)
|
|
|
+ {
|
|
|
+ $possibleAlias = $this->resolvePossibleAlias($path);
|
|
|
+ if ($possibleAlias === null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return Manager::instance()->getCollection()->findByCallback(
|
|
|
+ function (PharInvocation $candidate) use ($possibleAlias) {
|
|
|
+ return $candidate->isConfirmed() && $candidate->getAlias() === $possibleAlias;
|
|
|
+ },
|
|
|
+ true
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|