false, 'readFile' => false, 'writeFile' => false, 'copyFile' => false, 'moveFile' => false, 'renameFile' => false, 'deleteFile' => false, // Folder permissions 'addFolder' => false, 'readFolder' => false, 'writeFolder' => false, 'copyFolder' => false, 'moveFolder' => false, 'renameFolder' => false, 'deleteFolder' => false, 'recursivedeleteFolder' => false, ]; /** * Will contain map between upload ID and the final filename */ public array $internalUploadMap = []; /** * Container for FlashMessages so they can be localized * * @var FlashMessage[] */ protected array $flashMessages = []; protected array $fileCmdMap = []; /** * @var array> */ protected array $uploadedFiles = []; protected ResourceFactory $fileFactory; /** * Get existingFilesConflictMode */ public function getExistingFilesConflictMode(): string { return $this->existingFilesConflictMode->value; } /** * Set existingFilesConflictMode */ public function setExistingFilesConflictMode(DuplicationBehavior $existingFilesConflictMode): void { $this->existingFilesConflictMode = $existingFilesConflictMode; } /** * Initialization of the class * * @param array $fileCmds Array with the commands to execute. See "TYPO3 Core API" document */ public function start(array $fileCmds, array $uploadedFiles): void { // Initialize Object Factory $this->fileFactory = GeneralUtility::makeInstance(ResourceFactory::class); // Initializing file processing commands: $this->fileCmdMap = $fileCmds; $this->uploadedFiles = $uploadedFiles; } /** * Sets the file action permissions. * If no argument is given, permissions of the currently logged in backend user are taken into account. * * @param array $permissions File Permissions. */ public function setActionPermissions(array $permissions = []) { if (empty($permissions)) { $permissions = $this->getBackendUser()->getFilePermissions(); } $this->actionPerms = $permissions; } /** * Processing the command array in $this->fileCmdMap * * @return mixed FALSE, if the file functions were not initialized * @throws \UnexpectedValueException */ public function processData() { $result = []; if ($this->fileCmdMap !== []) { // Check if there were uploads expected, but no one made if ($this->fileCmdMap['upload'] ?? false) { $uploads = $this->fileCmdMap['upload']; foreach ($uploads as $upload) { $uploadedFileIndex = 'upload_' . $upload['data']; if (!$this->uploadedFileHasClientName($this->uploadedFiles[$uploadedFileIndex] ?? null)) { unset($this->fileCmdMap['upload'][$upload['data']]); } } if (empty($this->fileCmdMap['upload'])) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'No file was uploaded'); $this->addMessageToFlashMessageQueue('FileUtility.NoFileWasUploaded'); } } // Check if there were new folder names expected, but non given if ($this->fileCmdMap['newfolder'] ?? false) { foreach ($this->fileCmdMap['newfolder'] as $key => $cmdArr) { if ((string)($cmdArr['data'] ?? '') === '') { unset($this->fileCmdMap['newfolder'][$key]); } } if (empty($this->fileCmdMap['newfolder'])) { $this->writeLog(SystemLogFileAction::NEW_FOLDER, SystemLogErrorClassification::USER_ERROR, 'No name was provided for the new folder'); $this->addMessageToFlashMessageQueue('FileUtility.NoNameForNewFolderGiven'); } } // Traverse each set of actions foreach ($this->fileCmdMap as $action => $actionData) { // Traverse all action data. More than one file might be affected at the same time. if (is_array($actionData)) { $result[$action] = []; // We reset the array keys of $actionData to keep track of the corresponding // result, while not changing the previous behaviour of $result[$action][]. foreach (array_values($actionData) as $key => $cmdArr) { // Clear file stats clearstatcache(); // Branch out based on command: switch ($action) { case 'delete': $result[$action][$key] = $this->func_delete($cmdArr); break; case 'copy': $result[$action][$key] = $this->func_copy($cmdArr); break; case 'move': $result[$action][$key] = $this->func_move($cmdArr); break; case 'rename': $result[$action][$key] = $this->func_rename($cmdArr); break; case 'newfolder': $result[$action][$key] = $this->func_newfolder($cmdArr); break; case 'newfile': $result[$action][$key] = $this->func_newfile($cmdArr); break; case 'editfile': $result[$action][$key] = $this->func_edit($cmdArr); break; case 'upload': $result[$action][$key] = $this->func_upload($cmdArr); break; case 'replace': $result[$action][$key] = $this->replaceFile($cmdArr); break; } GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch( new AfterFileCommandProcessedEvent([$action => $cmdArr], $result[$action][$key], $this->existingFilesConflictMode->value) ); } } } } return $result; } /** * @param int $action The action number. See the functions in the class for a hint. Eg. edit is '9', upload is '1' ... * @param int $severity The severity: 0 = message, 1 = error, 2 = System Error, 3 = security notice (admin) * @param string $message This is the default, raw error message in english * @param array $context Additional information when the log is shown */ protected function writeLog(int $action, int $severity, string $message, array $context = []): void { $this->getBackendUser()->writelog(SystemLogType::FILE, $action, $severity, null, $message, $context); } /** * Adds a localized FlashMessage to the message queue * * @param string $localizationKey * @param ContextualFeedbackSeverity $severity * @throws \InvalidArgumentException */ protected function addMessageToFlashMessageQueue($localizationKey, array $replaceMarkers = [], ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::ERROR) { if ($this->isBackendScope()) { $label = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/fileMessages.xlf:' . $localizationKey); $message = vsprintf($label, $replaceMarkers); $flashMessage = new FlashMessage( $message, '', $severity, true ); $this->addFlashMessage($flashMessage); } } protected function addEvaluationResultHintsToFlashMessageQueue( ResultException $exception, ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::ERROR, ): void { if (!$this->isBackendScope()) { return; } foreach ($exception->messages as $messageItem) { $message = $messageItem->labelBag?->compile($this->getLanguageService()) ?? $messageItem->message; $flashMessage = new FlashMessage( $message, '', $severity, true ); $this->addFlashMessage($flashMessage); } } /************************************* * * File operation functions * **************************************/ /** * Deleting files and folders (action=4) * * @param array $cmds $cmds['data'] is the file/folder to delete * @return bool Returns TRUE upon success */ public function func_delete(array $cmds) { $result = false; // Example identifier for $cmds['data'] => "4:mypath/tomyfolder/myfile.jpg" // for backwards compatibility: the combined file identifier was the path+filename try { $fileObject = $this->getFileObject($cmds['data']); } catch (ResourceDoesNotExistException $e) { $flashMessage = new FlashMessage( sprintf( $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.description.fileNotFound'), $cmds['data'] ), $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.header.fileNotFound'), ContextualFeedbackSeverity::ERROR, true ); $this->addFlashMessage($flashMessage); return false; } // checks to delete the file if ($fileObject instanceof File) { // check if the file still has references // Exclude sys_file_metadata records as these are no use references $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex'); $refIndexRecords = $queryBuilder ->select('tablename', 'recuid', 'ref_uid') ->from('sys_refindex') ->where( $queryBuilder->expr()->eq( 'ref_table', $queryBuilder->createNamedParameter('sys_file') ), $queryBuilder->expr()->eq( 'ref_uid', $queryBuilder->createNamedParameter($fileObject->getUid(), Connection::PARAM_INT) ), $queryBuilder->expr()->neq( 'tablename', $queryBuilder->createNamedParameter('sys_file_metadata') ) ) ->executeQuery() ->fetchAllAssociative(); $deleteFile = true; if (!empty($refIndexRecords)) { $shortcutContent = []; $brokenReferences = []; foreach ($refIndexRecords as $fileReferenceRow) { if ($fileReferenceRow['tablename'] === 'sys_file_reference') { $row = $this->transformFileReferenceToRecordReference($fileReferenceRow); if ($row === null) { $brokenReferences[] = $fileReferenceRow['ref_uid']; continue; } $shortcutRecord = BackendUtility::getRecord($row['tablename'], $row['recuid']); if ($shortcutRecord) { $shortcutContent[] = '[record:' . $row['tablename'] . ':' . $row['recuid'] . ']'; } else { $brokenReferences[] = $fileReferenceRow['ref_uid']; } } else { $shortcutContent[] = '[record:' . $fileReferenceRow['tablename'] . ':' . $fileReferenceRow['recuid'] . ']'; } } if (!empty($brokenReferences)) { // render a message that the file has broken references $flashMessage = new FlashMessage( sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.description.fileHasBrokenReferences'), count($brokenReferences)), $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.header.fileHasBrokenReferences'), ContextualFeedbackSeverity::INFO, true ); $this->addFlashMessage($flashMessage); } if (!empty($shortcutContent)) { // render a message that the file could not be deleted $flashMessage = new FlashMessage( sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.description.fileNotDeletedHasReferences'), $fileObject->getName()) . ' ' . implode(', ', $shortcutContent), $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.header.fileNotDeletedHasReferences'), ContextualFeedbackSeverity::WARNING, true ); $this->addFlashMessage($flashMessage); $deleteFile = false; } } if ($deleteFile) { try { $result = $fileObject->delete(); // show the user that the file was deleted $flashMessage = new FlashMessage( sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.description.fileDeleted'), $fileObject->getName()), $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.header.fileDeleted'), ContextualFeedbackSeverity::OK, true ); $this->addFlashMessage($flashMessage); // Log success $this->writeLog(SystemLogFileAction::DELETE, SystemLogErrorClassification::MESSAGE, 'File "{identifier}" deleted', ['identifier' => $fileObject->getIdentifier()]); } catch (InsufficientFileAccessPermissionsException $e) { $this->writeLog(SystemLogFileAction::DELETE, SystemLogErrorClassification::USER_ERROR, 'Access denied for file "{identifier}" due to insufficient permissions', ['identifier' => $fileObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToAccessTheFile', [$fileObject->getIdentifier()]); } catch (NotInMountPointException $e) { $this->writeLog(SystemLogFileAction::DELETE, SystemLogErrorClassification::USER_ERROR, 'The file or folder {destination} was not accessible within permitted mountpoints', ['identifier' => $fileObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.TargetWasNotWithinYourMountpoints', [$fileObject->getIdentifier()]); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::DELETE, SystemLogErrorClassification::USER_ERROR, 'Delete failed for file "{identifier}": insufficient write permissions', ['identifier' => $fileObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.CouldNotDeleteFile', [$fileObject->getIdentifier()]); } } } else { if ($fileObject instanceof Folder && !$this->folderHasFilesInUse($fileObject)) { try { $result = $fileObject->delete(true); if ($result) { // notify the user that the folder was deleted $flashMessage = new FlashMessage( sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.description.folderDeleted'), $fileObject->getName()), $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.header.folderDeleted'), ContextualFeedbackSeverity::OK, true ); $this->addFlashMessage($flashMessage); // Log success $this->writeLog(SystemLogFileAction::DELETE, SystemLogErrorClassification::MESSAGE, 'Directory "{identifier}" deleted', ['identifier' => $fileObject->getIdentifier()]); } } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::DELETE, SystemLogErrorClassification::USER_ERROR, 'Delete failed for directory "{identifier}": recursive deletion is not supported', ['identifier' => $fileObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.CouldNotDeleteDirectory', [$fileObject->getIdentifier()]); } catch (InsufficientFolderAccessPermissionsException $e) { $this->writeLog(SystemLogFileAction::DELETE, SystemLogErrorClassification::USER_ERROR, 'Access denied for directory "{identifier}" due to insufficient permissions', ['identifier' => $fileObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToAccessTheDirectory', [$fileObject->getIdentifier()]); } catch (NotInMountPointException $e) { $this->writeLog(SystemLogFileAction::DELETE, SystemLogErrorClassification::USER_ERROR, 'The file or folder {destination} was not accessible within permitted mountpoints', ['identifier' => $fileObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.TargetWasNotWithinYourMountpoints', [$fileObject->getIdentifier()]); } catch (FileOperationErrorException $e) { $this->writeLog(SystemLogFileAction::DELETE, SystemLogErrorClassification::USER_ERROR, 'Delete failed for directory "{identifier}": insufficient write permissions', ['identifier' => $fileObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.CouldNotDeleteDirectory', [$fileObject->getIdentifier()]); } } } return $result; } /** * Checks files in given folder recursively for for existing references. * * Creates a flash message if there are references. * * @param Folder $folder * @return bool TRUE if folder has files in use, FALSE otherwise */ public function folderHasFilesInUse(Folder $folder) { $files = $folder->getFiles(0, 0, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, true); if (empty($files)) { return false; } /** @var int[] $fileUids */ $fileUids = []; foreach ($files as $file) { $fileUids[] = $file->getUid(); } $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex'); $numberOfReferences = $queryBuilder ->count('hash') ->from('sys_refindex') ->where( $queryBuilder->expr()->eq( 'ref_table', $queryBuilder->createNamedParameter('sys_file') ), $queryBuilder->expr()->in( 'ref_uid', $queryBuilder->createNamedParameter($fileUids, Connection::PARAM_INT_ARRAY) ), $queryBuilder->expr()->neq( 'tablename', $queryBuilder->createNamedParameter('sys_file_metadata') ) )->executeQuery()->fetchOne(); $hasReferences = $numberOfReferences > 0; if ($hasReferences) { $flashMessage = new FlashMessage( $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.description.folderNotDeletedHasFilesWithReferences'), $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:message.header.folderNotDeletedHasFilesWithReferences'), ContextualFeedbackSeverity::WARNING, true ); $this->addFlashMessage($flashMessage); } return $hasReferences; } /** * Maps results from the fal file reference table on the * structure of the normal reference index table. */ protected function transformFileReferenceToRecordReference(array $referenceRecord): ?array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex'); $queryBuilder->getRestrictions()->removeAll(); $fileReference = $queryBuilder ->select('uid_foreign', 'tablenames', 'fieldname', 'sorting_foreign') ->from('sys_file_reference') ->where( $queryBuilder->expr()->eq( 'uid', $queryBuilder->createNamedParameter($referenceRecord['recuid'], Connection::PARAM_INT) ) ) ->executeQuery() ->fetchAssociative(); if ($fileReference === false) { return null; } return [ 'recuid' => $fileReference['uid_foreign'], 'tablename' => $fileReference['tablenames'], 'field' => $fileReference['fieldname'], 'flexpointer' => '', 'softref_key' => '', 'sorting' => $fileReference['sorting_foreign'], ]; } /** * Gets a File or a Folder object from an identifier [storage]:[fileId] */ protected function getFileObject(string $identifier) { $object = $this->fileFactory->retrieveFileOrFolderObject($identifier); if ($object === null) { throw new InvalidFileException('The item ' . $identifier . ' was not a file or directory', 1320122453); } if ($object->getStorage()->isFallbackStorage()) { throw new InsufficientFileAccessPermissionsException('You are not allowed to access files outside your storages', 1375889830); } return $object; } /** * Copying files and folders (action=2) * * $cmds['data'] (string): The file/folder to copy * + example "4:mypath/tomyfolder/myfile.jpg") * + for backwards compatibility: the identifier was the path+filename * $cmds['target'] (string): The path where to copy to. * + example "2:targetpath/targetfolder/" * $cmds['altName'] (string): Use an alternative name if the target already exists * * @param array $cmds Command details as described above * @return \TYPO3\CMS\Core\Resource\File|false */ protected function func_copy($cmds) { $sourceFileObject = $this->getFileObject($cmds['data']); $targetFolderObject = $this->getFileObject($cmds['target']); // Basic check if (!$targetFolderObject instanceof Folder) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::SYSTEM_ERROR, 'Destination "{identifier}" was not a directory', ['identifier' => $cmds['target']]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationWasNotADirectory', [$cmds['target']]); return false; } // If this is TRUE, we append _XX to the file name if $appendSuffixOnConflict = (string)($cmds['altName'] ?? ''); $resultObject = null; $conflictMode = $appendSuffixOnConflict !== '' ? DuplicationBehavior::RENAME : DuplicationBehavior::CANCEL; // Copying the file if ($sourceFileObject instanceof File) { try { $resultObject = $sourceFileObject->copyTo($targetFolderObject, null, $conflictMode); } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::USER_ERROR, 'File copy denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToCopyFiles'); } catch (InsufficientFileAccessPermissionsException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::USER_ERROR, 'Resource access failed: source "{identifier}" or destination {"destination}" file is outside the configured mountpoints', ['identifier' => $sourceFileObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.CouldNotAccessAllNecessaryResources', [$sourceFileObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } catch (IllegalFileExtensionException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::USER_ERROR, 'Extension of file name "{identifier}" is not allowed in "{destination}"', ['identifier' => $sourceFileObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.ExtensionOfFileNameIsNotAllowedIn', [$sourceFileObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } catch (ExistingTargetFileNameException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::USER_ERROR, 'File "{identifier}" already exists in directory "{destination}"', ['identifier' => $sourceFileObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileAlreadyExistsInFolder', [$sourceFileObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } catch (NotImplementedMethodException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'The function to copy a file between storages is not yet implemented'); $this->addMessageToFlashMessageQueue('FileUtility.TheFunctionToCopyAFileBetweenStoragesIsNotYetImplemented'); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::SYSTEM_ERROR, 'Copy failed for file "{identifier}" in "{destination}": insufficient write permissions', ['identifier' => $sourceFileObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileWasNotCopiedTo', [$sourceFileObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } if ($resultObject) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::MESSAGE, 'File "{identifier}" copied to "{destination}"', ['identifier' => $sourceFileObject->getIdentifier(), 'destination' => $resultObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileCopiedTo', [$sourceFileObject->getIdentifier(), $resultObject->getIdentifier()], ContextualFeedbackSeverity::OK); } } else { // Else means this is a Folder $sourceFolderObject = $sourceFileObject; try { $resultObject = $sourceFolderObject->copyTo($targetFolderObject, null, $conflictMode); } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::USER_ERROR, 'Directory copy denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToCopyDirectories'); } catch (InsufficientFileAccessPermissionsException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::USER_ERROR, 'Resource access failed: source "{identifier}" or destination {"destination}" file is outside the configured mountpoints', ['identifier' => $sourceFolderObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.CouldNotAccessAllNecessaryResources', [$sourceFolderObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } catch (InsufficientFolderAccessPermissionsException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::USER_ERROR, 'Access denied: insufficient permissions for destination directory "{destination}"', ['destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.YouDontHaveFullAccessToTheDestinationDirectory', [$targetFolderObject->getIdentifier()]); } catch (InvalidTargetFolderException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::USER_ERROR, 'Copy failed: destination "{destination}" already contains a file or directory with the same name "{name}"', ['name' => $sourceFolderObject->getName(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.CannotCopyFolderIntoTargetFolderBecauseTheTargetFolderIsAlreadyWithinTheFolderToBeCopied', [$sourceFolderObject->getName(), $targetFolderObject->getIdentifier()]); } catch (ExistingTargetFolderException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::USER_ERROR, 'Target "{destination}" already exists', ['destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.TargetAlreadyExists', [$targetFolderObject->getIdentifier()]); } catch (NotImplementedMethodException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'The function to copy a folder between storages is not yet implemented'); $this->addMessageToFlashMessageQueue('FileUtility.TheFunctionToCopyAFolderBetweenStoragesIsNotYetImplemented'); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::SYSTEM_ERROR, 'Copy failed for directory "{identifier}" in "{destination}": insufficient write permissions', ['identifier' => $sourceFolderObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DirectoryWasNotCopiedTo', [$sourceFolderObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } if ($resultObject) { $this->writeLog(SystemLogFileAction::COPY, SystemLogErrorClassification::MESSAGE, 'Directory "{identifier}" copied to "{destination}"', ['identifier' => $sourceFolderObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DirectoryCopiedTo', [$sourceFolderObject->getIdentifier(), $targetFolderObject->getIdentifier()], ContextualFeedbackSeverity::OK); } } return $resultObject; } /** * Moving files and folders (action=3) * * $cmds['data'] (string): The file/folder to move * + example "4:mypath/tomyfolder/myfile.jpg") * + for backwards compatibility: the identifier was the path+filename * $cmds['target'] (string): The path where to move to. * + example "2:targetpath/targetfolder/" * $cmds['altName'] (string): Use an alternative name if the target already exists * * @param array $cmds Command details as described above * @return \TYPO3\CMS\Core\Resource\File|false */ protected function func_move($cmds) { $sourceFileObject = $this->getFileObject($cmds['data']); $targetFolderObject = $this->getFileObject($cmds['target']); // Basic check if (!$targetFolderObject instanceof Folder) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::SYSTEM_ERROR, 'Destination "{destination}" was not a directory', ['destination' => $cmds['target']]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationWasNotADirectory', [$cmds['target']]); return false; } $alternativeName = (string)($cmds['altName'] ?? ''); $resultObject = null; // Moving the file if ($sourceFileObject instanceof File) { try { $sourcePath = $sourceFileObject->getIdentifier(); if ($alternativeName !== '') { // Don't allow overwriting existing files, but find a new name $resultObject = $sourceFileObject->moveTo($targetFolderObject, $alternativeName, DuplicationBehavior::RENAME); } else { // Don't allow overwriting existing files $resultObject = $sourceFileObject->moveTo($targetFolderObject, null, DuplicationBehavior::CANCEL); } $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::MESSAGE, 'File "{identifier}" moved to "{destination}"', ['identifier' => $sourcePath, 'destination' => $resultObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileMovedTo', [$sourcePath, $resultObject->getIdentifier()], ContextualFeedbackSeverity::OK); } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'File moving denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToMoveFiles'); } catch (InsufficientFileAccessPermissionsException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'Resource access failed: source "{identifier}" or destination {"destination}" file is outside the configured mountpoints', ['identifier' => $sourceFileObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.CouldNotAccessAllNecessaryResources', [$sourceFileObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } catch (IllegalFileExtensionException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'Extension of file name "{identifier}" is not allowed in "{destination}"', ['identifier' => $sourceFileObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.ExtensionOfFileNameIsNotAllowedIn', [$sourceFileObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } catch (ExistingTargetFileNameException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'File "{identifier}" already exists in directory "{destination}"', ['identifier' => $sourceFileObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileAlreadyExistsInFolder', [$sourceFileObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } catch (NotImplementedMethodException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'The function to move a file between storages is not yet implemented'); $this->addMessageToFlashMessageQueue('FileUtility.TheFunctionToMoveAFileBetweenStoragesIsNotYetImplemented'); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::SYSTEM_ERROR, 'Copy failed for file "{identifier}" in "{destination}": insufficient write permissions', ['identifier' => $sourceFileObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileWasNotCopiedTo', [$sourceFileObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } } else { // Else means this is a Folder $sourceFolderObject = $sourceFileObject; try { if ($alternativeName !== '') { // Don't allow overwriting existing files, but find a new name $resultObject = $sourceFolderObject->moveTo($targetFolderObject, $alternativeName, DuplicationBehavior::RENAME); } else { // Don't allow overwriting existing files $resultObject = $sourceFolderObject->moveTo($targetFolderObject, null, DuplicationBehavior::RENAME); } $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::MESSAGE, 'Directory "{identifier}" moved to "{destination}"', ['identifier' => $sourceFolderObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DirectoryMovedTo', [$sourceFolderObject->getIdentifier(), $targetFolderObject->getIdentifier()], ContextualFeedbackSeverity::OK); } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'Directory moving denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToMoveDirectories'); } catch (InsufficientFileAccessPermissionsException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'Resource access failed: source folder "{identifier}" or destination {"destination}" directory is outside the configured mountpoints', ['identifier' => $sourceFolderObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.CouldNotAccessAllNecessaryResources', [$sourceFolderObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } catch (InsufficientFolderAccessPermissionsException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'You don\'t have full access to the destination directory "{destination}"', ['destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.YouDontHaveFullAccessToTheDestinationDirectory', [$targetFolderObject->getIdentifier()]); } catch (InvalidTargetFolderException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'Move failed: destination "{destination}" is located within the source directory "{identifier}"', ['identifier' => $sourceFolderObject->getName(), 'destination' => $targetFolderObject->getName()]); $this->addMessageToFlashMessageQueue('FileUtility.CannotMoveFolderIntoTargetFolderBecauseTheTargetFolderIsAlreadyWithinTheFolderToBeMoved', [$sourceFolderObject->getName(), $targetFolderObject->getName()]); } catch (ExistingTargetFolderException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'Target "{destination}" already exists', ['destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.TargetAlreadyExists', [$targetFolderObject->getIdentifier()]); } catch (NotImplementedMethodException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::USER_ERROR, 'The function to move a folder between storages is not yet implemented'); $this->addMessageToFlashMessageQueue('FileUtility.TheFunctionToMoveAFolderBetweenStoragesIsNotYetImplemented'); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::MOVE, SystemLogErrorClassification::SYSTEM_ERROR, 'Move failed for directory "{identifier}" in "{destination}": insufficient write permissions', ['identifier' => $sourceFolderObject->getIdentifier(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DirectoryWasNotMovedTo', [$sourceFolderObject->getIdentifier(), $targetFolderObject->getIdentifier()]); } } return $resultObject; } /** * Renaming files or folders (action=5) * * $cmds['data'] (string): The file/folder to copy * + example "4:mypath/tomyfolder/myfile.jpg") * + for backwards compatibility: the identifier was the path+filename * $cmds['target'] (string): New name of the file/folder * * @param array $cmds Command details as described above * @return \TYPO3\CMS\Core\Resource\File Returns the new file upon success */ public function func_rename($cmds) { $sourceFileObject = $this->getFileObject($cmds['data']); $sourceFile = $sourceFileObject->getName(); $targetFile = $cmds['target']; $resultObject = null; if ($sourceFileObject instanceof File) { try { // Try to rename the File $resultObject = $sourceFileObject->rename($targetFile, $this->existingFilesConflictMode); if ($resultObject->getName() !== $targetFile) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'File renamed from "{identifier}" to "{destination}": unsupported characters were replaced', ['identifier' => $sourceFile, 'destination' => $targetFile]); $this->addMessageToFlashMessageQueue('FileUtility.FileNameSanitized', [$targetFile, $resultObject->getName()], ContextualFeedbackSeverity::WARNING); } else { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::MESSAGE, 'File renamed from "{identifier}" to "{destination}"', ['identifier' => $sourceFile, 'destination' => $targetFile]); } if ($sourceFile === $resultObject->getName()) { $this->addMessageToFlashMessageQueue('FileUtility.FileRenamedSameName', [$sourceFile], ContextualFeedbackSeverity::INFO); } else { $this->addMessageToFlashMessageQueue('FileUtility.FileRenamedFromTo', [$sourceFile, $resultObject->getName()], ContextualFeedbackSeverity::OK); } } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'File renaming denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToRenameFiles'); } catch (IllegalFileExtensionException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Operation failed due to illegal file extension on "{identifier}" or "{destination}"', ['identifier' => $sourceFileObject->getName(), 'destination' => $targetFile]); $this->addMessageToFlashMessageQueue('FileUtility.ExtensionOfFileNameOrWasNotAllowed', [$sourceFileObject->getName(), $targetFile]); } catch (ExistingTargetFileNameException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Rename failed because the destination file "{destination}" already exists', ['destination' => $targetFile]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationExistedAlready', [$targetFile]); } catch (NotInMountPointException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Destination path "{destination}" is outside the configured mountpoints', ['destination' => $targetFile]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationPathWasNotWithinYourMountpoints', [$targetFile]); } catch (ResultException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'File {identifier} was not renamed to {destination}', ['identifier' => $sourceFileObject->getName(), 'destination' => $targetFile]); $this->addEvaluationResultHintsToFlashMessageQueue($e); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Rename failed for file "{identifier}" in "{destination}": insufficient write permissions', ['identifier' => $sourceFileObject->getName(), 'destination' => $targetFile]); $this->addMessageToFlashMessageQueue('FileUtility.FileWasNotRenamed', [$sourceFileObject->getName(), $targetFile]); } } else { // Else means this is a Folder try { // Try to rename the Folder $resultObject = $sourceFileObject->rename($targetFile); $newFolderName = $resultObject->getName(); $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::MESSAGE, 'Directory renamed from "{identifier}" to "{destination}"', ['identifier' => $sourceFile, 'destination' => $targetFile]); if ($sourceFile === $newFolderName) { $this->addMessageToFlashMessageQueue('FileUtility.DirectoryRenamedSameName', [$sourceFile], ContextualFeedbackSeverity::INFO); } else { if ($newFolderName === $targetFile) { $this->addMessageToFlashMessageQueue('FileUtility.DirectoryRenamedFromTo', [$sourceFile, $newFolderName], ContextualFeedbackSeverity::OK); } else { $this->addMessageToFlashMessageQueue('FileUtility.DirectoryRenamedFromToCharReplaced', [$sourceFile, $newFolderName], ContextualFeedbackSeverity::WARNING); } } } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Directory renaming denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToRenameDirectories'); } catch (ExistingTargetFileNameException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Rename failed because the destination folder "{destination}" already exists', ['destination' => $targetFile]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationExistedAlready', [$targetFile]); } catch (NotInMountPointException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Destination path "{destination}" is outside the configured mountpoints', ['destination' => $targetFile]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationPathWasNotWithinYourMountpoints', [$targetFile]); } catch (ResultException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'File {identifier} was not renamed to {destination}', ['identifier' => $sourceFileObject->getName(), 'destination' => $targetFile]); $this->addEvaluationResultHintsToFlashMessageQueue($e); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Rename failed for directory "{identifier}" in "{destination}": insufficient write permissions', ['identifier' => $sourceFileObject->getName(), 'destination' => $targetFile]); $this->addMessageToFlashMessageQueue('FileUtility.DirectoryWasNotRenamed', [$sourceFileObject->getName(), $targetFile]); } } return $resultObject; } /** * This creates a new folder. (action=6) * * $cmds['data'] (string): The new folder name * $cmds['target'] (string): The path where to copy to. * + example "2:targetpath/targetfolder/" * * @param array $cmds Command details as described above * @return Folder|false Returns the new foldername upon success */ public function func_newfolder($cmds) { $resultObject = false; $targetFolderObject = $this->getFileObject($cmds['target']); if (!$targetFolderObject instanceof Folder) { $this->writeLog(SystemLogFileAction::NEW_FOLDER, SystemLogErrorClassification::SYSTEM_ERROR, 'Destination "{destination}" was not a directory', ['destination' => $cmds['target']]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationWasNotADirectory', [$cmds['target']]); return false; } $folderName = $cmds['data']; try { $resultObject = $targetFolderObject->createFolder($folderName); $this->writeLog(SystemLogFileAction::NEW_FOLDER, SystemLogErrorClassification::MESSAGE, 'Directory "{identifier}" created in "{destination}"', ['identifier' => $folderName, 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DirectoryCreatedIn', [$folderName, $targetFolderObject->getIdentifier()], ContextualFeedbackSeverity::OK); } catch (InvalidFileNameException $e) { $this->writeLog(SystemLogFileAction::NEW_FOLDER, SystemLogErrorClassification::USER_ERROR, 'Invalid folder name "{identifier}"', ['identifier' => $folderName]); $this->addMessageToFlashMessageQueue('FileUtility.InvalidFolderName', [$folderName]); } catch (InsufficientFolderWritePermissionsException $e) { $this->writeLog(SystemLogFileAction::NEW_FOLDER, SystemLogErrorClassification::USER_ERROR, 'Directory creation denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToCreateDirectories'); } catch (NotInMountPointException $e) { $this->writeLog(SystemLogFileAction::NEW_FOLDER, SystemLogErrorClassification::USER_ERROR, 'Destination path "{destination}" is outside the configured mountpoints', ['destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationPathWasNotWithinYourMountpoints', [$targetFolderObject->getIdentifier()]); } catch (ExistingTargetFolderException $e) { $this->writeLog(SystemLogFileAction::NEW_FOLDER, SystemLogErrorClassification::USER_ERROR, 'File or directory "{identifier}" already exists', ['identifier' => $folderName]); $this->addMessageToFlashMessageQueue('FileUtility.FileOrDirectoryExistedAlready', [$folderName]); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::NEW_FOLDER, SystemLogErrorClassification::USER_ERROR, 'Creation failed for directory "{identifier}" in "{destination}": insufficient write permissions', ['identifier' => $folderName, 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DirectoryNotCreated', [$folderName, $targetFolderObject->getIdentifier()]); } return $resultObject; } /** * This creates a new file. (action=8) * $cmds['data'] (string): The new file name * $cmds['target'] (string): The path where to create it. * + example "2:targetpath/targetfolder/" * * @param array $cmds Command details as described above */ public function func_newfile($cmds): File|false|null { $targetFolderObject = $this->getFileObject($cmds['target']); if (!$targetFolderObject instanceof Folder) { $this->writeLog(SystemLogFileAction::NEW_FILE, SystemLogErrorClassification::SYSTEM_ERROR, 'Destination "{destination}" was not a directory', ['destination' => $cmds['target']]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationWasNotADirectory', [$cmds['target']]); return false; } $resultObject = null; $fileName = $cmds['data']; try { $resultObject = $targetFolderObject->createFile($fileName); $this->writeLog(SystemLogFileAction::NEW_FILE, SystemLogErrorClassification::MESSAGE, 'File "{identifier}" created', ['identifier' => $fileName]); if ($resultObject->getName() !== $fileName) { $this->addMessageToFlashMessageQueue('FileUtility.FileNameSanitized', [$fileName, $resultObject->getName()], ContextualFeedbackSeverity::WARNING); } $this->addMessageToFlashMessageQueue('FileUtility.FileCreated', [$resultObject->getName()], ContextualFeedbackSeverity::OK); } catch (IllegalFileExtensionException $e) { $this->writeLog(SystemLogFileAction::NEW_FILE, SystemLogErrorClassification::USER_ERROR, 'Extension of file "{identifier}" was not allowed', ['identifier' => $fileName]); $this->addMessageToFlashMessageQueue('FileUtility.ExtensionOfFileWasNotAllowed', [$fileName]); } catch (InsufficientFolderWritePermissionsException $e) { $this->writeLog(SystemLogFileAction::NEW_FILE, SystemLogErrorClassification::USER_ERROR, 'File creation denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToCreateFiles'); } catch (NotInMountPointException $e) { $this->writeLog(SystemLogFileAction::NEW_FILE, SystemLogErrorClassification::USER_ERROR, 'Destination path "{destination}" is outside the configured mountpoints', ['destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationPathWasNotWithinYourMountpoints', [$targetFolderObject->getIdentifier()]); } catch (ExistingTargetFileNameException $e) { $this->writeLog(SystemLogFileAction::NEW_FILE, SystemLogErrorClassification::USER_ERROR, 'File existed already in "{destination}"', ['destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileExistedAlreadyIn', [$targetFolderObject->getIdentifier()]); } catch (InvalidFileNameException $e) { $this->writeLog(SystemLogFileAction::NEW_FILE, SystemLogErrorClassification::USER_ERROR, 'File name "{identifier}" was not allowed', ['identifier' => $fileName]); $this->addMessageToFlashMessageQueue('FileUtility.FileNameWasNotAllowed', [$fileName]); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::NEW_FILE, SystemLogErrorClassification::USER_ERROR, 'Creation failed for file "{identifier}" in "{destination}": insufficient write permissions', ['identifier' => $fileName, 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileWasNotCreated', [$fileName, $targetFolderObject->getIdentifier()]); } return $resultObject; } /** * Editing textfiles or folders (action=9) * * @param array $cmds $cmds['data'] is the new content. $cmds['target'] is the target (file or dir) * @return bool Returns TRUE on success */ public function func_edit($cmds) { // Example identifier for $cmds['target'] => "4:mypath/tomyfolder/myfile.jpg" // for backwards compatibility: the combined file identifier was the path+filename $fileIdentifier = $cmds['target']; $fileObject = $this->getFileObject($fileIdentifier); if (!$fileObject instanceof File) { $this->writeLog(SystemLogFileAction::EDIT, SystemLogErrorClassification::SYSTEM_ERROR, 'Target "{destination}" was not a file', ['destination' => $fileIdentifier]); $this->addMessageToFlashMessageQueue('FileUtility.TargetWasNotAFile', [$fileIdentifier]); return false; } if (!$fileObject->isTextFile()) { $extList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext']; $this->writeLog(SystemLogFileAction::EDIT, SystemLogErrorClassification::USER_ERROR, 'Unsupported text file extension "{extension}" (allowed: {allowedExtensions})', ['extension' => $fileObject->getExtension(), 'allowedExtensions' => $extList]); $this->addMessageToFlashMessageQueue('FileUtility.FileExtensionIsNotATextfileFormat', [$fileObject->getExtension(), $extList]); return false; } try { // Example identifier for $cmds['target'] => "2:targetpath/targetfolder/" $content = $cmds['data']; $fileObject->setContents($content); clearstatcache(); $this->writeLog(SystemLogFileAction::EDIT, SystemLogErrorClassification::MESSAGE, 'File saved to "{identifier}", bytes: {size}', ['identifier' => $fileObject->getIdentifier(), 'size' => $fileObject->getSize()]); $this->addMessageToFlashMessageQueue('FileUtility.FileSavedTo', [$fileObject->getIdentifier()], ContextualFeedbackSeverity::OK); return true; } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::EDIT, SystemLogErrorClassification::USER_ERROR, 'File editing denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToEditFiles'); return false; } catch (InsufficientFileWritePermissionsException $e) { $this->writeLog(SystemLogFileAction::EDIT, SystemLogErrorClassification::USER_ERROR, 'Save failed for file "{identifier}": insufficient write permissions', ['identifier' => $fileObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileWasNotSaved', [$fileObject->getIdentifier()]); return false; } catch (IllegalFileExtensionException|\RuntimeException $e) { $this->writeLog(SystemLogFileAction::EDIT, SystemLogErrorClassification::USER_ERROR, 'Save failed for file "{identifier}": file extension rejected', ['identifier' => $fileObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.FileWasNotSaved', [$fileObject->getIdentifier()]); return false; } } /** * Upload of files (action=1) * in HTML you'd need sth like this: * * @param array $cmds $cmds['data'] is the ID-number (points to the global var that holds the filename-ref * ($this->uploadedFiles['upload_' . $id]['name']) . $cmds['target'] is the target directory, $cmds['charset'] * is the the character set of the file name (utf-8 is needed for JS-interaction) * @return File[]|bool Returns an array of new file objects upon success. False otherwise */ public function func_upload($cmds) { $uploadPosition = $cmds['data']; $uploadedFileData = $this->uploadedFiles['upload_' . $uploadPosition] ?? null; if (!$this->uploadedFileHasClientName($uploadedFileData)) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::SYSTEM_ERROR, 'No file was uploaded'); $this->addMessageToFlashMessageQueue('FileUtility.NoFileWasUploaded'); return false; } // Example identifier for $cmds['target'] => "2:targetpath/targetfolder/" $targetFolderObject = $this->getFileObject($cmds['target']); // Uploading with non HTML-5-style, thus, make an array out of it, so we can loop over it if (!is_array($uploadedFileData)) { $uploadedFileData = [$uploadedFileData]; } $resultObjects = []; // Loop through all uploaded files foreach ($uploadedFileData as $uploadedFile) { try { $fileObject = $targetFolderObject->addUploadedFile($uploadedFile, $this->existingFilesConflictMode); if ($this->existingFilesConflictMode === DuplicationBehavior::REPLACE) { $this->getIndexer($fileObject->getStorage())->updateIndexEntry($fileObject); } $resultObjects[] = $fileObject; $this->internalUploadMap[$uploadPosition] = $fileObject->getCombinedIdentifier(); if ($fileObject->getName() !== $uploadedFile->getClientFilename()) { $this->addMessageToFlashMessageQueue('FileUtility.FileNameSanitized', [$uploadedFile->getClientFilename(), $fileObject->getName()], ContextualFeedbackSeverity::WARNING); } $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::MESSAGE, 'File "{identifier}" uploaded to "{destination}"', ['identifier' => $uploadedFile->getClientFilename(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.UploadingFileTo', [$uploadedFile->getClientFilename(), $targetFolderObject->getIdentifier()], ContextualFeedbackSeverity::OK); } catch (InsufficientFileWritePermissionsException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Overwrite denied for "{identifier}" due to insufficient permissions', ['identifier' => $uploadedFile->getClientFilename()]); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToOverride', [$uploadedFile->getClientFilename()]); } catch (UploadException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::SYSTEM_ERROR, 'Upload failed because no file was provided'); $this->addMessageToFlashMessageQueue('FileUtility.TheUploadHasFailedNoUploadedFileFound'); } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'File uploading denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToUploadFiles'); } catch (UploadSizeException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Upload failed: file "{identifier}" exceeds the configured size limit', ['identifier' => $uploadedFile->getClientFilename()]); $this->addMessageToFlashMessageQueue('FileUtility.TheUploadedFileExceedsTheSize-limit', [$uploadedFile->getClientFilename()]); } catch (InsufficientFolderWritePermissionsException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Destination path "{destination}" is outside the configured mountpoints', ['destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationPathWasNotWithinYourMountpoints', [$targetFolderObject->getIdentifier()]); } catch (IllegalFileExtensionException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Extension of file name "{identifier}" is not allowed in "{destination}"', ['identifier' => $uploadedFile->getClientFilename(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.ExtensionOfFileNameIsNotAllowedIn', [$uploadedFile->getClientFilename(), $targetFolderObject->getIdentifier()]); } catch (ExistingTargetFileNameException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'No unique filename available in "{destination}"', ['destination' => $targetFolderObject->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.NoUniqueFilenameAvailableIn', [$targetFolderObject->getIdentifier()]); } catch (ResultException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Uploading file "{identifier}" to "{destination}" failed', ['identifier' => $uploadedFile->getClientFilename(), 'destination' => $targetFolderObject->getIdentifier()]); $this->addEvaluationResultHintsToFlashMessageQueue($e); } catch (\RuntimeException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Move failed for the uploaded file in "{destination}": insufficient write permissions. Error: {error}', ['destination' => $targetFolderObject->getIdentifier(), 'error' => $e->getMessage()]); $this->addMessageToFlashMessageQueue('FileUtility.UploadedFileCouldNotBeMoved', [$targetFolderObject->getIdentifier()]); } } return $resultObjects; } /** * 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. * * @return array|bool * @throws Exception\InsufficientFileAccessPermissionsException * @throws Exception\InvalidFileException * @throws \RuntimeException */ protected function replaceFile(array $cmdArr) { $fileObjectToReplace = null; $uploadPosition = $cmdArr['data']; $uploadedFile = $this->uploadedFiles['replace_' . $uploadPosition]; if (empty($uploadedFile->getClientFilename())) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::SYSTEM_ERROR, 'No file was uploaded for replacement'); $this->addMessageToFlashMessageQueue('FileUtility.NoFileWasUploadedForReplacing'); return false; } $keepFileName = (bool)($cmdArr['keepFilename'] ?? false); $resultObjects = []; try { $fileObjectToReplace = $this->getFileObject($cmdArr['uid']); $folder = $fileObjectToReplace->getParentFolder(); $resourceStorage = $fileObjectToReplace->getStorage(); $uploadedFileExtension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION); if (!$keepFileName) { $fileObject = $resourceStorage->replaceAndRenameUploadedFile($uploadedFile, $fileObjectToReplace); } elseif ($uploadedFileExtension !== $fileObjectToReplace->getExtension()) { // `keepFileName` would cause a failing consistency check, for instance, when adding `image/png` contents to an existing `file.pdf`. // This step ensures the file is renamed from `file.pdf` to `file.png`. $targetFileName = pathinfo($fileObjectToReplace->getName(), PATHINFO_FILENAME) . '.' . $uploadedFileExtension; $fileObject = $resourceStorage->replaceAndRenameUploadedFile($uploadedFile, $fileObjectToReplace, $targetFileName); } else { $fileObject = $resourceStorage->addUploadedFile($uploadedFile, $folder, $fileObjectToReplace->getName(), DuplicationBehavior::REPLACE); } $resultObjects[] = $fileObject; $this->internalUploadMap[$uploadPosition] = $fileObject->getCombinedIdentifier(); $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::MESSAGE, 'File "{identifier}" replaced with "{destination}"', ['identifier' => $uploadedFile->getClientFilename(), 'destination' => $fileObjectToReplace->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.ReplacingFileTo', [$uploadedFile->getClientFilename(), $fileObjectToReplace->getIdentifier()], ContextualFeedbackSeverity::OK); } catch (InsufficientFileWritePermissionsException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Overwrite denied for "{destination}" due to insufficient permissions', ['destination' => $uploadedFile->getClientFilename()]); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToOverride', [$uploadedFile->getClientFilename()]); } catch (UploadException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::SYSTEM_ERROR, 'Upload failed because no file was provided'); $this->addMessageToFlashMessageQueue('FileUtility.TheUploadHasFailedNoUploadedFileFound'); } catch (InsufficientUserPermissionsException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'File uploading denied due to insufficient permissions'); $this->addMessageToFlashMessageQueue('FileUtility.YouAreNotAllowedToUploadFiles'); } catch (UploadSizeException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Upload failed: file "{identifier}" exceeds the configured size limit', ['identifier' => $uploadedFile->getClientFilename()]); $this->addMessageToFlashMessageQueue('FileUtility.TheUploadedFileExceedsTheSize-limit', [$uploadedFile->getClientFilename()]); } catch (InsufficientFolderWritePermissionsException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Destination path "{destination}" is outside the configured mountpoints', ['destination' => $fileObjectToReplace->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.DestinationPathWasNotWithinYourMountpoints', [$fileObjectToReplace->getIdentifier()]); } catch (IllegalFileExtensionException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Extension of file name "{identifier}" is not allowed in "{destination}"', ['identifier' => $uploadedFile->getClientFilename(), 'destination' => $fileObjectToReplace->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.ExtensionOfFileNameIsNotAllowedIn', [$uploadedFile->getClientFilename(), $fileObjectToReplace->getIdentifier()]); } catch (ExistingTargetFileNameException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'No unique filename available in "{destination}"', ['destination' => $fileObjectToReplace->getIdentifier()]); $this->addMessageToFlashMessageQueue('FileUtility.NoUniqueFilenameAvailableIn', [$fileObjectToReplace->getIdentifier()]); } catch (ResultException $e) { $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Replacing file "{identifier}" to "{destination}" failed', ['identifier' => $uploadedFile->getClientFilename(), 'destination' => $fileObjectToReplace->getIdentifier()]); $this->addEvaluationResultHintsToFlashMessageQueue($e); } catch (\RuntimeException $e) { throw $e; } return $resultObjects; } /** * Add flash message to message queue */ protected function addFlashMessage(FlashMessage $flashMessage) { $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); $defaultFlashMessageQueue->enqueue($flashMessage); } protected function isBackendScope(): bool { return ($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend(); } protected function uploadedFileHasClientName(array|UploadedFileInterface|null $file): bool { if ($file instanceof UploadedFileInterface) { return !empty($file->getClientFilename()); } if (isset($file[0]) && $file[0] instanceof UploadedFileInterface) { return !empty($file[0]->getClientFilename()); } return false; } /** * Gets Indexer * * @return \TYPO3\CMS\Core\Resource\Index\Indexer */ protected function getIndexer(ResourceStorage $storage) { return GeneralUtility::makeInstance(Indexer::class, $storage); } protected function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } }