[!!!][FEATURE] Replace file feature for FAL file list 97/40797/10
authorSven Hartmann <sven.hartmann@aoe.com>
Wed, 1 Jul 2015 12:19:36 +0000 (14:19 +0200)
committerBenjamin Mack <benni@typo3.org>
Tue, 14 Jul 2015 22:05:19 +0000 (00:05 +0200)
Provides a new button "replace" at the extended view in FAL equal to
DAM. Its possible to replace a file
* with a new one -> old file will be overwritten; identifier of the file
object will be kept
* with a new one -> old file will be deleted; identifier of the file
object will be changed to the new filename

The file replacing also respects unique filenames.

To allow editors to replace files the need the "Files: Replace"
permissing needs to be set.

Change-Id: If5882ef620135d4e7238eb8bb56f020304cd1c0c
Resolves: #56133
Releases: master
Reviewed-on: http://review.typo3.org/40797
Reviewed-by: Frans Saris <franssaris@gmail.com>
Tested-by: Frans Saris <franssaris@gmail.com>
Reviewed-by: Benjamin Mack <benni@typo3.org>
Tested-by: Benjamin Mack <benni@typo3.org>
17 files changed:
typo3/sysext/backend/Classes/Controller/File/ReplaceFileController.php [new file with mode: 0644]
typo3/sysext/backend/Modules/File/Replace/index.php [new file with mode: 0644]
typo3/sysext/backend/Resources/Private/Templates/file_replace.html [new file with mode: 0644]
typo3/sysext/backend/ext_tables.php
typo3/sysext/core/Classes/Resource/ResourceStorage.php
typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
typo3/sysext/core/Configuration/TCA/be_groups.php
typo3/sysext/core/Configuration/TCA/be_users.php
typo3/sysext/core/Documentation/Changelog/master/Breaking-56133-NewBeUserPermissionFilesReplace.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-56133-ReplaceFileFeatureForFalFileList.rst [new file with mode: 0644]
typo3/sysext/core/ext_tables.php
typo3/sysext/filelist/Classes/FileList.php
typo3/sysext/install/Classes/Updates/FilesReplacePermissionUpdate.php [new file with mode: 0644]
typo3/sysext/install/ext_localconf.php
typo3/sysext/lang/locallang_core.xlf
typo3/sysext/lang/locallang_tca.xlf
typo3/sysext/t3skin/Classes/Slot/IconStyleModifier.php

diff --git a/typo3/sysext/backend/Classes/Controller/File/ReplaceFileController.php b/typo3/sysext/backend/Classes/Controller/File/ReplaceFileController.php
new file mode 100644 (file)
index 0000000..7ba5017
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+namespace TYPO3\CMS\Backend\Controller\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\Backend\Template\DocumentTemplate;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Backend\Utility\IconUtility;
+use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
+use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * Script Class for the rename-file form
+ */
+class ReplaceFileController {
+
+       /**
+        * Document template object
+        *
+        * @var \TYPO3\CMS\Backend\Template\DocumentTemplate
+        */
+       public $doc;
+
+       /**
+        * Name of the filemount
+        *
+        * @var string
+        */
+       public $title;
+
+       /**
+        * sys_file uid
+        *
+        * @var int
+        */
+       public $uid;
+
+       /**
+        * The file or folder object that should be renamed
+        *
+        * @var \TYPO3\CMS\Core\Resource\ResourceInterface $fileOrFolderObject
+        */
+       protected $fileOrFolderObject;
+
+       /**
+        * Return URL of list module.
+        *
+        * @var string
+        */
+       public $returnUrl;
+
+       /**
+        * Accumulating content
+        *
+        * @var string
+        */
+       public $content;
+
+       /**
+        * Constructor
+        */
+       public function __construct() {
+               $GLOBALS['SOBE'] = $this;
+               $GLOBALS['BACK_PATH'] = '';
+
+               $this->init();
+       }
+
+       /**
+        * Init
+        *
+        * @return void
+        * @throws \RuntimeException
+        * @throws InsufficientFileAccessPermissionsException
+        */
+       protected function init() {
+               // Initialize GPvars:
+               $this->uid = (int) GeneralUtility::_GP('uid');
+
+               $this->returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
+               // Cleaning and checking uid
+               if ($this->uid > 0) {
+                       $this->fileOrFolderObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject('file:' . $this->uid);
+               }
+               if (!$this->fileOrFolderObject) {
+                       $title = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_mod_file_list.xlf:paramError', TRUE);
+                       $message = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_mod_file_list.xlf:targetNoDir', TRUE);
+                       throw new \RuntimeException($title . ': ' . $message, 1436895930);
+               }
+               if ($this->fileOrFolderObject->getStorage()->getUid() === 0) {
+                       throw new InsufficientFileAccessPermissionsException('You are not allowed to access files outside your storages', 1436895931);
+               }
+
+               // If a folder should be renamed, AND the returnURL should go to the old directory name, the redirect is forced
+               // so the redirect will NOT end in a error message
+               // this case only happens if you select the folder itself in the foldertree and then use the clickmenu to
+               // rename the folder
+               if ($this->fileOrFolderObject instanceof Folder) {
+                       $parsedUrl = parse_url($this->returnUrl);
+                       $queryParts = GeneralUtility::explodeUrl2Array(urldecode($parsedUrl['query']));
+                       if ($queryParts['id'] === $this->fileOrFolderObject->getCombinedIdentifier()) {
+                               $this->returnUrl = str_replace(urlencode($queryParts['id']), urlencode($this->fileOrFolderObject->getStorage()->getRootLevelFolder()->getCombinedIdentifier()), $this->returnUrl);
+                       }
+               }
+               // Setting icon and title
+               $icon = IconUtility::getSpriteIcon('apps-filetree-root');
+               $this->title = $icon . htmlspecialchars($this->fileOrFolderObject->getStorage()->getName()) . ': ' . htmlspecialchars($this->fileOrFolderObject->getIdentifier());
+               // Setting template object
+               $this->doc = GeneralUtility::makeInstance(DocumentTemplate::class);
+               $this->doc->setModuleTemplate('EXT:backend/Resources/Private/Templates/file_replace.html');
+               $this->doc->backPath = $GLOBALS['BACK_PATH'];
+               $this->doc->JScode = $this->doc->wrapScriptTags('
+                       function backToList() { //
+                               top.goToModule("file_list");
+                       }
+               ');
+       }
+
+       /**
+        * Main function, rendering the content of the rename form
+        *
+        * @return void
+        */
+       public function main() {
+               // Make page header:
+               $this->content = $this->doc->startPage($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:file_replace.php.pagetitle'));
+               $pageContent = $this->doc->header($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:file_replace.php.pagetitle'));
+               $pageContent .= $this->doc->spacer(5);
+               $pageContent .= $this->doc->divider(5);
+
+               $code = '<form action="' . htmlspecialchars(BackendUtility::getModuleUrl('tce_file')) . '" role="form" method="post" name="editform" enctype="' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['form_enctype'] . '">';
+
+               // Making the formfields for renaming:
+               $code .= '
+                       <div class="form-group">
+                               <input type="checkbox" value="1" id="keepFilename" name="file[replace][1][keepFilename]"> <label for="keepFilename">' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:file_replace.php.keepfiletitle') . '</label>
+                       </div>
+
+                       <div class="form-group">
+                               <label for="file_replace">' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:file_replace.php.selectfile') . '</label>
+                               <div class="input-group col-xs-6">
+                                       <input type="text" name="fakefile" id="fakefile" class="form-control input-xlarge" readonly>
+                                       <a class="input-group-addon btn btn-primary" onclick="TYPO3.jQuery(\'#file_replace\').click();">' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:file_replace.php.browse') . '</a>
+                               </div>
+                               <input class="form-control" type="file" id="file_replace" multiple="false" name="replace_1" style="visibility: hidden;" />
+                       </div>
+
+                       <script>
+                       TYPO3.jQuery(\'#file_replace\').change(function(){
+                               TYPO3.jQuery(\'#fakefile\').val(TYPO3.jQuery(this).val());
+                       });
+                       </script>
+
+                       <input type="hidden" name="overwriteExistingFiles" value="1" />
+                       <input type="hidden" name="file[replace][1][data]" value="1" />
+                       <input type="hidden" name="file[replace][1][uid]" value="' . $this->uid . '" />
+               ';
+               // Making submit button:
+               $code .= '
+                               <div class="form-group">
+                                       <input class="btn btn-primary" type="submit" value="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:file_replace.php.submit', TRUE) . '" />
+                                       <input class="btn btn-danger" type="submit" value="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.cancel', TRUE) . '" onclick="backToList(); return false;" />
+                                       <input type="hidden" name="redirect" value="' . htmlspecialchars($this->returnUrl) . '" />
+                                       ' . \TYPO3\CMS\Backend\Form\FormEngine::getHiddenTokenField('tceAction') . '
+                               </div>
+               ';
+               $code .= '</form>';
+               // Add the HTML as a section:
+               $pageContent .= $code;
+               $docHeaderButtons = array(
+                               'back' => ''
+               );
+               $docHeaderButtons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'file_rename', $GLOBALS['BACK_PATH']);
+               // Back
+               if ($this->returnUrl) {
+                       $docHeaderButtons['back'] = '<a href="' . htmlspecialchars(GeneralUtility::linkThisUrl($this->returnUrl))
+                               . '" class="typo3-goBack" title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.goBack', TRUE) . '">'
+                               . IconUtility::getSpriteIcon('actions-view-go-back')
+                               . '</a>';
+               }
+               // Add the HTML as a section:
+               $markerArray = array(
+                               'CSH' => $docHeaderButtons['csh'],
+                               'FUNC_MENU' => BackendUtility::getFuncMenu($this->id, 'SET[function]', $this->MOD_SETTINGS['function'], $this->MOD_MENU['function']),
+                               'CONTENT' => $pageContent,
+                               'PATH' => $this->title
+               );
+               $this->content .= $this->doc->moduleBody(array(), $docHeaderButtons, $markerArray);
+               $this->content .= $this->doc->endPage();
+               $this->content = $this->doc->insertStylesAndJS($this->content);
+       }
+
+       /**
+        * Outputting the accumulated content to screen
+        *
+        * @return void
+        */
+       public function printContent() {
+               echo $this->content;
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+}
\ No newline at end of file
diff --git a/typo3/sysext/backend/Modules/File/Replace/index.php b/typo3/sysext/backend/Modules/File/Replace/index.php
new file mode 100644 (file)
index 0000000..fcaeb76
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+/*
+ * 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!
+ */
+
+$renameFileController = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(TYPO3\CMS\Backend\Controller\File\ReplaceFileController::class);
+$renameFileController->main();
+$renameFileController->printContent();
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Private/Templates/file_replace.html b/typo3/sysext/backend/Resources/Private/Templates/file_replace.html
new file mode 100644 (file)
index 0000000..f656ef2
--- /dev/null
@@ -0,0 +1,34 @@
+<!-- ###FULLDOC### begin -->
+<div class="typo3-fullDoc">
+       <div id="typo3-docheader">
+               <div class="typo3-docheader-functions">
+                       <div class="left">###CSH### ###FUNC_MENU###</div>
+                       <div class="right">###PATH###</div>
+               </div>
+               <div class="typo3-docheader-buttons">
+                       <div class="left">###BUTTONLIST_LEFT###</div>
+                       <div class="right">###BUTTONLIST_RIGHT###</div>
+               </div>
+       </div>
+
+       <div id="typo3-docbody">
+               <div id="typo3-inner-docbody">
+                       ###CONTENT###
+               </div>
+       </div>
+</div>
+<!-- ###FULLDOC### end -->
+
+<!-- Grouping the icons on top -->
+
+<!-- ###BUTTON_GROUP_WRAP### -->
+<div class="buttongroup">###BUTTONS###</div>
+<!-- ###BUTTON_GROUP_WRAP### -->
+
+<!-- ###BUTTON_GROUPS_LEFT### -->
+<!-- ###BUTTON_GROUP4### -->###BACK###<!-- ###BUTTON_GROUP4### -->
+<!-- ###BUTTON_GROUPS_LEFT### -->
+
+<!-- ###BUTTON_GROUPS_RIGHT### -->
+<!-- ###BUTTON_GROUP1### --><!-- ###BUTTON_GROUP1### -->
+<!-- ###BUTTON_GROUPS_RIGHT### -->
\ No newline at end of file
index 4401f8f..dd50c7e 100644 (file)
@@ -51,6 +51,11 @@ if (TYPO3_MODE === 'BE') {
                \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($_EXTKEY) . 'Modules/File/Rename/'
        );
 
+       \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModulePath(
+               'file_replace',
+               \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($_EXTKEY) . 'Modules/File/Replace/'
+       );
+
        // Register file_rename
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModulePath(
                'file_upload',
index f7abdc6..3b1d1df 100644 (file)
@@ -557,11 +557,11 @@ class ResourceStorage implements ResourceStorageInterface {
                        return FALSE;
                }
                $isReadCheck = FALSE;
-               if (in_array($action, array('read', 'copy', 'move'), TRUE)) {
+               if (in_array($action, array('read', 'copy', 'move', 'replace'), TRUE)) {
                        $isReadCheck = TRUE;
                }
                $isWriteCheck = FALSE;
-               if (in_array($action, array('add', 'write', 'move', 'rename', 'unzip', 'delete'), TRUE)) {
+               if (in_array($action, array('add', 'write', 'move', 'rename', 'replace', 'unzip', 'delete'), TRUE)) {
                        $isWriteCheck = TRUE;
                }
                // Check 3: Does the user have the right to perform the action?
@@ -784,6 +784,25 @@ class ResourceStorage implements ResourceStorageInterface {
        }
 
        /**
+        * Assure replace permission for given file.
+        *
+        * @param FileInterface $file
+        * @return void
+        * @throws Exception\InsufficientFileWritePermissionsException
+        * @throws Exception\InsufficientFolderWritePermissionsException
+        */
+       protected function assureFileReplacePermissions(FileInterface $file) {
+               // Check if user is allowed to replace the file and $file is writable
+               if (!$this->checkFileActionPermission('replace', $file)) {
+                       throw new Exception\InsufficientFileWritePermissionsException('Replacing file "' . $file->getIdentifier() . '" is not allowed.', 1436899571);
+               }
+               // Check if parentFolder is writable for the user
+               if (!$this->checkFolderActionPermission('write', $file->getParentFolder())) {
+                       throw new Exception\InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $file->getIdentifier() . '"', 1436899572);
+               }
+       }
+
+       /**
         * Assures delete permission for given file.
         *
         * @param FileInterface $file
@@ -1737,7 +1756,7 @@ class ResourceStorage implements ResourceStorageInterface {
         * @throws \InvalidArgumentException
         */
        public function replaceFile(FileInterface $file, $localFilePath) {
-               $this->assureFileWritePermissions($file);
+               $this->assureFileReplacePermissions($file);
                if (!$this->checkFileExtensionPermission($localFilePath)) {
                        throw new Exception\IllegalFileExtensionException('Source file extension not allowed.', 1378132239);
                }
@@ -1745,12 +1764,12 @@ class ResourceStorage implements ResourceStorageInterface {
                        throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1325842622);
                }
                $this->emitPreFileReplaceSignal($file, $localFilePath);
-               $result = $this->driver->replaceFile($file->getIdentifier(), $localFilePath);
+               $this->driver->replaceFile($file->getIdentifier(), $localFilePath);
                if ($file instanceof File) {
                        $this->getIndexer()->updateIndexEntry($file);
                }
                $this->emitPostFileReplaceSignal($file, $localFilePath);
-               return $result;
+               return $file;
        }
 
        /**
@@ -1770,6 +1789,7 @@ class ResourceStorage implements ResourceStorageInterface {
                if ($targetFileName === NULL) {
                        $targetFileName = $uploadedFileData['name'];
                }
+               $targetFileName = $this->driver->sanitizeFileName($targetFileName);
 
                $this->assureFileUploadPermissions($localFilePath, $targetFolder, $targetFileName, $uploadedFileData['size']);
                if ($this->hasFileInFolder($targetFileName, $targetFolder) && $conflictMode === 'replace') {
index 726ddde..48fa1b6 100644 (file)
@@ -245,6 +245,9 @@ class ExtendedFileUtility extends BasicFileUtility {
                                                        case 'upload':
                                                                $result[$action][] = $this->func_upload($cmdArr);
                                                                break;
+                                                       case 'replace':
+                                                               $result[$action][] = $this->replaceFile($cmdArr);
+                                                               break;
                                                        case 'unzip':
                                                                $result[$action][] = $this->func_unzip($cmdArr);
                                                                break;
@@ -952,6 +955,8 @@ class ExtendedFileUtility extends BasicFileUtility {
                                $resultObjects[] = $fileObject;
                                $this->internalUploadMap[$uploadPosition] = $fileObject->getCombinedIdentifier();
                                $this->writelog(1, 0, 1, 'Uploading file "%s" to "%s"', array($fileInfo['name'], $targetFolderObject->getIdentifier()));
+                       } catch (\TYPO3\CMS\Core\Resource\Exception\InsufficientFileWritePermissionsException $e) {
+                               $this->writelog(1, 1, 107, 'You are not allowed to override "%s"!', array($fileInfo['name']));
                        } catch (\TYPO3\CMS\Core\Resource\Exception\UploadException $e) {
                                $this->writelog(1, 2, 106, 'The upload has failed, no uploaded file found!', '');
                        } catch (\TYPO3\CMS\Core\Resource\Exception\InsufficientUserPermissionsException $e) {
@@ -1025,6 +1030,70 @@ class ExtendedFileUtility extends BasicFileUtility {
        }
 
        /**
+        * Replaces a file on the filesystem and changes the identifier of the persisted file object in sys_file if keepFilename
+        * is not checked. If keepFilename is checked, only the file content will be replaced.
+        *
+        * @param array $cmdArr
+        * @return array|bool
+        * @throws \TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException
+        * @throws \TYPO3\CMS\Core\Resource\Exception\InvalidFileException
+        */
+       protected function replaceFile(array $cmdArr) {
+               if (!$this->isInit) {
+                       return FALSE;
+               }
+
+               $uploadPosition = $cmdArr['data'];
+               $fileInfo = $_FILES['replace_' . $uploadPosition];
+               if (empty($fileInfo['name'])) {
+                       $this->writelog(1, 2, 108, 'No file was uploaded for replacing!', '');
+                       return FALSE;
+               }
+
+               $keepFileName = ($cmdArr['keepFilename'] == 1) ? TRUE : FALSE;
+               $resultObjects = array();
+
+               try {
+                       $fileObjectToReplace = $this->getFileObject($cmdArr['uid']);
+                       $folder = $fileObjectToReplace->getParentFolder();
+                       $resourceStorage = $fileObjectToReplace->getStorage();
+
+                       $fileObject = $resourceStorage->addUploadedFile($fileInfo, $folder, $fileObjectToReplace->getName(), 'replace');
+
+                       // Check if there is a file that is going to be uploaded that has a different name as the replacing one
+                       // but exists in that folder as well.
+                       // rename to another name, but check if the name is already given
+                       if ($keepFileName === FALSE) {
+                               // if a file with the same name already exists, we need to change it to _01 etc.
+                               // if the file does not exist, we can do a simple rename
+                               $resourceStorage->moveFile($fileObject, $folder, $fileInfo['name'], 'renameNewFile');
+                       }
+
+                       $resultObjects[] = $fileObject;
+                       $this->internalUploadMap[$uploadPosition] = $fileObject->getCombinedIdentifier();
+
+                       $this->writelog(1, 0, 1, 'Replacing file "%s" to "%s"', array($fileInfo['name'], $fileObjectToReplace->getIdentifier()));
+               } catch (\TYPO3\CMS\Core\Resource\Exception\InsufficientFileWritePermissionsException $e) {
+                       $this->writelog(1, 1, 107, 'You are not allowed to override "%s"!', array($fileInfo['name']));
+               } catch (\TYPO3\CMS\Core\Resource\Exception\UploadException $e) {
+                       $this->writelog(1, 2, 106, 'The upload has failed, no uploaded file found!', '');
+               } catch (\TYPO3\CMS\Core\Resource\Exception\InsufficientUserPermissionsException $e) {
+                       $this->writelog(1, 1, 105, 'You are not allowed to upload files!', '');
+               } catch (\TYPO3\CMS\Core\Resource\Exception\UploadSizeException $e) {
+                       $this->writelog(1, 1, 104, 'The uploaded file "%s" exceeds the size-limit', array($fileInfo['name']));
+               } catch (\TYPO3\CMS\Core\Resource\Exception\InsufficientFolderWritePermissionsException $e) {
+                       $this->writelog(1, 1, 103, 'Destination path "%s" was not within your mountpoints!', array($fileObjectToReplace->getIdentifier()));
+               } catch (\TYPO3\CMS\Core\Resource\Exception\IllegalFileExtensionException $e) {
+                       $this->writelog(1, 1, 102, 'Extension of file name "%s" is not allowed in "%s"!', array($fileInfo['name'], $fileObjectToReplace->getIdentifier()));
+               } catch (\TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException $e) {
+                       $this->writelog(1, 1, 101, 'No unique filename available in "%s"!', array($fileObjectToReplace->getIdentifier()));
+               } catch (\RuntimeException $e) {
+                       throw $e;
+               }
+               return $resultObjects;
+       }
+
+       /**
         * Add flash message to message queue
         *
         * @param FlashMessage $flashMessage
index 759a45b..02e5293 100644 (file)
@@ -121,6 +121,7 @@ return array(
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_write', 'writeFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_add', 'addFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_rename', 'renameFile', 'mimetypes-other-other'),
+                                       array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_replace', 'replaceFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_move', 'moveFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_copy', 'copyFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.fileoper_perms_unzip', 'unzipFile', 'mimetypes-other-other'),
@@ -129,7 +130,7 @@ return array(
                                'renderMode' => 'checkbox',
                                'size' => 17,
                                'maxitems' => 17,
-                               'default' => 'readFolder,writeFolder,addFolder,renameFolder,moveFolder,deleteFolder,readFile,writeFile,addFile,renameFile,moveFile,files_copy,deleteFile'
+                               'default' => 'readFolder,writeFolder,addFolder,renameFolder,moveFolder,deleteFolder,readFile,writeFile,addFile,renameFile,replaceFile,moveFile,files_copy,deleteFile'
                        )
                ),
                'workspace_perms' => array(
index 743d601..ed1bd2a 100644 (file)
@@ -257,6 +257,7 @@ return array(
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_write', 'writeFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_add', 'addFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_rename', 'renameFile', 'mimetypes-other-other'),
+                                       array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_replace', 'replaceFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_move', 'moveFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.file_permissions.files_copy', 'copyFile', 'mimetypes-other-other'),
                                        array('LLL:EXT:lang/locallang_tca.xlf:be_groups.fileoper_perms_unzip', 'unzipFile', 'mimetypes-other-other'),
@@ -265,7 +266,7 @@ return array(
                                'renderMode' => 'checkbox',
                                'size' => 17,
                                'maxitems' => 17,
-                               'default' => 'readFolder,writeFolder,addFolder,renameFolder,moveFolder,deleteFolder,readFile,writeFile,addFile,renameFile,moveFile,files_copy,deleteFile'
+                               'default' => 'readFolder,writeFolder,addFolder,renameFolder,moveFolder,deleteFolder,readFile,writeFile,addFile,renameFile,replaceFile,moveFile,files_copy,deleteFile'
                        )
                ),
                'workspace_perms' => array(
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-56133-NewBeUserPermissionFilesReplace.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-56133-NewBeUserPermissionFilesReplace.rst
new file mode 100644 (file)
index 0000000..827fe56
--- /dev/null
@@ -0,0 +1,26 @@
+==========================================================
+Breaking: #56133 - New BE user permission "Files: replace"
+==========================================================
+
+Description
+===========
+
+A new feature was introduced to replace files in the file list. For this feature an new permission was introduce "Files: replace". This permission is now also checked when a BE user uploads a file with the same name.  introducing proper handling of double quotes in link titles (TypoLink fields) the processing of the link title is adjusted. Escaping will be done automatically now.
+
+
+Impact
+======
+
+BE users need the permission "Files: replace" before they are allowed to replace a file by uploading a file with the same name.
+
+
+Affected Installations
+======================
+
+All installations.
+
+
+Migration
+=========
+
+A upgrade wizard was added to set this permission for all BE users that already are allowed to write files as this was the old permissions check.
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-56133-ReplaceFileFeatureForFalFileList.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-56133-ReplaceFileFeatureForFalFileList.rst
new file mode 100644 (file)
index 0000000..d2d4124
--- /dev/null
@@ -0,0 +1,17 @@
+========================================================
+Feature: #56133 - Replace file feature for fal file list
+========================================================
+
+Description
+===========
+
+Now its possible to replace files for a specific record at the extended view in the FAL record list.
+
+Impact
+======
+
+Provides a new button "replace" at the extended view in FAL equal to DAM. Its possible to replace a file
+* with a new one -> old file will be overwritten; identifier of the file object will be kept
+* with a new one -> old file will be deleted; identifier of the file object will be changed to the new filename
+
+The file replacing also respects unique filenames.
\ No newline at end of file
index 230d93d..a4c8d8f 100644 (file)
@@ -215,6 +215,7 @@ $GLOBALS['TBE_STYLES']['spriteIconApi']['coreSpriteImageNames'] = array(
        'actions-edit-merge-localization',
        'actions-edit-pick-date',
        'actions-edit-rename',
+       'actions-edit-replace',
        'actions-edit-restore',
        'actions-edit-undelete-edit',
        'actions-edit-undo',
index 5ccd7d6..3816c1a 100644 (file)
@@ -866,6 +866,7 @@ class FileList extends AbstractRecordList {
        public function makeEdit($fileOrFolderObject) {
                $cells = array();
                $fullIdentifier = $fileOrFolderObject->getCombinedIdentifier();
+
                // Edit file content (if editable)
                if ($fileOrFolderObject instanceof File && $fileOrFolderObject->checkActionPermission('write') && GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'], $fileOrFolderObject->getExtension())) {
                        $url = BackendUtility::getModuleUrl('file_edit', array('target' => $fullIdentifier));
@@ -885,6 +886,14 @@ class FileList extends AbstractRecordList {
                } else {
                        $cells['view'] = $this->spaceIcon;
                }
+
+               // replace file
+               if ($fileOrFolderObject instanceof File && $fileOrFolderObject->checkActionPermission('replace')) {
+                       $url = BackendUtility::getModuleUrl('file_replace', array('target' => $fullIdentifier, 'uid' => $fileOrFolderObject->getUid()));
+                       $replaceOnClick = 'top.content.list_frame.location.href = ' . GeneralUtility::quoteJSvalue($url) . '+\'&returnUrl=\'+top.rawurlencode(top.content.list_frame.document.location.pathname+top.content.list_frame.document.location.search);return false;';
+                       $cells['replace'] = '<a href="#" class="btn btn-default" onclick="' . $replaceOnClick . '"  title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:cm.replace') . '">' . IconUtility::getSpriteIcon('actions-edit-replace') . '</a>';
+               }
+
                // rename the file
                if ($fileOrFolderObject->checkActionPermission('rename')) {
                        $url = BackendUtility::getModuleUrl('file_rename', array('target' => $fullIdentifier));
diff --git a/typo3/sysext/install/Classes/Updates/FilesReplacePermissionUpdate.php b/typo3/sysext/install/Classes/Updates/FilesReplacePermissionUpdate.php
new file mode 100644 (file)
index 0000000..18af8d7
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+namespace TYPO3\CMS\Install\Updates;
+
+/*
+ * 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!
+ */
+
+/**
+ * Upgrade wizard which goes through all users and groups and set the "replaceFile" permission if "writeFile" is set
+ */
+class FilesReplacePermissionUpdate extends AbstractUpdate {
+
+       /**
+        * @var string
+        */
+       protected $title = 'Set the "Files:replace" permission for all BE user/groups with "Files:write" set';
+
+       /**
+        * Checks whether updates are required.
+        *
+        * @param string &$description The description for the update
+        * @return bool Whether an update is required (TRUE) or not (FALSE)
+        */
+       public function checkForUpdate(&$description) {
+               $description = 'A new file permission was introduced regarding replacing files.' .
+                       ' This update sets "Files:replace" for all BE users/groups with the permission "Files:write".';
+               $updateNeeded = FALSE;
+               $db = $this->getDatabaseConnection();
+
+               // Fetch user records where the writeFile is set and replaceFile is not
+               $notMigratedRowsCount = $db->exec_SELECTcountRows(
+                       'uid',
+                       'be_users',
+                       $this->getWhereClause()
+               );
+               if ($notMigratedRowsCount > 0) {
+                       $updateNeeded = TRUE;
+               }
+
+               if (!$updateNeeded) {
+                       // Fetch group records where the writeFile is set and replaceFile is not
+                       $notMigratedRowsCount = $db->exec_SELECTcountRows(
+                               'uid',
+                               'be_groups',
+                               $this->getWhereClause()
+                       );
+                       if ($notMigratedRowsCount > 0) {
+                               $updateNeeded = TRUE;
+                       }
+               }
+               return $updateNeeded;
+       }
+
+       /**
+        * Performs the accordant updates.
+        *
+        * @param array &$dbQueries Queries done in this update
+        * @param mixed &$customMessages Custom messages
+        * @return bool Whether everything went smoothly or not
+        */
+       public function performUpdate(array &$dbQueries, &$customMessages) {
+               $db = $this->getDatabaseConnection();
+
+               // Iterate over users and groups table to perform permission updates
+               $tablesToProcess = ['be_groups', 'be_users'];
+               foreach ($tablesToProcess as $table) {
+                       $records = $this->getRecordsFromTable($table);
+                       foreach ($records as $singleRecord) {
+                               $updateArray = [
+                                       'file_permissions' => $singleRecord['file_permissions'] . ',replaceFile'
+                               ];
+                               $db->exec_UPDATEquery($table, 'uid=' . (int)$singleRecord['uid'], $updateArray);
+                               // Get last executed query
+                               $dbQueries[] = str_replace(chr(10), ' ', $db->debug_lastBuiltQuery);
+                               // Check for errors
+                               if ($db->sql_error()) {
+                                       $customMessages = 'SQL-ERROR: ' . htmlspecialchars($db->sql_error());
+                                       return FALSE;
+                               }
+                       }
+               }
+               return TRUE;
+       }
+
+       /**
+        * Retrieve every record which needs to be processed
+        *
+        * @param string $table
+        * @return array
+        */
+       protected function getRecordsFromTable($table) {
+               $fields = implode(',', array('uid', 'file_permissions'));
+               $records = $this->getDatabaseConnection()->exec_SELECTgetRows($fields, $table, $this->getWhereClause());
+               return $records;
+       }
+
+       /**
+        * Returns the where clause for database requests
+        *
+        * @return string
+        */
+       protected function getWhereClause() {
+               return 'file_permissions LIKE "%writeFile%" AND file_permissions LIKE "%replaceFile%"';
+       }
+}
\ No newline at end of file
index d70b906..bacd6c2 100644 (file)
@@ -7,6 +7,7 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['languageIsoC
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['PageShortcutParent'] = \TYPO3\CMS\Install\Updates\PageShortcutParentUpdate::class;
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['backendShortcuts'] = \TYPO3\CMS\Install\Updates\MigrateShortcutUrlsUpdate::class;
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['processedFilesChecksum'] = \TYPO3\CMS\Install\Updates\ProcessedFileChecksumUpdate::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['filesReplacePermission'] = \TYPO3\CMS\Install\Updates\FilesReplacePermissionUpdate::class;
 
 $signalSlotDispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class);
 $signalSlotDispatcher->connect(
index d26e52d..5bc0c12 100644 (file)
@@ -517,6 +517,21 @@ Do you want to continue WITHOUT saving?</source>
                        <trans-unit id="file_rename.php.submit">
                                <source>Rename</source>
                        </trans-unit>
+                       <trans-unit id="file_replace.php.pagetitle">
+                               <source>Replace</source>
+                       </trans-unit>
+                       <trans-unit id="file_replace.php.selectfile">
+                               <source>Select new file</source>
+                       </trans-unit>
+                       <trans-unit id="file_replace.php.keepfiletitle">
+                               <source>Keep the current filename?</source>
+                       </trans-unit>
+                       <trans-unit id="file_replace.php.browse">
+                               <source>Browse</source>
+                       </trans-unit>
+                       <trans-unit id="file_replace.php.submit">
+                               <source>Replace</source>
+                       </trans-unit>
                        <trans-unit id="file_edit.php.pagetitle">
                                <source>Edit</source>
                        </trans-unit>
@@ -874,6 +889,9 @@ Would you like to save now in order to refresh the display?</source>
                        <trans-unit id="cm.rename">
                                <source>Rename</source>
                        </trans-unit>
+                       <trans-unit id="cm.replace">
+                               <source>Replace</source>
+                       </trans-unit>
                        <trans-unit id="cm.open">
                                <source>Open</source>
                        </trans-unit>
index 56e8469..c2c733f 100644 (file)
@@ -99,6 +99,9 @@
                        <trans-unit id="be_users.file_permissions.files_rename">
                                <source>Files: Rename</source>
                        </trans-unit>
+                       <trans-unit id="be_groups.file_permissions.files_replace">
+                               <source>Files: Replace</source>
+                       </trans-unit>
                        <trans-unit id="be_users.file_permissions.files_move">
                                <source>Files: Move</source>
                        </trans-unit>
index 02f0e11..f65d964 100644 (file)
@@ -46,6 +46,7 @@ class IconStyleModifier {
                't3-icon t3-icon-actions t3-icon-actions-document t3-icon-document-paste-after' => 'fa-clipboard',
                't3-icon t3-icon-actions t3-icon-actions-edit t3-icon-edit-pick-date' => 'fa-calendar',
                't3-icon t3-icon-actions t3-icon-actions-edit t3-icon-edit-rename' => 'fa-quote-right',
+               't3-icon t3-icon-actions t3-icon-actions-edit t3-icon-edit-replace' => 'fa-retweet',
                't3-icon t3-icon-actions t3-icon-actions-edit t3-icon-edit-undo' => 'fa-undo',
                't3-icon t3-icon-actions t3-icon-actions-edit t3-icon-edit-unhide' => 'fa-toggle-off warning',
                't3-icon t3-icon-actions t3-icon-actions-edit t3-icon-edit-upload' => 'fa-upload',