diff --git a/Build/tsconfig.json b/Build/tsconfig.json index 0aa6ead6ceea8a3e3dc52b8a44002988c2df9163..605d77a2069a88aad698d7c640ff36c2e7d658e4 100644 --- a/Build/tsconfig.json +++ b/Build/tsconfig.json @@ -171,6 +171,7 @@ "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 diff --git a/typo3/sysext/backend/Classes/Controller/File/FileController.php b/typo3/sysext/backend/Classes/Controller/File/FileController.php index 131414e5162478c7c5e3a12be0312d8786291f42..f2ffb3449b51264c35818ff771cdac471b809074 100644 --- a/typo3/sysext/backend/Classes/Controller/File/FileController.php +++ b/typo3/sysext/backend/Classes/Controller/File/FileController.php @@ -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); diff --git a/typo3/sysext/backend/Classes/Controller/File/RenameFileController.php b/typo3/sysext/backend/Classes/Controller/File/RenameFileController.php index d0122d6c05d14f78959bd8b932fedd286b8d42de..d73e87aea674824595cbb7add3c3fdff65d8fd20 100644 --- a/typo3/sysext/backend/Classes/Controller/File/RenameFileController.php +++ b/typo3/sysext/backend/Classes/Controller/File/RenameFileController.php @@ -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')]); diff --git a/typo3/sysext/backend/Resources/Private/Templates/File/RenameFile.html b/typo3/sysext/backend/Resources/Private/Templates/File/RenameFile.html index 91d739a241e3330477d4a6602e85e313ed9b5e6c..531a156d2d60a92eab4ea775ad49acb026fe98e9 100644 --- a/typo3/sysext/backend/Resources/Private/Templates/File/RenameFile.html +++ b/typo3/sysext/backend/Resources/Private/Templates/File/RenameFile.html @@ -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 index 0000000000000000000000000000000000000000..7661835e72d5109433b5bdbdfbc0f576b7af6481 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/RenameFile.ts @@ -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 index 0000000000000000000000000000000000000000..add74a0e54d3fc2a12e05c7a4925971db3041da1 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/RenameFile.js @@ -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(); +}); diff --git a/typo3/sysext/core/Classes/Resource/AbstractFile.php b/typo3/sysext/core/Classes/Resource/AbstractFile.php index 46ef39ade1f2bc49a0fd9634b009f5c3500a3287..d90feec97e5d80336ac127f4ecf9a832ffe430fa 100644 --- a/typo3/sysext/core/Classes/Resource/AbstractFile.php +++ b/typo3/sysext/core/Classes/Resource/AbstractFile.php @@ -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); } /** diff --git a/typo3/sysext/core/Classes/Resource/FileInterface.php b/typo3/sysext/core/Classes/Resource/FileInterface.php index e93f20d00a6e66c44401094bbf8c2f053fdf6306..99b3e30499aeb870deaac246eb24f4b5aa8582f3 100644 --- a/typo3/sysext/core/Classes/Resource/FileInterface.php +++ b/typo3/sysext/core/Classes/Resource/FileInterface.php @@ -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 diff --git a/typo3/sysext/core/Classes/Resource/FileReference.php b/typo3/sysext/core/Classes/Resource/FileReference.php index 66d23c48fe47bd6a09cb7d73bdedb085754c5277..5f6e4a358884531271b2ac4457b651508fc3fbc0 100644 --- a/typo3/sysext/core/Classes/Resource/FileReference.php +++ b/typo3/sysext/core/Classes/Resource/FileReference.php @@ -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. diff --git a/typo3/sysext/core/Classes/Resource/ResourceStorage.php b/typo3/sysext/core/Classes/Resource/ResourceStorage.php index f9a82026519469b53a925e8c2f56778e3b9152ea..cb776432bc6a2ca8d8ce45eddcfd30cf6fc533a4 100644 --- a/typo3/sysext/core/Classes/Resource/ResourceStorage.php +++ b/typo3/sysext/core/Classes/Resource/ResourceStorage.php @@ -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) { @@ -3021,6 +3031,14 @@ class ResourceStorage implements ResourceStorageInterface return $this->isDefault; } + /** + * @return ResourceFactory + */ + public function getResourceFactoryInstance(): ResourceFactory + { + return ResourceFactory::getInstance(); + } + /** * Returns the current BE user. * diff --git a/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php b/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php index aa839d3f1993688bf248459873eb322cb3554e7e..6691b0ea88bac3617d7ef5e2090412bfb82fd6c5 100644 --- a/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php +++ b/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php @@ -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!', []); diff --git a/typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php b/typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php index 8f17fab4be38d7d5b8cd59428add116c46d723d0..bb89482bb420366613cf71b03f0fa5bd398a360f 100644 --- a/typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php +++ b/typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php @@ -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); + } } diff --git a/typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf b/typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf index 149ec8bdcfb038534a3feede94f393b33c1e23bc..3c36e016b14643867a15dc9d9feb869ec0e87928 100644 --- a/typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf +++ b/typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf @@ -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>