e45c2f2aff029377045b1dc96fe150451917c47f
[Packages/TYPO3.CMS.git] / typo3 / sysext / extensionmanager / Tests / Unit / Utility / FileHandlingUtilityTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Extensionmanager\Tests\Unit\Utility;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17 use TYPO3\CMS\Core\Core\Environment;
18 use TYPO3\CMS\Core\Localization\LanguageService;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
21 use TYPO3\CMS\Extensionmanager\Utility\EmConfUtility;
22 use TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility;
23 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
24
25 /**
26 * Testcase
27 */
28 class FileHandlingUtilityTest extends UnitTestCase
29 {
30 /**
31 * @var array List of created fake extensions to be deleted in tearDown() again
32 */
33 protected $fakedExtensions = [];
34
35 /**
36 * Creates a fake extension inside typo3temp/. No configuration is created,
37 * just the folder
38 *
39 * @param bool $extkeyOnly
40 * @return string The extension key
41 */
42 protected function createFakeExtension($extkeyOnly = false)
43 {
44 $extKey = strtolower($this->getUniqueId('testing'));
45 $absExtPath = PATH_site . 'typo3temp/var/tests/ext-' . $extKey . '/';
46 $relPath = 'typo3temp/var/tests/ext-' . $extKey . '/';
47 $this->fakedExtensions[$extKey] = [
48 'siteRelPath' => $relPath,
49 'siteAbsPath' => $absExtPath
50 ];
51 if ($extkeyOnly === true) {
52 return $extKey;
53 }
54 GeneralUtility::mkdir($absExtPath);
55 $this->testFilesToDelete[] = PATH_site . 'typo3temp/var/tests/ext-' . $extKey;
56 return $extKey;
57 }
58
59 /**
60 * @test
61 */
62 public function makeAndClearExtensionDirRemovesExtensionDirIfAlreadyExists()
63 {
64 $extKey = $this->createFakeExtension();
65 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['removeDirectory', 'addDirectory', 'getExtensionDir'], [], '', false);
66 $fileHandlerMock->expects($this->once())
67 ->method('removeDirectory')
68 ->with(PATH_site . 'typo3temp/var/tests/ext-' . $extKey . '/');
69 $fileHandlerMock->expects($this->any())
70 ->method('getExtensionDir')
71 ->willReturn(PATH_site . 'typo3temp/var/tests/ext-' . $extKey . '/');
72 $fileHandlerMock->_call('makeAndClearExtensionDir', $extKey);
73 }
74
75 /**
76 * @return array
77 */
78 public function invalidRelativePathDataProvider()
79 {
80 return [
81 ['../../'],
82 ['/foo/bar'],
83 ['foo//bar'],
84 ['foo/bar' . chr(0)],
85 ];
86 }
87
88 /**
89 * @param string $invalidRelativePath
90 * @test
91 * @dataProvider invalidRelativePathDataProvider
92 */
93 public function getAbsolutePathThrowsExceptionForInvalidRelativePaths($invalidRelativePath)
94 {
95 $this->expectException(ExtensionManagerException::class);
96 $this->expectExceptionCode(1350742864);
97 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['dummy'], []);
98 $fileHandlerMock->_call('getAbsolutePath', $invalidRelativePath);
99 }
100
101 /**
102 * @return array
103 */
104 public function validRelativePathDataProvider()
105 {
106 return [
107 ['foo/../bar', PATH_site . 'bar'],
108 ['bas', PATH_site . 'bas'],
109 ];
110 }
111
112 /**
113 * @param string $validRelativePath
114 * @param string $expectedAbsolutePath
115 * @test
116 * @dataProvider validRelativePathDataProvider
117 */
118 public function getAbsolutePathReturnsAbsolutePathForValidRelativePaths($validRelativePath, $expectedAbsolutePath)
119 {
120 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['dummy']);
121 $this->assertSame($expectedAbsolutePath, $fileHandlerMock->_call('getAbsolutePath', $validRelativePath));
122 }
123
124 /**
125 * @test
126 */
127 public function makeAndClearExtensionDirAddsDir()
128 {
129 $extKey = $this->createFakeExtension();
130 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['removeDirectory', 'addDirectory', 'getExtensionDir']);
131 $fileHandlerMock->expects($this->once())
132 ->method('addDirectory')
133 ->with(PATH_site . 'typo3temp/var/tests/ext-' . $extKey . '/');
134 $fileHandlerMock->expects($this->any())
135 ->method('getExtensionDir')
136 ->willReturn(PATH_site . 'typo3temp/var/tests/ext-' . $extKey . '/');
137 $fileHandlerMock->_call('makeAndClearExtensionDir', $extKey);
138 }
139
140 /**
141 * @test
142 */
143 public function makeAndClearExtensionDirThrowsExceptionOnInvalidPath()
144 {
145 $this->expectException(ExtensionManagerException::class);
146 $this->expectExceptionCode(1337280417);
147 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['removeDirectory', 'addDirectory']);
148 $languageServiceMock = $this->getMockBuilder(LanguageService::class)->getMock();
149 $fileHandlerMock->_set('languageService', $languageServiceMock);
150 $fileHandlerMock->_call('makeAndClearExtensionDir', 'testing123', 'fakepath');
151 }
152
153 /**
154 * @test
155 */
156 public function addDirectoryAddsDirectory()
157 {
158 $extDirPath = PATH_site . '/typo3temp/var/tests/' . $this->getUniqueId('test-extensions-');
159 $this->testFilesToDelete[] = $extDirPath;
160 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['dummy']);
161 $fileHandlerMock->_call('addDirectory', $extDirPath);
162 $this->assertTrue(is_dir($extDirPath));
163 }
164
165 /**
166 * @test
167 */
168 public function removeDirectoryRemovesDirectory()
169 {
170 $extDirPath = PATH_site . '/typo3temp/var/tests/' . $this->getUniqueId('test-extensions-');
171 @mkdir($extDirPath);
172 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['dummy']);
173 $fileHandlerMock->_call('removeDirectory', $extDirPath);
174 $this->assertFalse(is_dir($extDirPath));
175 }
176
177 /**
178 * @test
179 */
180 public function removeDirectoryRemovesSymlink()
181 {
182 $absoluteSymlinkPath = PATH_site . 'typo3temp/var/tests/' . $this->getUniqueId('test_symlink_');
183 $absoluteFilePath = PATH_site . 'typo3temp/var/tests/' . $this->getUniqueId('test_file_');
184 touch($absoluteFilePath);
185 $this->testFilesToDelete[] = $absoluteFilePath;
186 symlink($absoluteFilePath, $absoluteSymlinkPath);
187 $fileHandler = new FileHandlingUtility();
188 $fileHandler->removeDirectory($absoluteSymlinkPath);
189 $this->assertFalse(is_link($absoluteSymlinkPath));
190 }
191
192 /**
193 * @test
194 */
195 public function removeDirectoryDoesNotRemoveContentOfSymlinkedTargetDirectory()
196 {
197 $absoluteSymlinkPath = PATH_site . 'typo3temp/var/tests/' . $this->getUniqueId('test_symlink_');
198 $absoluteDirectoryPath = PATH_site . 'typo3temp/var/tests/' . $this->getUniqueId('test_dir_') . '/';
199 $relativeFilePath = $this->getUniqueId('test_file_');
200
201 mkdir($absoluteDirectoryPath);
202 touch($absoluteDirectoryPath . $relativeFilePath);
203
204 $this->testFilesToDelete[] = $absoluteDirectoryPath . $relativeFilePath;
205 $this->testFilesToDelete[] = $absoluteDirectoryPath;
206
207 symlink($absoluteDirectoryPath, $absoluteSymlinkPath);
208
209 $fileHandler = new FileHandlingUtility();
210 $fileHandler->removeDirectory($absoluteSymlinkPath);
211 $this->assertTrue(is_file($absoluteDirectoryPath . $relativeFilePath));
212 }
213
214 /**
215 * @test
216 */
217 public function unpackExtensionFromExtensionDataArrayCreatesTheExtensionDirectory()
218 {
219 $extensionData = [
220 'extKey' => 'test'
221 ];
222 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, [
223 'makeAndClearExtensionDir',
224 'writeEmConfToFile',
225 'extractFilesArrayFromExtensionData',
226 'extractDirectoriesFromExtensionData',
227 'createDirectoriesForExtensionFiles',
228 'writeExtensionFiles',
229 'reloadPackageInformation',
230 ]);
231 $fileHandlerMock->expects($this->once())->method('extractFilesArrayFromExtensionData')->will($this->returnValue([]));
232 $fileHandlerMock->expects($this->once())->method('extractDirectoriesFromExtensionData')->will($this->returnValue([]));
233 $fileHandlerMock->expects($this->once())->method('makeAndClearExtensionDir')->with($extensionData['extKey']);
234 $fileHandlerMock->_call('unpackExtensionFromExtensionDataArray', $extensionData);
235 }
236
237 /**
238 * @test
239 */
240 public function unpackExtensionFromExtensionDataArrayStripsDirectoriesFromFilesArray()
241 {
242 $extensionData = [
243 'extKey' => 'test'
244 ];
245 $files = [
246 'ChangeLog' => [
247 'name' => 'ChangeLog',
248 'size' => 4559,
249 'mtime' => 1219448527,
250 'is_executable' => false,
251 'content' => 'some content to write'
252 ],
253 'doc/' => [
254 'name' => 'doc/',
255 'size' => 0,
256 'mtime' => 1219448527,
257 'is_executable' => false,
258 'content' => ''
259 ],
260 'doc/ChangeLog' => [
261 'name' => 'ChangeLog',
262 'size' => 4559,
263 'mtime' => 1219448527,
264 'is_executable' => false,
265 'content' => 'some content to write'
266 ],
267 ];
268 $cleanedFiles = [
269 'ChangeLog' => [
270 'name' => 'ChangeLog',
271 'size' => 4559,
272 'mtime' => 1219448527,
273 'is_executable' => false,
274 'content' => 'some content to write'
275 ],
276 'doc/ChangeLog' => [
277 'name' => 'ChangeLog',
278 'size' => 4559,
279 'mtime' => 1219448527,
280 'is_executable' => false,
281 'content' => 'some content to write'
282 ],
283 ];
284 $directories = [
285 'doc/',
286 'mod/doc/'
287 ];
288
289 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, [
290 'makeAndClearExtensionDir',
291 'writeEmConfToFile',
292 'extractFilesArrayFromExtensionData',
293 'extractDirectoriesFromExtensionData',
294 'createDirectoriesForExtensionFiles',
295 'writeExtensionFiles',
296 'reloadPackageInformation',
297 ]);
298 $fileHandlerMock->expects($this->once())->method('extractFilesArrayFromExtensionData')->will($this->returnValue($files));
299 $fileHandlerMock->expects($this->once())->method('extractDirectoriesFromExtensionData')->will($this->returnValue($directories));
300 $fileHandlerMock->expects($this->once())->method('createDirectoriesForExtensionFiles')->with($directories);
301 $fileHandlerMock->expects($this->once())->method('writeExtensionFiles')->with($cleanedFiles);
302 $fileHandlerMock->expects($this->once())->method('reloadPackageInformation')->with('test');
303 $fileHandlerMock->_call('unpackExtensionFromExtensionDataArray', $extensionData);
304 }
305
306 /**
307 * @test
308 */
309 public function extractFilesArrayFromExtensionDataReturnsFileArray()
310 {
311 $extensionData = [
312 'key' => 'test',
313 'FILES' => [
314 'filename1' => 'dummycontent',
315 'filename2' => 'dummycontent2'
316 ]
317 ];
318 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['makeAndClearExtensionDir']);
319 $extractedFiles = $fileHandlerMock->_call('extractFilesArrayFromExtensionData', $extensionData);
320 $this->assertArrayHasKey('filename1', $extractedFiles);
321 $this->assertArrayHasKey('filename2', $extractedFiles);
322 }
323
324 /**
325 * @test
326 */
327 public function writeExtensionFilesWritesFiles()
328 {
329 $files = [
330 'ChangeLog' => [
331 'name' => 'ChangeLog',
332 'size' => 4559,
333 'mtime' => 1219448527,
334 'is_executable' => false,
335 'content' => 'some content to write'
336 ],
337 'README' => [
338 'name' => 'README',
339 'size' => 4566,
340 'mtime' => 1219448533,
341 'is_executable' => false,
342 'content' => 'FEEL FREE TO ADD SOME DOCUMENTATION HERE'
343 ]
344 ];
345 $rootPath = ($extDirPath = $this->fakedExtensions[$this->createFakeExtension()]['siteAbsPath']);
346 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['makeAndClearExtensionDir']);
347 $fileHandlerMock->_call('writeExtensionFiles', $files, $rootPath);
348 $this->assertTrue(file_exists($rootPath . 'ChangeLog'));
349 }
350
351 /**
352 * @test
353 */
354 public function extractDirectoriesFromExtensionDataExtractsDirectories()
355 {
356 $files = [
357 'ChangeLog' => [
358 'name' => 'ChangeLog',
359 'size' => 4559,
360 'mtime' => 1219448527,
361 'is_executable' => false,
362 'content' => 'some content to write'
363 ],
364 'doc/' => [
365 'name' => 'doc/',
366 'size' => 0,
367 'mtime' => 1219448527,
368 'is_executable' => false,
369 'content' => ''
370 ],
371 'doc/ChangeLog' => [
372 'name' => 'ChangeLog',
373 'size' => 4559,
374 'mtime' => 1219448527,
375 'is_executable' => false,
376 'content' => 'some content to write'
377 ],
378 'doc/README' => [
379 'name' => 'README',
380 'size' => 4566,
381 'mtime' => 1219448533,
382 'is_executable' => false,
383 'content' => 'FEEL FREE TO ADD SOME DOCUMENTATION HERE'
384 ],
385 'mod/doc/README' => [
386 'name' => 'README',
387 'size' => 4566,
388 'mtime' => 1219448533,
389 'is_executable' => false,
390 'content' => 'FEEL FREE TO ADD SOME DOCUMENTATION HERE'
391 ]
392 ];
393 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['makeAndClearExtensionDir']);
394 $extractedDirectories = $fileHandlerMock->_call('extractDirectoriesFromExtensionData', $files);
395 $expected = [
396 'doc/',
397 'mod/doc/'
398 ];
399 $this->assertSame($expected, array_values($extractedDirectories));
400 }
401
402 /**
403 * @test
404 */
405 public function createDirectoriesForExtensionFilesCreatesDirectories()
406 {
407 $rootPath = $this->fakedExtensions[$this->createFakeExtension()]['siteAbsPath'];
408 $directories = [
409 'doc/',
410 'mod/doc/'
411 ];
412 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['makeAndClearExtensionDir']);
413 $this->assertFalse(is_dir($rootPath . 'doc/'));
414 $this->assertFalse(is_dir($rootPath . 'mod/doc/'));
415 $fileHandlerMock->_call('createDirectoriesForExtensionFiles', $directories, $rootPath);
416 $this->assertTrue(is_dir($rootPath . 'doc/'));
417 $this->assertTrue(is_dir($rootPath . 'mod/doc/'));
418 }
419
420 /**
421 * @test
422 */
423 public function writeEmConfWritesEmConfFile()
424 {
425 $extKey = $this->createFakeExtension();
426 $extensionData = [
427 'extKey' => $extKey,
428 'EM_CONF' => [
429 'title' => 'Plugin cache engine',
430 'description' => 'Provides an interface to cache plugin content elements based on 4.3 caching framework',
431 'category' => 'Frontend',
432 ]
433 ];
434 $rootPath = $this->fakedExtensions[$extKey]['siteAbsPath'];
435 $emConfUtilityMock = $this->getAccessibleMock(EmConfUtility::class, ['constructEmConf']);
436 $emConfUtilityMock->expects($this->once())->method('constructEmConf')->with($extensionData)->will($this->returnValue(var_export($extensionData['EM_CONF'], true)));
437 $fileHandlerMock = $this->getAccessibleMock(FileHandlingUtility::class, ['makeAndClearExtensionDir']);
438 $fileHandlerMock->_set('emConfUtility', $emConfUtilityMock);
439 $fileHandlerMock->_call('writeEmConfToFile', $extensionData, $rootPath);
440 $this->assertTrue(file_exists($rootPath . 'ext_emconf.php'));
441 }
442
443 /**
444 * @return \PHPUnit_Framework_MockObject_MockObject|FileHandlingUtility
445 */
446 protected function getPreparedFileHandlingMockForDirectoryCreationTests()
447 {
448 /** @var $fileHandlerMock FileHandlingUtility|\PHPUnit_Framework_MockObject_MockObject */
449 $fileHandlerMock = $this->getMockBuilder(FileHandlingUtility::class)
450 ->setMethods(['createNestedDirectory', 'getAbsolutePath', 'directoryExists'])
451 ->getMock();
452 $fileHandlerMock->expects($this->any())
453 ->method('getAbsolutePath')
454 ->will($this->returnArgument(0));
455 return $fileHandlerMock;
456 }
457
458 /**
459 * @test
460 */
461 public function uploadFolderIsNotCreatedIfNotRequested()
462 {
463 $fileHandlerMock = $this->getPreparedFileHandlingMockForDirectoryCreationTests();
464 $fileHandlerMock->expects($this->never())
465 ->method('createNestedDirectory');
466 $fileHandlerMock->ensureConfiguredDirectoriesExist(
467 [
468 'key' => 'foo_bar',
469 'uploadfolder' => 0,
470 ]
471 );
472 }
473
474 /**
475 * @test
476 */
477 public function additionalFoldersAreNotCreatedIfNotRequested()
478 {
479 $fileHandlerMock = $this->getPreparedFileHandlingMockForDirectoryCreationTests();
480 $fileHandlerMock->expects($this->never())
481 ->method('createNestedDirectory');
482 $fileHandlerMock->ensureConfiguredDirectoriesExist(
483 [
484 'key' => 'foo_bar',
485 'createDirs' => '',
486 ]
487 );
488 }
489
490 /**
491 * @test
492 */
493 public function configuredUploadFolderIsCreatedIfRequested()
494 {
495 $fileHandlerMock = $this->getPreparedFileHandlingMockForDirectoryCreationTests();
496 $fileHandlerMock->expects($this->once())
497 ->method('createNestedDirectory')
498 ->with('uploads/tx_foobar/');
499 $fileHandlerMock->ensureConfiguredDirectoriesExist(
500 [
501 'key' => 'foo_bar',
502 'uploadfolder' => 1,
503 ]
504 );
505 }
506
507 /**
508 * @test
509 */
510 public function configuredAdditionalDirectoriesAreCreatedIfRequested()
511 {
512 $fileHandlerMock = $this->getPreparedFileHandlingMockForDirectoryCreationTests();
513 $fileHandlerMock->expects($this->exactly(2))
514 ->method('createNestedDirectory')
515 ->will(
516 $this->returnCallback(function ($path) {
517 if (!\in_array($path, ['foo/bar', 'baz/foo'])) {
518 throw new \Exception('Path "' . $path . '" is not expected to be created', 1476108500);
519 }
520 })
521 );
522 $fileHandlerMock->ensureConfiguredDirectoriesExist(
523 [
524 'key' => 'foo_bar',
525 'createDirs' => 'foo/bar, baz/foo',
526 ]
527 );
528 }
529
530 /**
531 * @test
532 */
533 public function configuredDirectoriesAreNotCreatedIfTheyAlreadyExist()
534 {
535 $fileHandlerMock = $this->getPreparedFileHandlingMockForDirectoryCreationTests();
536 $fileHandlerMock->expects($this->exactly(3))
537 ->method('directoryExists')
538 ->will($this->returnValue(true));
539 $fileHandlerMock->expects($this->never())
540 ->method('createNestedDirectory');
541 $fileHandlerMock->ensureConfiguredDirectoriesExist(
542 [
543 'key' => 'foo_bar',
544 'uploadfolder' => 1,
545 'createDirs' => 'foo/bar, baz/foo',
546 ]
547 );
548 }
549
550 /**
551 * Warning: This test asserts multiple things at once to keep the setup short.
552 *
553 * @test
554 */
555 public function createZipFileFromExtensionGeneratesCorrectArchive()
556 {
557 // 42 second of first day in 1970 - used to have achieve stable file names
558 $GLOBALS['EXEC_TIME'] = 42;
559
560 // Create extension for testing:
561 $extKey = $this->createFakeExtension();
562 $extensionRoot = $this->fakedExtensions[$extKey]['siteAbsPath'];
563
564 // Build mocked fileHandlingUtility:
565 $fileHandlerMock = $this->getAccessibleMock(
566 FileHandlingUtility::class,
567 ['getAbsoluteExtensionPath', 'getExtensionVersion']
568 );
569 $fileHandlerMock->expects($this->any())
570 ->method('getAbsoluteExtensionPath')
571 ->will($this->returnValue($extensionRoot));
572 $fileHandlerMock->expects($this->any())
573 ->method('getExtensionVersion')
574 ->will($this->returnValue('0.0.0'));
575
576 // Add files and directories to extension:
577 touch($extensionRoot . 'emptyFile.txt');
578 file_put_contents($extensionRoot . 'notEmptyFile.txt', 'content');
579 touch($extensionRoot . '.hiddenFile');
580 mkdir($extensionRoot . 'emptyDir');
581 mkdir($extensionRoot . 'notEmptyDir');
582 touch($extensionRoot . 'notEmptyDir/file.txt');
583
584 // Create zip-file from extension
585 $filename = $fileHandlerMock->_call('createZipFileFromExtension', $extKey);
586
587 $expectedFilename = Environment::getVarPath() . '/transient/' . $extKey . '_0.0.0_' . date('YmdHi', 42) . '.zip';
588 $this->testFilesToDelete[] = $filename;
589 $this->assertEquals($expectedFilename, $filename, 'Archive file name differs from expectation');
590
591 // File was created
592 $this->assertTrue(file_exists($filename), 'Zip file not created');
593
594 // Read archive and check its contents
595 $archive = new \ZipArchive();
596 $this->assertTrue($archive->open($filename), 'Unable to open archive');
597 $this->assertEquals($archive->statName('emptyFile.txt')['size'], 0, 'Empty file not in archive');
598 $this->assertEquals($archive->getFromName('notEmptyFile.txt'), 'content', 'Expected content not found');
599 $this->assertFalse($archive->statName('.hiddenFile'), 'Hidden file not in archive');
600 $this->assertTrue(is_array($archive->statName('emptyDir/')), 'Empty directory not in archive');
601 $this->assertTrue(is_array($archive->statName('notEmptyDir/')), 'Not empty directory not in archive');
602 $this->assertTrue(is_array($archive->statName('notEmptyDir/file.txt')), 'File within directory not in archive');
603
604 // Check that the archive has no additional content
605 $this->assertEquals($archive->numFiles, 5, 'Too many or too less files in archive');
606 }
607 }