first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 TYPO3 project - https://typo3.org/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper;
/*
* 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!
*/
interface Assertable
{
/**
* @param string $path
* @param string $command
* @return bool
*/
public function assert(string $path, string $command): bool;
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper;
/*
* 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!
*/
class Behavior implements Assertable
{
const COMMAND_DIR_OPENDIR = 'dir_opendir';
const COMMAND_MKDIR = 'mkdir';
const COMMAND_RENAME = 'rename';
const COMMAND_RMDIR = 'rmdir';
const COMMAND_STEAM_METADATA = 'stream_metadata';
const COMMAND_STREAM_OPEN = 'stream_open';
const COMMAND_UNLINK = 'unlink';
const COMMAND_URL_STAT = 'url_stat';
/**
* @var string[]
*/
private $availableCommands = [
self::COMMAND_DIR_OPENDIR,
self::COMMAND_MKDIR,
self::COMMAND_RENAME,
self::COMMAND_RMDIR,
self::COMMAND_STEAM_METADATA,
self::COMMAND_STREAM_OPEN,
self::COMMAND_UNLINK,
self::COMMAND_URL_STAT,
];
/**
* @var Assertable[]
*/
private $assertions;
/**
* @param Assertable $assertable
* @param string ...$commands
* @return static
*/
public function withAssertion(Assertable $assertable, string ...$commands): self
{
$this->assertCommands($commands);
$commands = $commands ?: $this->availableCommands;
$target = clone $this;
foreach ($commands as $command) {
$target->assertions[$command] = $assertable;
}
return $target;
}
/**
* @param string $path
* @param string $command
* @return bool
*/
public function assert(string $path, string $command): bool
{
$this->assertCommand($command);
$this->assertAssertionCompleteness();
return $this->assertions[$command]->assert($path, $command);
}
/**
* @param array $commands
*/
private function assertCommands(array $commands)
{
$unknownCommands = array_diff($commands, $this->availableCommands);
if (empty($unknownCommands)) {
return;
}
throw new \LogicException(
sprintf(
'Unknown commands: %s',
implode(', ', $unknownCommands)
),
1535189881
);
}
private function assertCommand(string $command)
{
if (in_array($command, $this->availableCommands, true)) {
return;
}
throw new \LogicException(
sprintf(
'Unknown command "%s"',
$command
),
1535189882
);
}
private function assertAssertionCompleteness()
{
$undefinedAssertions = array_diff(
$this->availableCommands,
array_keys($this->assertions)
);
if (empty($undefinedAssertions)) {
return;
}
throw new \LogicException(
sprintf(
'Missing assertions for commands: %s',
implode(', ', $undefinedAssertions)
),
1535189883
);
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper;
/*
* 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\Resolver\PharInvocation;
interface Collectable
{
/**
* @param PharInvocation $invocation
* @return bool
*/
public function has(PharInvocation $invocation): bool;
/**
* @param PharInvocation $invocation
* @param int|null $flags
* @return bool
*/
public function collect(PharInvocation $invocation, int $flags = null): bool;
/**
* @param callable $callback
* @param bool $reverse
* @return null|PharInvocation
*/
public function findByCallback(callable $callback, $reverse = false);
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper;
/*
* 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!
*/
class Exception extends \RuntimeException
{
}

View File

@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper;
/*
* 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!
*/
/**
* Helper provides low-level tools on file name resolving. However it does not
* (and should not) maintain any runtime state information. In order to resolve
* Phar archive paths according resolvers have to be used.
*
* @see \TYPO3\PharStreamWrapper\Resolvable::resolve()
*/
class Helper
{
/*
* Resets PHP's OPcache if enabled as work-around for issues in `include()`
* or `require()` calls and OPcache delivering wrong results.
*
* @see https://bugs.php.net/bug.php?id=66569
*/
public static function resetOpCache()
{
if (function_exists('opcache_reset')
&& function_exists('opcache_get_status')
&& !empty(@opcache_get_status()['opcache_enabled'])
) {
@opcache_reset();
}
}
/**
* Determines base file that can be accessed using the regular file system.
* For e.g. "phar:///home/user/bundle.phar/content.txt" that would result
* into "/home/user/bundle.phar".
*
* @param string $path
* @return string|null
*/
public static function determineBaseFile(string $path)
{
$parts = explode('/', static::normalizePath($path));
while (count($parts)) {
$currentPath = implode('/', $parts);
if (@is_file($currentPath) && realpath($currentPath) !== false) {
return $currentPath;
}
array_pop($parts);
}
return null;
}
/**
* @param string $path
* @return bool
*/
public static function hasPharPrefix(string $path): bool
{
return stripos($path, 'phar://') === 0;
}
/**
* @param string $path
* @return string
*/
public static function removePharPrefix(string $path): string
{
$path = trim($path);
if (!static::hasPharPrefix($path)) {
return $path;
}
return substr($path, 7);
}
/**
* Normalizes a path, removes phar:// prefix, fixes Windows directory
* separators. Result is without trailing slash.
*
* @param string $path
* @return string
*/
public static function normalizePath(string $path): string
{
return rtrim(
static::normalizeWindowsPath(
static::removePharPrefix($path)
),
'/'
);
}
/**
* Fixes a path for windows-backslashes and reduces double-slashes to single slashes
*
* @param string $path File path to process
* @return string
*/
public static function normalizeWindowsPath(string $path): string
{
return str_replace('\\', '/', $path);
}
/**
* Resolves all dots, slashes and removes spaces after or before a path...
*
* @param string $path Input string
* @return string Canonical path, always without trailing slash
*/
private static function getCanonicalPath($path): string
{
$path = static::normalizeWindowsPath($path);
$absolutePathPrefix = '';
if (static::isAbsolutePath($path)) {
if (static::isWindows() && strpos($path, ':/') === 1) {
$absolutePathPrefix = substr($path, 0, 3);
$path = substr($path, 3);
} else {
$path = ltrim($path, '/');
$absolutePathPrefix = '/';
}
}
$pathParts = explode('/', $path);
$pathPartsLength = count($pathParts);
for ($partCount = 0; $partCount < $pathPartsLength; $partCount++) {
// double-slashes in path: remove element
if ($pathParts[$partCount] === '') {
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
}
// "." in path: remove element
if (($pathParts[$partCount] ?? '') === '.') {
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
}
// ".." in path:
if (($pathParts[$partCount] ?? '') === '..') {
if ($partCount === 0) {
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
} elseif ($partCount >= 1) {
// Rremove this and previous element
array_splice($pathParts, $partCount - 1, 2);
$partCount -= 2;
$pathPartsLength -= 2;
} elseif ($absolutePathPrefix) {
// can't go higher than root dir
// simply remove this part and continue
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
}
}
}
return $absolutePathPrefix . implode('/', $pathParts);
}
/**
* Checks if the $path is absolute or relative (detecting either '/' or
* 'x:/' as first part of string) and returns TRUE if so.
*
* @param string $path File path to evaluate
* @return bool
*/
private static function isAbsolutePath($path): bool
{
// Path starting with a / is always absolute, on every system
// On Windows also a path starting with a drive letter is absolute: X:/
return ($path[0] ?? null) === '/'
|| static::isWindows() && (
strpos($path, ':/') === 1
|| strpos($path, ':\\') === 1
);
}
/**
* @return bool
*/
private static function isWindows(): bool
{
return stripos(PHP_OS, 'WIN') === 0;
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper\Interceptor;
/*
* 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\Assertable;
use TYPO3\PharStreamWrapper\Exception;
class ConjunctionInterceptor implements Assertable
{
/**
* @var Assertable[]
*/
private $assertions;
public function __construct(array $assertions)
{
$this->assertAssertions($assertions);
$this->assertions = $assertions;
}
/**
* Executes assertions based on all contained assertions.
*
* @param string $path
* @param string $command
* @return bool
* @throws Exception
*/
public function assert(string $path, string $command): bool
{
if ($this->invokeAssertions($path, $command)) {
return true;
}
throw new Exception(
sprintf(
'Assertion failed in "%s"',
$path
),
1539625084
);
}
/**
* @param Assertable[] $assertions
*/
private function assertAssertions(array $assertions)
{
foreach ($assertions as $assertion) {
if (!$assertion instanceof Assertable) {
throw new \InvalidArgumentException(
sprintf(
'Instance %s must implement Assertable',
get_class($assertion)
),
1539624719
);
}
}
}
/**
* @param string $path
* @param string $command
* @return bool
*/
private function invokeAssertions(string $path, string $command): bool
{
try {
foreach ($this->assertions as $assertion) {
if (!$assertion->assert($path, $command)) {
return false;
}
}
} catch (Exception $exception) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper\Interceptor;
/*
* 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\Assertable;
use TYPO3\PharStreamWrapper\Exception;
use TYPO3\PharStreamWrapper\Manager;
class PharExtensionInterceptor implements Assertable
{
/**
* Determines whether the base file name has a ".phar" suffix.
*
* @param string $path
* @param string $command
* @return bool
* @throws Exception
*/
public function assert(string $path, string $command): bool
{
if ($this->baseFileContainsPharExtension($path)) {
return true;
}
throw new Exception(
sprintf(
'Unexpected file extension in "%s"',
$path
),
1535198703
);
}
/**
* @param string $path
* @return bool
*/
private function baseFileContainsPharExtension(string $path): bool
{
$invocation = Manager::instance()->resolve($path);
if ($invocation === null) {
return false;
}
$fileExtension = pathinfo($invocation->getBaseName(), PATHINFO_EXTENSION);
return strtolower($fileExtension) === 'phar';
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper\Interceptor;
/*
* 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\Assertable;
use TYPO3\PharStreamWrapper\Exception;
use TYPO3\PharStreamWrapper\Manager;
use TYPO3\PharStreamWrapper\Phar\DeserializationException;
use TYPO3\PharStreamWrapper\Phar\Reader;
/**
* @internal Experimental implementation of checking against serialized objects in Phar meta-data
* @internal This functionality has not been 100% pentested...
*/
class PharMetaDataInterceptor implements Assertable
{
/**
* Determines whether the according Phar archive contains
* (potential insecure) serialized objects.
*
* @param string $path
* @param string $command
* @return bool
* @throws Exception
*/
public function assert(string $path, string $command): bool
{
if ($this->baseFileDoesNotHaveMetaDataIssues($path)) {
return true;
}
throw new Exception(
sprintf(
'Problematic meta-data in "%s"',
$path
),
1539632368
);
}
/**
* @param string $path
* @return bool
*/
private function baseFileDoesNotHaveMetaDataIssues(string $path): bool
{
$invocation = Manager::instance()->resolve($path);
if ($invocation === null) {
return false;
}
// directly return in case invocation was checked before
if ($invocation->getVariable(self::class) === true) {
return true;
}
// otherwise analyze meta-data
try {
$reader = new Reader($invocation->getBaseName());
$reader->resolveContainer()->getManifest()->deserializeMetaData();
$invocation->setVariable(self::class, true);
} catch (DeserializationException $exception) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper;
/*
* 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\Resolver\PharInvocationResolver;
use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
use TYPO3\PharStreamWrapper\Resolver\PharInvocationCollection;
class Manager
{
/**
* @var self
*/
private static $instance;
/**
* @var Behavior
*/
private $behavior;
/**
* @var Resolvable
*/
private $resolver;
/**
* @var Collectable
*/
private $collection;
/**
* @param Behavior $behaviour
* @param Resolvable $resolver
* @param Collectable $collection
* @return self
*/
public static function initialize(
Behavior $behaviour,
Resolvable $resolver = null,
Collectable $collection = null
): self {
if (self::$instance === null) {
self::$instance = new self($behaviour, $resolver, $collection);
return self::$instance;
}
throw new \LogicException(
'Manager can only be initialized once',
1535189871
);
}
/**
* @return self
*/
public static function instance(): self
{
if (self::$instance !== null) {
return self::$instance;
}
throw new \LogicException(
'Manager needs to be initialized first',
1535189872
);
}
/**
* @return bool
*/
public static function destroy(): bool
{
if (self::$instance === null) {
return false;
}
self::$instance = null;
return true;
}
/**
* @param Behavior $behaviour
* @param Resolvable $resolver
* @param Collectable $collection
*/
private function __construct(
Behavior $behaviour,
Resolvable $resolver = null,
Collectable $collection = null
) {
$this->collection = $collection ?? new PharInvocationCollection();
$this->resolver = $resolver ?? new PharInvocationResolver();
$this->behavior = $behaviour;
}
/**
* @param string $path
* @param string $command
* @return bool
*/
public function assert(string $path, string $command): bool
{
return $this->behavior->assert($path, $command);
}
/**
* @param string $path
* @param null|int $flags
* @return PharInvocation|null
*/
public function resolve(string $path, int $flags = null)
{
return $this->resolver->resolve($path, $flags);
}
/**
* @return Collectable
*/
public function getCollection(): Collectable
{
return $this->collection;
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper\Phar;
/*
* 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!
*/
class Container
{
/**
* @var Stub
*/
private $stub;
/**
* @var Manifest
*/
private $manifest;
/**
* @param Stub $stub
* @param Manifest $manifest
*/
public function __construct(Stub $stub, Manifest $manifest)
{
$this->stub = $stub;
$this->manifest = $manifest;
}
/**
* @return Stub
*/
public function getStub(): Stub
{
return $this->stub;
}
/**
* @return Manifest
*/
public function getManifest(): Manifest
{
return $this->manifest;
}
/**
* @return string
*/
public function getAlias(): string
{
return $this->manifest->getAlias() ?: $this->stub->getMappedAlias();
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper\Phar;
/*
* 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\Exception;
class DeserializationException extends Exception
{
}

View File

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper\Phar;
/*
* 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!
*/
class Manifest
{
/**
* @param string $content
* @return self
* @see http://php.net/manual/en/phar.fileformat.phar.php
*/
public static function fromContent(string $content): self
{
$target = new static();
$target->manifestLength = Reader::resolveFourByteLittleEndian($content, 0);
$target->amountOfFiles = Reader::resolveFourByteLittleEndian($content, 4);
$target->flags = Reader::resolveFourByteLittleEndian($content, 10);
$target->aliasLength = Reader::resolveFourByteLittleEndian($content, 14);
$target->alias = substr($content, 18, $target->aliasLength);
$target->metaDataLength = Reader::resolveFourByteLittleEndian($content, 18 + $target->aliasLength);
$target->metaData = substr($content, 22 + $target->aliasLength, $target->metaDataLength);
$apiVersionNibbles = Reader::resolveTwoByteBigEndian($content, 8);
$target->apiVersion = implode('.', [
($apiVersionNibbles & 0xf000) >> 12,
($apiVersionNibbles & 0x0f00) >> 8,
($apiVersionNibbles & 0x00f0) >> 4,
]);
return $target;
}
/**
* @var int
*/
private $manifestLength;
/**
* @var int
*/
private $amountOfFiles;
/**
* @var string
*/
private $apiVersion;
/**
* @var int
*/
private $flags;
/**
* @var int
*/
private $aliasLength;
/**
* @var string
*/
private $alias;
/**
* @var int
*/
private $metaDataLength;
/**
* @var string
*/
private $metaData;
/**
* Avoid direct instantiation.
*/
private function __construct()
{
}
/**
* @return int
*/
public function getManifestLength(): int
{
return $this->manifestLength;
}
/**
* @return int
*/
public function getAmountOfFiles(): int
{
return $this->amountOfFiles;
}
/**
* @return string
*/
public function getApiVersion(): string
{
return $this->apiVersion;
}
/**
* @return int
*/
public function getFlags(): int
{
return $this->flags;
}
/**
* @return int
*/
public function getAliasLength(): int
{
return $this->aliasLength;
}
/**
* @return string
*/
public function getAlias(): string
{
return $this->alias;
}
/**
* @return int
*/
public function getMetaDataLength(): int
{
return $this->metaDataLength;
}
/**
* @return string
*/
public function getMetaData(): string
{
return $this->metaData;
}
/**
* @return mixed|null
*/
public function deserializeMetaData()
{
if (empty($this->metaData)) {
return null;
}
$result = unserialize($this->metaData, ['allowed_classes' => false]);
$serialized = json_encode($result);
if (strpos($serialized, '__PHP_Incomplete_Class_Name') !== false) {
throw new DeserializationException(
'Meta-data contains serialized object',
1539623382
);
}
return $result;
}
}

View File

@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper\Phar;
/*
* 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!
*/
class Reader
{
/**
* @var string
*/
private $fileName;
/**
* Mime-type in order to use zlib, bzip2 or no compression.
* In case ext-fileinfo is not present only the relevant types
* 'application/x-gzip' and 'application/x-bzip2' are assigned
* to this class property.
*
* @var string
*/
private $fileType;
/**
* @param string $fileName
*/
public function __construct(string $fileName)
{
if (strpos($fileName, '://') !== false) {
throw new ReaderException(
'File name must not contain stream prefix',
1539623708
);
}
$this->fileName = $fileName;
$this->fileType = $this->determineFileType();
}
/**
* @return Container
*/
public function resolveContainer(): Container
{
$data = $this->extractData($this->resolveStream() . $this->fileName);
if ($data['stubContent'] === null) {
throw new ReaderException(
'Cannot resolve stub',
1547807881
);
}
if ($data['manifestContent'] === null || $data['manifestLength'] === null) {
throw new ReaderException(
'Cannot resolve manifest',
1547807882
);
}
if (strlen($data['manifestContent']) < $data['manifestLength']) {
throw new ReaderException(
sprintf(
'Exected manifest length %d, got %d',
strlen($data['manifestContent']),
$data['manifestLength']
),
1547807883
);
}
return new Container(
Stub::fromContent($data['stubContent']),
Manifest::fromContent($data['manifestContent'])
);
}
/**
* @param string $fileName e.g. '/path/file.phar' or 'compress.zlib:///path/file.phar'
* @return array
*/
private function extractData(string $fileName): array
{
$stubContent = null;
$manifestContent = null;
$manifestLength = null;
$resource = fopen($fileName, 'r');
if (!is_resource($resource)) {
throw new ReaderException(
sprintf('Resource %s could not be opened', $fileName),
1547902055
);
}
while (!feof($resource)) {
$line = fgets($resource);
// stop processing in case the system fails to read from a stream
if ($line === false) {
break;
}
// stop reading file when manifest can be extracted
if ($manifestLength !== null && $manifestContent !== null && strlen($manifestContent) >= $manifestLength) {
break;
}
$manifestPosition = strpos($line, '__HALT_COMPILER();');
// first line contains start of manifest
if ($stubContent === null && $manifestContent === null && $manifestPosition !== false) {
$stubContent = substr($line, 0, $manifestPosition - 1);
$manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line);
$manifestLength = $this->resolveManifestLength($manifestContent);
// line contains start of stub
} elseif ($stubContent === null) {
$stubContent = $line;
// line contains start of manifest
} elseif ($manifestContent === null && $manifestPosition !== false) {
$manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line);
$manifestLength = $this->resolveManifestLength($manifestContent);
// manifest has been started (thus is cannot be stub anymore), add content
} elseif ($manifestContent !== null) {
$manifestContent .= $line;
$manifestLength = $this->resolveManifestLength($manifestContent);
// stub has been started (thus cannot be manifest here, yet), add content
} elseif ($stubContent !== null) {
$stubContent .= $line;
}
}
fclose($resource);
return [
'stubContent' => $stubContent,
'manifestContent' => $manifestContent,
'manifestLength' => $manifestLength,
];
}
/**
* Resolves stream in order to handle compressed Phar archives.
*
* @return string
*/
private function resolveStream(): string
{
if ($this->fileType === 'application/x-gzip' || $this->fileType === 'application/gzip') {
return 'compress.zlib://';
} elseif ($this->fileType === 'application/x-bzip2') {
return 'compress.bzip2://';
}
return '';
}
/**
* @return string
*/
private function determineFileType()
{
if (class_exists('\\finfo')) {
$fileInfo = new \finfo();
return $fileInfo->file($this->fileName, FILEINFO_MIME_TYPE);
}
return $this->determineFileTypeByHeader();
}
/**
* In case ext-fileinfo is not present only the relevant types
* 'application/x-gzip' and 'application/x-bzip2' are resolved.
*
* @return string
*/
private function determineFileTypeByHeader(): string
{
$resource = fopen($this->fileName, 'r');
if (!is_resource($resource)) {
throw new ReaderException(
sprintf('Resource %s could not be opened', $this->fileName),
1557753055
);
}
$header = fgets($resource, 4);
fclose($resource);
$mimeType = '';
if (strpos($header, "\x42\x5a\x68") === 0) {
$mimeType = 'application/x-bzip2';
} elseif (strpos($header, "\x1f\x8b") === 0) {
$mimeType = 'application/x-gzip';
}
return $mimeType;
}
/**
* @param string $content
* @return int|null
*/
private function resolveManifestLength(string $content)
{
if (strlen($content) < 4) {
return null;
}
return static::resolveFourByteLittleEndian($content, 0);
}
/**
* @param string $content
* @param int $start
* @return int
*/
public static function resolveFourByteLittleEndian(string $content, int $start): int
{
$payload = substr($content, $start, 4);
if (!is_string($payload)) {
throw new ReaderException(
sprintf('Cannot resolve value at offset %d', $start),
1539614260
);
}
$value = unpack('V', $payload);
if (!isset($value[1])) {
throw new ReaderException(
sprintf('Cannot resolve value at offset %d', $start),
1539614261
);
}
return $value[1];
}
/**
* @param string $content
* @param int $start
* @return int
*/
public static function resolveTwoByteBigEndian(string $content, int $start): int
{
$payload = substr($content, $start, 2);
if (!is_string($payload)) {
throw new ReaderException(
sprintf('Cannot resolve value at offset %d', $start),
1539614263
);
}
$value = unpack('n', $payload);
if (!isset($value[1])) {
throw new ReaderException(
sprintf('Cannot resolve value at offset %d', $start),
1539614264
);
}
return $value[1];
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper\Phar;
/*
* 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\Exception;
class ReaderException extends Exception
{
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper\Phar;
/*
* 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!
*/
/**
* @internal Experimental implementation of Phar archive internals
*/
class Stub
{
/**
* @param string $content
* @return self
*/
public static function fromContent(string $content): self
{
$target = new static();
$target->content = $content;
if (
stripos($content, 'Phar::mapPhar(') !== false
&& preg_match('#Phar\:\:mapPhar\(([^)]+)\)#', $content, $matches)
) {
// remove spaces, single & double quotes
// @todo `'my' . 'alias' . '.phar'` is not evaluated here
$target->mappedAlias = trim($matches[1], ' \'"');
}
return $target;
}
/**
* @var string
*/
private $content;
/**
* @var string
*/
private $mappedAlias = '';
/**
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* @return string
*/
public function getMappedAlias(): string
{
return $this->mappedAlias;
}
}

View File

@ -0,0 +1,514 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper;
/*
* 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\Resolver\PharInvocation;
class PharStreamWrapper
{
/**
* Internal stream constants that are not exposed to PHP, but used...
* @see https://github.com/php/php-src/blob/e17fc0d73c611ad0207cac8a4a01ded38251a7dc/main/php_streams.h
*/
const STREAM_OPEN_FOR_INCLUDE = 128;
/**
* @var resource
*/
public $context;
/**
* @var resource
*/
protected $internalResource;
/**
* @var PharInvocation
*/
protected $invocation;
/**
* @return bool
*/
public function dir_closedir(): bool
{
if (!is_resource($this->internalResource)) {
return false;
}
$this->invokeInternalStreamWrapper(
'closedir',
$this->internalResource
);
return !is_resource($this->internalResource);
}
/**
* @param string $path
* @param int $options
* @return bool
*/
public function dir_opendir(string $path, int $options): bool
{
$this->assert($path, Behavior::COMMAND_DIR_OPENDIR);
$this->internalResource = $this->invokeInternalStreamWrapper(
'opendir',
$path,
$this->context
);
return is_resource($this->internalResource);
}
/**
* @return string|false
*/
public function dir_readdir()
{
return $this->invokeInternalStreamWrapper(
'readdir',
$this->internalResource
);
}
/**
* @return bool
*/
public function dir_rewinddir(): bool
{
if (!is_resource($this->internalResource)) {
return false;
}
$this->invokeInternalStreamWrapper(
'rewinddir',
$this->internalResource
);
return is_resource($this->internalResource);
}
/**
* @param string $path
* @param int $mode
* @param int $options
* @return bool
*/
public function mkdir(string $path, int $mode, int $options): bool
{
$this->assert($path, Behavior::COMMAND_MKDIR);
return $this->invokeInternalStreamWrapper(
'mkdir',
$path,
$mode,
(bool) ($options & STREAM_MKDIR_RECURSIVE),
$this->context
);
}
/**
* @param string $path_from
* @param string $path_to
* @return bool
*/
public function rename(string $path_from, string $path_to): bool
{
$this->assert($path_from, Behavior::COMMAND_RENAME);
$this->assert($path_to, Behavior::COMMAND_RENAME);
return $this->invokeInternalStreamWrapper(
'rename',
$path_from,
$path_to,
$this->context
);
}
/**
* @param string $path
* @param int $options
* @return bool
*/
public function rmdir(string $path, int $options): bool
{
$this->assert($path, Behavior::COMMAND_RMDIR);
return $this->invokeInternalStreamWrapper(
'rmdir',
$path,
$this->context
);
}
/**
* @param int $cast_as
*/
public function stream_cast(int $cast_as)
{
throw new Exception(
'Method stream_select() cannot be used',
1530103999
);
}
public function stream_close()
{
$this->invokeInternalStreamWrapper(
'fclose',
$this->internalResource
);
}
/**
* @return bool
*/
public function stream_eof(): bool
{
return $this->invokeInternalStreamWrapper(
'feof',
$this->internalResource
);
}
/**
* @return bool
*/
public function stream_flush(): bool
{
return $this->invokeInternalStreamWrapper(
'fflush',
$this->internalResource
);
}
/**
* @param int $operation
* @return bool
*/
public function stream_lock(int $operation): bool
{
return $this->invokeInternalStreamWrapper(
'flock',
$this->internalResource,
$operation
);
}
/**
* @param string $path
* @param int $option
* @param string|int $value
* @return bool
*/
public function stream_metadata(string $path, int $option, $value): bool
{
$this->assert($path, Behavior::COMMAND_STEAM_METADATA);
if ($option === STREAM_META_TOUCH) {
return $this->invokeInternalStreamWrapper(
'touch',
$path,
...$value
);
}
if ($option === STREAM_META_OWNER_NAME || $option === STREAM_META_OWNER) {
return $this->invokeInternalStreamWrapper(
'chown',
$path,
$value
);
}
if ($option === STREAM_META_GROUP_NAME || $option === STREAM_META_GROUP) {
return $this->invokeInternalStreamWrapper(
'chgrp',
$path,
$value
);
}
if ($option === STREAM_META_ACCESS) {
return $this->invokeInternalStreamWrapper(
'chmod',
$path,
$value
);
}
return false;
}
/**
* @param string $path
* @param string $mode
* @param int $options
* @param string|null $opened_path
* @return bool
*/
public function stream_open(
string $path,
string $mode,
int $options,
string &$opened_path = null
): bool {
$this->assert($path, Behavior::COMMAND_STREAM_OPEN);
$arguments = [$path, $mode, (bool) ($options & STREAM_USE_PATH)];
// only add stream context for non include/require calls
if (!($options & static::STREAM_OPEN_FOR_INCLUDE)) {
$arguments[] = $this->context;
// work around https://bugs.php.net/bug.php?id=66569
// for including files from Phar stream with OPcache enabled
} else {
Helper::resetOpCache();
}
$this->internalResource = $this->invokeInternalStreamWrapper(
'fopen',
...$arguments
);
if (!is_resource($this->internalResource)) {
return false;
}
if ($opened_path !== null) {
$metaData = stream_get_meta_data($this->internalResource);
$opened_path = $metaData['uri'];
}
return true;
}
/**
* @param int $count
* @return string
*/
public function stream_read(int $count): string
{
return $this->invokeInternalStreamWrapper(
'fread',
$this->internalResource,
$count
);
}
/**
* @param int $offset
* @param int $whence
* @return bool
*/
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
{
return $this->invokeInternalStreamWrapper(
'fseek',
$this->internalResource,
$offset,
$whence
) !== -1;
}
/**
* @param int $option
* @param int $arg1
* @param int $arg2
* @return bool
*/
public function stream_set_option(int $option, int $arg1, int $arg2): bool
{
if ($option === STREAM_OPTION_BLOCKING) {
return $this->invokeInternalStreamWrapper(
'stream_set_blocking',
$this->internalResource,
$arg1
);
}
if ($option === STREAM_OPTION_READ_TIMEOUT) {
return $this->invokeInternalStreamWrapper(
'stream_set_timeout',
$this->internalResource,
$arg1,
$arg2
);
}
if ($option === STREAM_OPTION_WRITE_BUFFER) {
return $this->invokeInternalStreamWrapper(
'stream_set_write_buffer',
$this->internalResource,
$arg2
) === 0;
}
return false;
}
/**
* @return array
*/
public function stream_stat(): array
{
return $this->invokeInternalStreamWrapper(
'fstat',
$this->internalResource
);
}
/**
* @return int
*/
public function stream_tell(): int
{
return $this->invokeInternalStreamWrapper(
'ftell',
$this->internalResource
);
}
/**
* @param int $new_size
* @return bool
*/
public function stream_truncate(int $new_size): bool
{
return $this->invokeInternalStreamWrapper(
'ftruncate',
$this->internalResource,
$new_size
);
}
/**
* @param string $data
* @return int
*/
public function stream_write(string $data): int
{
return $this->invokeInternalStreamWrapper(
'fwrite',
$this->internalResource,
$data
);
}
/**
* @param string $path
* @return bool
*/
public function unlink(string $path): bool
{
$this->assert($path, Behavior::COMMAND_UNLINK);
return $this->invokeInternalStreamWrapper(
'unlink',
$path,
$this->context
);
}
/**
* @param string $path
* @param int $flags
* @return array|false
*/
public function url_stat(string $path, int $flags)
{
$this->assert($path, Behavior::COMMAND_URL_STAT);
$functionName = $flags & STREAM_URL_STAT_QUIET ? '@stat' : 'stat';
return $this->invokeInternalStreamWrapper($functionName, $path);
}
/**
* @param string $path
* @param string $command
*/
protected function assert(string $path, string $command)
{
if (Manager::instance()->assert($path, $command) === true) {
$this->collectInvocation($path);
return;
}
throw new Exception(
sprintf(
'Denied invocation of "%s" for command "%s"',
$path,
$command
),
1535189880
);
}
/**
* @param string $path
*/
protected function collectInvocation(string $path)
{
if (isset($this->invocation)) {
return;
}
$manager = Manager::instance();
$this->invocation = $manager->resolve($path);
if ($this->invocation === null) {
throw new Exception(
'Expected invocation could not be resolved',
1556389591
);
}
// confirm, previous interceptor(s) validated invocation
$this->invocation->confirm();
$collection = $manager->getCollection();
if (!$collection->has($this->invocation)) {
$collection->collect($this->invocation);
}
}
/**
* @return Manager|Assertable
* @deprecated Use Manager::instance() directly
*/
protected function resolveAssertable(): Assertable
{
return Manager::instance();
}
/**
* Invokes commands on the native PHP Phar stream wrapper.
*
* @param string $functionName
* @param mixed ...$arguments
* @return mixed
*/
private function invokeInternalStreamWrapper(string $functionName, ...$arguments)
{
$silentExecution = $functionName[0] === '@';
$functionName = ltrim($functionName, '@');
$this->restoreInternalSteamWrapper();
try {
if ($silentExecution) {
$result = @call_user_func_array($functionName, $arguments);
} else {
$result = call_user_func_array($functionName, $arguments);
}
} finally {
$this->registerStreamWrapper();
}
return $result;
}
private function restoreInternalSteamWrapper()
{
if (PHP_VERSION_ID < 70324
|| PHP_VERSION_ID >= 70400 && PHP_VERSION_ID < 70412) {
stream_wrapper_restore('phar');
} else {
// with https://github.com/php/php-src/pull/6183 (PHP #76943) the
// behavior of `stream_wrapper_restore()` did change for
// PHP 8.0-RC1, 7.4.12 and 7.3.24
@stream_wrapper_restore('phar');
}
}
private function registerStreamWrapper()
{
stream_wrapper_unregister('phar');
stream_wrapper_register('phar', static::class);
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace TYPO3\PharStreamWrapper;
/*
* 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\Resolver\PharInvocation;
interface Resolvable
{
/**
* @param string $path
* @param null|int $flags
* @return null|PharInvocation
*/
public function resolve(string $path, int $flags = null);
}

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
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\Exception;
class PharInvocation
{
/**
* @var string
*/
private $baseName;
/**
* @var string
*/
private $alias;
/**
* @var bool
* @see \TYPO3\PharStreamWrapper\PharStreamWrapper::collectInvocation()
*/
private $confirmed = false;
/**
* Arbitrary variables to be used by interceptors as registry
* (e.g. in order to avoid duplicate processing and assertions)
*
* @var array
*/
private $variables;
/**
* @param string $baseName
* @param string $alias
*/
public function __construct(string $baseName, string $alias = '')
{
if ($baseName === '') {
throw new Exception(
'Base-name cannot be empty',
1551283689
);
}
$this->baseName = $baseName;
$this->alias = $alias;
}
/**
* @return string
*/
public function __toString(): string
{
return $this->baseName;
}
/**
* @return string
*/
public function getBaseName(): string
{
return $this->baseName;
}
/**
* @return null|string
*/
public function getAlias(): string
{
return $this->alias;
}
/**
* @return bool
*/
public function isConfirmed(): bool
{
return $this->confirmed;
}
public function confirm()
{
$this->confirmed = true;
}
/**
* @param string $name
* @return mixed|null
*/
public function getVariable(string $name)
{
return $this->variables[$name] ?? null;
}
/**
* @param string $name
* @param mixed $value
*/
public function setVariable(string $name, $value)
{
$this->variables[$name] = $value;
}
/**
* @param PharInvocation $other
* @return bool
*/
public function equals(PharInvocation $other): bool
{
return $other->baseName === $this->baseName
&& $other->alias === $this->alias;
}
}

View File

@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
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\Collectable;
class PharInvocationCollection implements Collectable
{
const UNIQUE_INVOCATION = 1;
const UNIQUE_BASE_NAME = 2;
const DUPLICATE_ALIAS_WARNING = 32;
/**
* @var PharInvocation[]
*/
private $invocations = [];
/**
* @param PharInvocation $invocation
* @return bool
*/
public function has(PharInvocation $invocation): bool
{
return in_array($invocation, $this->invocations, true);
}
/**
* @param PharInvocation $invocation
* @param null|int $flags
* @return bool
*/
public function collect(PharInvocation $invocation, int $flags = null): bool
{
if ($flags === null) {
$flags = static::UNIQUE_INVOCATION | static::DUPLICATE_ALIAS_WARNING;
}
if ($invocation->getBaseName() === ''
|| $invocation->getAlias() === ''
|| !$this->assertUniqueBaseName($invocation, $flags)
|| !$this->assertUniqueInvocation($invocation, $flags)
) {
return false;
}
if ($flags & static::DUPLICATE_ALIAS_WARNING) {
$this->triggerDuplicateAliasWarning($invocation);
}
$this->invocations[] = $invocation;
return true;
}
/**
* @param callable $callback
* @param bool $reverse
* @return null|PharInvocation
*/
public function findByCallback(callable $callback, $reverse = false)
{
foreach ($this->getInvocations($reverse) as $invocation) {
if (call_user_func($callback, $invocation) === true) {
return $invocation;
}
}
return null;
}
/**
* Asserts that base-name is unique. This disallows having multiple invocations for
* same base-name but having different alias names.
*
* @param PharInvocation $invocation
* @param int $flags
* @return bool
*/
private function assertUniqueBaseName(PharInvocation $invocation, int $flags): bool
{
if (!($flags & static::UNIQUE_BASE_NAME)) {
return true;
}
return $this->findByCallback(
function (PharInvocation $candidate) use ($invocation) {
return $candidate->getBaseName() === $invocation->getBaseName();
}
) === null;
}
/**
* Asserts that combination of base-name and alias is unique. This allows having multiple
* invocations for same base-name but having different alias names (for whatever reason).
*
* @param PharInvocation $invocation
* @param int $flags
* @return bool
*/
private function assertUniqueInvocation(PharInvocation $invocation, int $flags): bool
{
if (!($flags & static::UNIQUE_INVOCATION)) {
return true;
}
return $this->findByCallback(
function (PharInvocation $candidate) use ($invocation) {
return $candidate->equals($invocation);
}
) === null;
}
/**
* Triggers warning for invocations with same alias and same confirmation state.
*
* @param PharInvocation $invocation
* @see \TYPO3\PharStreamWrapper\PharStreamWrapper::collectInvocation()
*/
private function triggerDuplicateAliasWarning(PharInvocation $invocation)
{
$sameAliasInvocation = $this->findByCallback(
function (PharInvocation $candidate) use ($invocation) {
return $candidate->isConfirmed() === $invocation->isConfirmed()
&& $candidate->getAlias() === $invocation->getAlias();
},
true
);
if ($sameAliasInvocation === null) {
return;
}
trigger_error(
sprintf(
'Alias %s cannot be used by %s, already used by %s',
$invocation->getAlias(),
$invocation->getBaseName(),
$sameAliasInvocation->getBaseName()
),
E_USER_WARNING
);
}
/**
* @param bool $reverse
* @return PharInvocation[]
*/
private function getInvocations(bool $reverse = false): array
{
if ($reverse) {
return array_reverse($this->invocations);
}
return $this->invocations;
}
}

View File

@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
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 = [
'include',
'include_once',
'require',
'require_once'
];
/**
* Contains resolved base names in order to reduce file IO.
*
* @var string[]
*/
private $baseNames = [];
/**
* 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(string $path, int $flags = null)
{
$hasPharPrefix = Helper::hasPharPrefix($path);
$flags = $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(string $baseName, int $flags): PharInvocation
{
$invocation = $this->findByBaseName($baseName);
if ($invocation !== null) {
return $invocation;
}
if ($flags & static::RESOLVE_ALIAS) {
$alias = (new Reader($baseName))->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(string $path, int $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 {
$currentAlias = (new Reader($currentBaseName))->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(string $path)
{
$normalizedPath = Helper::normalizePath($path);
return strstr($normalizedPath, '/', true) ?: null;
}
/**
* @param string $baseName
* @return null|PharInvocation
*/
private function findByBaseName(string $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(string $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(string $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(string $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
);
}
}