[TASK] Use typo3/phar-stream-wrapper package 77/58777/3
authorOliver Hader <oliver@typo3.org>
Sun, 26 Aug 2018 20:38:59 +0000 (22:38 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Tue, 30 Oct 2018 08:06:11 +0000 (09:06 +0100)
PharStreamWrapper has been released as standalone package under
the MIT license: https://github.com/TYPO3/phar-stream-wrapper

Stream invocation is handled by the new composer package, previous
classes PharStreamWrapper and PharStreamWrapperException have been
removed from the TYPO3 core but are still kept in class alias maps
for compatibility reasons. Since the standalone package is now
independent from TYPO3 constraints, the TYPO3 specific logic to
intercept Phar invocations has been moved to the new class
PharStreamWrapperInterceptor.

`composer require typo3/phar-stream-wrapper:^3.0.1`

Related: #85984
Resolves: #86666
Releases: 8.7, 7.6
Change-Id: I724c4238d1a8184a8c7c908f16d71c06f87244d8
Reviewed-on: https://review.typo3.org/58777
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
composer.json
composer.lock
typo3/sysext/core/Classes/Core/Bootstrap.php
typo3/sysext/core/Classes/IO/PharStreamWrapper.php [deleted file]
typo3/sysext/core/Classes/IO/PharStreamWrapperException.php [deleted file]
typo3/sysext/core/Classes/IO/PharStreamWrapperInterceptor.php [new file with mode: 0644]
typo3/sysext/core/Migrations/Code/ClassAliasMap.php [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/IO/PharStreamWrapperInterceptorTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/IO/PharStreamWrapperTest.php [deleted file]
typo3/sysext/core/composer.json

index 5948a52..6d236d4 100644 (file)
@@ -53,6 +53,7 @@
                "typo3/class-alias-loader": "^1.0",
                "typo3/cms-cli": "^1.0.2",
                "typo3/cms-composer-installers": "^1.2.8",
+               "typo3/phar-stream-wrapper": "^3.0.1",
                "typo3fluid/fluid": "^2.3"
        },
        "require-dev": {
index 92aa216..b772ca3 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "73f8ef6f02ae8a0be883b54423cc5cb4",
+    "content-hash": "fcd4ef2d6e8503242ec4561f39c9d361",
     "packages": [
         {
             "name": "cogpowered/finediff",
             "time": "2017-11-06T11:44:16+00:00"
         },
         {
+            "name": "typo3/phar-stream-wrapper",
+            "version": "v3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/TYPO3/phar-stream-wrapper.git",
+                "reference": "b3cbcfd2e4de5943bf05ec975d42d0e8d85c0009"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/TYPO3/phar-stream-wrapper/zipball/b3cbcfd2e4de5943bf05ec975d42d0e8d85c0009",
+                "reference": "b3cbcfd2e4de5943bf05ec975d42d0e8d85c0009",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "TYPO3\\PharStreamWrapper\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Interceptors for PHP's native phar:// stream handling",
+            "homepage": "https://typo3.org/",
+            "keywords": [
+                "phar",
+                "php",
+                "security",
+                "stream-wrapper"
+            ],
+            "time": "2018-10-18T08:50:44+00:00"
+        },
+        {
             "name": "typo3fluid/fluid",
             "version": "2.5.4",
             "source": {
index 9864956..6587b34 100644 (file)
@@ -14,10 +14,13 @@ namespace TYPO3\CMS\Core\Core;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\IO\PharStreamWrapper;
+use TYPO3\CMS\Core\IO\PharStreamWrapperInterceptor;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\PharStreamWrapper\Behavior;
+use TYPO3\PharStreamWrapper\Manager;
+use TYPO3\PharStreamWrapper\PharStreamWrapper;
 
 /**
  * This class encapsulates bootstrap related methods.
@@ -683,6 +686,13 @@ class Bootstrap
     protected function initializeIO()
     {
         if (in_array('phar', stream_get_wrappers())) {
+            // destroy and re-initialize PharStreamWrapper for TYPO3 core
+            Manager::destroy();
+            Manager::initialize(
+                (new Behavior())
+                    ->withAssertion(new PharStreamWrapperInterceptor())
+            );
+
             stream_wrapper_unregister('phar');
             stream_wrapper_register('phar', PharStreamWrapper::class);
         }
diff --git a/typo3/sysext/core/Classes/IO/PharStreamWrapper.php b/typo3/sysext/core/Classes/IO/PharStreamWrapper.php
deleted file mode 100644 (file)
index 490cc68..0000000
+++ /dev/null
@@ -1,556 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Core\IO;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\PathUtility;
-
-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;
-
-    /**
-     * @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->assertPath($path);
-        $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->assertPath($path);
-        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->assertPath($path_from);
-        $this->assertPath($path_to);
-        return $this->invokeInternalStreamWrapper(
-            'rename',
-            $path_from,
-            $path_to,
-            $this->context
-        );
-    }
-
-    public function rmdir(string $path, int $options): bool
-    {
-        $this->assertPath($path);
-        return $this->invokeInternalStreamWrapper(
-            'rmdir',
-            $path,
-            $this->context
-        );
-    }
-
-    /**
-     * @param int $cast_as
-     */
-    public function stream_cast(int $cast_as)
-    {
-        throw new PharStreamWrapperException(
-            '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->assertPath($path);
-        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->assertPath($path);
-        $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 {
-            $this->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->assertPath($path);
-        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->assertPath($path);
-        $functionName = $flags & STREAM_URL_STAT_QUIET ? '@stat' : 'stat';
-        return $this->invokeInternalStreamWrapper($functionName, $path);
-    }
-
-    /**
-     * @param string $path
-     * @return bool
-     */
-    protected function isAllowed(string $path): bool
-    {
-        $path = $this->determineBaseFile($path);
-        if (!GeneralUtility::isAbsPath($path)) {
-            $path = PATH_site . $path;
-        }
-
-        if (GeneralUtility::validPathStr($path)
-            && GeneralUtility::isFirstPartOfStr(
-                $path,
-                PATH_site . 'typo3conf/ext/'
-            )
-        ) {
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * Normalizes a path, removes phar:// prefix, fixes Windows directory
-     * separators. Result is without trailing slash.
-     *
-     * @param string $path
-     * @return string
-     */
-    protected function normalizePath(string $path): string
-    {
-        return rtrim(
-            PathUtility::getCanonicalPath(
-                GeneralUtility::fixWindowsFilePath(
-                    $this->removePharPrefix($path)
-                )
-            ),
-            '/'
-        );
-    }
-
-    /**
-     * @param string $path
-     * @return string
-     */
-    protected function removePharPrefix(string $path): string
-    {
-        return preg_replace('#^phar://#i', '', $path);
-    }
-
-    /**
-     * 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
-     */
-    protected function determineBaseFile(string $path)
-    {
-        $parts = explode('/', $this->normalizePath($path));
-
-        while (count($parts)) {
-            $currentPath = implode('/', $parts);
-            if (@file_exists($currentPath)) {
-                return $currentPath;
-            }
-            array_pop($parts);
-        }
-
-        return null;
-    }
-
-    /**
-     * Determines whether the requested path is the base file.
-     *
-     * @param string $path
-     * @return bool
-     * @deprecated Currently not used
-     */
-    protected function isBaseFile(string $path): bool
-    {
-        $path = $this->normalizePath($path);
-        $baseFile = $this->determineBaseFile($path);
-        return $path === $baseFile;
-    }
-
-    /**
-     * Asserts the given path to a Phar file.
-     *
-     * @param string $path
-     * @throws PharStreamWrapperException
-     */
-    protected function assertPath(string $path)
-    {
-        if (!$this->isAllowed($path)) {
-            throw new PharStreamWrapperException(
-                sprintf('Executing  %s is denied', $path),
-                1530103998
-            );
-        }
-    }
-
-    protected function resetOpCache()
-    {
-        if (function_exists('opcache_reset')
-            && function_exists('opcache_get_status')
-            && !empty(opcache_get_status()['opcache_enabled'])
-        ) {
-            opcache_reset();
-        }
-    }
-
-    /**
-     * Invokes commands on the native PHP Phar stream wrapper.
-     *
-     * @param string $functionName
-     * @param mixed ...$arguments
-     * @return mixed
-     */
-    protected function invokeInternalStreamWrapper(string $functionName, ...$arguments)
-    {
-        $silentExecution = $functionName{0} === '@';
-        $functionName = ltrim($functionName, '@');
-        $this->restoreInternalSteamWrapper();
-
-        if ($silentExecution) {
-            $result = @call_user_func_array($functionName, $arguments);
-        } else {
-            $result = call_user_func_array($functionName, $arguments);
-        }
-
-        $this->registerStreamWrapper();
-        return $result;
-    }
-
-    protected function restoreInternalSteamWrapper()
-    {
-        stream_wrapper_restore('phar');
-    }
-
-    protected function registerStreamWrapper()
-    {
-        stream_wrapper_unregister('phar');
-        stream_wrapper_register('phar', static::class);
-    }
-}
diff --git a/typo3/sysext/core/Classes/IO/PharStreamWrapperException.php b/typo3/sysext/core/Classes/IO/PharStreamWrapperException.php
deleted file mode 100644 (file)
index 57126c1..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Core\IO;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-class PharStreamWrapperException extends \RuntimeException
-{
-}
diff --git a/typo3/sysext/core/Classes/IO/PharStreamWrapperInterceptor.php b/typo3/sysext/core/Classes/IO/PharStreamWrapperInterceptor.php
new file mode 100644 (file)
index 0000000..834a219
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\IO;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
+use TYPO3\PharStreamWrapper\Exception;
+
+class PharStreamWrapperInterceptor implements \TYPO3\PharStreamWrapper\Assertable
+{
+    /**
+     * Asserts the given path of a Phar file is located in a valid path
+     * in typo3conf/ext/* of the local TYPO3 installation.
+     *
+     * @param string $path
+     * @param string $command
+     * @return bool
+     * @throws Exception
+     */
+    public function assert(string $path, string $command): bool
+    {
+        if ($this->isAllowed($path) === true) {
+            return true;
+        }
+        throw new Exception(
+            sprintf('Executing %s is denied', $path),
+            1530103998
+        );
+    }
+
+    /**
+     * @param string $path
+     * @return bool
+     */
+    protected function isAllowed(string $path): bool
+    {
+        $path = $this->determineBaseFile($path);
+        if (!GeneralUtility::isAbsPath($path)) {
+            $path = PATH_site . $path;
+        }
+
+        if (GeneralUtility::validPathStr($path)
+            && GeneralUtility::isFirstPartOfStr(
+                $path,
+                PATH_site . 'typo3conf/ext/'
+            )
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Normalizes a path, removes phar:// prefix, fixes Windows directory
+     * separators. Result is without trailing slash.
+     *
+     * @param string $path
+     * @return string
+     */
+    protected function normalizePath(string $path): string
+    {
+        return rtrim(
+            PathUtility::getCanonicalPath(
+                GeneralUtility::fixWindowsFilePath(
+                    $this->removePharPrefix($path)
+                )
+            ),
+            '/'
+        );
+    }
+
+    /**
+     * @param string $path
+     * @return string
+     */
+    protected function removePharPrefix(string $path): string
+    {
+        return preg_replace('#^phar://#i', '', $path);
+    }
+
+    /**
+     * 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
+     */
+    protected function determineBaseFile(string $path)
+    {
+        $parts = explode('/', $this->normalizePath($path));
+
+        while (count($parts)) {
+            $currentPath = implode('/', $parts);
+            if (@file_exists($currentPath)) {
+                return $currentPath;
+            }
+            array_pop($parts);
+        }
+
+        return null;
+    }
+}
diff --git a/typo3/sysext/core/Migrations/Code/ClassAliasMap.php b/typo3/sysext/core/Migrations/Code/ClassAliasMap.php
new file mode 100644 (file)
index 0000000..81319eb
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+return [
+    'TYPO3\\CMS\\Core\\IO\\PharStreamWrapper' => \TYPO3\PharStreamWrapper\PharStreamWrapper::class,
+    'TYPO3\\CMS\\Core\\IO\\PharStreamWrapperException' => \TYPO3\PharStreamWrapper\Exception::class,
+];
diff --git a/typo3/sysext/core/Tests/Functional/IO/PharStreamWrapperInterceptorTest.php b/typo3/sysext/core/Tests/Functional/IO/PharStreamWrapperInterceptorTest.php
new file mode 100644 (file)
index 0000000..a87ab7d
--- /dev/null
@@ -0,0 +1,395 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Functional\IO;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\PharStreamWrapper\Exception;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class PharStreamWrapperInterceptorTest extends FunctionalTestCase
+{
+    /**
+     * @var array
+     */
+    protected $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources'
+    ];
+
+    /**
+     * @var array
+     */
+    protected $pathsToLinkInTestInstance = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources/bundle.phar' => 'fileadmin/bundle.phar'
+    ];
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        if (!in_array('phar', stream_get_wrappers())) {
+            $this->markTestSkipped('Phar stream wrapper is not registered');
+        }
+        // PharStreamWrapper is not initialized here since it relies on being
+        // properly defined in \TYPO3\CMS\Core\Core\Bootstrap - thus, it tests
+        // are expected to fail in case PharStreamWrapper is not initialized
+    }
+
+    public function directoryActionAllowsInvocationDataProvider()
+    {
+        $allowedPath = 'typo3conf/ext/test_resources/bundle.phar';
+
+        return [
+            'root directory' => [
+                $allowedPath,
+                ['Classes', 'Resources']
+            ],
+            'Classes/Domain/Model directory' => [
+                $allowedPath . '/Classes/Domain/Model',
+                ['DemoModel.php']
+            ],
+            'Resources directory' => [
+                $allowedPath . '/Resources',
+                ['content.txt']
+            ],
+        ];
+    }
+
+    /**
+     * @param string $path
+     *
+     * @test
+     * @dataProvider directoryActionAllowsInvocationDataProvider
+     */
+    public function directoryOpenAllowsInvocation(string $path)
+    {
+        $path = $this->instancePath . '/' . $path;
+        $handle = opendir('phar://' . $path);
+        self::assertInternalType('resource', $handle);
+    }
+
+    /**
+     * @param string $path
+     * @param $expectation
+     *
+     * @test
+     * @dataProvider directoryActionAllowsInvocationDataProvider
+     */
+    public function directoryReadAllowsInvocation(string $path, array $expectation)
+    {
+        $path = $this->instancePath . '/' . $path;
+
+        $items = [];
+        $handle = opendir('phar://' . $path);
+        while (false !== $item = readdir($handle)) {
+            $items[] = $item;
+        }
+
+        self::assertSame($expectation, $items);
+    }
+
+    /**
+     * @param string $path
+     * @param $expectation
+     *
+     * @test
+     * @dataProvider directoryActionAllowsInvocationDataProvider
+     */
+    public function directoryCloseAllowsInvocation(string $path, array $expectation)
+    {
+        $path = $this->instancePath . '/' . $path;
+
+        $handle = opendir('phar://' . $path);
+        closedir($handle);
+
+        self::assertFalse(is_resource($handle));
+    }
+
+    public function directoryActionDeniesInvocationDataProvider()
+    {
+        $deniedPath = 'fileadmin/bundle.phar';
+
+        return [
+            'root directory' => [
+                $deniedPath,
+                ['Classes', 'Resources']
+            ],
+            'Classes/Domain/Model directory' => [
+                $deniedPath . '/Classes/Domain/Model',
+                ['DemoModel.php']
+            ],
+            'Resources directory' => [
+                $deniedPath . '/Resources',
+                ['content.txt']
+            ],
+        ];
+    }
+
+    /**
+     * @param string $path
+     *
+     * @test
+     * @dataProvider directoryActionDeniesInvocationDataProvider
+     */
+    public function directoryActionDeniesInvocation(string $path)
+    {
+        self::expectException(Exception::class);
+        self::expectExceptionCode(1530103998);
+
+        $path = $this->instancePath . '/' . $path;
+        opendir('phar://' . $path);
+    }
+
+    /**
+     * @return array
+     */
+    public function urlStatAllowsInvocationDataProvider(): array
+    {
+        $allowedPath = 'typo3conf/ext/test_resources/bundle.phar';
+
+        return [
+            'filesize base file' => [
+                'filesize',
+                $allowedPath,
+                0, // Phar base file always has zero size when accessed through phar://
+            ],
+            'filesize Resources/content.txt' => [
+                'filesize',
+                $allowedPath . '/Resources/content.txt',
+                21,
+            ],
+            'is_file base file' => [
+                'is_file',
+                $allowedPath,
+                false, // Phar base file is not a file when accessed through phar://
+            ],
+            'is_file Resources/content.txt' => [
+                'is_file',
+                $allowedPath . '/Resources/content.txt',
+                true,
+            ],
+            'is_dir base file' => [
+                'is_dir',
+                $allowedPath,
+                true, // Phar base file is a directory when accessed through phar://
+            ],
+            'is_dir Resources/content.txt' => [
+                'is_dir',
+                $allowedPath . '/Resources/content.txt',
+                false,
+            ],
+            'file_exists base file' => [
+                'file_exists',
+                $allowedPath,
+                true
+            ],
+            'file_exists Resources/content.txt' => [
+                'file_exists',
+                $allowedPath . '/Resources/content.txt',
+                true
+            ],
+        ];
+    }
+
+    /**
+     * @param string $functionName
+     * @param string $path
+     * @param mixed $expectation
+     *
+     * @test
+     * @dataProvider urlStatAllowsInvocationDataProvider
+     */
+    public function urlStatAllowsInvocation(string $functionName, string $path, $expectation)
+    {
+        $path = $this->instancePath . '/' . $path;
+
+        self::assertSame(
+            $expectation,
+            call_user_func($functionName, 'phar://' . $path)
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function urlStatDeniesInvocationDataProvider(): array
+    {
+        $deniedPath = 'fileadmin/bundle.phar';
+
+        return [
+            'filesize base file' => [
+                'filesize',
+                $deniedPath,
+                0, // Phar base file always has zero size when accessed through phar://
+            ],
+            'filesize Resources/content.txt' => [
+                'filesize',
+                $deniedPath . '/Resources/content.txt',
+                21,
+            ],
+            'is_file base file' => [
+                'is_file',
+                $deniedPath,
+                false, // Phar base file is not a file when accessed through phar://
+            ],
+            'is_file Resources/content.txt' => [
+                'is_file',
+                $deniedPath . '/Resources/content.txt',
+                true,
+            ],
+            'is_dir base file' => [
+                'is_dir',
+                $deniedPath,
+                true, // Phar base file is a directory when accessed through phar://
+            ],
+            'is_dir Resources/content.txt' => [
+                'is_dir',
+                $deniedPath . '/Resources/content.txt',
+                false,
+            ],
+            'file_exists base file' => [
+                'file_exists',
+                $deniedPath,
+                true
+            ],
+            'file_exists Resources/content.txt' => [
+                'file_exists',
+                $deniedPath . '/Resources/content.txt',
+                true
+            ],
+        ];
+    }
+
+    /**
+     * @param string $functionName
+     * @param string $path
+     * @param mixed $expectation
+     *
+     * @test
+     * @dataProvider urlStatDeniesInvocationDataProvider
+     */
+    public function urlStatDeniesInvocation(string $functionName, string $path)
+    {
+        self::expectException(Exception::class);
+        self::expectExceptionCode(1530103998);
+
+        $path = $this->instancePath . '/' . $path;
+        call_user_func($functionName, 'phar://' . $path);
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileOpen()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+        self::assertInternalType('resource', $handle);
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileRead()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+        $content = fread($handle, 1024);
+        self::assertSame('TYPO3 demo text file.', $content);
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileEnd()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+        fread($handle, 1024);
+        self::assertTrue(feof($handle));
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileClose()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+        fclose($handle);
+        self::assertFalse(is_resource($handle));
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileGetContents()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $content = file_get_contents('phar://' . $allowedPath . '/Resources/content.txt');
+        self::assertSame('TYPO3 demo text file.', $content);
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForInclude()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        include('phar://' . $allowedPath . '/Classes/Domain/Model/DemoModel.php');
+
+        self::assertTrue(
+            class_exists(
+                \TYPO3Demo\Demo\Domain\Model\DemoModel::class,
+                false
+            )
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenDeniesInvocationForFileOpen()
+    {
+        self::expectException(Exception::class);
+        self::expectExceptionCode(1530103998);
+
+        $allowedPath = $this->instancePath . '/fileadmin/bundle.phar';
+        fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenDeniesInvocationForFileGetContents()
+    {
+        self::expectException(Exception::class);
+        self::expectExceptionCode(1530103998);
+
+        $allowedPath = $this->instancePath . '/fileadmin/bundle.phar';
+        file_get_contents('phar://' . $allowedPath . '/Resources/content.txt');
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenDeniesInvocationForInclude()
+    {
+        self::expectException(Exception::class);
+        self::expectExceptionCode(1530103998);
+
+        $allowedPath = $this->instancePath . '/fileadmin/bundle.phar';
+        include('phar://' . $allowedPath . '/Classes/Domain/Model/DemoModel.php');
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/IO/PharStreamWrapperTest.php b/typo3/sysext/core/Tests/Functional/IO/PharStreamWrapperTest.php
deleted file mode 100644 (file)
index c886695..0000000
+++ /dev/null
@@ -1,402 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Core\Tests\Functional\IO;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Core\IO\PharStreamWrapper;
-use TYPO3\CMS\Core\IO\PharStreamWrapperException;
-use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
-
-class PharStreamWrapperTest extends FunctionalTestCase
-{
-    /**
-     * @var array
-     */
-    protected $testExtensionsToLoad = [
-        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources'
-    ];
-
-    /**
-     * @var array
-     */
-    protected $pathsToLinkInTestInstance = [
-        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources/bundle.phar' => 'fileadmin/bundle.phar'
-    ];
-
-    protected function setUp()
-    {
-        parent::setUp();
-
-        if (!in_array('phar', stream_get_wrappers())) {
-            $this->markTestSkipped('Phar stream wrapper is not registered');
-        }
-
-        stream_wrapper_unregister('phar');
-        stream_wrapper_register('phar', PharStreamWrapper::class);
-    }
-
-    protected function tearDown()
-    {
-        stream_wrapper_restore('phar');
-        parent::tearDown();
-    }
-
-    public function directoryActionAllowsInvocationDataProvider()
-    {
-        $allowedPath = 'typo3conf/ext/test_resources/bundle.phar';
-
-        return [
-            'root directory' => [
-                $allowedPath,
-                ['Classes', 'Resources']
-            ],
-            'Classes/Domain/Model directory' => [
-                $allowedPath . '/Classes/Domain/Model',
-                ['DemoModel.php']
-            ],
-            'Resources directory' => [
-                $allowedPath . '/Resources',
-                ['content.txt']
-            ],
-        ];
-    }
-
-    /**
-     * @param string $path
-     *
-     * @test
-     * @dataProvider directoryActionAllowsInvocationDataProvider
-     */
-    public function directoryOpenAllowsInvocation(string $path)
-    {
-        $path = $this->instancePath . '/' . $path;
-        $handle = opendir('phar://' . $path);
-        self::assertInternalType('resource', $handle);
-    }
-
-    /**
-     * @param string $path
-     * @param $expectation
-     *
-     * @test
-     * @dataProvider directoryActionAllowsInvocationDataProvider
-     */
-    public function directoryReadAllowsInvocation(string $path, array $expectation)
-    {
-        $path = $this->instancePath . '/' . $path;
-
-        $items = [];
-        $handle = opendir('phar://' . $path);
-        while (false !== $item = readdir($handle)) {
-            $items[] = $item;
-        }
-
-        self::assertSame($expectation, $items);
-    }
-
-    /**
-     * @param string $path
-     * @param $expectation
-     *
-     * @test
-     * @dataProvider directoryActionAllowsInvocationDataProvider
-     */
-    public function directoryCloseAllowsInvocation(string $path, array $expectation)
-    {
-        $path = $this->instancePath . '/' . $path;
-
-        $handle = opendir('phar://' . $path);
-        closedir($handle);
-
-        self::assertFalse(is_resource($handle));
-    }
-
-    public function directoryActionDeniesInvocationDataProvider()
-    {
-        $deniedPath = 'fileadmin/bundle.phar';
-
-        return [
-            'root directory' => [
-                $deniedPath,
-                ['Classes', 'Resources']
-            ],
-            'Classes/Domain/Model directory' => [
-                $deniedPath . '/Classes/Domain/Model',
-                ['DemoModel.php']
-            ],
-            'Resources directory' => [
-                $deniedPath . '/Resources',
-                ['content.txt']
-            ],
-        ];
-    }
-
-    /**
-     * @param string $path
-     *
-     * @test
-     * @dataProvider directoryActionDeniesInvocationDataProvider
-     */
-    public function directoryActionDeniesInvocation(string $path)
-    {
-        self::expectException(PharStreamWrapperException::class);
-        self::expectExceptionCode(1530103998);
-
-        $path = $this->instancePath . '/' . $path;
-        opendir('phar://' . $path);
-    }
-
-    /**
-     * @return array
-     */
-    public function urlStatAllowsInvocationDataProvider(): array
-    {
-        $allowedPath = 'typo3conf/ext/test_resources/bundle.phar';
-
-        return [
-            'filesize base file' => [
-                'filesize',
-                $allowedPath,
-                0, // Phar base file always has zero size when accessed through phar://
-            ],
-            'filesize Resources/content.txt' => [
-                'filesize',
-                $allowedPath . '/Resources/content.txt',
-                21,
-            ],
-            'is_file base file' => [
-                'is_file',
-                $allowedPath,
-                false, // Phar base file is not a file when accessed through phar://
-            ],
-            'is_file Resources/content.txt' => [
-                'is_file',
-                $allowedPath . '/Resources/content.txt',
-                true,
-            ],
-            'is_dir base file' => [
-                'is_dir',
-                $allowedPath,
-                true, // Phar base file is a directory when accessed through phar://
-            ],
-            'is_dir Resources/content.txt' => [
-                'is_dir',
-                $allowedPath . '/Resources/content.txt',
-                false,
-            ],
-            'file_exists base file' => [
-                'file_exists',
-                $allowedPath,
-                true
-            ],
-            'file_exists Resources/content.txt' => [
-                'file_exists',
-                $allowedPath . '/Resources/content.txt',
-                true
-            ],
-        ];
-    }
-
-    /**
-     * @param string $functionName
-     * @param string $path
-     * @param mixed $expectation
-     *
-     * @test
-     * @dataProvider urlStatAllowsInvocationDataProvider
-     */
-    public function urlStatAllowsInvocation(string $functionName, string $path, $expectation)
-    {
-        $path = $this->instancePath . '/' . $path;
-
-        self::assertSame(
-            $expectation,
-            call_user_func($functionName, 'phar://' . $path)
-        );
-    }
-
-    /**
-     * @return array
-     */
-    public function urlStatDeniesInvocationDataProvider(): array
-    {
-        $deniedPath = 'fileadmin/bundle.phar';
-
-        return [
-            'filesize base file' => [
-                'filesize',
-                $deniedPath,
-                0, // Phar base file always has zero size when accessed through phar://
-            ],
-            'filesize Resources/content.txt' => [
-                'filesize',
-                $deniedPath . '/Resources/content.txt',
-                21,
-            ],
-            'is_file base file' => [
-                'is_file',
-                $deniedPath,
-                false, // Phar base file is not a file when accessed through phar://
-            ],
-            'is_file Resources/content.txt' => [
-                'is_file',
-                $deniedPath . '/Resources/content.txt',
-                true,
-            ],
-            'is_dir base file' => [
-                'is_dir',
-                $deniedPath,
-                true, // Phar base file is a directory when accessed through phar://
-            ],
-            'is_dir Resources/content.txt' => [
-                'is_dir',
-                $deniedPath . '/Resources/content.txt',
-                false,
-            ],
-            'file_exists base file' => [
-                'file_exists',
-                $deniedPath,
-                true
-            ],
-            'file_exists Resources/content.txt' => [
-                'file_exists',
-                $deniedPath . '/Resources/content.txt',
-                true
-            ],
-        ];
-    }
-
-    /**
-     * @param string $functionName
-     * @param string $path
-     * @param mixed $expectation
-     *
-     * @test
-     * @dataProvider urlStatDeniesInvocationDataProvider
-     */
-    public function urlStatDeniesInvocation(string $functionName, string $path)
-    {
-        self::expectException(PharStreamWrapperException::class);
-        self::expectExceptionCode(1530103998);
-
-        $path = $this->instancePath . '/' . $path;
-        call_user_func($functionName, 'phar://' . $path);
-    }
-
-    /**
-     * @test
-     */
-    public function streamOpenAllowsInvocationForFileOpen()
-    {
-        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
-        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
-        self::assertInternalType('resource', $handle);
-    }
-
-    /**
-     * @test
-     */
-    public function streamOpenAllowsInvocationForFileRead()
-    {
-        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
-        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
-        $content = fread($handle, 1024);
-        self::assertSame('TYPO3 demo text file.', $content);
-    }
-
-    /**
-     * @test
-     */
-    public function streamOpenAllowsInvocationForFileEnd()
-    {
-        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
-        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
-        fread($handle, 1024);
-        self::assertTrue(feof($handle));
-    }
-
-    /**
-     * @test
-     */
-    public function streamOpenAllowsInvocationForFileClose()
-    {
-        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
-        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
-        fclose($handle);
-        self::assertFalse(is_resource($handle));
-    }
-
-    /**
-     * @test
-     */
-    public function streamOpenAllowsInvocationForFileGetContents()
-    {
-        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
-        $content = file_get_contents('phar://' . $allowedPath . '/Resources/content.txt');
-        self::assertSame('TYPO3 demo text file.', $content);
-    }
-
-    /**
-     * @test
-     */
-    public function streamOpenAllowsInvocationForInclude()
-    {
-        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
-        include('phar://' . $allowedPath . '/Classes/Domain/Model/DemoModel.php');
-
-        self::assertTrue(
-            class_exists(
-                \TYPO3Demo\Demo\Domain\Model\DemoModel::class,
-                false
-            )
-        );
-    }
-
-    /**
-     * @test
-     */
-    public function streamOpenDeniesInvocationForFileOpen()
-    {
-        self::expectException(PharStreamWrapperException::class);
-        self::expectExceptionCode(1530103998);
-
-        $allowedPath = $this->instancePath . '/fileadmin/bundle.phar';
-        fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
-    }
-
-    /**
-     * @test
-     */
-    public function streamOpenDeniesInvocationForFileGetContents()
-    {
-        self::expectException(PharStreamWrapperException::class);
-        self::expectExceptionCode(1530103998);
-
-        $allowedPath = $this->instancePath . '/fileadmin/bundle.phar';
-        file_get_contents('phar://' . $allowedPath . '/Resources/content.txt');
-    }
-
-    /**
-     * @test
-     */
-    public function streamOpenDeniesInvocationForInclude()
-    {
-        self::expectException(PharStreamWrapperException::class);
-        self::expectExceptionCode(1530103998);
-
-        $allowedPath = $this->instancePath . '/fileadmin/bundle.phar';
-        include('phar://' . $allowedPath . '/Classes/Domain/Model/DemoModel.php');
-    }
-}
index bc0be6d..1d917f6 100644 (file)
@@ -34,7 +34,8 @@
                "symfony/yaml": "^2.7 || ^3.0 || ^4.0",
                "typo3/class-alias-loader": "^1.0",
                "typo3/cms-cli": "^1.0.2",
-               "typo3/cms-composer-installers": "^1.2.8"
+               "typo3/cms-composer-installers": "^1.2.8",
+               "typo3/phar-stream-wrapper": "^3.0.1"
        },
        "suggest": {
                "ext-fileinfo": "Used for proper file type detection in the file abstraction layer",