[ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editcontent', 'iconIdentifier' => 'actions-page-open', 'callbackAction' => 'editFile', ], 'editMetadata' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata', 'iconIdentifier' => 'actions-open', 'callbackAction' => 'editMetadata', ], 'replaceFile' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.replace', 'iconIdentifier' => 'actions-edit-replace', 'callbackAction' => 'replaceFile', ], 'rename' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.rename', 'iconIdentifier' => 'actions-edit-rename', 'callbackAction' => 'renameFile', ], 'new' => [ 'label' => 'LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:actions.new_folder', 'iconIdentifier' => 'actions-folder-add', 'callbackAction' => 'createFolder', ], 'newFile' => [ 'label' => 'LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:actions.new_file', 'iconIdentifier' => 'actions-file-add', 'callbackAction' => 'createFile', ], 'downloadFile' => [ 'label' => 'LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:download', 'iconIdentifier' => 'actions-download', 'callbackAction' => 'downloadFile', ], 'downloadFolder' => [ 'label' => 'LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:download', 'iconIdentifier' => 'actions-download', 'callbackAction' => 'downloadFolder', ], 'newFileMount' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.newFilemount', 'iconIdentifier' => 'mimetypes-x-sys_filemounts', 'callbackAction' => 'createFilemount', ], 'info' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info', 'iconIdentifier' => 'actions-document-info', 'callbackAction' => 'openInfoPopUp', ], 'updateOnlineMedia' => [ 'label' => 'LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:reloadMetadata', 'iconIdentifier' => 'actions-refresh', 'callbackAction' => 'updateOnlineMedia', ], 'divider' => [ 'type' => 'divider', ], 'copy' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy', 'iconIdentifier' => 'actions-edit-copy', 'callbackAction' => 'copyFile', ], 'copyRelease' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy', 'iconIdentifier' => 'actions-edit-copy-release', 'callbackAction' => 'copyReleaseFile', ], 'cut' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut', 'iconIdentifier' => 'actions-edit-cut', 'callbackAction' => 'cutFile', ], 'cutRelease' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease', 'iconIdentifier' => 'actions-edit-cut-release', 'callbackAction' => 'cutReleaseFile', ], 'pasteInto' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteinto', 'iconIdentifier' => 'actions-document-paste-into', 'callbackAction' => 'pasteFileInto', ], 'divider2' => [ 'type' => 'divider', ], 'delete' => [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete', 'iconIdentifier' => 'actions-edit-delete', 'callbackAction' => 'deleteFile', ], ]; public function __construct( private readonly ResourceFactory $resourceFactory, private readonly UriBuilder $uriBuilder, ) { parent::__construct(); } public function canHandle(): bool { return $this->table === 'sys_file'; } /** * Initialize file object */ protected function initialize() { parent::initialize(); try { $this->record = $this->resourceFactory->retrieveFileOrFolderObject($this->identifier); } catch (ResourceDoesNotExistException $e) { $this->record = null; } } /** * Checks whether certain item can be rendered (e.g. check for disabled items or permissions) */ protected function canRender(string $itemName, string $type): bool { if (in_array($type, ['divider', 'submenu'], true)) { return true; } if (in_array($itemName, $this->disabledItems, true)) { return false; } $canRender = false; switch ($itemName) { //just for files case 'edit': $canRender = $this->canBeEdited(); break; case 'replaceFile': $canRender = $this->canBeReplaced(); break; case 'editMetadata': $canRender = $this->canEditMetadata(); break; case 'updateOnlineMedia': $canRender = $this->isOnlineMedia() && $this->canEditMetadata(); break; // just for folders case 'new': case 'newFile': $canRender = $this->canCreateNew(); break; case 'newFileMount': $canRender = $this->canCreateNewFilemount(); break; case 'pasteInto': $canRender = $this->canBePastedInto(); break; //for both files and folders case 'info': $canRender = $this->canShowInfo(); break; case 'rename': $canRender = $this->canBeRenamed(); break; case 'copy': $canRender = $this->canBeCopied(); break; case 'copyRelease': $canRender = $this->isRecordInClipboard('copy'); break; case 'cut': $canRender = $this->canBeCut(); break; case 'cutRelease': $canRender = $this->isRecordInClipboard('cut'); break; case 'downloadFile': $canRender = $this->isFile() && $this->canBeDownloaded(); break; case 'downloadFolder': $canRender = $this->isFolder() && $this->canBeDownloaded(); break; case 'delete': $canRender = $this->canBeDeleted(); break; } return $canRender; } protected function canBeReplaced(): bool { return $this->isFile() && $this->record->checkActionPermission('replace'); } protected function canBeEdited(): bool { return $this->isFile() && $this->record->checkActionPermission('write') && $this->record->isTextFile(); } protected function canEditMetadata(): bool { return $this->isFile() && $this->record->isIndexed() && $this->record->checkActionPermission('editMeta') && $this->record->getMetaData()->offsetExists('uid') && $this->backendUser->check('tables_modify', 'sys_file_metadata') && $this->backendUser->checkLanguageAccess(0); } protected function canBeRenamed(): bool { return $this->record->checkActionPermission('rename'); } protected function canBeDeleted(): bool { return $this->record->checkActionPermission('delete'); } protected function canShowInfo(): bool { return $this->record !== null; } protected function canCreateNew(): bool { return $this->isFolder() && $this->record->checkActionPermission('write'); } /** * New file mounts can only be created for readable folders by admins */ protected function canCreateNewFilemount(): bool { return $this->isFolder() && $this->record->checkActionPermission('read') && $this->backendUser->isAdmin(); } protected function canBeCopied(): bool { return $this->record->checkActionPermission('read') && $this->record->checkActionPermission('copy') && !$this->isRecordInClipboard('copy'); } protected function canBeCut(): bool { return $this->record->checkActionPermission('move') && !$this->isRecordInClipboard('cut'); } protected function canBePastedInto(): bool { $elArr = $this->clipboard->elFromTable('_FILE'); if (empty($elArr)) { return false; } $selItem = reset($elArr); $fileOrFolderInClipBoard = $this->resourceFactory->retrieveFileOrFolderObject($selItem); return $this->isFolder() && $this->record->checkActionPermission('write') && ( !$fileOrFolderInClipBoard instanceof Folder || !$fileOrFolderInClipBoard->getStorage()->isWithinFolder($fileOrFolderInClipBoard, $this->record) ) && $this->isFoldersAreInTheSameRoot($fileOrFolderInClipBoard); } protected function canBeDownloaded(): bool { if (!$this->record->checkActionPermission('read')) { // Early return if no read access return false; } $fileDownloadConfiguration = (array)($this->backendUser->getTSConfig()['options.']['file_list.']['fileDownload.'] ?? []); if (!($fileDownloadConfiguration['enabled'] ?? true)) { // File download is disabled return false; } if ($fileDownloadConfiguration === [] || $this->isFolder()) { // In case no configuration exists, or we deal with a folder, download is allowed at this point return true; } // Initialize file extension filter $filter = GeneralUtility::makeInstance(FileExtensionFilter::class); $filter->setAllowedFileExtensions( GeneralUtility::trimExplode(',', (string)($fileDownloadConfiguration['allowedFileExtensions'] ?? ''), true) ); $filter->setDisallowedFileExtensions( GeneralUtility::trimExplode(',', (string)($fileDownloadConfiguration['disallowedFileExtensions'] ?? ''), true) ); return $filter->isAllowed($this->record->getExtension()); } protected function isOnlineMedia(): bool { return $this->isFile() && GeneralUtility::makeInstance(OnlineMediaHelperRegistry::class)->hasOnlineMediaHelper($this->record->getExtension()); } /** * Checks if folder and record are in the same file mount * Cannot copy folders between file mounts * * @param File|Folder|null $fileOrFolderInClipBoard */ protected function isFoldersAreInTheSameRoot($fileOrFolderInClipBoard): bool { return (!$fileOrFolderInClipBoard instanceof Folder) || ( $this->record->getStorage()->getRootLevelFolder()->getCombinedIdentifier() == $fileOrFolderInClipBoard->getStorage()->getRootLevelFolder()->getCombinedIdentifier() ); } /** * Checks if a file record is in the "normal" pad of the clipboard * * @param string $mode "copy", "cut" or '' for any mode */ protected function isRecordInClipboard(string $mode = ''): bool { if ($mode !== '' && !$this->record->checkActionPermission($mode)) { return false; } $isSelected = ''; // Pseudo table name for use in the clipboard. $table = '_FILE'; $uid = md5($this->record->getCombinedIdentifier()); if ($this->clipboard->current === 'normal') { $isSelected = $this->clipboard->isSelected($table, $uid); } return $mode === '' ? !empty($isSelected) : $isSelected === $mode; } protected function isStorageRoot(): bool { return $this->record->getIdentifier() === $this->record->getStorage()->getRootLevelFolder()->getIdentifier(); } protected function isFile(): bool { return $this->record instanceof File; } protected function isFolder(): bool { return $this->record instanceof Folder; } protected function getAdditionalAttributes(string $itemName): array { $attributes = [ 'data-callback-module' => '@typo3/filelist/context-menu-actions', ]; if ($itemName === 'delete' && $this->backendUser->jsConfirmation(JsConfirmation::DELETE)) { $title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete'); if ($this->isFolder()) { $attributes += [ 'data-button-close-text' => $this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_folder.no'), 'data-button-ok-text' => $this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_folder.yes'), ]; } if ($this->isFile()) { $attributes += [ 'data-button-close-text' => $this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_file.no'), 'data-button-ok-text' => $this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_file.yes'), ]; } $recordInfo = BackendUtility::cropToTitleLength($this->record->getName()); if ($this->isFolder()) { if ($this->backendUser->shallDisplayDebugInformation()) { $recordInfo .= ' [' . $this->record->getIdentifier() . ']'; } $confirmMessage = sprintf( $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'), trim($recordInfo) ); } else { if ($this->backendUser->shallDisplayDebugInformation()) { $recordInfo .= ' [sys_file:' . $this->record->getUid() . ']'; } $confirmMessage = sprintf( $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'), trim($recordInfo) ) . BackendUtility::referenceCount( 'sys_file', (int)$this->record->getUid(), LF . $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToFile') ); } $attributes += [ 'data-title' => $title, 'data-message' => $confirmMessage, ]; } if ($itemName === 'new' && $this->isFolder()) { $attributes += [ 'data-identifier' => $this->record->getCombinedIdentifier(), 'data-mode' => CreateFolderBrowser::IDENTIFIER, ]; } if ($itemName === 'newFile' && $this->isFolder()) { $attributes += [ 'data-identifier' => $this->record->getCombinedIdentifier(), 'data-mode' => CreateFileBrowser::IDENTIFIER, ]; } if ($itemName === 'pasteInto' && $this->backendUser->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) { $elArr = $this->clipboard->elFromTable('_FILE'); $selItem = reset($elArr); $fileOrFolderInClipBoard = $this->resourceFactory->retrieveFileOrFolderObject($selItem); $title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste'); $confirmMessage = sprintf( $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.' . ($this->clipboard->currentMode() === 'copy' ? 'copy' : 'move') . '_into'), $fileOrFolderInClipBoard->getName(), $this->record->getName() ); $closeText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:button.cancel'); $okLabel = $this->clipboard->currentMode() === 'copy' ? 'copy' : 'pasteinto'; $okText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . $okLabel); $attributes += [ 'data-title' => $title, 'data-message' => $confirmMessage, 'data-button-close-text' => $closeText, 'data-button-ok-text' => $okText, ]; } if ($itemName === 'downloadFile') { $attributes += [ 'data-url' => (string)$this->record->getPublicUrl(), 'data-name' => $this->record->getName(), ]; } // Resource Settings $attributes['data-filecontext-type'] = $this->record instanceof File ? 'file' : 'folder'; $attributes['data-filecontext-identifier'] = $this->getIdentifier(); $attributes['data-filecontext-name'] = $this->record->getName(); $attributes['data-filecontext-uid'] = $this->record instanceof File ? $this->record->getUid() : ''; $attributes['data-filecontext-meta-uid'] = $this->record instanceof File ? $this->record->getMetaData()->offsetGet('uid') : ''; // Add action url for file operations switch ($itemName) { case 'downloadFolder': $attributes['data-action-url'] = (string)$this->uriBuilder->buildUriFromRoute('file_download'); break; case 'edit': $attributes['data-action-url'] = (string)$this->uriBuilder->buildUriFromRoute('file_edit'); break; case 'new': $attributes['data-action-url'] = (string)$this->uriBuilder->buildUriFromRoute('wizard_element_browser'); break; case 'newFile': $attributes['data-action-url'] = (string)$this->uriBuilder->buildUriFromRoute('wizard_element_browser'); break; case 'updateOnlineMedia': $attributes['data-action-url'] = (string)$this->uriBuilder->buildUriFromRoute('file_update_online_media'); break; } return $attributes; } protected function getIdentifier(): string { return $this->record->getCombinedIdentifier(); } }