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 0000000000000000000000000000000000000000..7ba501767e99919263a420b56e80567741b1b395 --- /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 0000000000000000000000000000000000000000..fcaeb7677b2b4ab726dbebc611c3c7b91e2363cc --- /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 0000000000000000000000000000000000000000..f656ef2f7b69c4e901bcf7f104d9f105b576810a --- /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 4401f8fc7c2da831e43e7677502e38f20c3cc487..dd50c7e873c08079e22fa5732fc846a068df65e0 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 f7abdc6689a9a3e4b3003d459430f3753909f6c9..3b1d1df93ad33318428852fc9bc76b2b19d7f734 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 726ddde9ff23305e7e27a6a96c5261bc3bab3458..48fa1b6fa81c33c5c9629c8e05a044466d69a597 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 759a45bc89c43b5584f259e89ff03dd5bae94229..02e52933ee2aebe5be4222ee897d365f038844f7 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 743d601313adc02c784c4bf80e0d19e455890430..ed1bd2a3161c35562238d583bf2bafc801ff24a1 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 0000000000000000000000000000000000000000..827fe56d19889441e5f27abd5a1308ffefaac3b1 --- /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 0000000000000000000000000000000000000000..d2d41240c2ddea2108fdc9d146ddc355e1cfc3ee --- /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 230d93dfabb67183cb5ddc2612696517e2a5f571..a4c8d8f790f14497eedadca98fab95b305396e42 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 5ccd7d6cb53f06e00e5db6205f051c0451e06e4a..3816c1a0d3eef5c1c490ba6bbd383abe52fad5b8 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 0000000000000000000000000000000000000000..18af8d76710462c54daaa1c872fba8c685e9ed03 --- /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 d70b9067ad6b2481e5e4c930935e29027f3e2a08..bacd6c21cb5a545903e106a4f7feb20a03dc5fd8 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 d26e52d77ef7736e393eb3ed2d6ceb8decef0c3b..5bc0c128630b151392b3f4574636574c5b4e6a37 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 56e846983122492f16495f41b39c3ada59a0d89a..c2c733f0e6d1bac9abb58ec05ee1b11a17e9c61e 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 02f0e11e5feef74ebc792c4665a581c13bb2c085..f65d964f06820f4d8b2bb0185c9b150adb351e05 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',