[BUGFIX] Prevent folder deletion with referenced files 13/36413/14
authorArmin Ruediger Vieweg <armin@v.ieweg.de>
Fri, 30 Jan 2015 11:57:34 +0000 (12:57 +0100)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Sun, 1 Feb 2015 12:46:17 +0000 (13:46 +0100)
Currently a folder can be deleted without any check if included files
are still in use anywhere within the website. This can lead to lost
files and can be harmful for the user. The patch adds a test if files
within a folder, which should be deleted, are still referenced and
prevents deleting it if files where found. The user is notified with a
warning shown above the file list.

Resolves: #48893
Releases: master, 6.2
Change-Id: If1c79d86e1f6c7a32a6a3e94e639951f882302d7
Reviewed-on: http://review.typo3.org/36413
Reviewed-by: Ingo Schmitt <is@marketing-factory.de>
Tested-by: Ingo Schmitt <is@marketing-factory.de>
Reviewed-by: Oliver Klee <typo3-coding@oliverklee.de>
Reviewed-by: Nicole Cordes <typo3@cordes.co>
Tested-by: Nicole Cordes <typo3@cordes.co>
Reviewed-by: Armin Ruediger Vieweg <armin@v.ieweg.de>
Tested-by: Andrea Herzog-Kienast <a.herzog@kienastdv.de>
Reviewed-by: Thomas Deuling <tdeuling@gmail.com>
Tested-by: Thomas Deuling <tdeuling@gmail.com>
Reviewed-by: Jan Helke <typo3@helke.de>
Tested-by: Oliver Klee <typo3-coding@oliverklee.de>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
typo3/sysext/core/Tests/Unit/Utility/File/ExtendedFileUtilityTest.php [new file with mode: 0644]
typo3/sysext/lang/locallang_core.xlf

index 0229341..1c591ee 100644 (file)
@@ -324,7 +324,7 @@ class ExtendedFileUtility extends BasicFileUtility {
         * @param array $cmds $cmds['data'] is the file/folder to delete
         * @return bool Returns TRUE upon success
         */
-       public function func_delete($cmds) {
+       public function func_delete(array $cmds) {
                $result = FALSE;
                if (!$this->isInit) {
                        return $result;
@@ -333,7 +333,7 @@ class ExtendedFileUtility extends BasicFileUtility {
                // for backwards compatibility: the combined file identifier was the path+filename
                try {
                        $fileObject = $this->getFileObject($cmds['data']);
-               } catch (ResourceDoesNotExistException $ex) {
+               } catch (ResourceDoesNotExistException $e) {
                        $flashMessage = GeneralUtility::makeInstance(
                                FlashMessage::class,
                                sprintf( $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:message.description.fileNotFound'), $cmds['data']),
@@ -431,37 +431,81 @@ class ExtendedFileUtility extends BasicFileUtility {
                                }
                        }
                } else {
-                       try {
-                               /** @var \TYPO3\CMS\Core\Resource\Folder $fileObject */
-                               $result = $fileObject->delete(TRUE);
-                               if ($result) {
-                                       // notify the user that the folder was deleted
-                                       /** @var FlashMessage $flashMessage */
-                                       $flashMessage = GeneralUtility::makeInstance(
-                                               FlashMessage::class,
-                                               sprintf($GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:message.description.folderDeleted'), $fileObject->getName()),
-                                               $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:message.header.folderDeleted'),
-                                               FlashMessage::OK,
-                                               TRUE
-                                       );
-                                       $this->addFlashMessage($flashMessage);
-                                       // Log success
-                                       $this->writelog(4, 0, 3, 'Directory "%s" deleted', array($fileObject->getIdentifier()));
+                       /** @var Folder $fileObject */
+                       if (!$this->folderHasFilesInUse($fileObject)) {
+                               try {
+                                       $result = $fileObject->delete(TRUE);
+                                       if ($result) {
+                                               // notify the user that the folder was deleted
+                                               /** @var FlashMessage $flashMessage */
+                                               $flashMessage = GeneralUtility::makeInstance(
+                                                       FlashMessage::class,
+                                                       sprintf($GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:message.description.folderDeleted'), $fileObject->getName()),
+                                                       $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:message.header.folderDeleted'),
+                                                       FlashMessage::OK,
+                                                       TRUE
+                                               );
+                                               $this->addFlashMessage($flashMessage);
+                                               // Log success
+                                               $this->writelog(4, 0, 3, 'Directory "%s" deleted', array($fileObject->getIdentifier()));
+                                       }
+                               } catch (\TYPO3\CMS\Core\Resource\Exception\InsufficientUserPermissionsException $e) {
+                                       $this->writelog(4, 1, 120, 'Could not delete directory! Is directory "%s" empty? (You are not allowed to delete directories recursively).', array($fileObject->getIdentifier()));
+                               } catch (\TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException $e) {
+                                       $this->writelog(4, 1, 123, 'You are not allowed to access the directory', array($fileObject->getIdentifier()));
+                               } catch (\TYPO3\CMS\Core\Resource\Exception\NotInMountPointException $e) {
+                                       $this->writelog(4, 1, 121, 'Target was not within your mountpoints! T="%s"', array($fileObject->getIdentifier()));
+                               } catch (\TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException $e) {
+                                       $this->writelog(4, 1, 120, 'Could not delete directory "%s"! Write-permission problem?', array($fileObject->getIdentifier()));
                                }
-                       } catch (\TYPO3\CMS\Core\Resource\Exception\InsufficientUserPermissionsException $e) {
-                               $this->writelog(4, 1, 120, 'Could not delete directory! Is directory "%s" empty? (You are not allowed to delete directories recursively).', array($fileObject->getIdentifier()));
-                       } catch (\TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException $e) {
-                               $this->writelog(4, 1, 123, 'You are not allowed to access the directory', array($fileObject->getIdentifier()));
-                       } catch (\TYPO3\CMS\Core\Resource\Exception\NotInMountPointException $e) {
-                               $this->writelog(4, 1, 121, 'Target was not within your mountpoints! T="%s"', array($fileObject->getIdentifier()));
-                       } catch (\TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException $e) {
-                               $this->writelog(4, 1, 120, 'Could not delete directory "%s"! Write-permission problem?', array($fileObject->getIdentifier()));
                        }
                }
+
                return $result;
        }
 
        /**
+        * Checks files in given folder recursively for for existing references.
+        *
+        * Creates a flash message if there are references.
+        *
+        * @param Folder $folder
+        * @return bool TRUE if folder has files in use, FALSE otherwise
+        */
+       public function folderHasFilesInUse(Folder $folder) {
+               $files = $folder->getFiles(0, 0, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, TRUE);
+               if (empty($files)) {
+                       return FALSE;
+               }
+
+               /** @var int[] $fileUids */
+               $fileUids = array();
+               foreach ($files as $file) {
+                       $fileUids[] = $file->getUid();
+               }
+               $numberOfReferences = $this->getDatabaseConnection()->exec_SELECTcountRows(
+                       '*',
+                       'sys_refindex',
+                       'deleted=0 AND ref_table="sys_file" AND ref_uid IN (' . implode(',', $fileUids) . ') AND tablename<>"sys_file_metadata"'
+               );
+
+               $hasReferences = $numberOfReferences > 0;
+               if ($hasReferences) {
+                       /** @var FlashMessage $flashMessage */
+                       $flashMessage = GeneralUtility::makeInstance(
+                               FlashMessage::class,
+                               $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:message.description.folderNotDeletedHasFilesWithReferences'),
+                               $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:message.header.folderNotDeletedHasFilesWithReferences'),
+                               FlashMessage::WARNING,
+                               TRUE
+                       );
+                       $this->addFlashMessage($flashMessage);
+               }
+
+               return $hasReferences;
+       }
+
+       /**
         * Maps results from the fal file reference table on the
         * structure of  the normal reference index table.
         *
diff --git a/typo3/sysext/core/Tests/Unit/Utility/File/ExtendedFileUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/File/ExtendedFileUtilityTest.php
new file mode 100644 (file)
index 0000000..86c89e6
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Utility\File;
+
+/*
+ * 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\Resource\Folder;
+use TYPO3\CMS\Core\Resource\File;
+
+/**
+ * Testcase for class \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility
+ *
+ * @author Armin RĂ¼diger Vieweg <armin@v.ieweg.de>
+ */
+class ExtendedFileUtilityTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * Sets up this testcase
+        */
+       protected function setUp() {
+               $GLOBALS['LANG'] = $this->getMock(\TYPO3\CMS\Lang\LanguageService::class, array('sL'));
+               $GLOBALS['TYPO3_DB'] = $this->getMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class, array());
+       }
+
+       /**
+        * @test
+        */
+       public function folderHasFilesInUseReturnsTrueIfItHasFiles() {
+               $fileUid = 1;
+               $file = $this->getMock(File::class, array('getUid'), array(), '', FALSE);
+               $file->expects($this->once())->method('getUid')->will($this->returnValue($fileUid));
+
+               $folder = $this->getMock(Folder::class, array('getFiles'), array(), '', FALSE);
+               $folder->expects($this->once())
+                       ->method('getFiles')->with(0, 0, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, TRUE)
+                       ->will($this->returnValue(array($file))
+               );
+
+               /** @var \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility $subject */
+               $subject = $this->getMock(\TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::class, array('addFlashMessage'), array(), '');
+               $GLOBALS['TYPO3_DB']->expects($this->once())
+                       ->method('exec_SELECTcountRows')->with('*', 'sys_refindex', 'deleted=0 AND ref_table="sys_file" AND ref_uid IN (' . $fileUid . ') AND tablename<>"sys_file_metadata"')
+                       ->will($this->returnValue(1));
+
+               $GLOBALS['LANG']->expects($this->at(0))->method('sL')
+                       ->with('LLL:EXT:lang/locallang_core.xlf:message.description.folderNotDeletedHasFilesWithReferences')
+                       ->will($this->returnValue('folderNotDeletedHasFilesWithReferences'));
+               $GLOBALS['LANG']->expects($this->at(1))->method('sL')
+                       ->with('LLL:EXT:lang/locallang_core.xlf:message.header.folderNotDeletedHasFilesWithReferences')
+                       ->will($this->returnValue('folderNotDeletedHasFilesWithReferences'));
+
+               $result = $subject->folderHasFilesInUse($folder);
+               $this->assertTrue($result);
+       }
+
+       /**
+        * @test
+        */
+       public function folderHasFilesInUseReturnsFalseIfItHasNoFiles() {
+               $folder = $this->getMock(Folder::class, array('getFiles'), array(), '', FALSE);
+               $folder->expects($this->once())->method('getFiles')->with(0, 0, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, TRUE)->will(
+                       $this->returnValue(array())
+               );
+
+               /** @var \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility $subject */
+               $subject = $this->getMock(\TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::class, array('addFlashMessage'), array(), '');
+               $this->assertFalse($subject->folderHasFilesInUse($folder));
+       }
+
+}
\ No newline at end of file
index 5712ea7..6a59f39 100644 (file)
@@ -756,6 +756,12 @@ Would you like to save now in order to refresh the display?</source>
                        <trans-unit id="message.description.fileNotDeletedHasReferences" xml:space="preserve">
                                <source>The file cannot be deleted since it is still used at the following places:</source>
                        </trans-unit>
+                       <trans-unit id="message.header.folderNotDeletedHasFilesWithReferences" xml:space="preserve">
+                               <source>Folder not deleted</source>
+                       </trans-unit>
+                       <trans-unit id="message.description.folderNotDeletedHasFilesWithReferences" xml:space="preserve">
+                               <source>The folder cannot be deleted since it still contains files in use.</source>
+                       </trans-unit>
                        <trans-unit id="message.header.fileHasBrokenReferences" xml:space="preserve">
                                <source>File has broken references</source>
                        </trans-unit>