<?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\Phar\ReaderException; 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; } 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 try { $reader = new Reader($currentBaseName); $currentAlias = $reader->resolveContainer()->getAlias(); } catch (ReaderException $exception) { // most probably that was not a Phar file continue; } if (empty($currentAlias) || $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] = Helper::normalizeWindowsPath( 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 ); } }