[TASK] Implement conflict handling into renameFile() 48/52048/14
authorAnja Leichsenring <aleichsenring@ab-softlab.de>
Wed, 15 Mar 2017 09:33:10 +0000 (10:33 +0100)
committerAndreas Fernandez <typo3@scripting-base.de>
Sat, 18 Mar 2017 18:46:32 +0000 (19:46 +0100)
Upon DuplicationBehaviour::RENAME and ~::REPLACE, the according function
is called for the given file. For DuplicationBehaviour::CANCEL, the same
exception as before is thrown.

For a better UX, the existance of a possible duplicate is checked and a
modal window is rendered to the users to let them choose the desired
action if a duplicate was found.

Resolves: #80282
Releases: master
Change-Id: Ie67f04184a232fc23a3cda648692783771ba5171
Reviewed-on: https://review.typo3.org/52048
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
Reviewed-by: Andreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez <typo3@scripting-base.de>
13 files changed:
Build/tsconfig.json
typo3/sysext/backend/Classes/Controller/File/FileController.php
typo3/sysext/backend/Classes/Controller/File/RenameFileController.php
typo3/sysext/backend/Resources/Private/Templates/File/RenameFile.html
typo3/sysext/backend/Resources/Private/TypeScript/RenameFile.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/RenameFile.js [new file with mode: 0644]
typo3/sysext/core/Classes/Resource/AbstractFile.php
typo3/sysext/core/Classes/Resource/FileInterface.php
typo3/sysext/core/Classes/Resource/FileReference.php
typo3/sysext/core/Classes/Resource/ResourceStorage.php
typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php
typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf

index 0aa6ead..605d77a 100644 (file)
     "files": [
         "../typo3/sysext/backend/Resources/Private/TypeScript/ColorPicker.ts",
         "../typo3/sysext/backend/Resources/Private/TypeScript/FormEngineReview.ts",
-        "../typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts"
+        "../typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts",
+        "../typo3/sysext/backend/Resources/Private/TypeScript/RenameFile.ts"
     ]
-}
+}
\ No newline at end of file
index 131414e..f2ffb34 100644 (file)
@@ -96,7 +96,13 @@ class FileController
         // Set the GPvars from outside
         $this->file = GeneralUtility::_GP('file');
         $this->CB = GeneralUtility::_GP('CB');
-        $this->overwriteExistingFiles = DuplicationBehavior::cast(GeneralUtility::_GP('overwriteExistingFiles'));
+        if (isset($this->file['rename'][0]['conflictMode'])) {
+            $conflictMode = $this->file['rename'][0]['conflictMode'];
+            unset($this->file['rename'][0]['conflictMode']);
+            $this->overwriteExistingFiles = DuplicationBehavior::cast($conflictMode);
+        } else {
+            $this->overwriteExistingFiles = DuplicationBehavior::cast(GeneralUtility::_GP('overwriteExistingFiles'));
+        }
         $this->redirect = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('redirect'));
         $this->initClipboard();
         $this->fileProcessor = GeneralUtility::makeInstance(ExtendedFileUtility::class);
index d0122d6..d73e87a 100644 (file)
@@ -19,6 +19,7 @@ use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Module\AbstractModule;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Resource\DuplicationBehavior;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 
@@ -119,6 +120,7 @@ class RenameFileController extends AbstractModule
 
         // Setting up the context sensitive menu
         $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
+        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/RenameFile');
 
         // Add javaScript
         $this->moduleTemplate->addJavaScriptCode(
@@ -134,16 +136,20 @@ class RenameFileController extends AbstractModule
      */
     public function main()
     {
+        $assigns = [];
+        $assigns['moduleUrlTceFile'] = BackendUtility::getModuleUrl('tce_file');
+        $assigns['returnUrl'] = $this->returnUrl;
+
         if ($this->fileOrFolderObject instanceof \TYPO3\CMS\Core\Resource\Folder) {
             $fileIdentifier = $this->fileOrFolderObject->getCombinedIdentifier();
         } else {
             $fileIdentifier = $this->fileOrFolderObject->getUid();
+            $assigns['conflictMode'] = DuplicationBehavior::cast(DuplicationBehavior::RENAME);
+            $assigns['destination'] = substr($this->fileOrFolderObject->getCombinedIdentifier(), 0, -strlen($this->fileOrFolderObject->getName()));
         }
-        $assigns = [];
-        $assigns['moduleUrlTceFile'] = BackendUtility::getModuleUrl('tce_file');
+
         $assigns['fileName'] = $this->fileOrFolderObject->getName();
         $assigns['fileIdentifier'] = $fileIdentifier;
-        $assigns['returnUrl'] = $this->returnUrl;
 
         // Create buttons
         $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
@@ -163,6 +169,14 @@ class RenameFileController extends AbstractModule
             $buttonBar->addButton($backButton);
         }
 
+        $this->moduleTemplate->getPageRenderer()->addInlineLanguageLabelArray([
+            'file_rename.actions.cancel' => $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:file_rename.actions.cancel'),
+            'file_rename.actions.rename' => $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:file_rename.actions.rename'),
+            'file_rename.actions.override' => $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:file_rename.actions.override'),
+            'file_rename.exists.title' => $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:file_rename.exists.title'),
+            'file_rename.exists.description' => $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:file_rename.exists.description'),
+        ]);
+
         // Rendering of the output via fluid
         $view = GeneralUtility::makeInstance(StandaloneView::class);
         $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
index 91d739a..531a156 100644 (file)
@@ -1,14 +1,24 @@
-<h1><f:translate key="LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:file_rename.php.pagetitle" /></h1>
+<h1>
+    <f:translate key="LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:file_rename.php.pagetitle"/>
+</h1>
 <div>
     <form action="{moduleUrlTceFile}" method="post" name="editform" role="form">
         <div class="form-group">
-            <input class="form-control" type="text" name="file[rename][0][target]" value="{fileName}" style="width:384px;" />
-            <input type="hidden" name="file[rename][0][data]" value="{fileIdentifier}" />
+            <input class="form-control" type="text" name="file[rename][0][target]" value="{fileName}"
+                   data-original="{fileName}" style="width:384px;"/>
+            <input type="hidden" name="file[rename][0][data]" value="{fileIdentifier}"/>
+            <f:if condition="{destination}">
+                <input type="hidden" name="file[rename][0][destination]" value="{destination}"/>
+                <input type="hidden" name="file[rename][0][conflictMode]" value="{conflictMode}"/>
+            </f:if>
         </div>
         <div class="form-group">
-            <input class="btn btn-primary" type="submit" value="{f:translate(key: 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:file_rename.php.submit')}" />
-            <input class="btn btn-danger" type="submit" value="{f:translate(key: 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.cancel')}" onclick="backToList(); return false;" />
-            <input type="hidden" name="redirect" value="{returnUrl}" />
+            <input class="btn btn-primary t3js-submit-file-rename" type="submit"
+                   value="{f:translate(key: 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:file_rename.php.submit')}"/>
+            <input class="btn btn-danger" type="submit"
+                   value="{f:translate(key: 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.cancel')}"
+                   onclick="backToList(); return false;"/>
+            <input type="hidden" name="redirect" value="{returnUrl}"/>
         </div>
     </form>
 </div>
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/RenameFile.ts b/typo3/sysext/backend/Resources/Private/TypeScript/RenameFile.ts
new file mode 100644 (file)
index 0000000..7661835
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * 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!
+ */
+
+/// <amd-dependency path='TYPO3/CMS/Backend/Modal' name='Modal'>
+/// <amd-dependency path='TYPO3/CMS/Backend/Severity' name='Severity'>
+
+import $ = require('jquery');
+declare const Modal: any;
+declare const Severity: any;
+declare const TYPO3: any;
+
+/**
+ * Module: TYPO3/CMS/Backend/RenameFile
+ * Modal to pick the required conflict strategy for colliding filenames
+ * @exports TYPO3/CMS/Backend/RenameFile
+ */
+class RenameFile {
+
+  constructor() {
+    this.initialize();
+  }
+
+  public initialize(): void {
+    (<any> $('.t3js-submit-file-rename')).on('click', this.checkForDuplicate);
+  }
+
+  private checkForDuplicate(e: any): void {
+    e.preventDefault();
+
+    const form: any = $(e.currentTarget).closest('form');
+    const fileNameField: any = form.find('input[name="file[rename][0][target]"]');
+    const conflictModeField: any = form.find('input[name="file[rename][0][conflictMode]"]');
+    const ajaxUrl: string = TYPO3.settings.ajaxUrls.file_exists;
+
+    $.ajax({
+      cache: false,
+      data: {
+        fileName: fileNameField.val(),
+        fileTarget: form.find('input[name="file[rename][0][destination]"]').val(),
+      },
+      success: function (response: any): void {
+        const fileExists: boolean = response !== false;
+        const originalFileName: string = fileNameField.data('original');
+        const newFileName: string = fileNameField.val();
+
+        if (fileExists && originalFileName !== newFileName) {
+          const description: string = TYPO3.lang['file_rename.exists.description']
+            .replace('{0}', originalFileName).replace('{1}', newFileName);
+
+          const modal: boolean = Modal.confirm(
+            TYPO3.lang['file_rename.exists.title'],
+            description,
+            Severity.warning,
+            [
+              {
+                active: true,
+                btnClass: 'btn-default',
+                name: 'cancel',
+                text: TYPO3.lang['file_rename.actions.cancel'],
+              },
+              {
+                btnClass: 'btn-primary',
+                name: 'rename',
+                text: TYPO3.lang['file_rename.actions.rename'],
+              },
+              {
+                btnClass: 'btn-default',
+                name: 'replace',
+                text: TYPO3.lang['file_rename.actions.override'],
+              },
+            ]);
+
+          (<any> modal).on('button.clicked', function (event: any): void {
+            conflictModeField.val(event.target.name);
+            form.submit();
+            Modal.dismiss();
+          });
+        } else {
+          form.submit();
+        }
+      },
+      url: ajaxUrl,
+    });
+  };
+}
+
+export = new RenameFile();
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/RenameFile.js b/typo3/sysext/backend/Resources/Public/JavaScript/RenameFile.js
new file mode 100644 (file)
index 0000000..add74a0
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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!
+ */
+define(["require", "exports", "TYPO3/CMS/Backend/Modal", "TYPO3/CMS/Backend/Severity", "jquery"], function (require, exports, Modal, Severity, $) {
+    "use strict";
+    /**
+     * Module: TYPO3/CMS/Backend/RenameFile
+     * Modal to pick the required conflict strategy for colliding filenames
+     * @exports TYPO3/CMS/Backend/RenameFile
+     */
+    var RenameFile = (function () {
+        function RenameFile() {
+            this.initialize();
+        }
+        RenameFile.prototype.initialize = function () {
+            $('.t3js-submit-file-rename').on('click', this.checkForDuplicate);
+        };
+        RenameFile.prototype.checkForDuplicate = function (e) {
+            e.preventDefault();
+            var form = $(e.currentTarget).closest('form');
+            var fileNameField = form.find('input[name="file[rename][0][target]"]');
+            var conflictModeField = form.find('input[name="file[rename][0][conflictMode]"]');
+            var ajaxUrl = TYPO3.settings.ajaxUrls.file_exists;
+            $.ajax({
+                cache: false,
+                data: {
+                    fileName: fileNameField.val(),
+                    fileTarget: form.find('input[name="file[rename][0][destination]"]').val(),
+                },
+                success: function (response) {
+                    var fileExists = response !== false;
+                    var originalFileName = fileNameField.data('original');
+                    var newFileName = fileNameField.val();
+                    if (fileExists && originalFileName !== newFileName) {
+                        var description = TYPO3.lang['file_rename.exists.description']
+                            .replace('{0}', originalFileName).replace('{1}', newFileName);
+                        var modal = Modal.confirm(TYPO3.lang['file_rename.exists.title'], description, Severity.warning, [
+                            {
+                                active: true,
+                                btnClass: 'btn-default',
+                                name: 'cancel',
+                                text: TYPO3.lang['file_rename.actions.cancel'],
+                            },
+                            {
+                                btnClass: 'btn-primary',
+                                name: 'rename',
+                                text: TYPO3.lang['file_rename.actions.rename'],
+                            },
+                            {
+                                btnClass: 'btn-default',
+                                name: 'replace',
+                                text: TYPO3.lang['file_rename.actions.override'],
+                            },
+                        ]);
+                        modal.on('button.clicked', function (event) {
+                            conflictModeField.val(event.target.name);
+                            form.submit();
+                            Modal.dismiss();
+                        });
+                    }
+                    else {
+                        form.submit();
+                    }
+                },
+                url: ajaxUrl,
+            });
+        };
+        ;
+        return RenameFile;
+    }());
+    return new RenameFile();
+});
index 46ef39a..d90feec 100644 (file)
@@ -468,15 +468,15 @@ abstract class AbstractFile implements FileInterface
      *
      * @param string $newName The new file name
      *
-     * @throws \RuntimeException
-     * @return File
+     * @param string $conflictMode
+     * @return FileInterface
      */
-    public function rename($newName)
+    public function rename($newName, $conflictMode = DuplicationBehavior::RENAME)
     {
         if ($this->deleted) {
             throw new \RuntimeException('File has been deleted.', 1329821482);
         }
-        return $this->getStorage()->renameFile($this, $newName);
+        return $this->getStorage()->renameFile($this, $newName, $conflictMode);
     }
 
     /**
index e93f20d..99b3e30 100644 (file)
@@ -121,9 +121,10 @@ interface FileInterface extends ResourceInterface
      * Renames this file.
      *
      * @param string $newName The new file name
+     * @param string $conflictMode
      * @return File
      */
-    public function rename($newName);
+    public function rename($newName, $conflictMode = DuplicationBehavior::RENAME);
 
     /*****************
      * SPECIAL METHODS
index 66d23c4..5f6e4a3 100644 (file)
@@ -423,10 +423,10 @@ class FileReference implements FileInterface
      * Renames the fileName in this particular usage.
      *
      * @param string $newName The new name
-     * @throws \BadMethodCallException
+     * @param string $conflictMode
      * @return FileReference
      */
-    public function rename($newName)
+    public function rename($newName, $conflictMode = DuplicationBehavior::RENAME)
     {
         // @todo Implement this function. This should only rename the
         // FileReference (sys_file_reference) record, not the file itself.
index f9a8202..cb77643 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Resource;
 
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Registry;
+use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException;
 use TYPO3\CMS\Core\Resource\Exception\InvalidTargetFolderException;
 use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
 use TYPO3\CMS\Core\Resource\Index\Indexer;
@@ -168,7 +169,7 @@ class ResourceStorage implements ResourceStorageInterface
     public function __construct(Driver\DriverInterface $driver, array $storageRecord)
     {
         $this->storageRecord = $storageRecord;
-        $this->configuration = ResourceFactory::getInstance()->convertFlexFormDataToConfigurationArray($storageRecord['configuration']);
+        $this->configuration = $this->getResourceFactoryInstance()->convertFlexFormDataToConfigurationArray($storageRecord['configuration']);
         $this->capabilities =
             ($this->storageRecord['is_browsable'] ? self::CAPABILITY_BROWSABLE : 0) |
             ($this->storageRecord['is_public'] ? self::CAPABILITY_PUBLIC : 0) |
@@ -457,7 +458,7 @@ class ResourceStorage implements ResourceStorageInterface
             throw new Exception\FolderDoesNotExistException('Folder for file mount ' . $folderIdentifier . ' does not exist.', 1334427099);
         }
         $data = $this->driver->getFolderInfoByIdentifier($folderIdentifier);
-        $folderObject = ResourceFactory::getInstance()->createFolderObject($this, $data['identifier'], $data['name']);
+        $folderObject = $this->getResourceFactoryInstance()->createFolderObject($this, $data['identifier'], $data['name']);
         // Use the canonical identifier instead of the user provided one!
         $folderIdentifier = $folderObject->getIdentifier();
         if (
@@ -1192,7 +1193,7 @@ class ResourceStorage implements ResourceStorageInterface
         }
 
         $fileIdentifier = $this->driver->addFile($localFilePath, $targetFolder->getIdentifier(), $targetFileName, $removeOriginal);
-        $file = ResourceFactory::getInstance()->getFileObjectByStorageAndIdentifier($this->getUid(), $fileIdentifier);
+        $file = $this->getResourceFactoryInstance()->getFileObjectByStorageAndIdentifier($this->getUid(), $fileIdentifier);
 
         if ($this->autoExtractMetadataEnabled()) {
             $indexer = GeneralUtility::makeInstance(Indexer::class, $this);
@@ -1588,7 +1589,7 @@ class ResourceStorage implements ResourceStorageInterface
                 if (empty($processingFolderIdentifier) || (int)$storageUid !== $this->getUid()) {
                     continue;
                 }
-                $potentialProcessingFolder = ResourceFactory::getInstance()->getInstance()->createFolderObject($this, $processingFolderIdentifier, $processingFolderIdentifier);
+                $potentialProcessingFolder = $this->getResourceFactoryInstance()->getInstance()->createFolderObject($this, $processingFolderIdentifier, $processingFolderIdentifier);
                 if ($potentialProcessingFolder->getStorage() === $this && $potentialProcessingFolder->getIdentifier() !== $this->getProcessingFolder()->getIdentifier()) {
                     $this->processingFolders[] = $potentialProcessingFolder;
                 }
@@ -1719,7 +1720,7 @@ class ResourceStorage implements ResourceStorageInterface
         $this->assureFileAddPermissions($targetFolderObject, $fileName);
         $newFileIdentifier = $this->driver->createFile($fileName, $targetFolderObject->getIdentifier());
         $this->emitPostFileCreateSignal($newFileIdentifier, $targetFolderObject);
-        return ResourceFactory::getInstance()->getFileObjectByStorageAndIdentifier($this->getUid(), $newFileIdentifier);
+        return $this->getResourceFactoryInstance()->getFileObjectByStorageAndIdentifier($this->getUid(), $newFileIdentifier);
     }
 
     /**
@@ -1792,7 +1793,7 @@ class ResourceStorage implements ResourceStorageInterface
             $tempPath = $file->getForLocalProcessing();
             $newFileObjectIdentifier = $this->driver->addFile($tempPath, $targetFolder->getIdentifier(), $sanitizedTargetFileName);
         }
-        $newFileObject = ResourceFactory::getInstance()->getFileObjectByStorageAndIdentifier($this->getUid(), $newFileObjectIdentifier);
+        $newFileObject = $this->getResourceFactoryInstance()->getFileObjectByStorageAndIdentifier($this->getUid(), $newFileObjectIdentifier);
         $this->emitPostFileCopySignal($file, $targetFolder);
         return $newFileObject;
     }
@@ -1860,16 +1861,12 @@ class ResourceStorage implements ResourceStorageInterface
      *
      * @param FileInterface $file
      * @param string $targetFileName
-     *
-     * @throws Exception\InsufficientFileWritePermissionsException
-     * @throws Exception\InsufficientFileReadPermissionsException
-     * @throws Exception\InsufficientUserPermissionsException
+     * @param string $conflictMode
      * @return FileInterface
+     * @throws ExistingTargetFileNameException
      */
-    public function renameFile($file, $targetFileName)
+    public function renameFile($file, $targetFileName, $conflictMode = DuplicationBehavior::RENAME)
     {
-        // @todo add $conflictMode setting
-
         // The name should be different from the current.
         if ($file->getName() === $targetFileName) {
             return $file;
@@ -1878,6 +1875,8 @@ class ResourceStorage implements ResourceStorageInterface
         $this->assureFileRenamePermissions($file, $sanitizedTargetFileName);
         $this->emitPreFileRenameSignal($file, $sanitizedTargetFileName);
 
+        $conflictMode = DuplicationBehavior::cast($conflictMode);
+
         // Call driver method to rename the file and update the index entry
         try {
             $newIdentifier = $this->driver->renameFile($file->getIdentifier(), $sanitizedTargetFileName);
@@ -1885,6 +1884,17 @@ class ResourceStorage implements ResourceStorageInterface
                 $file->updateProperties(['identifier' => $newIdentifier]);
             }
             $this->getIndexer()->updateIndexEntry($file);
+        } catch (ExistingTargetFileNameException $exception) {
+            if ($conflictMode->equals(DuplicationBehavior::RENAME)) {
+                $newName = $this->getUniqueName($file->getParentFolder(), $sanitizedTargetFileName);
+                $file = $this->renameFile($file, $newName);
+            } elseif ($conflictMode->equals(DuplicationBehavior::CANCEL)) {
+                throw $exception;
+            } elseif ($conflictMode->equals(DuplicationBehavior::REPLACE)) {
+                $sourceFileIdentifier = substr($file->getCombinedIdentifier(), 0, strrpos($file->getCombinedIdentifier(), '/') + 1) . $targetFileName;
+                $sourceFile = $this->getResourceFactoryInstance()->getFileObjectFromCombinedIdentifier($sourceFileIdentifier);
+                $file = $this->replaceFile($sourceFile, PATH_site . $file->getPublicUrl());
+            }
         } catch (\RuntimeException $e) {
         }
 
@@ -2341,7 +2351,7 @@ class ResourceStorage implements ResourceStorageInterface
     public function getFolder($identifier, $returnInaccessibleFolderObject = false)
     {
         $data = $this->driver->getFolderInfoByIdentifier($identifier);
-        $folder = ResourceFactory::getInstance()->createFolderObject($this, $data['identifier'], $data['name']);
+        $folder = $this->getResourceFactoryInstance()->createFolderObject($this, $data['identifier'], $data['name']);
 
         try {
             $this->assureFolderReadPermission($folder);
@@ -2415,7 +2425,7 @@ class ResourceStorage implements ResourceStorageInterface
             $mount = reset($this->fileMounts);
             return $mount['folder'];
         } else {
-            return ResourceFactory::getInstance()->createFolderObject($this, $this->driver->getRootLevelFolder(), '');
+            return $this->getResourceFactoryInstance()->createFolderObject($this, $this->driver->getRootLevelFolder(), '');
         }
     }
 
@@ -2739,7 +2749,7 @@ class ResourceStorage implements ResourceStorageInterface
      * If $theFile exists in $theDest (directory) the file have numbers appended up to $this->maxNumber. Hereafter a unique string will be appended.
      * This function is used by fx. DataHandler when files are attached to records and needs to be uniquely named in the uploads/* folders
      *
-     * @param Folder $folder
+     * @param FolderInterface $folder
      * @param string $theFile The input fileName to check
      * @param bool $dontCheckForUnique If set the fileName is returned with the path prepended without checking whether it already existed!
      *
@@ -2747,7 +2757,7 @@ class ResourceStorage implements ResourceStorageInterface
      * @return string A unique fileName inside $folder, based on $theFile.
      * @see \TYPO3\CMS\Core\Utility\File\BasicFileUtility::getUniqueName()
      */
-    protected function getUniqueName(Folder $folder, $theFile, $dontCheckForUnique = false)
+    protected function getUniqueName(FolderInterface $folder, $theFile, $dontCheckForUnique = false)
     {
         static $maxNumber = 99, $uniqueNamePrefix = '';
         // Fetches info about path, name, extension of $theFile
@@ -2884,7 +2894,7 @@ class ResourceStorage implements ResourceStorageInterface
             try {
                 if (strpos($processingFolder, ':') !== false) {
                     list($storageUid, $processingFolderIdentifier) = explode(':', $processingFolder, 2);
-                    $storage = ResourceFactory::getInstance()->getStorageObject($storageUid);
+                    $storage = $this->getResourceFactoryInstance()->getStorageObject($storageUid);
                     if ($storage->hasFolder($processingFolderIdentifier)) {
                         $this->processingFolder = $storage->getFolder($processingFolderIdentifier);
                     } else {
@@ -2909,7 +2919,7 @@ class ResourceStorage implements ResourceStorageInterface
                         $this->evaluatePermissions = $currentEvaluatePermissions;
                     } else {
                         $data = $this->driver->getFolderInfoByIdentifier($processingFolder);
-                        $this->processingFolder = ResourceFactory::getInstance()->createFolderObject($this, $data['identifier'], $data['name']);
+                        $this->processingFolder = $this->getResourceFactoryInstance()->createFolderObject($this, $data['identifier'], $data['name']);
                     }
                 }
             } catch (Exception\InsufficientFolderWritePermissionsException $e) {
@@ -3022,6 +3032,14 @@ class ResourceStorage implements ResourceStorageInterface
     }
 
     /**
+     * @return ResourceFactory
+     */
+    public function getResourceFactoryInstance(): ResourceFactory
+    {
+        return ResourceFactory::getInstance();
+    }
+
+    /**
      * Returns the current BE user.
      *
      * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
index aa839d3..6691b0e 100644 (file)
@@ -842,12 +842,12 @@ class ExtendedFileUtility extends BasicFileUtility
         if ($sourceFileObject instanceof File) {
             try {
                 // Try to rename the File
-                $resultObject = $sourceFileObject->rename($targetFile);
+                $resultObject = $sourceFileObject->rename($targetFile, $this->existingFilesConflictMode);
                 $this->writeLog(5, 0, 1, 'File renamed from "%s" to "%s"', [$sourceFile, $targetFile]);
                 if ($sourceFile === $targetFile) {
                     $this->addMessageToFlashMessageQueue('FileUtility.FileRenamedSameName', [$sourceFile], FlashMessage::INFO);
                 } else {
-                    $this->addMessageToFlashMessageQueue('FileUtility.FileRenamedFromTo', [$sourceFile, $targetFile], FlashMessage::OK);
+                    $this->addMessageToFlashMessageQueue('FileUtility.FileRenamedFromTo', [$sourceFile, $resultObject->getName()], FlashMessage::OK);
                 }
             } catch (InsufficientUserPermissionsException $e) {
                 $this->writeLog(5, 1, 102, 'You are not allowed to rename files!', []);
index 8f17fab..bb89482 100644 (file)
@@ -14,12 +14,17 @@ namespace TYPO3\CMS\Core\Tests\Unit\Resource;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Prophecy\Argument;
 use TYPO3\CMS\Core\Resource\Driver\AbstractDriver;
 use TYPO3\CMS\Core\Resource\Driver\LocalDriver;
+use TYPO3\CMS\Core\Resource\DuplicationBehavior;
+use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException;
 use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Resource\FileInterface;
 use TYPO3\CMS\Core\Resource\FileRepository;
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Resource\ResourceStorage;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -64,11 +69,11 @@ class ResourceStorageTest extends BaseTestCase
      * @param bool $mockPermissionChecks
      * @param AbstractDriver|\PHPUnit_Framework_MockObject_MockObject $driverObject
      * @param array $storageRecord
+     * @param array $mockedMethods
      */
-    protected function prepareSubject(array $configuration, $mockPermissionChecks = false, AbstractDriver $driverObject = null, array $storageRecord = [])
+    protected function prepareSubject(array $configuration, $mockPermissionChecks = false, AbstractDriver $driverObject = null, array $storageRecord = [], array $mockedMethods = [])
     {
-        $permissionMethods = ['assureFileAddPermissions', 'checkFolderActionPermission', 'checkFileActionPermission', 'checkUserActionPermission', 'checkFileExtensionPermission', 'isWithinFileMountBoundaries'];
-        $mockedMethods = [];
+        $permissionMethods = ['assureFileAddPermissions', 'checkFolderActionPermission', 'checkFileActionPermission', 'checkUserActionPermission', 'checkFileExtensionPermission', 'isWithinFileMountBoundaries', 'assureFileRenamePermissions'];
         $configuration = $this->convertConfigurationArrayToFlexformXml($configuration);
         $overruleArray = ['configuration' => $configuration];
         ArrayUtility::mergeRecursiveWithOverrule($storageRecord, $overruleArray);
@@ -76,12 +81,12 @@ class ResourceStorageTest extends BaseTestCase
             $driverObject = $this->getMockForAbstractClass(AbstractDriver::class, [], '', false);
         }
         if ($mockPermissionChecks) {
-            $mockedMethods = $permissionMethods;
+            $mockedMethods = array_merge($mockedMethods, $permissionMethods);
         }
         $mockedMethods[] = 'getIndexer';
 
         $this->subject = $this->getMockBuilder(ResourceStorage::class)
-            ->setMethods($mockedMethods)
+            ->setMethods(array_unique($mockedMethods))
             ->setConstructorArgs([$driverObject, $storageRecord])
             ->getMock();
         $this->subject->expects($this->any())->method('getIndexer')->will($this->returnValue($this->createMock(\TYPO3\CMS\Core\Resource\Index\Indexer::class)));
@@ -705,4 +710,73 @@ class ResourceStorageTest extends BaseTestCase
         $mockedDriver->expects($this->once())->method('folderExists')->with($this->equalTo('/someFolder/'))->will($this->returnValue(false));
         $this->subject->createFolder('newFolder', $mockedParentFolder);
     }
+
+    /**
+     * @test
+     */
+    public function renameFileRenamesFileAsRequested()
+    {
+        $mockedDriver = $this->createDriverMock([], $this->subject);
+        $mockedDriver->expects($this->once())->method('renameFile')->will($this->returnValue('bar'));
+        $this->prepareSubject([], true, $mockedDriver, [], ['emitPreFileRenameSignal', 'emitPostFileRenameSignal']);
+        /** @var File $file */
+        $file = new File(['identifier' => 'foo', 'name' => 'foo'], $this->subject);
+        $result = $this->subject->renameFile($file, 'bar');
+        // fake what the indexer does in updateIndexEntry
+        $result->updateProperties(['name' => $result->getIdentifier()]);
+        $this->assertSame('bar', $result->getName());
+    }
+
+    /**
+     * @test
+     */
+    public function renameFileRenamesWithUniqueNameIfConflictAndConflictModeIsRename()
+    {
+        $mockedDriver = $this->createDriverMock([], $this->subject);
+        $mockedDriver->expects($this->any())->method('renameFile')->will($this->onConsecutiveCalls($this->throwException(new ExistingTargetFileNameException('foo', 1489593090)), 'bar_01'));
+        //$mockedDriver->expects($this->at(1))->method('renameFile')->will($this->returnValue('bar_01'));
+        $mockedDriver->expects($this->any())->method('sanitizeFileName')->will($this->onConsecutiveCalls('bar', 'bar_01'));
+        $this->prepareSubject([], true, $mockedDriver, [], ['emitPreFileRenameSignal', 'emitPostFileRenameSignal', 'getUniqueName']);
+        /** @var File $file */
+        $file = new File(['identifier' => 'foo', 'name' => 'foo'], $this->subject);
+        $this->subject->expects($this->once())->method('getUniqueName')->will($this->returnValue('bar_01'));
+        $result = $this->subject->renameFile($file, 'bar');
+        // fake what the indexer does in updateIndexEntry
+        $result->updateProperties(['name' => $result->getIdentifier()]);
+        $this->assertSame('bar_01', $result->getName());
+    }
+
+    /**
+     * @test
+     */
+    public function renameFileThrowsExceptionIfConflictAndConflictModeIsCancel()
+    {
+        $mockedDriver = $this->createDriverMock([], $this->subject);
+        $mockedDriver->expects($this->once())->method('renameFile')->will($this->throwException(new ExistingTargetFileNameException('foo', 1489593099)));
+        $this->prepareSubject([], true, $mockedDriver, [], ['emitPreFileRenameSignal', 'emitPostFileRenameSignal']);
+        /** @var File $file */
+        $file = new File(['identifier' => 'foo', 'name' => 'foo'], $this->subject);
+        $this->expectException(ExistingTargetFileNameException::class);
+        $this->subject->renameFile($file, 'bar', DuplicationBehavior::CANCEL);
+    }
+
+     /**
+     * @test
+     */
+    public function renameFileReplacesIfConflictAndConflictModeIsReplace()
+    {
+        $mockedDriver = $this->createDriverMock([], $this->subject);
+        $mockedDriver->expects($this->once())->method('renameFile')->will($this->throwException(new ExistingTargetFileNameException('foo', 1489593098)));
+        $mockedDriver->expects($this->any())->method('sanitizeFileName')->will($this->returnValue('bar'));
+        $this->prepareSubject([], true, $mockedDriver, [], ['emitPreFileRenameSignal', 'emitPostFileRenameSignal', 'replaceFile', 'getPublicUrl', 'getResourceFactoryInstance']);
+        $this->subject->expects($this->once())->method('getPublicUrl')->will($this->returnValue('somePath'));
+        $resourceFactory = $this->prophesize(ResourceFactory::class);
+        $file = $this->prophesize(FileInterface::class);
+        $resourceFactory->getFileObjectFromCombinedIdentifier(Argument::any())->willReturn($file->reveal());
+        $this->subject->expects($this->once())->method('replaceFile')->will($this->returnValue($file->reveal()));
+        $this->subject->expects($this->any())->method('getResourceFactoryInstance')->will(self::returnValue($resourceFactory->reveal()));
+        /** @var File $file */
+        $file = new File(['identifier' => 'foo', 'name' => 'foo', 'missing' => false], $this->subject);
+        $this->subject->renameFile($file, 'bar', DuplicationBehavior::REPLACE);
+    }
 }
index 149ec8b..3c36e01 100644 (file)
@@ -613,6 +613,21 @@ Do you want to continue WITHOUT saving?</source>
                        <trans-unit id="file_upload.php.number_of_files">
                                <source>Number of files:</source>
                        </trans-unit>
+                       <trans-unit id="file_rename.exists.title">
+                               <source>File exists already</source>
+                       </trans-unit>
+                       <trans-unit id="file_rename.exists.description">
+                               <source>You want to rename the file "{0}" to "{1}", but such a file already exists. How do you want to proceed?</source>
+                       </trans-unit>
+                       <trans-unit id="file_rename.actions.cancel">
+                               <source>Cancel</source>
+                       </trans-unit>
+                       <trans-unit id="file_rename.actions.rename">
+                               <source>Rename with unique name</source>
+                       </trans-unit>
+                       <trans-unit id="file_rename.actions.override">
+                               <source>Overwrite</source>
+                       </trans-unit>
                        <trans-unit id="file_rename.php.pagetitle">
                                <source>Rename</source>
                        </trans-unit>