updated core to 7.67

This commit is contained in:
2019-07-09 11:56:03 +02:00
parent aa5c4a5a74
commit cc3b64a193
175 changed files with 2420 additions and 428 deletions

View File

@@ -0,0 +1,3 @@
.idea
vendor/
composer.lock

View File

@@ -63,7 +63,7 @@ adjusted to according requirements.
```
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
Manager::initialize(
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new PharExtensionInterceptor())
);
@@ -90,7 +90,7 @@ if (in_array('phar', stream_get_wrappers())) {
+ `COMMAND_UNLINK`
+ `COMMAND_URL_STAT`
## Interceptor
## Interceptors
The following interceptor is shipped with the package and ready to use in order
to block any Phar invocation of files not having a `.phar` suffix. Besides that
@@ -137,9 +137,72 @@ class PharExtensionInterceptor implements Assertable
}
```
### ConjunctionInterceptor
This interceptor combines multiple interceptors implementing `Assertable`.
It succeeds when all nested interceptors succeed as well (logical `AND`).
```
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new ConjunctionInterceptor(array(
new PharExtensionInterceptor(),
new PharMetaDataInterceptor()
)))
);
```
### PharExtensionInterceptor
This (basic) interceptor just checks whether the invoked Phar archive has
an according `.phar` file extension. Resolving symbolic links as well as
Phar internal alias resolving are considered as well.
```
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new PharExtensionInterceptor())
);
```
### PharMetaDataInterceptor
This interceptor is actually checking serialized Phar meta-data against
PHP objects and would consider a Phar archive malicious in case not only
scalar values are found. A custom low-level `Phar\Reader` is used in order to
avoid using PHP's `Phar` object which would trigger the initial vulnerability.
```
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new PharMetaDataInterceptor())
);
```
## Reader
* `Phar\Reader::__construct(string $fileName)`: Creates low-level reader for Phar archive
* `Phar\Reader::resolveContainer(): Phar\Container`: Resolves model representing Phar archive
* `Phar\Container::getStub(): Phar\Stub`: Resolves (plain PHP) stub section of Phar archive
* `Phar\Container::getManifest(): Phar\Manifest`: Resolves parsed Phar archive manifest as
documented at http://php.net/manual/en/phar.fileformat.manifestfile.php
* `Phar\Stub::getMappedAlias(): string`: Resolves internal Phar archive alias defined in stub
using `Phar::mapPhar('alias.phar')` - actually the plain PHP source is analyzed here
* `Phar\Manifest::getAlias(): string` - Resolves internal Phar archive alias defined in manifest
using `Phar::setAlias('alias.phar')`
* `Phar\Manifest::getMetaData(): string`: Resolves serialized Phar archive meta-data
* `Phar\Manifest::deserializeMetaData(): mixed`: Resolves deserialized Phar archive meta-data
containing only scalar values - in case an object is determined, an according
`Phar\DeserializationException` will be thrown
```
$reader = new Phar\Reader('example.phar');
var_dump($reader->resolveContainer()->getManifest()->deserializeMetaData());
```
## Helper
* `Helper::determineBaseFile(string $path)`: Determines base file that can be
* `Helper::determineBaseFile(string $path): string`: Determines base file that can be
accessed using the regular file system. For instance the following path
`phar:///home/user/bundle.phar/content.txt` would be resolved to
`/home/user/bundle.phar`.

View File

@@ -6,9 +6,13 @@
"homepage": "https://typo3.org/",
"keywords": ["php", "phar", "stream-wrapper", "security"],
"require": {
"php": "^5.3.3|^7.0"
"php": "^5.3.3|^7.0",
"ext-fileinfo": "*",
"ext-json": "*",
"brumann/polyfill-unserialize": "^1.0"
},
"require-dev": {
"ext-xdebug": "*",
"phpunit/phpunit": "^4.8.36"
},
"autoload": {

View File

@@ -0,0 +1,37 @@
<?php
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);
/**
* @param PharInvocation $invocation
* @param null $flags
* @return bool
*/
public function collect(PharInvocation $invocation, $flags = null);
/**
* @param callable $callback
* @param bool $reverse
* @return null|PharInvocation
*/
public function findByCallback($callback, $reverse = false);
}

View File

@@ -11,6 +11,13 @@ namespace TYPO3\PharStreamWrapper;
* 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
{
/*
@@ -54,6 +61,15 @@ class Helper
return null;
}
/**
* @param string $path
* @return bool
*/
public static function hasPharPrefix($path)
{
return stripos($path, 'phar://') === 0;
}
/**
* @param string $path
* @return string
@@ -61,7 +77,7 @@ class Helper
public static function removePharPrefix($path)
{
$path = trim($path);
if (stripos($path, 'phar://') !== 0) {
if (!static::hasPharPrefix($path)) {
return $path;
}
return substr($path, 7);
@@ -77,7 +93,7 @@ class Helper
public static function normalizePath($path)
{
return rtrim(
static::getCanonicalPath(
static::normalizeWindowsPath(
static::removePharPrefix($path)
),
'/'

View File

@@ -0,0 +1,88 @@
<?php
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($path, $command)
{
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($path, $command)
{
try {
foreach ($this->assertions as $assertion) {
if (!$assertion->assert($path, $command)) {
return false;
}
}
} catch (Exception $exception) {
return false;
}
return true;
}
}

View File

@@ -12,8 +12,8 @@ namespace TYPO3\PharStreamWrapper\Interceptor;
*/
use TYPO3\PharStreamWrapper\Assertable;
use TYPO3\PharStreamWrapper\Helper;
use TYPO3\PharStreamWrapper\Exception;
use TYPO3\PharStreamWrapper\Manager;
class PharExtensionInterceptor implements Assertable
{
@@ -45,11 +45,11 @@ class PharExtensionInterceptor implements Assertable
*/
private function baseFileContainsPharExtension($path)
{
$baseFile = Helper::determineBaseFile($path);
if ($baseFile === null) {
$invocation = Manager::instance()->resolve($path);
if ($invocation === null) {
return false;
}
$fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
$fileExtension = pathinfo($invocation->getBaseName(), PATHINFO_EXTENSION);
return strtolower($fileExtension) === 'phar';
}
}

View File

@@ -0,0 +1,73 @@
<?php
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($path, $command)
{
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($path)
{
$invocation = Manager::instance()->resolve($path);
if ($invocation === null) {
return false;
}
// directly return in case invocation was checked before
if ($invocation->getVariable(__CLASS__) === true) {
return true;
}
// otherwise analyze meta-data
try {
$reader = new Reader($invocation->getBaseName());
$reader->resolveContainer()->getManifest()->deserializeMetaData();
$invocation->setVariable(__CLASS__, true);
} catch (DeserializationException $exception) {
return false;
}
return true;
}
}

View File

@@ -11,7 +11,11 @@ namespace TYPO3\PharStreamWrapper;
* The TYPO3 project - inspiring people to share!
*/
class Manager implements Assertable
use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
use TYPO3\PharStreamWrapper\Resolver\PharInvocationCollection;
use TYPO3\PharStreamWrapper\Resolver\PharInvocationResolver;
class Manager
{
/**
* @var self
@@ -23,14 +27,29 @@ class Manager implements Assertable
*/
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)
{
public static function initialize(
Behavior $behaviour,
Resolvable $resolver = null,
Collectable $collection = null
) {
if (self::$instance === null) {
self::$instance = new self($behaviour);
self::$instance = new self($behaviour, $resolver, $collection);
return self::$instance;
}
throw new \LogicException(
@@ -67,9 +86,22 @@ class Manager implements Assertable
/**
* @param Behavior $behaviour
* @param Resolvable $resolver
* @param Collectable $collection
*/
private function __construct(Behavior $behaviour)
{
private function __construct(
Behavior $behaviour,
Resolvable $resolver = null,
Collectable $collection = null
) {
if ($collection === null) {
$collection = new PharInvocationCollection();
}
if ($resolver === null) {
$resolver = new PharInvocationResolver();
}
$this->collection = $collection;
$this->resolver = $resolver;
$this->behavior = $behaviour;
}
@@ -82,4 +114,22 @@ class Manager implements Assertable
{
return $this->behavior->assert($path, $command);
}
/**
* @param string $path
* @param null|int $flags
* @return null|PharInvocation
*/
public function resolve($path, $flags = null)
{
return $this->resolver->resolve($path, $flags);
}
/**
* @return Collectable
*/
public function getCollection()
{
return $this->collection;
}
}

View File

@@ -0,0 +1,59 @@
<?php
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()
{
return $this->stub;
}
/**
* @return Manifest
*/
public function getManifest()
{
return $this->manifest;
}
/**
* @return string
*/
public function getAlias()
{
return $this->manifest->getAlias() ?: $this->stub->getMappedAlias();
}
}

View File

@@ -0,0 +1,18 @@
<?php
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,176 @@
<?php
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 Brumann\Polyfill\Unserialize;
class Manifest
{
/**
* @param string $content
* @return self
* @see http://php.net/manual/en/phar.fileformat.phar.php
*/
public static function fromContent($content)
{
$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('.', array(
($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()
{
return $this->manifestLength;
}
/**
* @return int
*/
public function getAmountOfFiles()
{
return $this->amountOfFiles;
}
/**
* @return string
*/
public function getApiVersion()
{
return $this->apiVersion;
}
/**
* @return int
*/
public function getFlags()
{
return $this->flags;
}
/**
* @return int
*/
public function getAliasLength()
{
return $this->aliasLength;
}
/**
* @return string
*/
public function getAlias()
{
return $this->alias;
}
/**
* @return int
*/
public function getMetaDataLength()
{
return $this->metaDataLength;
}
/**
* @return string
*/
public function getMetaData()
{
return $this->metaData;
}
/**
* @return mixed|null
*/
public function deserializeMetaData()
{
if (empty($this->metaData)) {
return null;
}
$result = Unserialize::unserialize($this->metaData, array('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,220 @@
<?php
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;
/**
* @var string
*/
private $fileType;
/**
* @param string $fileName
*/
public function __construct($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()
{
$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($fileName)
{
$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 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 array(
'stubContent' => $stubContent,
'manifestContent' => $manifestContent,
'manifestLength' => $manifestLength,
);
}
/**
* Resolves stream in order to handle compressed Phar archives.
*
* @return string
*/
private function resolveStream()
{
if ($this->fileType === 'application/x-gzip') {
return 'compress.zlib://';
} elseif ($this->fileType === 'application/x-bzip2') {
return 'compress.bzip2://';
}
return '';
}
/**
* @return string
*/
private function determineFileType()
{
$fileInfo = new \finfo();
return $fileInfo->file($this->fileName, FILEINFO_MIME_TYPE);
}
/**
* @param string $content
* @return int|null
*/
private function resolveManifestLength($content)
{
if (strlen($content) < 4) {
return null;
}
return static::resolveFourByteLittleEndian($content, 0);
}
/**
* @param string $content
* @param int $start
* @return int
*/
public static function resolveFourByteLittleEndian($content, $start)
{
$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($content, $start)
{
$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,18 @@
<?php
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,65 @@
<?php
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($content)
{
$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()
{
return $this->mappedAlias;
}
}

View File

@@ -11,6 +11,8 @@ namespace TYPO3\PharStreamWrapper;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
class PharStreamWrapper
{
/**
@@ -29,6 +31,11 @@ class PharStreamWrapper
*/
protected $internalResource;
/**
* @var PharInvocation
*/
protected $invocation;
/**
* @return bool
*/
@@ -409,7 +416,8 @@ class PharStreamWrapper
*/
protected function assert($path, $command)
{
if ($this->resolveAssertable()->assert($path, $command) === true) {
if (Manager::instance()->assert($path, $command) === true) {
$this->collectInvocation($path);
return;
}
@@ -424,7 +432,33 @@ class PharStreamWrapper
}
/**
* @return Assertable
* @param string $path
*/
protected function collectInvocation($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()
{

View File

@@ -0,0 +1,24 @@
<?php
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($path, $flags = null);
}

View File

@@ -0,0 +1,125 @@
<?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\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($baseName, $alias = '')
{
if ($baseName === '') {
throw new Exception(
'Base-name cannot be empty',
1551283689
);
}
$this->baseName = $baseName;
$this->alias = $alias;
}
/**
* @return string
*/
public function __toString()
{
return $this->baseName;
}
/**
* @return string
*/
public function getBaseName()
{
return $this->baseName;
}
/**
* @return null|string
*/
public function getAlias()
{
return $this->alias;
}
/**
* @return bool
*/
public function isConfirmed()
{
return $this->confirmed;
}
public function confirm()
{
$this->confirmed = true;
}
/**
* @param string $name
* @return mixed|null
*/
public function getVariable($name)
{
if (!isset($this->variables[$name])) {
return null;
}
return $this->variables[$name];
}
/**
* @param string $name
* @param mixed $value
*/
public function setVariable($name, $value)
{
$this->variables[$name] = $value;
}
/**
* @param PharInvocation $other
* @return bool
*/
public function equals(PharInvocation $other)
{
return $other->baseName === $this->baseName
&& $other->alias === $this->alias;
}
}

View File

@@ -0,0 +1,156 @@
<?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\Collectable;
class PharInvocationCollection implements Collectable
{
const UNIQUE_INVOCATION = 1;
const UNIQUE_BASE_NAME = 2;
const DUPLICATE_ALIAS_WARNING = 32;
/**
* @var PharInvocation[]
*/
private $invocations = array();
/**
* @param PharInvocation $invocation
* @return bool
*/
public function has(PharInvocation $invocation)
{
return in_array($invocation, $this->invocations, true);
}
/**
* @param PharInvocation $invocation
* @param null|int $flags
* @return bool
*/
public function collect(PharInvocation $invocation, $flags = null)
{
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($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, $flags)
{
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, $flags)
{
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($reverse = false)
{
if ($reverse) {
return array_reverse($this->invocations);
}
return $this->invocations;
}
}

View File

@@ -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
);
}
}