diff --git a/typo3/sysext/extensionmanager/Classes/Controller/ActionController.php b/typo3/sysext/extensionmanager/Classes/Controller/ActionController.php index 62abb871e9514d9ed219b2fe781487cfef0fd441..00b1c6a13f5d599459b4e0e72412656b3653c85e 100644 --- a/typo3/sysext/extensionmanager/Classes/Controller/ActionController.php +++ b/typo3/sysext/extensionmanager/Classes/Controller/ActionController.php @@ -27,7 +27,6 @@ use TYPO3\CMS\Extbase\Utility\LocalizationUtility; use TYPO3\CMS\Extensionmanager\Domain\Model\Extension; use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException; use TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService; -use TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility; use TYPO3\CMS\Extensionmanager\Utility\InstallUtility; /** @@ -42,11 +41,6 @@ class ActionController extends AbstractController */ protected $installUtility; - /** - * @var FileHandlingUtility - */ - protected $fileHandlingUtility; - /** * @var ExtensionManagementService */ @@ -60,14 +54,6 @@ class ActionController extends AbstractController $this->installUtility = $installUtility; } - /** - * @param FileHandlingUtility $fileHandlingUtility - */ - public function injectFileHandlingUtility(FileHandlingUtility $fileHandlingUtility) - { - $this->fileHandlingUtility = $fileHandlingUtility; - } - /** * @param ExtensionManagementService $managementService */ @@ -157,7 +143,7 @@ class ActionController extends AbstractController */ protected function downloadExtensionZipAction($extension) { - $fileName = $this->fileHandlingUtility->createZipFileFromExtension($extension); + $fileName = $this->createZipFileFromExtension($extension); $this->sendZipFileToBrowserAndDelete($fileName); } @@ -193,4 +179,66 @@ class ActionController extends AbstractController $this->redirect('index', 'List'); } + + /** + * Create a zip file from an extension + * + * @param string $extensionKey + * @return string Name and path of create zip file + */ + protected function createZipFileFromExtension(string $extensionKey): string + { + $extensionDetails = $this->installUtility->enrichExtensionWithDetails($extensionKey); + $extensionPath = $extensionDetails['packagePath']; + + // Add trailing slash to the extension path, getAllFilesAndFoldersInPath explicitly requires that. + $extensionPath = PathUtility::sanitizeTrailingSeparator($extensionPath); + + $version = (string)$extensionDetails['version']; + if (empty($version)) { + $version = '0.0.0'; + } + + $temporaryPath = Environment::getVarPath() . '/transient/'; + if (!@is_dir($temporaryPath)) { + GeneralUtility::mkdir($temporaryPath); + } + $fileName = $temporaryPath . $extensionKey . '_' . $version . '_' . date('YmdHi', $GLOBALS['EXEC_TIME']) . '.zip'; + + $zip = new \ZipArchive(); + $zip->open($fileName, \ZipArchive::CREATE); + + $excludePattern = $GLOBALS['TYPO3_CONF_VARS']['EXT']['excludeForPackaging']; + + // Get all the files of the extension, but exclude the ones specified in the excludePattern + $files = GeneralUtility::getAllFilesAndFoldersInPath( + [], // No files pre-added + $extensionPath, // Start from here + '', // Do not filter files by extension + true, // Include subdirectories + PHP_INT_MAX, // Recursion level + $excludePattern // Files and directories to exclude. + ); + + // Make paths relative to extension root directory. + $files = GeneralUtility::removePrefixPathFromList($files, $extensionPath); + $files = is_array($files) ? $files : []; + + // Remove the one empty path that is the extension dir itself. + $files = array_filter($files); + + foreach ($files as $file) { + $fullPath = $extensionPath . $file; + // Distinguish between files and directories, as creation of the archive + // fails on Windows when trying to add a directory with "addFile". + if (is_dir($fullPath)) { + $zip->addEmptyDir($file); + } else { + $zip->addFile($fullPath, $file); + } + } + + $zip->close(); + return $fileName; + } } diff --git a/typo3/sysext/extensionmanager/Classes/Utility/FileHandlingUtility.php b/typo3/sysext/extensionmanager/Classes/Utility/FileHandlingUtility.php index 4289c93c90775bb19ad4f266d15ae9a3cdbd0945..728ee274c18bc3f34e7c1debceaf4acaa858c060 100644 --- a/typo3/sysext/extensionmanager/Classes/Utility/FileHandlingUtility.php +++ b/typo3/sysext/extensionmanager/Classes/Utility/FileHandlingUtility.php @@ -272,8 +272,7 @@ class FileHandlingUtility implements SingletonInterface, LoggerAwareInterface $emConfFileData = $this->emConfUtility->includeEmConf( $extensionData['extKey'], [ - 'packagePath' => $rootPath, - 'siteRelPath' => PathUtility::stripPathSitePrefix($rootPath) + 'packagePath' => $rootPath ] ); $emConfFileData = is_array($emConfFileData) ? $emConfFileData : []; @@ -300,22 +299,6 @@ class FileHandlingUtility implements SingletonInterface, LoggerAwareInterface return false; } - /** - * Returns absolute path - * - * @param string $relativePath - * @throws ExtensionManagerException - * @return string - */ - protected function getAbsolutePath($relativePath) - { - $absolutePath = GeneralUtility::getFileAbsFileName(GeneralUtility::resolveBackPath(Environment::getPublicPath() . '/' . $relativePath)); - if (empty($absolutePath)) { - throw new ExtensionManagerException('Illegal relative path given', 1350742864); - } - return $absolutePath; - } - /** * Returns relative path * @@ -327,91 +310,6 @@ class FileHandlingUtility implements SingletonInterface, LoggerAwareInterface return PathUtility::stripPathSitePrefix($absolutePath); } - /** - * Get extension path for an available or installed extension - * - * @param string $extensionKey - * @return string - */ - public function getAbsoluteExtensionPath(string $extensionKey): string - { - $extension = $this->installUtility->enrichExtensionWithDetails($extensionKey); - return $this->getAbsolutePath($extension['siteRelPath']); - } - - /** - * Get version of an available or installed extension - * - * @param string $extensionKey - * @return string - */ - protected function getExtensionVersion(string $extensionKey): string - { - $extensionData = $this->installUtility->enrichExtensionWithDetails($extensionKey); - return (string)$extensionData['version']; - } - - /** - * Create a zip file from an extension - * - * @param string $extensionKey - * @return string Name and path of create zip file - */ - public function createZipFileFromExtension($extensionKey): string - { - $extensionPath = $this->getAbsoluteExtensionPath($extensionKey); - - // Add trailing slash to the extension path, getAllFilesAndFoldersInPath explicitly requires that. - $extensionPath = PathUtility::sanitizeTrailingSeparator($extensionPath); - - $version = $this->getExtensionVersion($extensionKey); - if (empty($version)) { - $version = '0.0.0'; - } - - $temporaryPath = Environment::getVarPath() . '/transient/'; - if (!@is_dir($temporaryPath)) { - GeneralUtility::mkdir($temporaryPath); - } - $fileName = $temporaryPath . $extensionKey . '_' . $version . '_' . date('YmdHi', $GLOBALS['EXEC_TIME']) . '.zip'; - - $zip = new \ZipArchive(); - $zip->open($fileName, \ZipArchive::CREATE); - - $excludePattern = $GLOBALS['TYPO3_CONF_VARS']['EXT']['excludeForPackaging']; - - // Get all the files of the extension, but exclude the ones specified in the excludePattern - $files = GeneralUtility::getAllFilesAndFoldersInPath( - [], // No files pre-added - $extensionPath, // Start from here - '', // Do not filter files by extension - true, // Include subdirectories - PHP_INT_MAX, // Recursion level - $excludePattern // Files and directories to exclude. - ); - - // Make paths relative to extension root directory. - $files = GeneralUtility::removePrefixPathFromList($files, $extensionPath); - $files = is_array($files) ? $files : []; - - // Remove the one empty path that is the extension dir itself. - $files = array_filter($files); - - foreach ($files as $file) { - $fullPath = $extensionPath . $file; - // Distinguish between files and directories, as creation of the archive - // fails on Windows when trying to add a directory with "addFile". - if (is_dir($fullPath)) { - $zip->addEmptyDir($file); - } else { - $zip->addFile($fullPath, $file); - } - } - - $zip->close(); - return $fileName; - } - /** * Unzip an extension.zip. * diff --git a/typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php b/typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php index f481eff9693f78ee38a55be5783ec1fa437dd022..03fb803aea9b49b46c06fff3c1e09313fb06f285 100644 --- a/typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php +++ b/typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php @@ -420,7 +420,7 @@ class InstallUtility implements SingletonInterface, LoggerAwareInterface */ public function removeExtension($extension) { - $absolutePath = $this->fileHandlingUtility->getAbsoluteExtensionPath($extension); + $absolutePath = $this->enrichExtensionWithDetails($extension)['packagePath']; if ($this->fileHandlingUtility->isValidExtensionPath($absolutePath)) { if ($this->packageManager->isPackageAvailable($extension)) { // Package manager deletes the extension and removes the entry from PackageStates.php diff --git a/typo3/sysext/extensionmanager/Classes/Utility/ListUtility.php b/typo3/sysext/extensionmanager/Classes/Utility/ListUtility.php index 5f8561aefacbcbac1c31611237c0b1ec0ec7bc84..820725c4b785ef4458e136878935bfe6c171d5a4 100644 --- a/typo3/sysext/extensionmanager/Classes/Utility/ListUtility.php +++ b/typo3/sysext/extensionmanager/Classes/Utility/ListUtility.php @@ -122,6 +122,7 @@ class ListUtility implements SingletonInterface if ($filter === '' || $filter === $installationType) { $this->availableExtensions[$package->getPackageKey()] = [ 'siteRelPath' => str_replace(Environment::getPublicPath() . '/', '', $package->getPackagePath()), + 'packagePath' => $package->getPackagePath(), 'type' => $installationType, 'key' => $package->getPackageKey(), 'icon' => PathUtility::getAbsoluteWebPath($package->getPackagePath() . ExtensionManagementUtility::getExtensionIcon($package->getPackagePath())), diff --git a/typo3/sysext/extensionmanager/Tests/Unit/Controller/ActionControllerTest.php b/typo3/sysext/extensionmanager/Tests/Unit/Controller/ActionControllerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f3e97535208d55162dbb0546c8a8525ed8d331af --- /dev/null +++ b/typo3/sysext/extensionmanager/Tests/Unit/Controller/ActionControllerTest.php @@ -0,0 +1,108 @@ +<?php + +declare(strict_types=1); + +/* + * 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! + */ + +namespace TYPO3\CMS\Extensionmanager\Tests\Unit\Controller; + +use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\StringUtility; +use TYPO3\CMS\Extensionmanager\Controller\ActionController; +use TYPO3\CMS\Extensionmanager\Domain\Model\Extension; +use TYPO3\CMS\Extensionmanager\Utility\InstallUtility; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +class ActionControllerTest extends UnitTestCase +{ + /** + * @var array List of created fake extensions to be deleted in tearDown() again + */ + protected $fakedExtensions = []; + + /** + * Creates a fake extension inside typo3temp/. No configuration is created, + * just the folder + * + * @return array + */ + protected function createFakeExtension() + { + $extKey = strtolower(StringUtility::getUniqueId('testing')); + $absExtPath = Environment::getVarPath() . '/tests/ext-' . $extKey . '/'; + GeneralUtility::mkdir($absExtPath); + $this->testFilesToDelete[] = Environment::getVarPath() . '/tests/ext-' . $extKey; + return [ + 'extensionKey' => $extKey, + 'version' => '0.0.0', + 'packagePath' => $absExtPath + ]; + } + + /** + * Warning: This test asserts multiple things at once to keep the setup short. + * + * @test + */ + public function createZipFileFromExtensionGeneratesCorrectArchive() + { + // 42 second of first day in 1970 - used to have achieve stable file names + $GLOBALS['EXEC_TIME'] = 42; + + // Create extension for testing: + $fakeExtension = $this->createFakeExtension(); + $extKey = $fakeExtension['extensionKey']; + $extensionRoot = $fakeExtension['packagePath']; + $installUtility = $this->prophesize(InstallUtility::class); + $installUtility->enrichExtensionWithDetails($extKey)->willReturn($fakeExtension); + // Build mocked fileHandlingUtility: + $subject = $this->getAccessibleMock( + ActionController::class, + ['dummy'] + ); + $subject->injectInstallUtility($installUtility->reveal()); + + // Add files and directories to extension: + touch($extensionRoot . 'emptyFile.txt'); + file_put_contents($extensionRoot . 'notEmptyFile.txt', 'content'); + touch($extensionRoot . '.hiddenFile'); + mkdir($extensionRoot . 'emptyDir'); + mkdir($extensionRoot . 'notEmptyDir'); + touch($extensionRoot . 'notEmptyDir/file.txt'); + + // Create zip-file from extension + $filename = $subject->_call('createZipFileFromExtension', $extKey); + + $expectedFilename = Environment::getVarPath() . '/transient/' . $extKey . '_0.0.0_' . date('YmdHi', 42) . '.zip'; + $this->testFilesToDelete[] = $filename; + self::assertEquals($expectedFilename, $filename, 'Archive file name differs from expectation'); + + // File was created + self::assertTrue(file_exists($filename), 'Zip file not created'); + + // Read archive and check its contents + $archive = new \ZipArchive(); + self::assertTrue($archive->open($filename), 'Unable to open archive'); + self::assertEquals($archive->statName('emptyFile.txt')['size'], 0, 'Empty file not in archive'); + self::assertEquals($archive->getFromName('notEmptyFile.txt'), 'content', 'Expected content not found'); + self::assertFalse($archive->statName('.hiddenFile'), 'Hidden file not in archive'); + self::assertTrue(is_array($archive->statName('emptyDir/')), 'Empty directory not in archive'); + self::assertTrue(is_array($archive->statName('notEmptyDir/')), 'Not empty directory not in archive'); + self::assertTrue(is_array($archive->statName('notEmptyDir/file.txt')), 'File within directory not in archive'); + + // Check that the archive has no additional content + self::assertEquals($archive->numFiles, 5, 'Too many or too less files in archive'); + } +} diff --git a/typo3/sysext/extensionmanager/Tests/Unit/Utility/FileHandlingUtilityTest.php b/typo3/sysext/extensionmanager/Tests/Unit/Utility/FileHandlingUtilityTest.php index fd9af2fb7e77e278c760d2497dc03334852ee757..5b149c240bf31dc7041490e78cfd914c5dd6f3a5 100644 --- a/typo3/sysext/extensionmanager/Tests/Unit/Utility/FileHandlingUtilityTest.php +++ b/typo3/sysext/extensionmanager/Tests/Unit/Utility/FileHandlingUtilityTest.php @@ -76,55 +76,6 @@ class FileHandlingUtilityTest extends UnitTestCase $fileHandlerMock->_call('makeAndClearExtensionDir', $extKey); } - /** - * @return array - */ - public function invalidRelativePathDataProvider() - { - return [ - ['../../'], - ['/foo/bar'], - ['foo//bar'], - ['foo/bar' . "\0"], - ]; - } - - /** - * @param string $invalidRelativePath - * @test - * @dataProvider invalidRelativePathDataProvider - */ - public function getAbsolutePathThrowsExceptionForInvalidRelativePaths($invalidRelativePath) - { - $this->expectException(ExtensionManagerException::class); - $this->expectExceptionCode(1350742864); - $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['dummy'], []); - $fileHandlerMock->_call('getAbsolutePath', $invalidRelativePath); - } - - /** - * @return array - */ - public function validRelativePathDataProvider() - { - return [ - ['foo/../bar', Environment::getPublicPath() . '/bar'], - ['bas', Environment::getPublicPath() . '/bas'], - ]; - } - - /** - * @param string $validRelativePath - * @param string $expectedAbsolutePath - * @test - * @dataProvider validRelativePathDataProvider - */ - public function getAbsolutePathReturnsAbsolutePathForValidRelativePaths($validRelativePath, $expectedAbsolutePath) - { - $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['dummy']); - self::assertSame($expectedAbsolutePath, $fileHandlerMock->_call('getAbsolutePath', $validRelativePath)); - } - /** * @test */ @@ -443,77 +394,4 @@ class FileHandlingUtilityTest extends UnitTestCase $fileHandlerMock->_call('writeEmConfToFile', $extensionData, $rootPath); self::assertTrue(file_exists($rootPath . 'ext_emconf.php')); } - - /** - * @return \PHPUnit\Framework\MockObject\MockObject|FileHandlingUtility - */ - protected function getPreparedFileHandlingMockForDirectoryCreationTests() - { - /** @var $fileHandlerMock FileHandlingUtility|\PHPUnit\Framework\MockObject\MockObject */ - $fileHandlerMock = $this->getMockBuilder(FileHandlingUtility::class) - ->setMethods(['createNestedDirectory', 'getAbsolutePath', 'directoryExists']) - ->getMock(); - $fileHandlerMock->expects(self::any()) - ->method('getAbsolutePath') - ->willReturnArgument(0); - return $fileHandlerMock; - } - - /** - * Warning: This test asserts multiple things at once to keep the setup short. - * - * @test - */ - public function createZipFileFromExtensionGeneratesCorrectArchive() - { - // 42 second of first day in 1970 - used to have achieve stable file names - $GLOBALS['EXEC_TIME'] = 42; - - // Create extension for testing: - $extKey = $this->createFakeExtension(); - $extensionRoot = $this->fakedExtensions[$extKey]['siteAbsPath']; - - // Build mocked fileHandlingUtility: - $fileHandlerMock = $this->getAccessibleMock( - FileHandlingUtility::class, - ['getAbsoluteExtensionPath', 'getExtensionVersion'] - ); - $fileHandlerMock->expects(self::any()) - ->method('getAbsoluteExtensionPath') - ->willReturn($extensionRoot); - $fileHandlerMock->expects(self::any()) - ->method('getExtensionVersion') - ->willReturn('0.0.0'); - - // Add files and directories to extension: - touch($extensionRoot . 'emptyFile.txt'); - file_put_contents($extensionRoot . 'notEmptyFile.txt', 'content'); - touch($extensionRoot . '.hiddenFile'); - mkdir($extensionRoot . 'emptyDir'); - mkdir($extensionRoot . 'notEmptyDir'); - touch($extensionRoot . 'notEmptyDir/file.txt'); - - // Create zip-file from extension - $filename = $fileHandlerMock->_call('createZipFileFromExtension', $extKey); - - $expectedFilename = Environment::getVarPath() . '/transient/' . $extKey . '_0.0.0_' . date('YmdHi', 42) . '.zip'; - $this->testFilesToDelete[] = $filename; - self::assertEquals($expectedFilename, $filename, 'Archive file name differs from expectation'); - - // File was created - self::assertTrue(file_exists($filename), 'Zip file not created'); - - // Read archive and check its contents - $archive = new \ZipArchive(); - self::assertTrue($archive->open($filename), 'Unable to open archive'); - self::assertEquals($archive->statName('emptyFile.txt')['size'], 0, 'Empty file not in archive'); - self::assertEquals($archive->getFromName('notEmptyFile.txt'), 'content', 'Expected content not found'); - self::assertFalse($archive->statName('.hiddenFile'), 'Hidden file not in archive'); - self::assertTrue(is_array($archive->statName('emptyDir/')), 'Empty directory not in archive'); - self::assertTrue(is_array($archive->statName('notEmptyDir/')), 'Not empty directory not in archive'); - self::assertTrue(is_array($archive->statName('notEmptyDir/file.txt')), 'File within directory not in archive'); - - // Check that the archive has no additional content - self::assertEquals($archive->numFiles, 5, 'Too many or too less files in archive'); - } }