From 01663c47e17afdc49eaee757a7cdf6614d89cb04 Mon Sep 17 00:00:00 2001
From: Sven Hartmann <sven.hartmann@aoe.com>
Date: Wed, 1 Jul 2015 14:19:36 +0200
Subject: [PATCH] [!!!][FEATURE] Replace file feature for FAL file list

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>
---
 .../Controller/File/ReplaceFileController.php | 222 ++++++++++++++++++
 .../backend/Modules/File/Replace/index.php    |  17 ++
 .../Private/Templates/file_replace.html       |  34 +++
 typo3/sysext/backend/ext_tables.php           |   5 +
 .../core/Classes/Resource/ResourceStorage.php |  30 ++-
 .../Utility/File/ExtendedFileUtility.php      |  69 ++++++
 .../core/Configuration/TCA/be_groups.php      |   3 +-
 .../core/Configuration/TCA/be_users.php       |   3 +-
 ...-56133-NewBeUserPermissionFilesReplace.rst |  26 ++
 ...56133-ReplaceFileFeatureForFalFileList.rst |  17 ++
 typo3/sysext/core/ext_tables.php              |   1 +
 typo3/sysext/filelist/Classes/FileList.php    |   9 +
 .../Updates/FilesReplacePermissionUpdate.php  | 114 +++++++++
 typo3/sysext/install/ext_localconf.php        |   1 +
 typo3/sysext/lang/locallang_core.xlf          |  18 ++
 typo3/sysext/lang/locallang_tca.xlf           |   3 +
 .../t3skin/Classes/Slot/IconStyleModifier.php |   1 +
 17 files changed, 566 insertions(+), 7 deletions(-)
 create mode 100644 typo3/sysext/backend/Classes/Controller/File/ReplaceFileController.php
 create mode 100644 typo3/sysext/backend/Modules/File/Replace/index.php
 create mode 100644 typo3/sysext/backend/Resources/Private/Templates/file_replace.html
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-56133-NewBeUserPermissionFilesReplace.rst
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-56133-ReplaceFileFeatureForFalFileList.rst
 create mode 100644 typo3/sysext/install/Classes/Updates/FilesReplacePermissionUpdate.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
index 000000000000..7ba501767e99
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Controller/File/ReplaceFileController.php
@@ -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
index 000000000000..fcaeb7677b2b
--- /dev/null
+++ b/typo3/sysext/backend/Modules/File/Replace/index.php
@@ -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
index 000000000000..f656ef2f7b69
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Templates/file_replace.html
@@ -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
diff --git a/typo3/sysext/backend/ext_tables.php b/typo3/sysext/backend/ext_tables.php
index 4401f8fc7c2d..dd50c7e873c0 100644
--- a/typo3/sysext/backend/ext_tables.php
+++ b/typo3/sysext/backend/ext_tables.php
@@ -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',
diff --git a/typo3/sysext/core/Classes/Resource/ResourceStorage.php b/typo3/sysext/core/Classes/Resource/ResourceStorage.php
index f7abdc6689a9..3b1d1df93ad3 100644
--- a/typo3/sysext/core/Classes/Resource/ResourceStorage.php
+++ b/typo3/sysext/core/Classes/Resource/ResourceStorage.php
@@ -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?
@@ -783,6 +783,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.
 	 *
@@ -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') {
diff --git a/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php b/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
index 726ddde9ff23..48fa1b6fa81c 100644
--- a/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
+++ b/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
@@ -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) {
@@ -1024,6 +1029,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
 	 *
diff --git a/typo3/sysext/core/Configuration/TCA/be_groups.php b/typo3/sysext/core/Configuration/TCA/be_groups.php
index 759a45bc89c4..02e52933ee2a 100644
--- a/typo3/sysext/core/Configuration/TCA/be_groups.php
+++ b/typo3/sysext/core/Configuration/TCA/be_groups.php
@@ -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(
diff --git a/typo3/sysext/core/Configuration/TCA/be_users.php b/typo3/sysext/core/Configuration/TCA/be_users.php
index 743d601313ad..ed1bd2a3161c 100644
--- a/typo3/sysext/core/Configuration/TCA/be_users.php
+++ b/typo3/sysext/core/Configuration/TCA/be_users.php
@@ -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
index 000000000000..827fe56d1988
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-56133-NewBeUserPermissionFilesReplace.rst
@@ -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
index 000000000000..d2d41240c2dd
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-56133-ReplaceFileFeatureForFalFileList.rst
@@ -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
diff --git a/typo3/sysext/core/ext_tables.php b/typo3/sysext/core/ext_tables.php
index 230d93dfabb6..a4c8d8f790f1 100644
--- a/typo3/sysext/core/ext_tables.php
+++ b/typo3/sysext/core/ext_tables.php
@@ -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',
diff --git a/typo3/sysext/filelist/Classes/FileList.php b/typo3/sysext/filelist/Classes/FileList.php
index 5ccd7d6cb53f..3816c1a0d3ee 100644
--- a/typo3/sysext/filelist/Classes/FileList.php
+++ b/typo3/sysext/filelist/Classes/FileList.php
@@ -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
index 000000000000..18af8d767104
--- /dev/null
+++ b/typo3/sysext/install/Classes/Updates/FilesReplacePermissionUpdate.php
@@ -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
diff --git a/typo3/sysext/install/ext_localconf.php b/typo3/sysext/install/ext_localconf.php
index d70b9067ad6b..bacd6c21cb5a 100644
--- a/typo3/sysext/install/ext_localconf.php
+++ b/typo3/sysext/install/ext_localconf.php
@@ -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(
diff --git a/typo3/sysext/lang/locallang_core.xlf b/typo3/sysext/lang/locallang_core.xlf
index d26e52d77ef7..5bc0c128630b 100644
--- a/typo3/sysext/lang/locallang_core.xlf
+++ b/typo3/sysext/lang/locallang_core.xlf
@@ -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>
diff --git a/typo3/sysext/lang/locallang_tca.xlf b/typo3/sysext/lang/locallang_tca.xlf
index 56e846983122..c2c733f0e6d1 100644
--- a/typo3/sysext/lang/locallang_tca.xlf
+++ b/typo3/sysext/lang/locallang_tca.xlf
@@ -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>
diff --git a/typo3/sysext/t3skin/Classes/Slot/IconStyleModifier.php b/typo3/sysext/t3skin/Classes/Slot/IconStyleModifier.php
index 02f0e11e5fee..f65d964f0682 100644
--- a/typo3/sysext/t3skin/Classes/Slot/IconStyleModifier.php
+++ b/typo3/sysext/t3skin/Classes/Slot/IconStyleModifier.php
@@ -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',
-- 
GitLab