From 1da5e665e77f1843311f3f387e40504b4610073c Mon Sep 17 00:00:00 2001 From: Sven Wappler Date: Mon, 10 Aug 2026 22:31:18 +0200 Subject: [PATCH] TYPO3 v15 dev-main snapshot () --- .gitignore | 1 + .../ItemProviders/FileProvider.php | 522 +++++ .../Controller/File/EditFileController.php | 199 ++ Classes/Controller/FileDownloadController.php | 171 ++ Classes/Controller/FileListController.php | 762 +++++++ .../FileUpdateOnlineMediaController.php | 93 + Classes/Dto/ResourceCollection.php | 180 ++ Classes/Dto/ResourceView.php | 291 +++ Classes/Dto/UserPermissions.php | 28 + .../AbstractResourceBrowser.php | 268 +++ Classes/ElementBrowser/CreateFileBrowser.php | 129 ++ .../ElementBrowser/CreateFolderBrowser.php | 101 + Classes/ElementBrowser/FileBrowser.php | 183 ++ Classes/ElementBrowser/FolderBrowser.php | 113 ++ .../Event/AfterFileListRowPreparedEvent.php | 64 + Classes/Event/ModifyEditFileFormDataEvent.php | 54 + Classes/Event/ProcessFileListActionsEvent.php | 151 ++ .../AfterBackendPageRenderEventListener.php | 47 + Classes/FileList.php | 1773 +++++++++++++++++ .../AbstractResourceLinkHandler.php | 368 ++++ Classes/LinkHandler/FileLinkHandler.php | 140 ++ Classes/LinkHandler/FolderLinkHandler.php | 102 + Classes/Matcher/AndMatcher.php | 64 + Classes/Matcher/Matcher.php | 47 + Classes/Matcher/MatcherInterface.php | 27 + .../Matcher/ResourceFileExtensionMatcher.php | 92 + Classes/Matcher/ResourceFileTypeMatcher.php | 37 + Classes/Matcher/ResourceFolderTypeMatcher.php | 37 + Classes/Matcher/ResourceMatcher.php | 58 + .../ResourceCollectionPaginator.php | 61 + Classes/Search/LiveSearch/FileProvider.php | 211 ++ Classes/Type/LinkType.php | 47 + Classes/Type/Mode.php | 35 + Classes/Type/SortDirection.php | 35 + Classes/Type/ViewMode.php | 27 + Configuration/Backend/Modules.php | 29 + Configuration/Backend/Routes.php | 32 + Configuration/JavaScriptModules.php | 11 + Configuration/Services.yaml | 18 + Configuration/page.tsconfig | 15 + Configuration/user.tsconfig | 8 + LICENSE.txt | 339 ++++ README.rst | 13 + Resources/Private/Language/locallang.xlf | 146 ++ .../Language/locallang_mod_file_list.xlf | 107 + .../Language/locallang_transfer_handler.xlf | 29 + Resources/Private/Language/module.xlf | 17 + .../Private/Partials/Pagination.fluid.html | 51 + .../Templates/ElementBrowser/Files.fluid.html | 27 + .../ElementBrowser/Folder.fluid.html | 42 + .../ResourceCreation.fluid.html | 34 + .../Templates/File/EditFile.fluid.html | 18 + .../Private/Templates/File/List.fluid.html | 168 ++ .../Templates/Filelist/List.fluid.html | 26 + .../Templates/Filelist/Tiles.fluid.html | 86 + .../Templates/LinkHandler/File.fluid.html | 25 + .../Templates/LinkHandler/Folder.fluid.html | 41 + Resources/Public/Icons/Extension.png | Bin 0 -> 350 bytes Resources/Public/JavaScript/browse-files.js | 13 + Resources/Public/JavaScript/browse-folders.js | 13 + .../Public/JavaScript/context-menu-actions.js | 13 + Resources/Public/JavaScript/file-delete.js | 13 + .../Public/JavaScript/file-list-actions.js | 13 + .../Public/JavaScript/file-list-dragdrop.js | 13 + .../JavaScript/file-list-rename-handler.js | 13 + .../JavaScript/file-list-replace-handler.js | 13 + .../JavaScript/file-list-transfer-handler.js | 13 + Resources/Public/JavaScript/file-list.js | 13 + .../JavaScript/linkbrowser-file-handler.js | 13 + .../JavaScript/linkbrowser-folder-handler.js | 13 + Resources/Public/JavaScript/rename-file.js | 13 + .../Public/JavaScript/resource-creation.js | 13 + composer.json | 58 + 73 files changed, 8040 insertions(+) create mode 100644 .gitignore create mode 100644 Classes/ContextMenu/ItemProviders/FileProvider.php create mode 100644 Classes/Controller/File/EditFileController.php create mode 100644 Classes/Controller/FileDownloadController.php create mode 100644 Classes/Controller/FileListController.php create mode 100644 Classes/Controller/FileUpdateOnlineMediaController.php create mode 100644 Classes/Dto/ResourceCollection.php create mode 100644 Classes/Dto/ResourceView.php create mode 100644 Classes/Dto/UserPermissions.php create mode 100644 Classes/ElementBrowser/AbstractResourceBrowser.php create mode 100644 Classes/ElementBrowser/CreateFileBrowser.php create mode 100644 Classes/ElementBrowser/CreateFolderBrowser.php create mode 100644 Classes/ElementBrowser/FileBrowser.php create mode 100644 Classes/ElementBrowser/FolderBrowser.php create mode 100644 Classes/Event/AfterFileListRowPreparedEvent.php create mode 100644 Classes/Event/ModifyEditFileFormDataEvent.php create mode 100644 Classes/Event/ProcessFileListActionsEvent.php create mode 100644 Classes/EventListener/AfterBackendPageRenderEventListener.php create mode 100644 Classes/FileList.php create mode 100644 Classes/LinkHandler/AbstractResourceLinkHandler.php create mode 100644 Classes/LinkHandler/FileLinkHandler.php create mode 100644 Classes/LinkHandler/FolderLinkHandler.php create mode 100644 Classes/Matcher/AndMatcher.php create mode 100644 Classes/Matcher/Matcher.php create mode 100644 Classes/Matcher/MatcherInterface.php create mode 100644 Classes/Matcher/ResourceFileExtensionMatcher.php create mode 100644 Classes/Matcher/ResourceFileTypeMatcher.php create mode 100644 Classes/Matcher/ResourceFolderTypeMatcher.php create mode 100644 Classes/Matcher/ResourceMatcher.php create mode 100644 Classes/Pagination/ResourceCollectionPaginator.php create mode 100644 Classes/Search/LiveSearch/FileProvider.php create mode 100644 Classes/Type/LinkType.php create mode 100644 Classes/Type/Mode.php create mode 100644 Classes/Type/SortDirection.php create mode 100644 Classes/Type/ViewMode.php create mode 100644 Configuration/Backend/Modules.php create mode 100644 Configuration/Backend/Routes.php create mode 100644 Configuration/JavaScriptModules.php create mode 100644 Configuration/Services.yaml create mode 100644 Configuration/page.tsconfig create mode 100644 Configuration/user.tsconfig create mode 100644 LICENSE.txt create mode 100644 README.rst create mode 100644 Resources/Private/Language/locallang.xlf create mode 100644 Resources/Private/Language/locallang_mod_file_list.xlf create mode 100644 Resources/Private/Language/locallang_transfer_handler.xlf create mode 100644 Resources/Private/Language/module.xlf create mode 100644 Resources/Private/Partials/Pagination.fluid.html create mode 100644 Resources/Private/Templates/ElementBrowser/Files.fluid.html create mode 100644 Resources/Private/Templates/ElementBrowser/Folder.fluid.html create mode 100644 Resources/Private/Templates/ElementBrowser/ResourceCreation.fluid.html create mode 100644 Resources/Private/Templates/File/EditFile.fluid.html create mode 100644 Resources/Private/Templates/File/List.fluid.html create mode 100644 Resources/Private/Templates/Filelist/List.fluid.html create mode 100644 Resources/Private/Templates/Filelist/Tiles.fluid.html create mode 100644 Resources/Private/Templates/LinkHandler/File.fluid.html create mode 100644 Resources/Private/Templates/LinkHandler/Folder.fluid.html create mode 100644 Resources/Public/Icons/Extension.png create mode 100644 Resources/Public/JavaScript/browse-files.js create mode 100644 Resources/Public/JavaScript/browse-folders.js create mode 100644 Resources/Public/JavaScript/context-menu-actions.js create mode 100644 Resources/Public/JavaScript/file-delete.js create mode 100644 Resources/Public/JavaScript/file-list-actions.js create mode 100644 Resources/Public/JavaScript/file-list-dragdrop.js create mode 100644 Resources/Public/JavaScript/file-list-rename-handler.js create mode 100644 Resources/Public/JavaScript/file-list-replace-handler.js create mode 100644 Resources/Public/JavaScript/file-list-transfer-handler.js create mode 100644 Resources/Public/JavaScript/file-list.js create mode 100644 Resources/Public/JavaScript/linkbrowser-file-handler.js create mode 100644 Resources/Public/JavaScript/linkbrowser-folder-handler.js create mode 100644 Resources/Public/JavaScript/rename-file.js create mode 100644 Resources/Public/JavaScript/resource-creation.js create mode 100644 composer.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/Classes/ContextMenu/ItemProviders/FileProvider.php b/Classes/ContextMenu/ItemProviders/FileProvider.php new file mode 100644 index 0000000..2dc6567 --- /dev/null +++ b/Classes/ContextMenu/ItemProviders/FileProvider.php @@ -0,0 +1,522 @@ + [ + '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(); + } +} diff --git a/Classes/Controller/File/EditFileController.php b/Classes/Controller/File/EditFileController.php new file mode 100644 index 0000000..6705186 --- /dev/null +++ b/Classes/Controller/File/EditFileController.php @@ -0,0 +1,199 @@ + '', + 'config' => [ + 'type' => 'text', + 'cols' => 48, + 'wrap' => 'off', + 'enableTabulator' => true, + 'fixedFont' => true, + ], + ]; + + protected array $formEngineData = [ + 'databaseRow' => [ + 'uid' => 0, + 'data' => '', + 'target' => 0, + 'redirect' => '', + ], + 'tableName' => 'editfile', + 'processedTca' => [ + 'columns' => [ + 'data' => [], + 'target' => [ + 'config' => [ + 'type' => 'input', + 'renderType' => 'hidden', + ], + ], + 'redirect' => [ + 'config' => [ + 'type' => 'input', + 'renderType' => 'hidden', + ], + ], + ], + 'types' => [ + 1 => [ + 'showitem' => 'data,target,redirect', + ], + ], + ], + 'recordTypeValue' => 1, + 'inlineStructure' => [], + 'renderType' => 'fullRecordContainer', + ]; + + public function __construct( + protected readonly IconFactory $iconFactory, + protected readonly UriBuilder $uriBuilder, + protected readonly ResourceFactory $resourceFactory, + protected readonly ModuleTemplateFactory $moduleTemplateFactory, + protected readonly ResponseFactory $responseFactory, + protected readonly StreamFactoryInterface $streamFactory, + protected readonly EventDispatcherInterface $eventDispatcher, + protected readonly NodeFactory $nodeFactory, + protected readonly ComponentFactory $componentFactory, + protected readonly FormResultFactory $formResultFactory, + protected readonly FormResultHandler $formResultHandler, + ) {} + + /** + * Render the edit file content form using FormEngine. + */ + public function mainAction(ServerRequestInterface $request): ResponseInterface + { + $languageService = $this->getLanguageService(); + $view = $this->moduleTemplateFactory->create($request); + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + + $combinedIdentifier = $parsedBody['target'] ?? $queryParams['target'] ?? ''; + $file = $this->resourceFactory->retrieveFileOrFolderObject($combinedIdentifier); + + if (!$file instanceof FileInterface) { + throw new InvalidFileException('Referenced target "' . $combinedIdentifier . '" could not be resolved to a valid file', 1294586841); + } + if ($file->getStorage()->isFallbackStorage()) { + throw new InsufficientFileAccessPermissionsException('You are not allowed to access files outside your storages', 1375889832); + } + + /** @var Folder $parentFolder */ + $parentFolder = $file->getParentFolder(); + + $returnUrl = GeneralUtility::sanitizeLocalUrl( + $parsedBody['returnUrl'] + ?? $queryParams['returnUrl'] + ?? (string)$this->uriBuilder->buildUriFromRoute('media_management', [ + 'id' => $parentFolder->getCombinedIdentifier(), + ]), + $request + ); + + if (!$file->isTextFile()) { + $extList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext']; + $view->addFlashMessage('Files with that extension are not editable. Allowed extensions are: ' . $extList, '', ContextualFeedbackSeverity::ERROR, true); + return $this->responseFactory->createResponse(400)->withHeader('location', $returnUrl); + } + + $this->addDocHeaderButtons($view, $returnUrl); + + $dataColumnDefinition = $this->dataColumnTca; + $dataColumnDefinition['label'] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:file')) . ' ' . htmlspecialchars($combinedIdentifier); + + $formData = $this->formEngineData; + $formData['databaseRow']['data'] = $file->getContents(); + $formData['databaseRow']['target'] = $file->getUid(); + $formData['databaseRow']['redirect'] = (string)$this->uriBuilder->buildUriFromRoute('file_edit', ['target' => $combinedIdentifier, 'returnUrl' => $returnUrl]); + $formData['processedTca']['columns']['data'] = $dataColumnDefinition; + + $formData = $this->eventDispatcher->dispatch( + new ModifyEditFileFormDataEvent($formData, $file, $request) + )->getFormData(); + + $resultArray = $this->nodeFactory->create($formData)->render(); + $formResult = $this->formResultFactory->create($resultArray); + + $this->formResultHandler->addAssets($formResult); + // Rendering of the output via fluid and PageRenderer + $view->assignMultiple([ + 'moduleUrlTceFile' => (string)$this->uriBuilder->buildUriFromRoute('tce_file'), + 'fileName' => $file->getName(), + 'form' => $formResult->html, + ]); + $content = $view->render('File/EditFile'); + + return $this->responseFactory->createResponse() + ->withHeader('Content-Type', 'text/html; charset=utf-8') + ->withBody($this->streamFactory->createStream($content)); + } + + protected function addDocHeaderButtons(ModuleTemplate $view, string $returnUrl): void + { + $languageService = $this->getLanguageService(); + $view->addButtonToButtonBar( + $this->componentFactory->createSaveButton('EditFileController') + ->setTitle($languageService->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:file_edit.php.submit')), + ButtonBar::BUTTON_POSITION_LEFT, + 20 + ); + $view->addButtonToButtonBar($this->componentFactory->createCloseButton($returnUrl), ButtonBar::BUTTON_POSITION_LEFT, 10); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/FileDownloadController.php b/Classes/Controller/FileDownloadController.php new file mode 100644 index 0000000..1056d36 --- /dev/null +++ b/Classes/Controller/FileDownloadController.php @@ -0,0 +1,171 @@ +resourceFactory = $resourceFactory; + $this->responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->context = $context; + } + + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $items = (array)($request->getParsedBody()['items'] ?? []); + if ($items === []) { + // Return in case no items are given + return $this->responseFactory->createResponse(400); + } + + $fileExtensionFilter = null; + $fileDownloadConfiguration = (array)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['fileDownload.'] ?? []); + if ($fileDownloadConfiguration !== []) { + if (!($fileDownloadConfiguration['enabled'] ?? true)) { + // Return if file download is disabled + return $this->responseFactory->createResponse(403); + } + // Initialize file extension filter, if configured + $fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class); + $fileExtensionFilter->setAllowedFileExtensions( + GeneralUtility::trimExplode(',', (string)($fileDownloadConfiguration['allowedFileExtensions'] ?? ''), true) + ); + $fileExtensionFilter->setDisallowedFileExtensions( + GeneralUtility::trimExplode(',', (string)($fileDownloadConfiguration['disallowedFileExtensions'] ?? ''), true) + ); + } + + $zipStream = tmpfile(); + if (!is_resource($zipStream)) { + throw new \RuntimeException('Could not open temporary resource for creating archive', 1630346631); + } + $zipFileName = stream_get_meta_data($zipStream)['uri']; + $zipFile = new \ZipArchive(); + $zipFile->open($zipFileName, \ZipArchive::OVERWRITE); + $filesAdded = 0; + foreach ($this->collectFiles($items) as $fileName => $fileObject) { + // Add files with read permission and allowed file extension + if (!$fileObject->getStorage()->checkFileActionPermission('read', $fileObject) + || ($fileExtensionFilter !== null && !$fileExtensionFilter->isAllowed($fileObject->getExtension())) + ) { + continue; + } + $filesAdded++; + $zipFile->addFile($fileObject->getForLocalProcessing(false), $fileName); + } + $zipFile->close(); + $response = $this->createResponse($zipFileName, $filesAdded); + if ($filesAdded > 0) { + unlink($zipFileName); + } + return $response; + } + + protected function createResponse(string $temporaryFileName, int $filesAdded): ResponseInterface + { + if ($filesAdded === 0) { + return $this->responseFactory->createResponse() + ->withHeader('Content-Type', 'application/json; charset=utf-8') + ->withBody($this->streamFactory->createStream(json_encode(['success' => false, 'status' => 'noFiles']))); + } + + $downloadFileName = 'typo3_download_' . $this->context->getAspect('date')->getDateTime()->format('Y-m-d-His') . '.zip'; + return $this->responseFactory->createResponse() + ->withHeader('Content-Type', 'application/zip') + ->withHeader('Content-Disposition', 'attachment; filename=' . $downloadFileName) + ->withHeader('Content-Transfer-Encoding', 'binary') + ->withHeader('Pragma', 'no-cache') + ->withHeader('Cache-Control', 'no-cache, no-store') + ->withBody($this->streamFactory->createStreamFromFile($temporaryFileName)); + } + + /** + * @return FileInterface[] + */ + protected function collectFiles(array $items): array + { + $files = []; + foreach ($items as $itemIdentifier) { + $fileOrFolderObject = $this->resourceFactory->retrieveFileOrFolderObject($itemIdentifier); + if ($fileOrFolderObject === null) { + continue; + } + // Files from fallback storage must be skipped in general + if ($fileOrFolderObject->getStorage()->isFallbackStorage()) { + continue; + } + $baseIdentifier = dirname($fileOrFolderObject->getIdentifier()); + if ($fileOrFolderObject instanceof Folder) { + // handle file / folder structure + foreach ($this->getFilesAndFoldersRecursive($fileOrFolderObject) as $fileObject) { + $commonPrefix = (string)PathUtility::getCommonPrefix([$baseIdentifier, $fileObject->getIdentifier()]); + $files[substr($fileObject->getIdentifier(), strlen($commonPrefix))] = $fileObject; + } + } else { + $files[$fileOrFolderObject->getName()] = $fileOrFolderObject; + } + } + return $files; + } + + protected function getFilesAndFoldersRecursive(Folder $folder): iterable + { + foreach ($folder->getSubfolders() as $subFolder) { + yield from $this->getFilesAndFoldersRecursive($subFolder); + } + foreach ($folder->getFiles() as $file) { + yield $file; + } + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/FileListController.php b/Classes/Controller/FileListController.php new file mode 100644 index 0000000..a9ee3c7 --- /dev/null +++ b/Classes/Controller/FileListController.php @@ -0,0 +1,762 @@ + Filelist module. + * + * @internal this is a concrete TYPO3 controller implementation and solely used for EXT:filelist and not part of TYPO3's Core API. + */ +#[AsController] +class FileListController +{ + protected string $id = ''; + protected string $cmd = ''; + protected string $searchTerm = ''; + protected int $currentPage = 1; + protected bool $allowClipboard = true; + + protected ?Folder $folderObject = null; + protected ?DuplicationBehavior $overwriteExistingFiles = null; + protected ?ModuleTemplate $view = null; + protected ?FileList $filelist = null; + protected ?ModuleData $moduleData = null; + + public function __construct( + protected readonly UriBuilder $uriBuilder, + protected readonly PageRenderer $pageRenderer, + protected readonly IconFactory $iconFactory, + protected readonly ResourceFactory $resourceFactory, + protected readonly ModuleTemplateFactory $moduleTemplateFactory, + protected readonly BackendViewFactory $viewFactory, + protected readonly ResponseFactoryInterface $responseFactory, + protected readonly TcaSchemaFactory $tcaSchemaFactory, + protected readonly ComponentFactory $componentFactory, + protected readonly LoggerInterface $logger, + protected readonly FlashMessageService $flashMessageService, + ) {} + + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $lang = $this->getLanguageService(); + $backendUser = $this->getBackendUser(); + + $this->moduleData = $request->getAttribute('moduleData'); + + $this->view = $this->moduleTemplateFactory->create($request); + $this->view->setTitle($lang->translate('title', 'filelist.module')); + + $queryParams = $request->getQueryParams(); + $parsedBody = $request->getParsedBody(); + + $this->id = (string)($parsedBody['id'] ?? $queryParams['id'] ?? ''); + $this->cmd = (string)($parsedBody['cmd'] ?? $queryParams['cmd'] ?? ''); + $this->searchTerm = (string)trim($parsedBody['searchTerm'] ?? $queryParams['searchTerm'] ?? ''); + $this->currentPage = (int)($parsedBody['currentPage'] ?? $queryParams['currentPage'] ?? 1); + $duplicationBehaviorFromRequest = $parsedBody['overwriteExistingFiles'] ?? $queryParams['overwriteExistingFiles'] ?? ''; + $this->overwriteExistingFiles = DuplicationBehavior::tryFrom($duplicationBehaviorFromRequest) ?? DuplicationBehavior::getDefaultDuplicationBehaviour(); + + $storage = null; + try { + if ($this->id !== '') { + $backendUser->evaluateUserSpecificFileFilterSettings(); + $storage = GeneralUtility::makeInstance(StorageRepository::class)->findByCombinedIdentifier($this->id); + if ($storage !== null) { + $identifier = substr($this->id, strpos($this->id, ':') + 1); + if (!$storage->hasFolder($identifier)) { + // @todo: should we redirect to form engine instead of implicitly showing the folder, when a file is requested? + // (that way breadcrumb would not need to handle module-specific formegine routes) + $identifier = $storage->getFolderIdentifierFromFileIdentifier($identifier); + } + $this->folderObject = $storage->getFolder($identifier); + // Disallow access to fallback storage 0 + if ($storage->isFallbackStorage()) { + throw new InsufficientFolderAccessPermissionsException( + 'You are not allowed to access files outside your storages', + 1434539815 + ); + } + // Disallow the rendering of the processing folder (e.g. could be called manually) + if ($storage->isProcessingFolder($this->folderObject)) { + $this->folderObject = $storage->getRootLevelFolder(); + } + } + } else { + // Take the first available storage + $fileStorages = array_filter($backendUser->getFileStorages(), static fn(ResourceStorage $storage) => $storage->isBrowsable()); + $fileStorage = reset($fileStorages); + if ($fileStorage) { + $this->folderObject = $fileStorage->getRootLevelFolder(); + } else { + throw new \RuntimeException('Could not find any folder to be displayed.', 1349276894); + } + } + + if ($this->folderObject && !$this->folderObject->getStorage()->isWithinFileMountBoundaries($this->folderObject)) { + throw new \RuntimeException('Folder not accessible.', 1430409089); + } + } catch (FolderDoesNotExistException|InsufficientFolderAccessPermissionsException $permissionException) { + $this->folderObject = null; + if ($storage->getDriverType() === 'Local' && !$storage->isOnline()) { + // If the base folder for a local storage does not exists, the storage is marked as offline and the + // access permission exception is thrown. In this case we however want to display another error message. + // @see https://forge.typo3.org/issues/85323 + $this->addFlashMessage( + sprintf($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:localStorageOfflineMessage'), $storage->getName()), + $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:localStorageOfflineTitle'), + ContextualFeedbackSeverity::ERROR + ); + } else { + $this->addFlashMessage( + sprintf($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:missingFolderPermissionsMessage'), $this->id), + $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:missingFolderPermissionsTitle'), + ContextualFeedbackSeverity::ERROR + ); + } + } catch (Exception $fileException) { + $this->folderObject = null; + // Take the first object of the first storage + $fileStorages = $backendUser->getFileStorages(); + $fileStorage = reset($fileStorages); + if ($fileStorage instanceof ResourceStorage) { + $this->folderObject = $fileStorage->getRootLevelFolder(); + if (!$fileStorage->isWithinFileMountBoundaries($this->folderObject)) { + $this->folderObject = null; + } + } + if (!$this->folderObject) { + $this->addFlashMessage( + sprintf($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:folderNotFoundMessage'), $this->id), + $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:folderNotFoundTitle'), + ContextualFeedbackSeverity::ERROR + ); + } + } catch (\RuntimeException $e) { + $this->folderObject = null; + $this->addFlashMessage( + $e->getMessage() . ' (' . $e->getCode() . ')', + $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:folderNotFoundTitle'), + ContextualFeedbackSeverity::ERROR + ); + } + + if ($this->folderObject + && !$this->folderObject->getStorage()->checkFolderActionPermission('read', $this->folderObject) + ) { + $this->folderObject = null; + } + + $this->view->assign('currentIdentifier', $this->folderObject ? $this->folderObject->getCombinedIdentifier() : ''); + $javaScriptRenderer = $this->pageRenderer->getJavaScriptRenderer(); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/filelist/file-list.js')->instance() + ); + + $this->pageRenderer->loadJavaScriptModule('@typo3/filelist/file-list-dragdrop.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/filelist/file-list-transfer-handler.js'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:filelist/Resources/Private/Language/locallang_transfer_handler.xlf'); + + $this->pageRenderer->loadJavaScriptModule('@typo3/filelist/file-list-actions.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/filelist/file-list-rename-handler.js'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf', 'file_rename'); + $this->pageRenderer->loadJavaScriptModule('@typo3/filelist/file-list-replace-handler.js'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf', 'file_replace'); + + $this->pageRenderer->loadJavaScriptModule('@typo3/filelist/file-delete.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/context-menu.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/clipboard-panel.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/localization.js'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/Wizards/localization.xlf'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/multi-record-selection.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/column-selector-button.js'); + + $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf', 'buttons'); + + $this->initializeModule(); + + // In case the folderObject is NULL, the request is either invalid or the user + // does not have necessary permissions. Just render and return the "empty" view. + if ($this->folderObject === null) { + return $this->view->renderResponse('File/List'); + } + + return $this->processRequest($request); + } + + protected function processRequest(ServerRequestInterface $request): ResponseInterface + { + $lang = $this->getLanguageService(); + + // Initialize FileList, including the clipboard actions + $this->initializeFileList($request); + + // Generate the file listing markup + $this->generateFileList($request); + + // Generate the clipboard, if enabled + $this->view->assign('showClipboardPanel', (bool)$this->moduleData->get('clipBoard')); + + // Register drag-uploader + $this->registerDragUploader(); + + // Register the display thumbnails / show clipboard checkboxes + $this->registerFileListCheckboxes(); + + // Register additional doc header buttons + $this->registerAdditionalDocHeaderButtons(); + + // Add additional view variables + $this->view->assignMultiple([ + 'headline' => $this->getModuleHeadline(), + 'folderIdentifier' => $this->folderObject->getCombinedIdentifier(), + 'searchTerm' => $this->searchTerm, + ]); + + // Overwrite the default module title, adding the specific module headline (the folder name) + $this->view->setTitle( + $lang->translate('title', 'filelist.module'), + $this->getModuleHeadline() + ); + + $this->view->getDocHeaderComponent()->setBreadcrumbContext(new BreadcrumbContext($this->folderObject)); + + return $this->view->renderResponse('File/List'); + } + + protected function initializeModule(): void + { + $userTsConfig = $this->getBackendUser()->getTSConfig(); + + // Set predefined value for DisplayThumbnails: + if (($userTsConfig['options.']['file_list.']['enableDisplayThumbnails'] ?? '') === 'activated') { + $this->moduleData->set('displayThumbs', true); + } elseif (($userTsConfig['options.']['file_list.']['enableDisplayThumbnails'] ?? '') === 'deactivated') { + $this->moduleData->set('displayThumbs', false); + } + // Set predefined value for Clipboard: + if (($userTsConfig['options.']['file_list.']['enableClipBoard'] ?? '') === 'activated') { + $this->moduleData->set('clipBoard', true); + $this->allowClipboard = false; + } elseif (($userTsConfig['options.']['file_list.']['enableClipBoard'] ?? '') === 'selectable') { + $this->allowClipboard = true; + } elseif (($userTsConfig['options.']['file_list.']['enableClipBoard'] ?? '') === 'deactivated') { + $this->moduleData->set('clipBoard', false); + $this->allowClipboard = false; + } + // Set predefined value for viewMode: + $viewMode = ViewMode::tryFrom($this->moduleData->get('viewMode') ?? '') + ?? ViewMode::tryFrom($userTsConfig['options.']['defaultResourcesViewMode'] ?? '') + ?? ViewMode::TILES; + $this->moduleData->set('viewMode', $viewMode->value); + } + + protected function initializeFileList(ServerRequestInterface $request): void + { + // Create the file list + $this->filelist = GeneralUtility::makeInstance(FileList::class, $request); + $this->filelist->viewMode = ViewMode::tryFrom($this->moduleData->get('viewMode')) ?? ViewMode::TILES; + $this->filelist->thumbs = ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] ?? false) && $this->moduleData->get('displayThumbs'); + + // Create clipboard object and initialize it + $CB = array_replace_recursive($request->getQueryParams()['CB'] ?? [], $request->getParsedBody()['CB'] ?? []); + if (($this->cmd === 'copyMarked' || $this->cmd === 'removeMarked')) { + // Get CBC from request, and map the element values, since they must either be the file identifier, + // in case the element should be transferred to the clipboard, or false if it should be removed. + $CBC = array_map(fn($item) => $this->cmd === 'copyMarked' ? $item : false, (array)($request->getParsedBody()['CBC'] ?? [])); + // Cleanup CBC + $CB['el'] = $this->filelist->clipObj->cleanUpCBC($CBC, '_FILE'); + } + if (!$this->moduleData->get('clipBoard')) { + $CB['setP'] = 'normal'; + } + $this->filelist->clipObj->setCmd($CB); + $this->filelist->clipObj->cleanCurrent(); + $this->filelist->clipObj->endClipboard(); + + // If the "cmd" was to delete files from the list, do that: + if ($this->cmd === 'delete') { + $items = $this->filelist->clipObj->cleanUpCBC( + (array)($request->getParsedBody()['CBC'] ?? []), + '_FILE', + true + ); + if (!empty($items)) { + // Make command array: + $FILE = []; + foreach ($items as $clipboardIdentifier => $combinedIdentifier) { + $FILE['delete'][] = ['data' => $combinedIdentifier]; + $this->filelist->clipObj->removeElement($clipboardIdentifier); + } + // Init file processing object for deleting and pass the cmd array. + $fileProcessor = GeneralUtility::makeInstance(ExtendedFileUtility::class); + $fileProcessor->setActionPermissions(); + $fileProcessor->setExistingFilesConflictMode($this->overwriteExistingFiles); + $fileProcessor->start($FILE, []); + $fileProcessor->processData(); + // Clean & Save clipboard state + $this->filelist->clipObj->cleanCurrent(); + $this->filelist->clipObj->endClipboard(); + } + } + + // Start up the file list by including processed settings. + $this->filelist->start( + $this->folderObject, + MathUtility::forceIntegerInRange($this->currentPage, 1, 100000), + (string)($this->moduleData->get('sortField') ?: 'name'), + SortDirection::tryFrom($this->moduleData->get('sortDirection') ?? '') ?? SortDirection::ASCENDING + ); + + // Only add selected columns if the feature is enabled + if ($this->getBackendUser()->getTSConfig()['options.']['file_list.']['displayColumnSelector'] ?? true) { + $this->filelist->setColumnsToRender($this->getBackendUser()->getModuleData('list/displayFields')['_FILE'] ?? []); + } + + $resourceSelectableMatcher = GeneralUtility::makeInstance(Matcher::class); + $resourceSelectableMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFileTypeMatcher::class)); + $resourceSelectableMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + $this->filelist->setResourceSelectableMatcher($resourceSelectableMatcher); + + $resourceDownloadMatcher = GeneralUtility::makeInstance(Matcher::class); + $resourceDownloadMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFileTypeMatcher::class)); + $resourceDownloadMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + $this->filelist->setResourceDownloadMatcher($resourceDownloadMatcher); + } + + protected function generateFileList(ServerRequestInterface $request): void + { + $lang = $this->getLanguageService(); + + // If a searchTerm is provided, create the searchDemand object + $searchDemand = $this->searchTerm !== '' + ? FileSearchDemand::createForSearchTerm($this->searchTerm)->withRecursive() + : null; + + // Generate the list, if accessible + if ($this->folderObject->getStorage()->isBrowsable()) { + $fileListView = $this->viewFactory->create($request); + $this->view->assignMultiple([ + 'listHtml' => $this->filelist->render($searchDemand, $fileListView), + 'listUrl' => $this->filelist->createModuleUri(), + 'totalItems' => $this->filelist->totalItems, + ]); + + // Add edit metadata configuration, if user can edit default language + if ($this->getBackendUser()->checkLanguageAccess(0)) { + $this->view->assign( + 'editActionConfiguration', + GeneralUtility::jsonEncodeForHtmlAttribute([ + 'idField' => 'filelistMetaUid', + 'table' => 'sys_file_metadata', + 'returnUrl' => $this->filelist->createModuleUri(), + ]) + ); + $allowedFields = BackendUtility::getAllowedFieldsForTable('sys_file_metadata'); + $columnsOnly = array_filter($this->filelist->fieldArray, static fn($field) => in_array($field, $allowedFields, true)); + if ($columnsOnly !== []) { + $this->view->assign( + 'editColumnsActionConfiguration', + GeneralUtility::jsonEncodeForHtmlAttribute([ + 'idField' => 'filelistMetaUid', + 'table' => 'sys_file_metadata', + 'columnsOnly' => array_values($columnsOnly), + 'returnUrl' => $this->filelist->createModuleUri(), + ]) + ); + } + } + + // Assign meta information for the multi record selection + $this->view->assign( + 'deleteActionConfiguration', + GeneralUtility::jsonEncodeForHtmlAttribute([ + 'ok' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete'), + 'title' => $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:deleteMarked'), + 'content' => $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:deleteMarkedWarning'), + ]), + ); + + // Add download button configuration, if file download is enabled + if ($this->getBackendUser()->getTSConfig()['options.']['file_list.']['fileDownload.']['enabled'] ?? true) { + $this->view->assign( + 'downloadActionConfiguration', + GeneralUtility::jsonEncodeForHtmlAttribute([ + 'downloadUrl' => (string)$this->uriBuilder->buildUriFromRoute('file_download'), + ]) + ); + } + } else { + $this->addFlashMessage( + $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:storageNotBrowsableMessage'), + $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:storageNotBrowsableTitle') + ); + } + } + + protected function registerDragUploader(): void + { + // Include DragUploader only if we have write access + if ($this->folderObject->checkActionPermission('write') + && $this->folderObject->getStorage()->checkUserActionPermission('add', 'File') + ) { + $lang = $this->getLanguageService(); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/drag-uploader.js'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf', 'file_upload'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf', 'file_download'); + $this->pageRenderer->addInlineLanguageLabelArray([ + 'type.file' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:file'), + 'permissions.read' => $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:read'), + 'permissions.write' => $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:write'), + 'online_media.update.success' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.update.success'), + 'online_media.update.error' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.update.error'), + 'labels.contextMenu.open' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.contextMenu.open'), + ]); + $defaultDuplicationBehavior = DuplicationBehavior::getDefaultDuplicationBehaviour($this->getBackendUser()); + $this->view->assign('dragUploader', [ + 'fileDenyPattern' => $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] ?? null, + 'maxFileSize' => GeneralUtility::getMaxUploadFileSize() * 1024, + 'defaultDuplicationBehaviourAction' => $defaultDuplicationBehavior->value, + ]); + } + } + + protected function registerFileListCheckboxes(): void + { + $lang = $this->getLanguageService(); + $userTsConfig = $this->getBackendUser()->getTSConfig(); + $enableClipBoard = ($userTsConfig['options.']['file_list.']['enableClipBoard'] ?? ''); + + $this->view->assign('enableClipBoard', [ + 'enabled' => $enableClipBoard === 'activated' || $enableClipBoard === 'selectable', + 'label' => htmlspecialchars($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:clipBoard')), + 'mode' => $this->filelist->clipObj->current, + ]); + } + + /** + * Create the panel of buttons for submitting the form or otherwise perform operations. + */ + protected function registerAdditionalDocHeaderButtons(): void + { + $lang = $this->getLanguageService(); + // ViewMode + $viewModeItems = []; + $viewModeItems[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->moduleData->get('viewMode') === ViewMode::TILES->value) + ->setHref($this->filelist->createModuleUri(['viewMode' => ViewMode::TILES->value])) + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.tiles')) + ->setIcon($this->iconFactory->getIcon('actions-viewmode-tiles')); + $viewModeItems[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->moduleData->get('viewMode') === ViewMode::LIST->value) + ->setHref($this->filelist->createModuleUri(['viewMode' => ViewMode::LIST->value])) + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.list')) + ->setIcon($this->iconFactory->getIcon('actions-viewmode-list')); + $viewModeItems[] = $this->componentFactory->createDropDownDivider(); + if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] && ($this->getBackendUser()->getTSConfig()['options.']['file_list.']['enableDisplayThumbnails'] ?? '') === 'selectable') { + $viewModeItems[] = $this->componentFactory->createDropDownToggle() + ->setActive((bool)$this->moduleData->get('displayThumbs')) + ->setHref($this->filelist->createModuleUri(['displayThumbs' => $this->moduleData->get('displayThumbs') ? 0 : 1])) + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.showThumbnails')) + ->setIcon($this->iconFactory->getIcon('actions-image')); + } + if ($this->allowClipboard) { + $viewModeItems[] = $this->componentFactory->createDropDownToggle() + ->setActive((bool)$this->moduleData->get('clipBoard')) + ->setHref($this->filelist->createModuleUri(['clipBoard' => $this->moduleData->get('clipBoard') ? 0 : 1])) + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.showClipboard')) + ->setIcon($this->iconFactory->getIcon('actions-clipboard')); + } + if (($this->getBackendUser()->getTSConfig()['options.']['file_list.']['displayColumnSelector'] ?? true) + && $this->moduleData->get('viewMode') === ViewMode::LIST->value) { + $viewModeItems[] = $this->componentFactory->createDropDownDivider(); + $viewModeItems[] = $this->componentFactory->createDropDownItem() + ->setTag('typo3-backend-column-selector-button') + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.selectColumns')) + ->setAttributes([ + 'data-url' => (string)$this->uriBuilder->buildUriFromRoute( + 'ajax_show_columns_selector', + ['table' => '_FILE'] + ), + 'data-target' => (string)$this->filelist->createModuleUri(), + 'data-title' => sprintf( + $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:showColumnsSelection'), + $this->tcaSchemaFactory->get('sys_file')->getTitle($lang->sL(...)), + ), + 'data-button-ok' => $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView'), + 'data-button-close' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel'), + 'data-error-message' => $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView.error'), + ]) + ->setIcon($this->iconFactory->getIcon('actions-options')); + } + + $sortingButton = $this->componentFactory->createDropDownButton() + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting')) + ->setIcon($this->iconFactory->getIcon($this->filelist->sortDirection->getIconIdentifier())) + ->setShowLabelText(true); + + $sortingModeButtons = []; + $sortableFields = $this->filelist->getSortableFields(); + if (count($sortableFields) > 1) { + foreach ($sortableFields as $field) { + $label = $this->filelist->getFieldLabel($field); + + $sortingModeButtons[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->filelist->sortField === $field) + ->setHref($this->filelist->createModuleUri([ + 'sortField' => $field, + 'currentPage' => 0, + 'sortDirection' => (int)($this->filelist->sortDirection === SortDirection::DESCENDING), + ])) + ->setLabel($label); + } + + $sortingModeButtons[] = $this->componentFactory->createDropDownDivider(); + } + $defaultSortingDirectionParams = ['sortField' => $this->filelist->sortField, 'currentPage' => 0]; + $sortingModeButtons[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->filelist->sortDirection === SortDirection::ASCENDING) + ->setHref($this->filelist->createModuleUri(array_merge($defaultSortingDirectionParams, ['sortDirection' => SortDirection::ASCENDING->value]))) + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting.asc')); + $sortingModeButtons[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->filelist->sortDirection === SortDirection::DESCENDING) + ->setHref($this->filelist->createModuleUri(array_merge($defaultSortingDirectionParams, ['sortDirection' => SortDirection::DESCENDING->value]))) + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting.desc')); + + foreach ($sortingModeButtons as $sortingModeButton) { + $sortingButton->addItem($sortingModeButton); + } + + $this->view->addButtonToButtonBar($sortingButton, ButtonBar::BUTTON_POSITION_RIGHT, 2); + + $viewModeButton = $this->componentFactory->createDropDownButton() + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view')) + ->setIcon($this->iconFactory->getIcon('actions-cog')) + ->setShowLabelText(true); + foreach ($viewModeItems as $viewModeItem) { + $viewModeButton->addItem($viewModeItem); + } + $this->view->addButtonToButtonBar($viewModeButton, ButtonBar::BUTTON_POSITION_RIGHT, 3); + + // Level up + try { + $currentStorage = $this->folderObject->getStorage(); + $parentFolder = $this->folderObject->getParentFolder(); + if ($currentStorage->isWithinFileMountBoundaries($parentFolder) + && $parentFolder->getIdentifier() !== $this->folderObject->getIdentifier() + ) { + $levelUpButton = $this->componentFactory->createLinkButton() + ->setDataAttributes([ + 'tree-update-request' => htmlspecialchars('folder' . GeneralUtility::md5int($parentFolder->getCombinedIdentifier())), + ]) + ->setHref( + (string)$this->uriBuilder->buildUriFromRoute( + 'media_management', + ['id' => $parentFolder->getCombinedIdentifier()] + ) + ) + ->setShowLabelText(true) + ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.upOneLevel')) + ->setIcon($this->iconFactory->getIcon('actions-view-go-up', IconSize::SMALL)); + $this->view->addButtonToButtonBar($levelUpButton); + } + } catch (\Exception $e) { + } + + // Shortcut + $this->view->getDocHeaderComponent()->setShortcutContext( + 'media_management', + sprintf( + '%s: %s', + $lang->translate('title', 'filelist.module'), + $this->folderObject->getName() ?: $this->folderObject->getIdentifier() + ), + array_filter([ + 'id' => $this->id, + 'searchTerm' => $this->searchTerm, + ]) + ); + + // New file button + if ($this->folderObject && $this->folderObject->checkActionPermission('write') + && $this->folderObject->getStorage()->checkUserActionPermission('add', 'File') + ) { + $newButton = $this->componentFactory->createLinkButton() + ->setClasses('t3js-element-browser') + ->setHref((string)$this->uriBuilder->buildUriFromRoute('wizard_element_browser')) + ->setDataAttributes([ + 'identifier' => $this->folderObject->getCombinedIdentifier(), + 'mode' => CreateFileBrowser::IDENTIFIER, + ]) + ->setShowLabelText(true) + ->setTitle($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:actions.new_file')) + ->setIcon($this->iconFactory->getIcon('actions-file-add', IconSize::SMALL)); + $this->view->addButtonToButtonBar($newButton, ButtonBar::BUTTON_POSITION_LEFT, 2); + } + + // New folder button + if ($this->folderObject && $this->folderObject->checkActionPermission('write') && $this->folderObject->checkActionPermission('add')) { + $newButton = $this->componentFactory->createLinkButton() + ->setClasses('t3js-element-browser') + ->setHref((string)$this->uriBuilder->buildUriFromRoute('wizard_element_browser')) + ->setDataAttributes([ + 'identifier' => $this->folderObject->getCombinedIdentifier(), + 'mode' => CreateFolderBrowser::IDENTIFIER, + ]) + ->setShowLabelText(true) + ->setTitle($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:actions.new_folder')) + ->setIcon($this->iconFactory->getIcon('actions-folder-add', IconSize::SMALL)); + $this->view->addButtonToButtonBar($newButton, ButtonBar::BUTTON_POSITION_LEFT, 3); + } + + // Add paste button + if ($this->folderObject->checkActionPermission('write')) { + $elFromTable = $this->filelist->clipObj->elFromTable('_FILE'); + if ($elFromTable !== []) { + $addPasteButton = true; + foreach ($elFromTable as $element) { + $clipBoardElement = $this->resourceFactory->retrieveFileOrFolderObject($element); + if ($clipBoardElement instanceof Folder && $clipBoardElement->getStorage()->isWithinFolder( + $clipBoardElement, + $this->folderObject + ) + ) { + $addPasteButton = false; + } + } + if ($addPasteButton) { + $confirmText = $this->filelist->clipObj + ->confirmMsgText('_FILE', $this->folderObject->getReadablePath(), 'into'); + $pastButtonTitle = $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:clip_paste'); + $pasteButton = $this->componentFactory->createLinkButton() + ->setHref($this->filelist->clipObj + ->pasteUrl('_FILE', $this->folderObject->getCombinedIdentifier())) + ->setClasses('t3js-modal-trigger') + ->setDataAttributes([ + 'severity' => 'warning', + 'content' => $confirmText, + 'title' => $pastButtonTitle, + ]) + ->setShowLabelText(true) + ->setTitle($pastButtonTitle) + ->setIcon($this->iconFactory->getIcon('actions-document-paste-into', IconSize::SMALL)); + $this->view->addButtonToButtonBar($pasteButton, ButtonBar::BUTTON_POSITION_LEFT, 10); + } + } + } + } + + /** + * Get main headline based on active folder or storage for backend module + * Folder names are resolved to their special names like done in the tree view. + */ + protected function getModuleHeadline(): string + { + $name = $this->folderObject->getName(); + if ($name === '') { + // Show storage name on storage root + if ($this->folderObject->getIdentifier() === '/') { + $name = $this->folderObject->getStorage()->getName(); + } + } else { + $name = key(ListUtility::resolveSpecialFolderNames( + [$name => $this->folderObject] + )); + } + return (string)$name; + } + + /** + * Generate a response by either the given $html or by rendering the module content. + */ + protected function htmlResponse(string $html): ResponseInterface + { + $response = $this->responseFactory + ->createResponse() + ->withHeader('Content-Type', 'text/html; charset=utf-8'); + + $response->getBody()->write($html); + return $response; + } + + /** + * Adds a flash message to the default flash message queue + */ + protected function addFlashMessage(string $message, string $title = '', ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::INFO): void + { + $flashMessage = new FlashMessage($message, $title, $severity, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/FileUpdateOnlineMediaController.php b/Classes/Controller/FileUpdateOnlineMediaController.php new file mode 100644 index 0000000..6d96734 --- /dev/null +++ b/Classes/Controller/FileUpdateOnlineMediaController.php @@ -0,0 +1,93 @@ +getParsedBody()['resource'] ?? []; + + if (($resource['type'] ?? '') !== 'file' || !isset($resource['uid'])) { + return $this->createResponse(['success' => false], 400); + } + + $fileObject = null; + try { + $fileObject = $this->resourceFactory->getFileObject($resource['uid']); + } catch (FileDoesNotExistException $e) { + } + + if ($fileObject === null + || !($onlineMediaHelper = $this->onlineMediaHelperRegistry->getOnlineMediaHelper($fileObject)) + || !$fileObject->checkActionPermission('editMeta') + || !$fileObject->getMetaData()->offsetExists('uid') + || !$this->getBackendUser()->check('tables_modify', 'sys_file_metadata') + ) { + return $this->createResponse(['success' => false], 400); + } + + try { + $this->previewService->updatePreviewImage($fileObject); + } catch (\InvalidArgumentException $e) { + return $this->createResponse(['success' => false], 400); + } + + // Update remaining meta data from online media helper + $fileObject->getMetaData()->add($onlineMediaHelper->getMetaData($fileObject))->save(); + + return $this->createResponse(['success' => true]); + } + + protected function createResponse(array $data = [], int $status = 200): ResponseInterface + { + return $this->responseFactory->createResponse($status) + ->withHeader('Content-Type', 'application/json; charset=utf-8') + ->withBody($this->streamFactory->createStream((string)json_encode($data))); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Dto/ResourceCollection.php b/Classes/Dto/ResourceCollection.php new file mode 100644 index 0000000..86b0ce6 --- /dev/null +++ b/Classes/Dto/ResourceCollection.php @@ -0,0 +1,180 @@ +setResources($resources); + } + + public function addResource(ResourceInterface $resource): self + { + $this->resources[] = $resource; + return $this; + } + + public function addResources(array $resources): self + { + foreach ($resources as $resource) { + $this->addResource($resource); + } + return $this; + } + + public function setResources(array $resources): self + { + $this->resources = []; + $this->addResources($resources); + return $this; + } + + /** + * @return ResourceInterface[] + */ + public function getResources(): array + { + return $this->resources; + } + + /** + * @return Folder[] + */ + public function getFolders(): array + { + return array_filter($this->resources, static function (ResourceInterface $resource): bool { + return $resource instanceof Folder; + }); + } + + /** + * @return File[] + */ + public function getFiles(): array + { + return array_filter($this->resources, static function (ResourceInterface $resource): bool { + return $resource instanceof File; + }); + } + + public function getTotalBytes(): int + { + $totalBytes = 0; + foreach ($this->getFiles() as $file) { + $totalBytes += $file->getSize(); + } + + return $totalBytes; + } + + public function getTotalFolderCount(): int + { + return count($this->getFolders()); + } + + public function getTotalFileCount(): int + { + return count($this->getFiles()); + } + + public function getTotalCount(): int + { + return count($this->resources); + } + + /** + * Array Access + */ + public function offsetSet($offset, $value): void + { + if (is_null($offset)) { + $this->resources[] = $value; + } else { + $this->resources[$offset] = $value; + } + } + + public function offsetExists($offset): bool + { + return isset($this->resources[$offset]); + } + + public function offsetUnset($offset): void + { + unset($this->resources[$offset]); + } + + public function offsetGet($offset): ?ResourceInterface + { + return $this->resources[$offset] ?? null; + } + + /** + * Iterator + */ + public function rewind(): void + { + $this->position = 0; + } + + public function current(): ?ResourceInterface + { + return $this->resources[$this->position]; + } + + public function key(): int + { + return $this->position; + } + + public function next(): void + { + ++$this->position; + } + + public function valid(): bool + { + return isset($this->resources[$this->position]); + } + + /** + * Countable + */ + public function count(): int + { + return $this->getTotalCount(); + } +} diff --git a/Classes/Dto/ResourceView.php b/Classes/Dto/ResourceView.php new file mode 100644 index 0000000..a4d0d22 --- /dev/null +++ b/Classes/Dto/ResourceView.php @@ -0,0 +1,291 @@ +resource instanceof File) { + return $this->resource->getUid(); + } + + return null; + } + + public function getIdentifier(): string + { + return $this->resource->getStorage()->getUid() . ':' . $this->resource->getIdentifier(); + } + + public function getMetaDataUid(): ?int + { + if ($this->resource instanceof File + && $this->canEditMetadata()) { + return (int)$this->resource->getMetaData()->offsetGet('uid'); + } + + return null; + } + + public function getType(): string + { + if ($this->resource instanceof Folder) { + return 'folder'; + } + if ($this->resource instanceof File) { + return 'file'; + } + + return 'resource'; + } + + public function getName(): string + { + if ($this->resource instanceof Folder) { + return ListUtility::resolveSpecialFolderName($this->resource); + } + + return $this->resource->getName(); + } + + public function getPath(): string + { + $resource = $this->resource; + if ($resource instanceof File && !$resource->isMissing()) { + $resource = $resource->getParentFolder(); + } + if ($resource instanceof Folder) { + return $resource->getReadablePath(); + } + + return ''; + } + + public function getPublicUrl(): ?string + { + if (!$this->resource instanceof File) { + return null; + } + + return $this->resource->getPublicUrl(); + } + + public function getPreview(): ?File + { + if ($this->resource instanceof File + && ($this->resource->isImage() || $this->resource->isMediaFile()) + ) { + return $this->resource; + } + + return null; + } + + public function getIconIdentifier(): string + { + return $this->icon->getIdentifier(); + } + + public function getIconSmall(): Icon + { + $icon = clone $this->icon; + $icon->setSize(IconSize::SMALL); + + return $icon; + } + + public function getIconMedium(): Icon + { + $icon = clone $this->icon; + $icon->setSize(IconSize::MEDIUM); + + return $icon; + } + + public function getIconLarge(): Icon + { + $icon = clone $this->icon; + $icon->setSize(IconSize::LARGE); + + return $icon; + } + + public function getCreatedAt(): ?int + { + if ($this->resource instanceof File) { + return $this->resource->getCreationTime(); + } + if ($this->resource instanceof Folder) { + return $this->resource->getCreationTime(); + } + + return null; + } + + public function getUpdatedAt(): ?int + { + if ($this->resource instanceof File) { + return $this->resource->getModificationTime(); + } + if ($this->resource instanceof Folder) { + return $this->resource->getModificationTime(); + } + + return null; + } + + public function getSize(): ?int + { + if ($this->resource instanceof File) { + return $this->resource->getSize(); + } + + return null; + } + + public function getCheckboxConfig(): ?array + { + if (($this->resource instanceof Folder || $this->resource instanceof File) + && !$this->resource->checkActionPermission('read')) { + return null; + } + + return [ + 'class' => 't3js-multi-record-selection-check', + 'name' => 'CBC[_FILE|' . md5($this->getIdentifier()) . ']', + 'value' => $this->getIdentifier(), + 'checked' => $this->isSelected, + ]; + } + + public function isMissing(): ?bool + { + if ($this->resource instanceof File) { + return $this->resource->isMissing(); + } + + return null; + } + + public function isLocked(): bool + { + if ($this->resource instanceof InaccessibleFolder) { + return true; + } + + return false; + } + + public function canEditMetadata(): bool + { + return $this->resource instanceof File + && $this->resource->isIndexed() + && $this->resource->checkActionPermission('editMeta') + && $this->userPermissions->editMetaData; + } + + public function canRead(): ?bool + { + if ($this->resource instanceof File || $this->resource instanceof Folder) { + return $this->resource->checkActionPermission('read'); + } + + return null; + } + + public function canWrite(): ?bool + { + if ($this->resource instanceof File || $this->resource instanceof Folder) { + return $this->resource->checkActionPermission('write'); + } + + return null; + } + + public function canDelete(): ?bool + { + if ($this->resource instanceof File || $this->resource instanceof Folder) { + return $this->resource->checkActionPermission('delete'); + } + + return null; + } + + public function canCopy(): ?bool + { + if ($this->resource instanceof File || $this->resource instanceof Folder) { + return $this->resource->checkActionPermission('copy'); + } + + return null; + } + + public function canRename(): ?bool + { + if ($this->resource instanceof File || $this->resource instanceof Folder) { + return $this->resource->checkActionPermission('rename'); + } + + return null; + } + + public function canReplace(): ?bool + { + if ($this->resource instanceof File) { + return $this->resource->checkActionPermission('replace'); + } + + return null; + } + + public function canMove(): ?bool + { + if ($this->resource instanceof File || $this->resource instanceof Folder) { + return $this->resource->checkActionPermission('move'); + } + + return null; + } +} diff --git a/Classes/Dto/UserPermissions.php b/Classes/Dto/UserPermissions.php new file mode 100644 index 0000000..c3886ce --- /dev/null +++ b/Classes/Dto/UserPermissions.php @@ -0,0 +1,28 @@ +view = $this->backendViewFactory->create($this->getRequest(), ['typo3/cms-filelist']); + $this->view->assign('initialNavigationWidth', $this->getBackendUser()->uc['selector']['navigation']['width'] ?? 250); + + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/tree/file-storage-browser.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/filelist/file-list-actions.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/multi-record-selection.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/global-event-handler.js'); + } + + protected function initVariables(ServerRequestInterface $request): void + { + parent::initVariables($request); + + $this->currentPage = (int)($request->getParsedBody()['currentPage'] ?? $request->getQueryParams()['currentPage'] ?? 1); + $this->expandFolder = $request->getParsedBody()['expandFolder'] ?? $request->getQueryParams()['expandFolder'] ?? null; + $this->sortField = ($request->getParsedBody()['sortField'] ?? $request->getQueryParams()['sortField'] ?? 'name'); + $this->sortDirection = SortDirection::tryFrom($request->getParsedBody()['sortDirection'] ?? $request->getQueryParams()['sortDirection'] ?? '') ?? SortDirection::ASCENDING; + + $this->viewMode = ViewMode::tryFrom($request->getParsedBody()['viewMode'] ?? $request->getQueryParams()['viewMode'] ?? ''); + if ($this->viewMode !== null) { + $this->getBackendUser()->pushModuleData( + $this->moduleStorageIdentifier, + array_merge($this->getBackendUser()->getModuleData($this->moduleStorageIdentifier) ?? [], ['viewMode' => $this->viewMode->value]) + ); + } else { + $this->viewMode = ViewMode::tryFrom($this->getBackendUser()->getModuleData($this->moduleStorageIdentifier)['viewMode'] ?? '') + ?? ViewMode::tryFrom($this->getBackendUser()->getTSConfig()['options.']['defaultResourcesViewMode'] ?? '') + ?? ViewMode::TILES; + } + + $displayThumbs = $request->getParsedBody()['displayThumbs'] ?? $request->getQueryParams()['displayThumbs'] ?? null; + if ($displayThumbs !== null) { + $this->displayThumbs = (bool)$displayThumbs; + $this->getBackendUser()->pushModuleData( + $this->moduleStorageIdentifier, + array_merge($this->getBackendUser()->getModuleData($this->moduleStorageIdentifier) ?? [], ['displayThumbs' => $this->displayThumbs]) + ); + } else { + $this->displayThumbs = (bool)($this->getBackendUser()->getModuleData($this->moduleStorageIdentifier)['displayThumbs'] ?? true); + } + + $this->filelist = GeneralUtility::makeInstance(FileList::class, $this->getRequest()); + $this->filelist->viewMode = $this->viewMode; + $this->filelist->thumbs = ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] ?? false) && $this->displayThumbs; + } + + /** + * Last selected folder is stored in user module session. Sanitize it + * to set $this->selectedFolder or keep it null. + */ + protected function initSelectedFolder(): void + { + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + if ($this->expandFolder) { + try { + $this->selectedFolder = $resourceFactory->getFolderObjectFromCombinedIdentifier($this->expandFolder); + } catch (FolderDoesNotExistException|InsufficientFolderAccessPermissionsException) { + // Outdated module session data: Last used folder has been removed meanwhile, or + // access to last used folder has been removed. Do not set a preselected folder. + } + } + } + + protected function getSortingModeButtons(): ButtonInterface + { + $sortingButton = $this->componentFactory->createDropDownButton() + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting')) + ->setIcon($this->iconFactory->getIcon($this->sortDirection->getIconIdentifier())); + + $sortingModeButtons = []; + $sortableFields = $this->filelist->getSortableFields(); + if (count($sortableFields) > 1) { + foreach ($sortableFields as $field) { + $label = $this->filelist->getFieldLabel($field); + + $sortingModeButtons[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->sortField === $field) + ->setHref($this->createUri([ + 'sortField' => $field, + 'sortDirection' => SortDirection::ASCENDING->value, + 'currentPage' => 1, + ])) + ->setLabel($label); + } + + $sortingModeButtons[] = $this->componentFactory->createDropDownDivider(); + } + + $defaultSortingDirectionParams = ['sortField' => $this->sortField, 'currentPage' => 1]; + $sortingModeButtons[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->sortDirection === SortDirection::ASCENDING) + ->setHref($this->createUri(array_merge($defaultSortingDirectionParams, ['sortDirection' => SortDirection::ASCENDING->value]))) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting.asc')); + $sortingModeButtons[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->sortDirection === SortDirection::DESCENDING) + ->setHref($this->createUri(array_merge($defaultSortingDirectionParams, ['sortDirection' => SortDirection::DESCENDING->value]))) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting.desc')); + + foreach ($sortingModeButtons as $sortingModeButton) { + $sortingButton->addItem($sortingModeButton); + } + + return $sortingButton; + } + + protected function getViewModeButton(): ButtonInterface + { + $viewModeItems = []; + $viewModeItems[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->viewMode === ViewMode::TILES) + ->setHref($this->createUri(['viewMode' => ViewMode::TILES->value])) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.tiles')) + ->setIcon($this->iconFactory->getIcon('actions-viewmode-tiles')); + $viewModeItems[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->viewMode === ViewMode::LIST) + ->setHref($this->createUri(['viewMode' => ViewMode::LIST->value])) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.list')) + ->setIcon($this->iconFactory->getIcon('actions-viewmode-list')); + if (!($this->getBackendUser()->getTSConfig()['options.']['noThumbsInEB'] ?? false)) { + $viewModeItems[] = $this->componentFactory->createDropDownDivider(); + $viewModeItems[] = $this->componentFactory->createDropDownToggle() + ->setActive($this->displayThumbs) + ->setHref($this->createUri(['displayThumbs' => $this->displayThumbs ? 0 : 1])) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.showThumbnails')) + ->setIcon($this->iconFactory->getIcon('actions-image')); + } + if ( + ($this->getBackendUser()->getTSConfig()['options.']['file_list.']['displayColumnSelector'] ?? true) + && $this->viewMode === ViewMode::LIST + && $this->identifier === 'file' + ) { + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/column-selector-button.js'); + $viewModeItems[] = $this->componentFactory->createDropDownDivider(); + $viewModeItems[] = $this->componentFactory->createDropDownItem() + ->setTag('typo3-backend-column-selector-button') + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.selectColumns')) + ->setAttributes([ + 'data-url' => (string)$this->uriBuilder->buildUriFromRoute( + 'ajax_show_columns_selector', + ['table' => '_FILE'] + ), + 'data-target' => (string)$this->filelist->createModuleUri(), + 'data-title' => sprintf( + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:showColumnsSelection'), + $this->tcaSchemaFactory->get('sys_file')->getTitle($this->getLanguageService()->sL(...)), + ), + 'data-button-ok' => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView'), + 'data-button-close' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel'), + 'data-error-message' => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView.error'), + ]) + ->setIcon($this->iconFactory->getIcon('actions-options')); + } + + $viewModeButton = $this->componentFactory->createDropDownButton() + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view')) + ->setIcon($this->iconFactory->getIcon('actions-cog')); + foreach ($viewModeItems as $viewModeItem) { + $viewModeButton->addItem($viewModeItem); + } + + return $viewModeButton; + } + + /** + * @param array $values Array of values to include into the parameters + * @return string[] Array of parameters which have to be added to URLs + */ + public function getUrlParameters(array $values): array + { + $values = array_replace_recursive( + array_merge( + [ + 'mode' => $this->identifier, + 'expandFolder' => $values['identifier'] ?? $this->expandFolder, + ], + $this->browserParameters->toQueryParameters() + ), + $values + ); + + return array_filter($values, static function ($value) { + return $value !== null && trim((string)$value) !== ''; + }); + } + + protected function createUri(array $parameters = []): string + { + $parameters = $this->getUrlParameters($parameters); + return (string)$this->uriBuilder->buildUriFromRequest($this->getRequest(), $parameters); + } + + /** + * Session data for this class can be set from outside with this method. + * + * @param mixed[] $data Session data array + * @return array Session data and boolean which indicates that data needs to be stored in session because it's changed + */ + public function processSessionData($data): array + { + if ($this->expandFolder !== null) { + $data['expandFolder'] = $this->expandFolder; + $store = true; + } else { + $this->expandFolder = $data['expandFolder'] ?? null; + $store = false; + } + return [$data, $store]; + } +} diff --git a/Classes/ElementBrowser/CreateFileBrowser.php b/Classes/ElementBrowser/CreateFileBrowser.php new file mode 100644 index 0000000..0f33789 --- /dev/null +++ b/Classes/ElementBrowser/CreateFileBrowser.php @@ -0,0 +1,129 @@ +pageRenderer->loadJavaScriptModule('@typo3/filelist/resource-creation.js'); + } + + protected function initializeDragUploader(): void + { + $lang = $this->getLanguageService(); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/drag-uploader.js'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf', 'file_upload'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf', 'file_download'); + $this->pageRenderer->addInlineLanguageLabelArray([ + 'type.file' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:file'), + 'permissions.read' => $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:read'), + 'permissions.write' => $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:write'), + 'online_media.update.success' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.update.success'), + 'online_media.update.error' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.update.error'), + 'labels.contextMenu.open' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.contextMenu.open'), + ]); + $defaultDuplicationBehavior = DuplicationBehavior::getDefaultDuplicationBehaviour($this->getBackendUser()); + $this->view->assign('dragUploader', [ + 'fileDenyPattern' => $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] ?? null, + 'maxFileSize' => GeneralUtility::getMaxUploadFileSize() * 1024, + 'defaultDuplicationBehaviourAction' => $defaultDuplicationBehavior->value, + ]); + } + + protected function initVariables(ServerRequestInterface $request): void + { + parent::initVariables($request); + $this->resourceDisplayMatcher = GeneralUtility::makeInstance(Matcher::class); + $this->resourceDisplayMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + $this->resourceDisplayMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFileTypeMatcher::class)); + } + + public function render(): string + { + $this->initSelectedFolder(); + $this->initializeDragUploader(); + $contentHtml = ''; + + if ($this->selectedFolder !== null) { + $markup = []; + + // Build the file creation and upload forms + $resourceUtilityRenderer = GeneralUtility::makeInstance(ResourceUtilityRenderer::class, $this); + $markup[] = $resourceUtilityRenderer->createDragUpload($this->selectedFolder); + $markup[] = $resourceUtilityRenderer->addOnlineMedia($this->getRequest(), $this->selectedFolder); + $markup[] = $resourceUtilityRenderer->createRegularFile($this->getRequest(), $this->selectedFolder); + + // Create the filelist + $this->filelist->start( + $this->selectedFolder, + MathUtility::forceIntegerInRange($this->currentPage, 1, 100000), + $this->sortField, + $this->sortDirection, + Mode::BROWSE + ); + + // Create the filelist header bar + $markup[] = '
'; + $markup[] = '
'; + $markup[] = '
'; + $markup[] = ' ' . $this->getSortingModeButtons(); + $markup[] = ' ' . $this->getViewModeButton(); + $markup[] = '
'; + $markup[] = '
'; + + $this->filelist->setResourceDisplayMatcher($this->resourceDisplayMatcher); + $this->filelist->setResourceSelectableMatcher($this->resourceSelectableMatcher); + $markup[] = $this->filelist->render(null, $this->view); + + $contentHtml = implode(PHP_EOL, $markup); + } + + $contentOnly = (bool)($this->getRequest()->getQueryParams()['contentOnly'] ?? false); + $this->pageRenderer->setTitle($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:actions.new_file')); + $this->view->assign('selectedFolder', $this->selectedFolder); + $this->view->assign('content', $contentHtml); + $this->view->assign('contentOnly', $contentOnly); + + $content = $this->view->render('ElementBrowser/ResourceCreation'); + if ($contentOnly) { + return $content; + } + $this->pageRenderer->setBodyContent('getBodyTagParameters() . '>' . $content); + return $this->pageRenderer->render($this->getRequest()); + } +} diff --git a/Classes/ElementBrowser/CreateFolderBrowser.php b/Classes/ElementBrowser/CreateFolderBrowser.php new file mode 100644 index 0000000..b12e70a --- /dev/null +++ b/Classes/ElementBrowser/CreateFolderBrowser.php @@ -0,0 +1,101 @@ +pageRenderer->loadJavaScriptModule('@typo3/filelist/resource-creation.js'); + } + + protected function initVariables(ServerRequestInterface $request): void + { + parent::initVariables($request); + $this->resourceDisplayMatcher = GeneralUtility::makeInstance(Matcher::class); + $this->resourceDisplayMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + } + + public function render(): string + { + $this->initSelectedFolder(); + $contentHtml = ''; + + if ($this->selectedFolder !== null) { + $markup = []; + + // Build the folder creation form + $resourceUtilityRenderer = GeneralUtility::makeInstance(ResourceUtilityRenderer::class, $this); + $markup[] = $resourceUtilityRenderer->createFolder($this->getRequest(), $this->selectedFolder); + + // Create the filelist + $this->filelist->start( + $this->selectedFolder, + MathUtility::forceIntegerInRange($this->currentPage, 1, 100000), + $this->sortField, + $this->sortDirection, + Mode::BROWSE + ); + + // Create the filelist header bar + $markup[] = '
'; + $markup[] = '
'; + $markup[] = '
'; + $markup[] = ' ' . $this->getSortingModeButtons(); + $markup[] = ' ' . $this->getViewModeButton(); + $markup[] = '
'; + $markup[] = '
'; + + $this->filelist->setResourceDisplayMatcher($this->resourceDisplayMatcher); + $this->filelist->setResourceSelectableMatcher($this->resourceSelectableMatcher); + $markup[] = $this->filelist->render(null, $this->view); + + $contentHtml = implode(PHP_EOL, $markup); + } + + $contentOnly = (bool)($this->getRequest()->getQueryParams()['contentOnly'] ?? false); + $this->pageRenderer->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:createFolder')); + $this->view->assign('selectedFolder', $this->selectedFolder); + $this->view->assign('content', $contentHtml); + $this->view->assign('contentOnly', $contentOnly); + + $content = $this->view->render('ElementBrowser/ResourceCreation'); + if ($contentOnly) { + return $content; + } + $this->pageRenderer->setBodyContent('getBodyTagParameters() . '>' . $content); + return $this->pageRenderer->render($this->getRequest()); + } +} diff --git a/Classes/ElementBrowser/FileBrowser.php b/Classes/ElementBrowser/FileBrowser.php new file mode 100644 index 0000000..811cfcd --- /dev/null +++ b/Classes/ElementBrowser/FileBrowser.php @@ -0,0 +1,183 @@ +pageRenderer->loadJavaScriptModule('@typo3/filelist/browse-files.js'); + } + + protected function initVariables(ServerRequestInterface $request): void + { + parent::initVariables($request); + + $this->searchWord = trim((string)($request->getParsedBody()['searchTerm'] ?? $request->getQueryParams()['searchTerm'] ?? '')); + + $fileExtensions = $this->browserParameters->getFileExtensions(); + + $this->fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class); + if ($fileExtensions['allowed'] !== []) { + $this->fileExtensionFilter->setAllowedFileExtensions(implode(',', $fileExtensions['allowed'])); + } + if ($fileExtensions['disallowed'] !== []) { + $this->fileExtensionFilter->setDisallowedFileExtensions(implode(',', $fileExtensions['disallowed'])); + } + + $this->resourceDisplayMatcher = GeneralUtility::makeInstance(Matcher::class); + $this->resourceDisplayMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + $this->resourceDisplayMatcher->addMatcher( + GeneralUtility::makeInstance( + AndMatcher::class, + GeneralUtility::makeInstance(ResourceFileExtensionMatcher::class) + ->setExtensions($this->fileExtensionFilter->getAllowedFileExtensions() ?? ['*']) + ->setIgnoredExtensions($this->fileExtensionFilter->getDisallowedFileExtensions() ?? []), + new class (GeneralUtility::makeInstance(EventDispatcherInterface::class)) implements MatcherInterface { + public function __construct(private readonly EventDispatcherInterface $eventDispatcher) {} + public function supports(mixed $item): bool + { + return $item instanceof ResourceInterface; + } + public function match(mixed $item): bool + { + return $this->eventDispatcher->dispatch(new IsFileSelectableEvent($item))->isFileSelectable(); + } + } + ) + ); + + $this->resourceSelectableMatcher = GeneralUtility::makeInstance(Matcher::class); + $this->resourceSelectableMatcher->addMatcher( + GeneralUtility::makeInstance(ResourceFileExtensionMatcher::class) + ->setExtensions($this->fileExtensionFilter->getAllowedFileExtensions() ?? ['*']) + ->setIgnoredExtensions($this->fileExtensionFilter->getDisallowedFileExtensions() ?? []) + ); + } + + public function render(): string + { + $this->initSelectedFolder(); + $contentHtml = ''; + + if ($this->selectedFolder instanceof Folder) { + $markup = []; + + // Prepare search box, since the component should always be displayed, even if no files are available + $markup[] = '
'; + $markup[] = GeneralUtility::makeInstance(RecordSearchBoxComponent::class) + ->setSearchWord($this->searchWord ?? '') + ->render($this->getRequest(), $this->createUri()); + $markup[] = '
'; + + // Create the filelist + $this->filelist->start( + $this->selectedFolder, + MathUtility::forceIntegerInRange($this->currentPage, 1, 100000), + $this->sortField, + $this->sortDirection, + Mode::BROWSE + ); + + // Only add selected columns if the feature is enabled + if ($this->getBackendUser()->getTSConfig()['options.']['file_list.']['displayColumnSelector'] ?? true) { + $this->filelist->setColumnsToRender($this->getBackendUser()->getModuleData('list/displayFields')['_FILE'] ?? []); + } + + // Create the filelist header bar + $markup[] = '
'; + $markup[] = '
'; + $markup[] = ' '; + $markup[] = '
'; + $markup[] = '
'; + $markup[] = ' ' . $this->getSortingModeButtons(); + $markup[] = ' ' . $this->getViewModeButton(); + $markup[] = '
'; + $markup[] = '
'; + + $this->filelist->setResourceDisplayMatcher($this->resourceDisplayMatcher); + $this->filelist->setResourceSelectableMatcher($this->resourceSelectableMatcher); + $searchDemand = $this->searchWord !== '' + ? FileSearchDemand::createForSearchTerm($this->searchWord)->withFolder($this->selectedFolder)->withRecursive() + : null; + $markup[] = $this->filelist->render($searchDemand, $this->view); + + // Build the file upload and folder creation form + $resourceUtilityRenderer = GeneralUtility::makeInstance(ResourceUtilityRenderer::class, $this); + $markup[] = $resourceUtilityRenderer->uploadForm($this->getRequest(), $this->selectedFolder, $this->fileExtensionFilter); + $markup[] = $resourceUtilityRenderer->addOnlineMedia($this->getRequest(), $this->selectedFolder, $this->fileExtensionFilter); + $markup[] = $resourceUtilityRenderer->createFolder($this->getRequest(), $this->selectedFolder); + + $contentHtml = implode('', $markup); + } + + $contentOnly = (bool)($this->getRequest()->getQueryParams()['contentOnly'] ?? false); + $this->pageRenderer->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:fileSelector')); + $this->view->assign('selectedFolder', $this->selectedFolder); + $this->view->assign('content', $contentHtml); + $this->view->assign('contentOnly', $contentOnly); + + $content = $this->view->render('ElementBrowser/Files'); + if ($contentOnly) { + return $content; + } + $this->pageRenderer->setBodyContent('getBodyTagParameters() . '>' . $content); + return $this->pageRenderer->render($this->getRequest()); + } +} diff --git a/Classes/ElementBrowser/FolderBrowser.php b/Classes/ElementBrowser/FolderBrowser.php new file mode 100644 index 0000000..aee670c --- /dev/null +++ b/Classes/ElementBrowser/FolderBrowser.php @@ -0,0 +1,113 @@ +pageRenderer->loadJavaScriptModule('@typo3/filelist/browse-folders.js'); + } + + protected function initVariables(ServerRequestInterface $request): void + { + parent::initVariables($request); + $this->resourceDisplayMatcher = GeneralUtility::makeInstance(Matcher::class); + $this->resourceDisplayMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + $this->resourceSelectableMatcher = GeneralUtility::makeInstance(Matcher::class); + $this->resourceSelectableMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + } + + public function render(): string + { + $this->initSelectedFolder(); + $contentHtml = ''; + + if ($this->selectedFolder instanceof Folder) { + $markup = []; + + // Create the filelist + $this->filelist->start( + $this->selectedFolder, + MathUtility::forceIntegerInRange($this->currentPage, 1, 100000), + $this->sortField, + $this->sortDirection, + Mode::BROWSE + ); + + // Create the filelist header bar + $markup[] = '
'; + $markup[] = '
'; + $markup[] = ' '; + $markup[] = '
'; + $markup[] = '
'; + $markup[] = ' ' . $this->getSortingModeButtons(); + $markup[] = ' ' . $this->getViewModeButton(); + $markup[] = '
'; + $markup[] = '
'; + + $this->filelist->setResourceDisplayMatcher($this->resourceDisplayMatcher); + $this->filelist->setResourceSelectableMatcher($this->resourceSelectableMatcher); + $markup[] = $this->filelist->render(null, $this->view); + + // Build the folder creation form + $resourceUtilityRenderer = GeneralUtility::makeInstance(ResourceUtilityRenderer::class, $this); + $markup[] = $resourceUtilityRenderer->createFolder($this->getRequest(), $this->selectedFolder); + + $contentHtml = implode('', $markup); + } + + $contentOnly = (bool)($this->getRequest()->getQueryParams()['contentOnly'] ?? false); + $this->pageRenderer->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:folderSelector')); + $this->view->assign('selectedFolder', $this->selectedFolder); + $this->view->assign('content', $contentHtml); + $this->view->assign('contentOnly', $contentOnly); + + $content = $this->view->render('ElementBrowser/Folder'); + if ($contentOnly) { + return $content; + } + $this->pageRenderer->setBodyContent('getBodyTagParameters() . '>' . $content); + return $this->pageRenderer->render($this->getRequest()); + } +} diff --git a/Classes/Event/AfterFileListRowPreparedEvent.php b/Classes/Event/AfterFileListRowPreparedEvent.php new file mode 100644 index 0000000..350b032 --- /dev/null +++ b/Classes/Event/AfterFileListRowPreparedEvent.php @@ -0,0 +1,64 @@ +resource; + } + + public function getData(): array + { + return $this->data; + } + + public function setData(array $data): void + { + $this->data = $data; + } + + public function getFileList(): FileList + { + return $this->fileList; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function setAttributes(array $attributes): void + { + $this->attributes = $attributes; + } +} diff --git a/Classes/Event/ModifyEditFileFormDataEvent.php b/Classes/Event/ModifyEditFileFormDataEvent.php new file mode 100644 index 0000000..22ec90c --- /dev/null +++ b/Classes/Event/ModifyEditFileFormDataEvent.php @@ -0,0 +1,54 @@ +formData; + } + + public function setFormData(array $formData): void + { + $this->formData = $formData; + } + + public function getFile(): FileInterface + { + return $this->file; + } + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } +} diff --git a/Classes/Event/ProcessFileListActionsEvent.php b/Classes/Event/ProcessFileListActionsEvent.php new file mode 100644 index 0000000..a6fdbd5 --- /dev/null +++ b/Classes/Event/ProcessFileListActionsEvent.php @@ -0,0 +1,151 @@ +fileOrFolder; + } + + public function isFile(): bool + { + return $this->fileOrFolder instanceof FileInterface; + } + + /** + * Add a new action or override an existing one. Latter is only possible, + * in case $columnName is given. Otherwise, the column will be added with + * a numeric index, which is generally not recommended. It's also possible + * to define the position of an action with either the "before" or "after" + * argument, while their value must be an existing action. + * + * Note: In case none or an invalid $group is provided, the new action will + * be added to the secondary group. + */ + public function setAction( + ?ComponentInterface $action, + string $actionName, + ActionGroup $group = ActionGroup::secondary, + string $before = '', + string $after = '', + ): void { + if ($actionName === '') { + throw new \Exception('You must provide a valid action name when adding a new action.', 1761584689); + } + + $componentGroup = match ($group) { + ActionGroup::primary => $this->primary, + ActionGroup::secondary => $this->secondary, + }; + $componentGroup->add($actionName, $action, $before, $after); + } + + /** + * Whether the action exists in the given group. In case none or + * an invalid $group is provided, both groups will be checked. + */ + public function hasAction(string $actionName, ?ActionGroup $group = null): bool + { + return match ($group) { + ActionGroup::primary => $this->primary->has($actionName), + ActionGroup::secondary => $this->secondary->has($actionName), + null => $this->primary->has($actionName) || $this->secondary->has($actionName), + }; + } + + /** + * Get action by its name. In case the action exists in both groups + * and none or an invalid $group is provided, the action from the + * "primary" group will be returned. + */ + public function getAction(string $actionName, ?ActionGroup $group = null): ?ComponentInterface + { + return match ($group) { + ActionGroup::primary => $this->primary->get($actionName), + ActionGroup::secondary => $this->secondary->get($actionName), + null => $this->primary->get($actionName) ?? $this->secondary->get($actionName), + }; + } + + /** + * Remove action by its name. In case the action exists in both groups + * and none or an invalid $group is provided, the action will be removed + * from both groups. + */ + public function removeAction(string $actionName, ?ActionGroup $group = null): void + { + if ($group === null) { + $this->primary->remove($actionName); + $this->secondary->remove($actionName); + return; + } + match ($group) { + ActionGroup::primary => $this->primary->remove($actionName), + ActionGroup::secondary => $this->secondary->remove($actionName), + }; + } + + public function moveActionTo( + string $actionName, + ActionGroup $group, + string $before = '', + string $after = '', + ): void { + if (!$this->hasAction($actionName)) { + throw new \RuntimeException('The action "' . $actionName . '" does not exist and therefore cannot be moved.', 1761646465); + } + $action = $this->getAction($actionName); + $this->removeAction($actionName); + $this->setAction($action, $actionName, $group, $before, $after); + } + + /** + * Get the actions of a specific group + */ + public function getActionGroup(ActionGroup $group): ComponentGroup + { + return match ($group) { + ActionGroup::primary => $this->primary, + ActionGroup::secondary => $this->secondary, + }; + } + + public function getRequest(): RequestInterface + { + return $this->request; + } +} diff --git a/Classes/EventListener/AfterBackendPageRenderEventListener.php b/Classes/EventListener/AfterBackendPageRenderEventListener.php new file mode 100644 index 0000000..3d64292 --- /dev/null +++ b/Classes/EventListener/AfterBackendPageRenderEventListener.php @@ -0,0 +1,47 @@ +pageRenderer->addInlineLanguageLabelFile('EXT:filelist/Resources/Private/Language/locallang.xlf'); + $this->pageRenderer->addInlineLanguageLabelArray([ + 'file_info_filename' => $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:c_name'), + 'file_info_filesize' => $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:c_size'), + 'file_info_creation_date' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.crdate'), + ]); + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/FileList.php b/Classes/FileList.php new file mode 100644 index 0000000..6140608 --- /dev/null +++ b/Classes/FileList.php @@ -0,0 +1,1773 @@ +Filelist (basically used in FileListController) + * @see \TYPO3\CMS\Filelist\Controller\FileListController + * @internal this is a concrete TYPO3 controller implementation and solely used for EXT:filelist and not part of TYPO3's Core API. + */ +class FileList +{ + public Mode $mode = Mode::MANAGE; + public ViewMode $viewMode = ViewMode::TILES; + + /** + * Default Max items shown + */ + public int $itemsPerPage = 40; + + /** + * Current Page + */ + public int $currentPage = 1; + + /** + * Total file size of the current selection + */ + public int $totalbytes = 0; + + /** + * Total count of folders and files + */ + public int $totalItems = 0; + + /** + * The field to sort by + */ + public string $sortField = ''; + + public SortDirection $sortDirection = SortDirection::DESCENDING; + + /** + * Thumbnails on records containing files (pictures) + */ + public bool $thumbs = false; + + /** + * Max length of strings + */ + public int $maxTitleLength = 30; + + /** + * Decides the columns shown. Filled with values that refers to the keys of the data-array. $this->fieldArray[0] is the title column. + */ + public array $fieldArray = []; + + /** + * Keys are fieldnames and values are td-css-classes to add in addElement(); + * + * @var array + */ + public array $addElement_tdCssClass = [ + '_CONTROL_' => 'col-control', + '_SELECTOR_' => 'col-checkbox', + 'icon' => 'col-icon', + 'name' => 'col-title col-responsive', + ]; + + /** + * @var Folder + */ + protected $folderObject; + + public Clipboard $clipObj; + + // Evaluates if a resource can be downloaded + protected ?Matcher $resourceDownloadMatcher = null; + // Evaluates if a resource can be displayed + protected ?Matcher $resourceDisplayMatcher = null; + // Evaluates if a resource can be selected + protected ?Matcher $resourceSelectableMatcher = null; + // Evaluates if a resource is currently selected + protected ?Matcher $resourceSelectedMatcher = null; + + protected ?FileSearchDemand $searchDemand = null; + protected EventDispatcherInterface $eventDispatcher; + protected ServerRequestInterface $request; + protected IconFactory $iconFactory; + protected ResourceFactory $resourceFactory; + protected UriBuilder $uriBuilder; + protected TranslationConfigurationProvider $translateTools; + protected OnlineMediaHelperRegistry $onlineMediaHelperRegistry; + protected TcaSchemaFactory $tcaSchemaFactory; + protected ComponentFactory $componentFactory; + + public function __construct(ServerRequestInterface $request) + { + $this->request = $request; + + // Setting the maximum length of the filenames to the user's settings or minimum 30 (= $this->maxTitleLength) + $this->maxTitleLength = max($this->maxTitleLength, (int)($this->getBackendUser()->uc['titleLen'] ?? 1)); + $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); + $this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $this->translateTools = GeneralUtility::makeInstance(TranslationConfigurationProvider::class); + $this->tcaSchemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class); + $this->itemsPerPage = MathUtility::forceIntegerInRange( + $this->getBackendUser()->getTSConfig()['options.']['file_list.']['filesPerPage'] ?? $this->itemsPerPage, + 1 + ); + // Create clipboard object and initialize that + $this->clipObj = GeneralUtility::makeInstance(Clipboard::class); + $this->clipObj->initializeClipboard($request); + $this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $this->onlineMediaHelperRegistry = GeneralUtility::makeInstance(OnlineMediaHelperRegistry::class); + + // Initialize Resource Download + $this->resourceDownloadMatcher = GeneralUtility::makeInstance(Matcher::class); + $this->resourceDownloadMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + + // Create filter for file extensions + $fileExtensionMatcher = GeneralUtility::makeInstance(ResourceFileExtensionMatcher::class); + $fileDownloadConfiguration = (array)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['fileDownload.'] ?? []); + if ($fileDownloadConfiguration !== []) { + $allowedExtensions = GeneralUtility::trimExplode(',', (string)($fileDownloadConfiguration['allowedFileExtensions'] ?? ''), true); + $disallowedExtensions = GeneralUtility::trimExplode(',', (string)($fileDownloadConfiguration['disallowedFileExtensions'] ?? ''), true); + $fileExtensionMatcher = GeneralUtility::makeInstance(ResourceFileExtensionMatcher::class); + $fileExtensionMatcher->setExtensions($allowedExtensions); + $fileExtensionMatcher->setIgnoredExtensions($disallowedExtensions); + } else { + $fileExtensionMatcher->addExtension('*'); + } + $this->resourceDownloadMatcher->addMatcher($fileExtensionMatcher); + $this->componentFactory = GeneralUtility::makeInstance(ComponentFactory::class); + } + + public function setResourceDownloadMatcher(?Matcher $matcher): self + { + $this->resourceDownloadMatcher = $matcher; + return $this; + } + + public function setResourceDisplayMatcher(?Matcher $matcher): self + { + $this->resourceDisplayMatcher = $matcher; + return $this; + } + + public function setResourceSelectableMatcher(?Matcher $matcher): self + { + $this->resourceSelectableMatcher = $matcher; + return $this; + } + + public function setResourceSelectedMatcher(?Matcher $matcher): self + { + $this->resourceSelectedMatcher = $matcher; + return $this; + } + + /** + * Initialization of class + * + * @param Folder $folderObject The folder to work on + * @param int $currentPage The current page to render + * @param string $sortField Sorting column + * @param Mode $mode Mode of the file list + */ + public function start(Folder $folderObject, int $currentPage, string $sortField, SortDirection $sortDirection, Mode $mode = Mode::MANAGE): void + { + $this->folderObject = $folderObject; + $this->currentPage = MathUtility::forceIntegerInRange($currentPage, 1, 100000); + $this->sortField = $sortField; + $this->sortDirection = $sortDirection; + $this->totalbytes = 0; + $this->resourceDownloadMatcher = null; + $this->resourceDisplayMatcher = null; + $this->resourceSelectableMatcher = null; + $this->setMode($mode); + } + + public function setMode(Mode $mode) + { + $this->mode = $mode; + $this->fieldArray = $mode->fieldArray(); + } + + public function setColumnsToRender(array $additionalFields = []): void + { + // Passed fields might have fields utilized that are no longer / not yet part of TCA Schema. + // For example, EXT:filemetadata might not be available, so fields that this extension provide + // must only be allowed when the extension is active. + $allowedAdditionalFields = []; + foreach ($additionalFields as $field) { + if (!$this->tcaSchemaFactory->get('sys_file')->hasField($field) + && (!$this->tcaSchemaFactory->has('sys_file_metadata') || !$this->tcaSchemaFactory->get('sys_file_metadata')->hasField($field)) + ) { + continue; + } + $allowedAdditionalFields[] = $field; + } + $this->fieldArray = array_unique(array_merge($this->fieldArray, $allowedAdditionalFields)); + } + + /** + * @param ResourceView[] $resourceViews + */ + protected function renderTiles(array $resourceViews, ViewInterface $view): string + { + $view->assignMultiple([ + 'displayThumbs' => $this->thumbs, + 'displayCheckbox' => (bool)$this->resourceSelectableMatcher, + 'defaultLanguageAccess' => $this->getBackendUser()->checkLanguageAccess(0), + 'resources' => $resourceViews, + ]); + + return $view->render('Filelist/Tiles'); + } + + /** + * @param ResourceView[] $resourceViews + */ + protected function renderList(array $resourceViews, ViewInterface $view): string + { + $view->assignMultiple([ + 'mode' => $this->mode->value, + 'tableHeader' => $this->renderListTableHeader(), + 'tableBody' => $this->renderListTableBody($resourceViews), + ]); + + return $view->render('Filelist/List'); + } + + public function render(?FileSearchDemand $searchDemand, ViewInterface $view): string + { + $storage = $this->folderObject->getStorage(); + $storage->resetFileAndFolderNameFiltersToDefault(); + if (!$this->folderObject->getStorage()->isBrowsable()) { + return ''; + } + + if ($searchDemand !== null) { + $this->searchDemand = $searchDemand; + if ($searchDemand->hasSearchTerm()) { + $folders = []; + // Add special "Path" field for the search result + array_splice($this->fieldArray, 3, 0, '_PATH_'); + } else { + $folders = $storage->getFoldersInFolder($this->folderObject); + } + $files = iterator_to_array($this->folderObject->searchFiles($searchDemand)); + } else { + $folders = $storage->getFoldersInFolder($this->folderObject); + $files = $this->folderObject->getFiles(); + } + + // Cleanup field array + $this->fieldArray = array_filter($this->fieldArray, function (string $fieldName) { + if ($fieldName === '_SELECTOR_' && $this->resourceSelectableMatcher === null) { + return false; + } + return true; + }); + + // Remove processing folders + $folders = array_filter($folders, function (Folder $folder) { + return $folder->getRole() !== FolderInterface::ROLE_PROCESSING; + }); + + // Apply filter + $resources = array_filter($folders + $files, function (ResourceInterface $resource) { + return $this->resourceDisplayMatcher === null || $this->resourceDisplayMatcher->match($resource); + }); + + $resourceCollection = new ResourceCollection($resources); + $this->totalItems = $resourceCollection->getTotalCount(); + $this->totalbytes = $resourceCollection->getTotalBytes(); + + // Sort the files before sending it to the renderer + if (trim($this->sortField) !== '') { + $resourceCollection->setResources($this->sortResources($resourceCollection->getResources(), $this->sortField)); + } + + $paginator = new ResourceCollectionPaginator($resourceCollection, $this->currentPage, $this->itemsPerPage); + + // Prepare Resources for View + $resourceViews = []; + $userPermissions = $this->getUserPermissions(); + foreach ($paginator->getPaginatedItems() as $resource) { + $resourceView = new ResourceView( + $resource, + $userPermissions, + $this->iconFactory->getIconForResource($resource, IconSize::SMALL) + ); + $resourceView->moduleUri = $this->createModuleUriForResource($resource); + $resourceView->editDataUri = $this->createEditDataUriForResource($resource); + $resourceView->editContentUri = $this->createEditContentUriForResource($resource); + + $resourceView->isDownloadable = $this->resourceDownloadMatcher !== null && $this->resourceDownloadMatcher->match($resource); + $resourceView->isSelectable = $this->resourceSelectableMatcher !== null && $this->resourceSelectableMatcher->match($resource); + $resourceView->isSelected = $this->resourceSelectedMatcher !== null && $this->resourceSelectedMatcher->match($resource); + + $resourceViews[] = $resourceView; + } + + $pagination = new SimplePagination($paginator); + $currentPage = $paginator->getCurrentPageNumber(); + $totalItems = $this->totalItems; + $itemsPerPage = $this->itemsPerPage; + if ($totalItems > $currentPage * $itemsPerPage) { + $lastElementNumber = $currentPage * $itemsPerPage; + } else { + $lastElementNumber = $totalItems; + } + + $view->assignMultiple([ + 'currentUrl' => $this->getListURL(), + 'paginator' => $paginator, + 'pagination' => $pagination, + 'currentPage' => $currentPage, + 'totalPages' => $paginator->getNumberOfPages(), + 'firstElement' => ((($currentPage - 1) * $itemsPerPage) + 1), + 'lastElement' => $lastElementNumber, + ]); + + if ($this->viewMode === ViewMode::TILES) { + return $this->renderTiles($resourceViews, $view); + } + + return $this->renderList($resourceViews, $view); + } + + protected function getListURL(): UriInterface + { + $uri = new Uri($this->request->getAttribute('normalizedParams')->getRequestUri()); + parse_str($uri->getQuery(), $queryParameters); + unset($queryParameters['contentOnly'], $queryParameters['currentPage']); + if ($this->searchDemand) { + $queryParameters['searchTerm'] = $this->searchDemand->getSearchTerm() ?? ''; + } + return $uri->withQuery(HttpUtility::buildQueryString($queryParameters, '&')); + } + + /** + * Returns a table-row with the content from the fields in the input data array. + * OBS: $this->fieldArray MUST be set! (represents the list of fields to display) + * + * @param array $data Is the data array, record with the fields. Notice: These fields are (currently) NOT htmlspecialchar'ed before being wrapped in -tags + * @param array $attributes Attributes for the table row. Values will be htmlspecialchar'ed! + * @param bool $isTableHeader Whether the element to be added is a table header + * + * @return string HTML content for the table row + */ + public function addElement(array $data, array $attributes = [], bool $isTableHeader = false): string + { + // Initialize rendering. + $cols = []; + $colType = $isTableHeader ? 'th' : 'td'; + // Traverse field array which contains the data to present: + foreach ($this->fieldArray as $fieldName) { + $cellAttributes = []; + $cellAttributes['class'] = $this->addElement_tdCssClass[$fieldName] ?? 'col-nowrap'; + + // Special handling to combine icon and name column + if ($isTableHeader && $fieldName === 'icon') { + continue; + } + if ($isTableHeader && $fieldName === 'name') { + $cellAttributes['colspan'] = 2; + } + + $cols[] = '<' . $colType . ' ' . GeneralUtility::implodeAttributes($cellAttributes, true) . '>' . ($data[$fieldName] ?? '') . ''; + } + + // Add the table row + return ' + + ' . implode(PHP_EOL, $cols) . ' + '; + } + + /** + * Gets the number of files and total size of a folder + */ + public function getFolderInfo(): string + { + if ($this->totalItems == 1) { + $fileLabel = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:file'); + } else { + $fileLabel = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:files'); + } + return $this->totalItems . ' ' . htmlspecialchars($fileLabel) . ', ' . GeneralUtility::formatSize( + $this->totalbytes, + htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:byteSizeUnits')) + ); + } + + /** + * @return array + */ + public function getSortableFields(): array + { + return array_filter($this->fieldArray, $this->isFieldSortable(...)); + } + + protected function renderListTableHeader(): string + { + $data = []; + foreach ($this->fieldArray as $field) { + $data[$field] = match ($field) { + 'icon' => '', + '_SELECTOR_' => $this->renderCheckboxActions(), + default => $this->renderListTableFieldHeader($field), + }; + } + + return $this->addElement($data, [], true); + } + + protected function isFieldSortable(string $field): bool + { + return !in_array($field, ['icon', 'rw', '_SELECTOR_', '_CONTROL_', '_PATH_'], true); + } + + protected function renderListTableFieldHeader(string $field): string + { + $label = $this->getFieldLabel($field); + if (!$this->isFieldSortable($field)) { + return $label; + } + + $params = ['sortField' => $field, 'currentPage' => 0]; + $paramsAsc = $params; + $paramsAsc['sortDirection'] = SortDirection::ASCENDING->value; + $paramsDesc = $params; + $paramsDesc['sortDirection'] = SortDirection::DESCENDING->value; + + $icon = $this->sortField === $field + ? $this->iconFactory->getIcon($this->sortDirection->getIconIdentifier(), IconSize::SMALL)->render() + : $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render(); + + return ' + + '; + } + + /** + * @param ResourceView[] $resourceViews + */ + protected function renderListTableBody(array $resourceViews): string + { + $output = ''; + foreach ($resourceViews as $resourceView) { + $data = []; + $attributes = [ + 'class' => $resourceView->isSelected ? 'selected' : '', + 'data-filelist-element' => 'true', + 'data-filelist-type' => $resourceView->getType(), + 'data-filelist-identifier' => $resourceView->getIdentifier(), + 'data-filelist-name' => htmlspecialchars($resourceView->getName()), + 'data-filelist-icon' => $resourceView->getIconIdentifier(), + 'data-filelist-preview' => $resourceView->getPreview() !== null ? 'true' : 'false', + 'data-filelist-uid' => $resourceView->getUid(), + 'data-filelist-meta-uid' => $resourceView->getMetaDataUid(), + 'data-filelist-url' => $resourceView->getPublicUrl(), + 'data-filelist-selectable' => $resourceView->isSelectable ? 'true' : 'false', + 'data-filelist-selected' => $resourceView->isSelected ? 'true' : 'false', + 'data-multi-record-selection-element' => 'true', + 'draggable' => $resourceView->canMove() ? 'true' : 'false', + ]; + if ($this->getBackendUser()->checkLanguageAccess(0)) { + $attributes['data-default-language-access'] = 'true'; + } + foreach ($this->fieldArray as $field) { + switch ($field) { + case 'icon': + $data[$field] = $this->renderIcon($resourceView); + break; + case 'name': + $data[$field] = $this->renderName($resourceView) + . $this->renderThumbnail($resourceView); + break; + case 'size': + $data[$field] = $this->renderSize($resourceView); + break; + case 'rw': + $data[$field] = $this->renderPermission($resourceView); + break; + case 'record_type': + $data[$field] = $this->renderType($resourceView); + break; + case 'crdate': + $data[$field] = $this->renderCreationTime($resourceView); + break; + case 'tstamp': + $data[$field] = $this->renderModificationTime($resourceView); + break; + case '_SELECTOR_': + $data[$field] = $this->renderSelector($resourceView); + break; + case '_PATH_': + $data[$field] = $this->renderPath($resourceView); + break; + case '_REF_': + $data[$field] = $this->renderReferenceCount($resourceView); + break; + case '_CONTROL_': + $data[$field] = $this->renderControl($resourceView); + break; + default: + $data[$field] = $this->renderField($resourceView, $field); + } + } + + $event = $this->eventDispatcher->dispatch( + new AfterFileListRowPreparedEvent($resourceView->resource, $data, $this, $attributes) + ); + + $output .= $this->addElement($event->getData(), $event->getAttributes()); + } + + return $output; + } + + /** + * Fetch the translations for a sys_file_metadata record + * + * @param array $metaDataRecord + * @return array> keys are the site language ids, values are the $rows + */ + protected function getTranslationsForMetaData(array $metaDataRecord): array + { + $schema = $this->tcaSchemaFactory->get('sys_file_metadata'); + if (!$schema->isLanguageAware()) { + return []; + } + /** @var LanguageAwareSchemaCapability $languageCapability */ + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_metadata'); + $queryBuilder->getRestrictions()->removeAll(); + $translationRecords = $queryBuilder->select('*') + ->from('sys_file_metadata') + ->where( + $queryBuilder->expr()->eq( + $languageCapability->getTranslationOriginPointerField()->getName(), + $queryBuilder->createNamedParameter($metaDataRecord['uid'] ?? 0, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + $languageCapability->getLanguageField()->getName(), + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchAllAssociative(); + + $translations = []; + foreach ($translationRecords as $record) { + $languageId = $record[$languageCapability->getLanguageField()->getName()]; + $translations[$languageId] = $record; + } + return $translations; + } + + /** + * Render icon + */ + protected function renderIcon(ResourceView $resourceView): string + { + return $this->mode === Mode::BROWSE + ? $resourceView->getIconSmall()->render() + : BackendUtility::wrapClickMenuOnIcon($resourceView->getIconSmall()->render(), 'sys_file', $resourceView->getIdentifier()); + } + + /** + * Render name + */ + protected function renderName(ResourceView $resourceView): string + { + $resourceName = htmlspecialchars($resourceView->getName()); + if ($resourceView->resource instanceof Folder + && $resourceView->resource->getRole() !== FolderInterface::ROLE_DEFAULT) { + $resourceName = '' . $resourceName . ''; + } + + $attributes = []; + $attributes['title'] = $resourceView->getName(); + $attributes['type'] = 'button'; + $attributes['class'] = 'btn btn-link'; + $attributes['data-filelist-action'] = 'primary'; + + $output = ''; + if ($resourceView->isMissing()) { + $label = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing')); + $output = '' . $label . ' ' . $output; + } + + return $output; + } + + /** + * Render thumbnail + */ + protected function renderThumbnail(ResourceView $resourceView): string + { + if ($this->thumbs === false + || $resourceView->getPreview() === null + || !($resourceView->getPreview()->isImage() || $resourceView->getPreview()->isMediaFile()) + ) { + return ''; + } + + $processedFile = $resourceView->getPreview()->process( + ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, + [ + 'maxWidth' => (int)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['thumbnail.']['width'] ?? 64), + 'maxHeight' => (int)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['thumbnail.']['height'] ?? 64), + ] + ); + + if (($thumbnailUrl = ($processedFile->getPublicUrl() ?? '')) === '') { + // Prevent rendering of a "img" tag with an empty "src" attribute + return ''; + } + + if (!str_contains($thumbnailUrl, '?') && !PathUtility::hasProtocolAndScheme($thumbnailUrl)) { + $thumbnailUrl .= '?' . $processedFile->getModificationTime(); + } + + return '
'; + } + + /** + * Render type + */ + protected function renderType(ResourceView $resourceView): string + { + $type = $resourceView->getType(); + $content = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:' . $type); + if ($resourceView->resource instanceof File && $resourceView->resource->getExtension() !== '') { + $content .= ' (' . strtoupper($resourceView->resource->getExtension()) . ')'; + } + + return htmlspecialchars($content); + } + + /** + * Render creation time + */ + protected function renderCreationTime(ResourceView $resourceView): string + { + if ($resourceView->resource instanceof File) { + $timestamp = $resourceView->getCreatedAt(); + } elseif ($resourceView->resource instanceof Folder) { + $timestamp = $resourceView->resource->getCreationTime(); + } else { + $timestamp = null; + } + + return $timestamp ? BackendUtility::datetime($timestamp) : ''; + } + + /** + * Render modification time + */ + protected function renderModificationTime(ResourceView $resourceView): string + { + if ($resourceView->resource instanceof File) { + $timestamp = $resourceView->getUpdatedAt(); + } elseif ($resourceView->resource instanceof Folder) { + $timestamp = $resourceView->resource->getModificationTime(); + } else { + $timestamp = null; + } + + return $timestamp ? BackendUtility::datetime($timestamp) : ''; + } + + /** + * Render size + */ + protected function renderSize(ResourceView $resourceView): string + { + if ($resourceView->resource instanceof File) { + return GeneralUtility::formatSize((int)$resourceView->resource->getSize(), htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:byteSizeUnits'))); + } + + if ($resourceView->resource instanceof Folder) { + try { + $numFiles = $resourceView->resource->getFileCount(); + } catch (InsufficientFolderAccessPermissionsException $e) { + $numFiles = 0; + } + if ($numFiles === 1) { + return $numFiles . ' ' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:file')); + } + return $numFiles . ' ' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:files')); + } + + return ''; + } + + /** + * Render resource permission + */ + protected function renderPermission(ResourceView $resourceView): string + { + return '' + . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:read')) + . ($resourceView->canWrite() ? htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:write')) : '') + . ''; + } + + /** + * Render any resource field + */ + protected function renderField(ResourceView $resourceView, string $field): string + { + if ($resourceView->resource instanceof File && $resourceView->resource->hasProperty($field)) { + if ($field === 'storage') { + // Fetch storage name of the current file + $storage = GeneralUtility::makeInstance(StorageRepository::class)->findByUid((int)$resourceView->resource->getProperty($field)); + if ($storage !== null) { + return htmlspecialchars($storage->getName()); + } + } else { + $metaData = $resourceView->resource->getMetaData()->get(); + return htmlspecialchars( + (string)BackendUtility::getProcessedValueExtra( + $this->getConcreteTableName($field), + $field, + $resourceView->resource->getProperty($field), + $this->maxTitleLength, + $metaData['uid'], + false, + 0, + $metaData, + ) + ); + } + } + + return ''; + } + + /** + * Renders the checkbox to select a resource in the listing + */ + protected function renderSelector(ResourceView $resourceView): string + { + $checkboxConfig = $resourceView->getCheckboxConfig(); + if ($checkboxConfig === null) { + return ''; + } + if (!$resourceView->isSelectable) { + return ''; + } + + $attributes = [ + 'class' => 'form-check-input ' . $checkboxConfig['class'], + 'type' => 'checkbox', + 'name' => $checkboxConfig['name'], + 'value' => $checkboxConfig['value'], + 'checked' => $checkboxConfig['checked'], + ]; + + return '' + . '' + . ''; + } + + /** + * Render resource path + */ + protected function renderPath(ResourceView $resourceView): string + { + return htmlspecialchars($resourceView->getPath()); + } + + /** + * Render reference count. Wraps the count into a button to + * open the element information in case references exists. + */ + protected function renderReferenceCount(ResourceView $resourceView): string + { + if (!$resourceView->resource instanceof File) { + return '-'; + } + + $referenceCount = $this->getFileReferenceCount($resourceView->resource); + if (!$referenceCount) { + return '-'; + } + + $attributes = [ + 'type' => 'button', + 'class' => 'btn btn-sm btn-link', + 'data-filelist-action' => 'show', + 'title' => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:show_references') . ' (' . $referenceCount . ')', + ]; + + return ''; + } + + /** + * Renders the control section + */ + protected function renderControl(ResourceView $resourceView): string + { + return match ($this->mode) { + Mode::MANAGE => $this->renderControlManage($resourceView), + Mode::BROWSE => $this->renderControlBrowse($resourceView), + }; + } + + /** + * Creates the control section for the file list module + */ + protected function renderControlManage(ResourceView $resourceView): string + { + if (!$resourceView->resource instanceof File && !$resourceView->resource instanceof Folder) { + return ''; + } + + // primary actions + $userTsConfig = $this->getBackendUser()->getTSConfig(); + $primaryActions = GeneralUtility::trimExplode(',', $userTsConfig['options.']['file_list.']['primaryActions'] ?? 'view,metadata,translations,delete'); + + $primary = new ComponentGroup('primary'); + $secondary = new ComponentGroup('secondary'); + + $actions = [ + 'edit' => $this->createControlEditContent($resourceView), + 'metadata' => $this->createControlEditMetaData($resourceView), + 'translations' => $this->createControlTranslation($resourceView), + 'view' => $this->createControlView($resourceView), + 'replace' => $this->createControlReplace($resourceView), + 'rename' => $this->createControlRename($resourceView), + 'download' => $this->createControlDownload($resourceView), + 'info' => $this->createControlInfo($resourceView), + 'delete' => $this->createControlDelete($resourceView), + 'copy' => $this->createControlCopy($resourceView), + 'cut' => $this->createControlCut($resourceView), + 'paste' => $this->createControlPaste($resourceView), + 'updateOnlineMedia' => $this->createControlUpdateOnlineMedia($resourceView), + ]; + + foreach ($actions as $actionName => $action) { + if (in_array($actionName, $primaryActions, true)) { + $primary->add($actionName, $action); + } else { + $secondary->add($actionName, $action); + } + } + + $event = new ProcessFileListActionsEvent($primary, $secondary, $resourceView->resource, $this->request); + $event = $this->eventDispatcher->dispatch($event); + + if ($event->hasAction('translation')) { + // Always move "translations" to primary as this action has an own dropdown container and therefore cannot be a secondary action + $event->moveActionTo('translation', ActionGroup::primary); + } + + $cellOutput = ''; + $output = ''; + foreach ($event->getActionGroup(ActionGroup::primary)->getItems() as $action) { + if (method_exists($action, 'setSize')) { + $action->setSize(ButtonSize::MEDIUM); + } + $output .= $action; + } + foreach ($event->getActionGroup(ActionGroup::secondary)->getItems() as $action) { + if ($action instanceof GenericButton) { + $action = $this->componentFactory + ->createDropDownItem() + ->setTag($action->getTag()) + ->setLabel($action->getLabel() ?: $action->getTitle()) + ->setIcon($action->getIcon()) + ->setHref($action->getHref()) + ->setAttributes($action->getAttributes()); + $cellOutput .= '
  • ' . $action->render() . '
  • '; + continue; + } + if ($action instanceof LinkButton) { + $attributes = []; + foreach ($action->getDataAttributes() as $key => $value) { + $attributes['data-' . $key] = $value; + } + $action = $this->componentFactory + ->createDropDownItem() + ->setLabel($action->getTitle()) + ->setIcon($action->getIcon()) + ->setHref($action->getHref()) + ->setAttributes([ + ...$action->getAttributes(), + ...$attributes, + 'role' => $action->getRole(), + ]); + $cellOutput .= '
  • ' . $action->render() . '
  • '; + continue; + } + $cellOutput .= '
  • ' . $action->render() . '
  • '; + } + + if ($cellOutput !== '') { + $title = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more'); + $output .= ''; + } + + return '
    ' . $output . '
    '; + } + + /** + * Creates the control section for the element browser + */ + protected function renderControlBrowse(ResourceView $resourceView): string + { + $fileOrFolderObject = $resourceView->resource; + if (!$fileOrFolderObject instanceof File && !$fileOrFolderObject instanceof Folder) { + return ''; + } + + $actions = [ + 'select' => $this->createControlSelect($resourceView), + 'info' => $this->createControlInfo($resourceView), + ]; + + // Remove empty actions + $actions = array_filter($actions, static fn($action) => $action !== null && trim($action) !== ''); + if (empty($actions)) { + return ''; + } + foreach ($actions as $action) { + if (method_exists($action, 'setSize')) { + $action->setSize(ButtonSize::MEDIUM); + } + } + return '
    ' . implode(' ', $actions) . '
    '; + } + + protected function createControlSelect(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->isSelectable) { + return null; + } + + $title = sprintf( + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.selectFile'), + $resourceView->getName(), + ); + $button = $this->componentFactory->createGenericButton(); + $button->setTitle($title); + $button->setAttributes([ + 'type' => 'button', + 'data-filelist-action' => 'select', + 'aria-label' => $title, + ]); + $button->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL)); + + return $button; + } + + protected function createControlEditContent(ResourceView $resourceView): ?ButtonInterface + { + if (!($resourceView->resource instanceof File && $resourceView->resource->isTextFile()) + || !$resourceView->canWrite()) { + return null; + } + + $button = $this->componentFactory->createLinkButton(); + $button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editcontent')); + $button->setHref($resourceView->editContentUri); + $button->setIcon($this->iconFactory->getIcon('actions-page-open', IconSize::SMALL)); + + return $button; + } + + protected function createControlEditMetaData(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->getMetaDataUid() || !$this->getBackendUser()->checkLanguageAccess(0)) { + return null; + } + + $button = $this->componentFactory->createLinkButton(); + $button->setSize(ButtonSize::MEDIUM); + $button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata')); + $button->setHref($resourceView->editDataUri); + $button->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL)); + + return $button; + } + + protected function createControlView(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->getPublicUrl()) { + return null; + } + + $button = $this->componentFactory->createLinkButton(); + $button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view')); + $button->setHref($resourceView->getPublicUrl()); + $button->setAttributes(['target' => '_blank']); + $button->setSize(ButtonSize::MEDIUM); + $button->setIcon($this->iconFactory->getIcon('actions-document-view', IconSize::SMALL)); + + return $button; + } + + protected function createControlReplace(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->canReplace()) { + return null; + } + + $button = $this->componentFactory->createGenericButton(); + $button->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.replace')); + $button->setAttributes(['type' => 'button', 'data-filelist-action' => 'replace']); + $button->setIcon($this->iconFactory->getIcon('actions-edit-replace', IconSize::SMALL)); + + return $button; + } + + protected function createControlRename(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->canRename()) { + return null; + } + + $button = $this->componentFactory->createGenericButton(); + $button->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.rename')); + $button->setAttributes(['type' => 'button', 'data-filelist-action' => 'rename']); + $button->setIcon($this->iconFactory->getIcon('actions-edit-rename', IconSize::SMALL)); + + return $button; + } + + protected function createControlDownload(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->canRead() || !(bool)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['fileDownload.']['enabled'] ?? true)) { + return null; + } + + if (!$resourceView->isDownloadable) { + return null; + } + + $button = $this->componentFactory->createGenericButton(); + $button->setLabel($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:download')); + $button->setAttributes([ + 'type' => 'button', + 'data-filelist-action' => 'download', + 'data-filelist-action-url' => $this->uriBuilder->buildUriFromRoute('file_download'), + ]); + $button->setIcon($this->iconFactory->getIcon('actions-download', IconSize::SMALL)); + + return $button; + } + + protected function createControlInfo(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->canRead()) { + return null; + } + + $button = $this->componentFactory->createGenericButton(); + $button->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info')); + $button->setAttributes([ + 'type' => 'button', + 'data-filelist-action' => 'show', + ]); + $button->setIcon($this->iconFactory->getIcon('actions-document-info', IconSize::SMALL)); + + return $button; + } + + protected function createControlDelete(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->canDelete()) { + return null; + } + $recordInfo = $resourceView->getName(); + $referenceCountText = ''; + if ($resourceView->resource instanceof Folder) { + $identifier = $resourceView->getIdentifier(); + $deleteType = 'delete_folder'; + if ($this->getBackendUser()->shallDisplayDebugInformation()) { + $recordInfo .= ' [' . $identifier . ']'; + } + } else { + $referenceCountText = BackendUtility::referenceCount('sys_file', (int)$resourceView->getUid(), LF . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToFile')); + $deleteType = 'delete_file'; + if ($this->getBackendUser()->shallDisplayDebugInformation()) { + $recordInfo .= ' [sys_file:' . $resourceView->getUid() . ']'; + } + } + + $title = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete'); + $button = $this->componentFactory->createGenericButton(); + $button->setLabel($title); + $button->setIcon($this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL)); + $button->setAttributes([ + 'type' => 'button', + 'data-title' => $title, + 'data-content' => sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'), trim($recordInfo)) . $referenceCountText, + 'data-filelist-action' => 'delete', + 'data-filelist-delete' => 'true', + 'data-filelist-delete-identifier' => $resourceView->getIdentifier(), + 'data-filelist-delete-url' => $this->uriBuilder->buildUriFromRoute('tce_file'), + 'data-filelist-delete-type' => $deleteType, + 'data-filelist-delete-check' => $this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE) ? '1' : '0', + 'data-redirect-url' => $this->createModuleUri(), + ]); + $button->setSize(ButtonSize::MEDIUM); + + return $button; + } + + /** + * Creates the file metadata translation dropdown. Each item links + * to the corresponding metadata translation, while depending on + * the current state, either a new translation can be created or + * an existing translation can be edited. + */ + protected function createControlTranslation(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->resource instanceof File) { + return null; + } + + $backendUser = $this->getBackendUser(); + + // Fetch all system languages except "default (0)" and "all languages (-1)" + $systemLanguages = array_filter( + $this->translateTools->getSystemLanguages(), + static fn(array $languageRecord): bool => $languageRecord['uid'] > 0 && $backendUser->checkLanguageAccess($languageRecord['uid']) + ); + + if ($systemLanguages === [] + || !$this->tcaSchemaFactory->get('sys_file_metadata')->isLanguageAware() + || !$resourceView->resource->isIndexed() + || !$resourceView->resource->checkActionPermission('editMeta') + || !$backendUser->check('tables_modify', 'sys_file_metadata') + ) { + // Early return in case no system languages exists or metadata + // of this file can not be created / edited by the current user. + return null; + } + + $dropdownItems = []; + $metaDataRecord = $resourceView->resource->getMetaData()->get(); + $existingTranslations = $this->getTranslationsForMetaData($metaDataRecord); + + foreach ($systemLanguages as $languageId => $language) { + if (!isset($existingTranslations[$languageId]) && !($metaDataRecord['uid'] ?? false)) { + // Skip if neither a translation nor the metadata uid exists + continue; + } + + if (isset($existingTranslations[$languageId])) { + // Set options for edit action of an existing translation + $title = sprintf($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:editMetadataForLanguage'), $language['title']); + $actionType = 'edit'; + $url = (string)$this->uriBuilder->buildUriFromRoute( + 'record_edit', + [ + 'edit' => [ + 'sys_file_metadata' => [ + $existingTranslations[$languageId]['uid'] => 'edit', + ], + ], + 'module' => 'media_management', + 'returnUrl' => $this->createModuleUri(), + ] + ); + } else { + // Set options for "create new" action of a new translation using localization wizard + $title = sprintf($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:createMetadataForLanguage'), $language['title']); + $actionType = 'new'; + $metaDataRecordId = (int)($metaDataRecord['uid'] ?? 0); + + $dropdownItem = $this->componentFactory->createDropDownItem() + ->setTag('typo3-backend-localization-button') + ->setAttribute('record-type', 'sys_file_metadata') + ->setAttribute('record-uid', (string)$metaDataRecordId) + ->setAttribute('target-language', (string)$languageId) + ->setLabel($title); + if (!empty($language['flagIcon'])) { + $dropdownItem->setIcon($this->iconFactory->getIcon($language['flagIcon'], IconSize::SMALL, 'overlay-' . $actionType)); + } + $dropdownItems[] = $dropdownItem; + continue; + } + + $dropdownItem = $this->componentFactory->createDropDownItem(); + $dropdownItem->setLabel($title); + $dropdownItem->setHref($url); + $dropdownItem->setIcon($this->iconFactory->getIcon($language['flagIcon'], IconSize::SMALL, 'overlay-' . $actionType)); + $dropdownItems[] = $dropdownItem; + } + + if (empty($dropdownItems)) { + return null; + } + + $dropdownButton = $this->componentFactory->createDropDownButton(); + $dropdownButton->setLabel($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:translations')); + $dropdownButton->setIcon($this->iconFactory->getIcon('actions-translate', IconSize::SMALL)); + foreach ($dropdownItems as $dropdownItem) { + $dropdownButton->addItem($dropdownItem); + } + + return $dropdownButton; + } + + protected function createControlCopy(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->canRead() || !$resourceView->canCopy()) { + return null; + } + + if ($this->clipObj->current === 'normal') { + $isSelected = $this->clipObj->isSelected('_FILE', md5($resourceView->getIdentifier())); + $button = $this->componentFactory->createLinkButton(); + $button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . ($isSelected === 'copy' ? 'copyrelease' : 'copy'))); + $button->setHref($this->clipObj->selUrlFile($resourceView->getIdentifier(), true, $isSelected === 'copy')); + $button->setIcon($this->iconFactory->getIcon($isSelected === 'copy' ? 'actions-edit-copy-release' : 'actions-edit-copy', IconSize::SMALL)); + return $button; + } + + return null; + } + + protected function createControlCut(ResourceView $resourceView): ?ButtonInterface + { + if (!$resourceView->canRead() || !$resourceView->canMove()) { + return null; + } + + if ($this->clipObj->current === 'normal') { + $isSelected = $this->clipObj->isSelected('_FILE', md5($resourceView->getIdentifier())); + $button = $this->componentFactory->createLinkButton(); + $button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . ($isSelected === 'cut' ? 'cutrelease' : 'cut'))); + $button->setHref($this->clipObj->selUrlFile($resourceView->getIdentifier(), false, $isSelected === 'cut')); + $button->setIcon($this->iconFactory->getIcon($isSelected === 'cut' ? 'actions-edit-cut-release' : 'actions-edit-cut', IconSize::SMALL)); + + return $button; + } + + return null; + } + + protected function createControlPaste(ResourceView $resourceView): ?ButtonInterface + { + $permission = ($this->clipObj->clipData[$this->clipObj->current]['mode'] ?? '') === 'copy' ? 'copy' : 'move'; + $addPasteButton = $this->folderObject->checkActionPermission($permission); + $elementFromTable = $this->clipObj->elFromTable('_FILE'); + if ($elementFromTable === [] + || !$addPasteButton + || !$resourceView->canRead() + || !$resourceView->canWrite() + || !$resourceView->resource instanceof Folder) { + return null; + } + + foreach ($elementFromTable as $element) { + $clipBoardElement = $this->resourceFactory->retrieveFileOrFolderObject($element); + if ($clipBoardElement instanceof Folder + && $clipBoardElement->getStorage()->isWithinFolder($clipBoardElement, $resourceView->resource) + ) { + // In case folder is already present in the target folder, return actions without paste button + return null; + } + } + + $pasteTitle = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:clip_pasteInto'); + $button = $this->componentFactory->createLinkButton(); + $button->setTitle($pasteTitle); + $button->setHref($this->clipObj->pasteUrl('_FILE', $resourceView->getIdentifier())); + $button->setDataAttributes([ + 'title' => $pasteTitle, + 'bs-content' => $this->clipObj->confirmMsgText('_FILE', $resourceView->getName(), 'into'), + ]); + $button->setIcon($this->iconFactory->getIcon('actions-document-paste-into', IconSize::SMALL)); + + return $button; + } + + protected function createControlUpdateOnlineMedia(ResourceView $resourceView): ?ButtonInterface + { + if (!($resourceView->resource instanceof File) + || !$resourceView->canEditMetadata() + || !$this->getBackendUser()->checkLanguageAccess(0) + || !$this->onlineMediaHelperRegistry->hasOnlineMediaHelper($resourceView->resource->getExtension()) + ) { + return null; + } + + $title = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:reloadMetadata'); + $button = $this->componentFactory->createGenericButton(); + $button->setLabel($title); + $button->setIcon($this->iconFactory->getIcon('actions-refresh', IconSize::SMALL)); + $button->setAttributes([ + 'type' => 'button', + 'data-title' => $title, + 'data-filelist-action' => 'updateOnlineMedia', + 'data-filelist-action-url' => $this->uriBuilder->buildUriFromRoute('file_update_online_media'), + ]); + + return $button; + } + + protected function isEditMetadataAllowed(File $file): bool + { + return $file->isIndexed() + && $file->checkActionPermission('editMeta') + && $this->getUserPermissions()->editMetaData; + } + + /** + * Render convenience actions, such as "check all" + * + * @return string HTML markup for the checkbox actions + */ + protected function renderCheckboxActions(): string + { + // Early return in case there are no items + if (!$this->totalItems) { + return ''; + } + + $lang = $this->getLanguageService(); + + $dropdownItems['checkAll'] = ' +
  • + +
  • '; + + $dropdownItems['checkNone'] = ' +
  • + +
  • '; + + $dropdownItems['toggleSelection'] = ' +
  • + +
  • '; + + return ' + '; + } + + /** + * Determine the concrete table name by checking if + * the field exists, while sys_file takes precedence. + */ + protected function getConcreteTableName(string $fieldName): string + { + if ($this->tcaSchemaFactory->get('sys_file')->hasField($fieldName)) { + return 'sys_file'; + } + return 'sys_file_metadata'; + } + + /** + * Returns list URL; This is the URL of the current script with id and imagemode parameters, that's all. + */ + public function createModuleUri(array $params = []): ?string + { + $request = $this->request; + $queryParams = $request->getQueryParams(); + $parsedBody = $request->getParsedBody(); + + $route = $request->getAttribute('route'); + if (!$route instanceof Route) { + return null; + } + + $baseParams = [ + 'currentPage' => $this->currentPage, + 'id' => $this->folderObject->getCombinedIdentifier(), + 'searchTerm' => $this->searchDemand ? $this->searchDemand->getSearchTerm() : '', + ]; + + // Keep ElementBrowser Settings + if ($mode = $parsedBody['mode'] ?? $queryParams['mode'] ?? null) { + $baseParams['mode'] = $mode; + } + foreach (['fieldReference', 'allowedTypes', 'disallowedFileExtensions', 'irreObjectId'] as $paramName) { + if ($value = $parsedBody[$paramName] ?? $queryParams[$paramName] ?? null) { + $baseParams[$paramName] = $value; + } + } + + // Keep LinkHandler Settings + if ($act = ($parsedBody['act'] ?? $queryParams['act'] ?? null)) { + $baseParams['act'] = $act; + } + if ($linkHandlerParams = ($parsedBody['P'] ?? $queryParams['P'] ?? null)) { + $baseParams['P'] = $linkHandlerParams; + } + + $params = array_replace_recursive($baseParams, $params); + + // Expanded folder is used in the element browser. + // We always map it to the id here. + $params['expandFolder'] = $params['id']; + $params = array_filter($params, static function ($value) { + return (is_array($value) && $value !== []) || (trim((string)$value) !== ''); + }); + + return (string)$this->uriBuilder->buildUriFromRequest($request, $params); + } + + protected function createEditDataUriForResource(ResourceInterface $resource): ?string + { + if ($resource instanceof File + && $this->isEditMetadataAllowed($resource) + && ($metaDataUid = $resource->getMetaData()->offsetGet('uid')) + ) { + $parameter = [ + 'edit' => ['sys_file_metadata' => [$metaDataUid => 'edit']], + 'module' => 'media_management', + 'returnUrl' => $this->createModuleUri(), + ]; + return (string)$this->uriBuilder->buildUriFromRoute('record_edit', $parameter); + } + + return null; + } + + protected function createEditContentUriForResource(ResourceInterface $resource): ?string + { + if ($resource instanceof File + && $resource->checkActionPermission('write') + && $resource->isTextFile() + ) { + $parameter = [ + 'target' => $resource->getCombinedIdentifier(), + 'returnUrl' => $this->createModuleUri(), + ]; + return (string)$this->uriBuilder->buildUriFromRoute('file_edit', $parameter); + } + + return null; + } + + protected function createModuleUriForResource(ResourceInterface $resource): ?string + { + if ($resource instanceof Folder) { + $parameter = [ + 'id' => $resource->getCombinedIdentifier(), + 'searchTerm' => '', + 'currentPage' => 1, + ]; + return (string)$this->createModuleUri($parameter); + } + + if ($resource instanceof File) { + return $this->createEditDataUriForResource($resource); + } + + return null; + } + + /** + * @return ResourceInterface[] + */ + protected function sortResources(array $resources, string $sortField): array + { + $collator = new \Collator((string)($this->getLanguageService()->getLocale() ?? 'en')); + $collator->setAttribute(\Collator::NUMERIC_COLLATION, \Collator::ON); + + $sortMultiplier = $this->sortDirection === SortDirection::DESCENDING ? -1 : 1; + uksort($resources, function (int $index1, int $index2) use ($sortField, $sortMultiplier, $resources, $collator) { + $resource1 = $resources[$index1]; + $resource2 = $resources[$index2]; + + // Folders are always prioritized above files + if ($resource1 instanceof File && $resource2 instanceof Folder) { + return 1 * $sortMultiplier; + } + if ($resource1 instanceof Folder && $resource2 instanceof File) { + return -1 * $sortMultiplier; + } + + // Sort by value first + $result = (int)$collator->compare( + $this->getSortingValue($resource1, $sortField), + $this->getSortingValue($resource2, $sortField) + ); + + // Use index as tiebreaker for stable sorting + if ($result === 0) { + $result = $index1 <=> $index2; + } + + return $result * $sortMultiplier; + }); + + return $resources; + } + + protected function getSortingValue(ResourceInterface $resource, string $sortField): string + { + if ($resource instanceof File) { + return $this->getSortingValueForFile($resource, $sortField); + } + if ($resource instanceof Folder) { + return $this->getSortingValueForFolder($resource, $sortField); + } + + return ''; + } + + protected function getSortingValueForFile(File $resource, string $sortField): string + { + switch ($sortField) { + case 'fileext': + return $resource->getExtension(); + case 'size': + return $resource->getSize() . 's'; + case 'rw': + return ($resource->checkActionPermission('read') ? 'R' : '') + . ($resource->checkActionPermission('write') ? 'W' : ''); + case '_REF_': + return $this->getFileReferenceCount($resource) . 'ref'; + case 'tstamp': + return $resource->getModificationTime() . 't'; + case 'crdate': + return $resource->getCreationTime() . 'c'; + case 'file': + return $resource->getName(); + default: + return $resource->hasProperty($sortField) ? (string)$resource->getProperty($sortField) : ''; + } + } + + protected function getSortingValueForFolder(Folder $resource, string $sortField): string + { + switch ($sortField) { + case 'size': + try { + $fileCount = $resource->getFileCount(); + } catch (InsufficientFolderAccessPermissionsException $e) { + $fileCount = 0; + } + return '0' . $fileCount . 's'; + case 'rw': + return ($resource->checkActionPermission('read') ? 'R' : '') + . ($resource->checkActionPermission('write') ? 'W' : ''); + case 'name': + return $resource->getName(); + case 'tstamp': + return $resource->getModificationTime() . 't'; + case 'crdate': + return $resource->getCreationTime() . 'c'; + default: + return ''; + } + } + + public function getFieldLabel(string $field): string + { + $lang = $this->getLanguageService(); + + if ($specialLabel = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $field)) { + return $specialLabel; + } + if ($customLabel = $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:c_' . $field)) { + return $customLabel; + } + + $concreteTableName = $this->getConcreteTableName($field); + $schema = $this->tcaSchemaFactory->has($concreteTableName) ? $this->tcaSchemaFactory->get($concreteTableName) : null; + $label = ($schema?->hasField($field) ? $schema->getField($field)->getLabel() : '') ?: null; + + // In case global TSconfig exists we have to check if the label is overridden there + $tsConfig = BackendUtility::getPagesTSconfig(0); + $label = $lang->translateLabel( + $tsConfig['TCEFORM.'][$concreteTableName . '.'][$field . '.']['label.'] ?? [], + $tsConfig['TCEFORM.'][$concreteTableName . '.'][$field . '.']['label'] + ?? $label + ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $field + ); + + return $label ?: $field; + } + + /** + * Counts how often the given file is referenced. This is done by + * looking up the file in the "sys_refindex" table, while excluding + * sys_file_metadata relations as these are no such references. + */ + protected function getFileReferenceCount(File $file): int + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex'); + return (int)$queryBuilder + ->count('*') + ->from('sys_refindex') + ->where( + $queryBuilder->expr()->eq( + 'ref_table', + $queryBuilder->createNamedParameter('sys_file') + ), + $queryBuilder->expr()->eq( + 'ref_uid', + $queryBuilder->createNamedParameter($file->getUid(), Connection::PARAM_INT) + ), + $queryBuilder->expr()->neq( + 'tablename', + $queryBuilder->createNamedParameter('sys_file_metadata') + ) + ) + ->executeQuery() + ->fetchOne(); + } + + protected function getUserPermissions(): UserPermissions + { + return new UserPermissions($this->getBackendUser()->check('tables_modify', 'sys_file_metadata')); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/LinkHandler/AbstractResourceLinkHandler.php b/Classes/LinkHandler/AbstractResourceLinkHandler.php new file mode 100644 index 0000000..978deff --- /dev/null +++ b/Classes/LinkHandler/AbstractResourceLinkHandler.php @@ -0,0 +1,368 @@ +languageService = $this->languageServiceFactory->createFromUserPreferences($this->getBackendUser()); + } + + public function canHandleLink(array $linkParts): bool + { + if (!$linkParts['url']) { + return false; + } + if (isset($linkParts['url'][$this->type->value]) && $linkParts['url'][$this->type->value] instanceof ($this->type->getResourceType())) { + $this->linkParts = $linkParts; + return true; + } + return false; + } + + public function formatCurrentUrl(): string + { + $resource = $this->linkParts['url'][$this->type->value]; + if (!$resource->checkActionPermission('read')) { + return ''; + } + if ($resource->getStorage()->isFallbackStorage()) { + return ''; + } + return $this->linkParts['url'][$this->type->value]->getName(); + } + + public function createView(BackendViewFactory $backendViewFactory, ServerRequestInterface $request): ViewInterface + { + return $backendViewFactory->create($request, ['typo3/cms-filelist']); + } + + public function setView(ViewInterface $view): self + { + $this->view = $view; + return $this; + } + + public function getView(): ViewInterface + { + return $this->view; + } + + public function getLinkAttributes(): array + { + return ['target', 'title', 'class', 'params', 'rel', 'download']; + } + + public function initialize(AbstractLinkBrowserController $linkBrowser, $identifier, array $configuration) + { + $this->linkBrowser = $linkBrowser; + } + + public function initializeVariables(ServerRequestInterface $request): void + { + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/tree/file-storage-browser.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/filelist/file-list-actions.js'); + + $this->currentPage = (int)($request->getParsedBody()['currentPage'] ?? $request->getQueryParams()['currentPage'] ?? 1); + $this->sortField = ($request->getParsedBody()['sortField'] ?? $request->getQueryParams()['sortField'] ?? 'name'); + $this->sortDirection = SortDirection::tryFrom($request->getParsedBody()['sortDirection'] ?? $request->getQueryParams()['sortDirection'] ?? '') ?? SortDirection::ASCENDING; + + $this->viewMode = ViewMode::tryFrom($request->getParsedBody()['viewMode'] ?? $request->getQueryParams()['viewMode'] ?? ''); + if ($this->viewMode !== null) { + $this->getBackendUser()->pushModuleData( + $this->moduleStorageIdentifier, + array_merge($this->getBackendUser()->getModuleData($this->moduleStorageIdentifier) ?? [], ['viewMode' => $this->viewMode->value]) + ); + } else { + $this->viewMode = ViewMode::tryFrom($this->getBackendUser()->getModuleData($this->moduleStorageIdentifier)['viewMode'] ?? '') + ?? ViewMode::tryFrom($this->getBackendUser()->getTSConfig()['options.']['defaultResourcesViewMode'] ?? '') + ?? ViewMode::TILES; + } + + $displayThumbs = $request->getParsedBody()['displayThumbs'] ?? $request->getQueryParams()['displayThumbs'] ?? null; + if ($displayThumbs !== null) { + $this->displayThumbs = (bool)$displayThumbs; + $this->getBackendUser()->pushModuleData( + $this->moduleStorageIdentifier, + array_merge($this->getBackendUser()->getModuleData($this->moduleStorageIdentifier) ?? [], ['displayThumbs' => $this->displayThumbs]) + ); + } else { + $this->displayThumbs = (bool)($this->getBackendUser()->getModuleData($this->moduleStorageIdentifier)['displayThumbs'] ?? true); + } + + // Selected Folder folder + $this->expandFolder = $request->getParsedBody()['expandFolder'] ?? $request->getQueryParams()['expandFolder'] ?? null; + if ($this->expandFolder === null) { + if (!empty($this->linkParts)) { + $resource = $this->linkParts['url'][$this->type->value]; + if ($resource instanceof File) { + $resource = $resource->getParentFolder(); + } + if ($resource instanceof Folder) { + $this->expandFolder = $resource->getCombinedIdentifier(); + if ($this->type === LinkType::FOLDER) { + // Select the parent folder of selected folder as entry point. + $this->expandFolder = $resource->getParentFolder()->getCombinedIdentifier(); + } + } + } else { + // Look up in the user's session which folder was opened the last time + $moduleSessionData = $this->getBackendUser()->getModuleData('browse_links.php', 'ses'); + $this->expandFolder = $moduleSessionData['expandFolder'] ?? null; + } + } + if ($this->expandFolder) { + try { + $selectedFolder = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($this->expandFolder); + if ($selectedFolder->checkActionPermission('read') && !$selectedFolder->getStorage()->isFallbackStorage()) { + $this->selectedFolder = $selectedFolder; + } + } catch (FolderDoesNotExistException|InsufficientFolderAccessPermissionsException) { + // Outdated module session data: Last used folder has been removed meanwhile, or + // access to last used folder has been removed. Do not set a preselected folder. + } + } + + $this->filelist = GeneralUtility::makeInstance(FileList::class, $request); + $this->filelist->viewMode = $this->viewMode; + $this->filelist->thumbs = ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] ?? false) && $this->displayThumbs; + } + + public function modifyLinkAttributes(array $fieldDefinitions): array + { + return $fieldDefinitions; + } + + public function isUpdateSupported(): bool + { + $resource = $this->linkParts['url'][$this->type->value]; + if (!$resource->checkActionPermission('read')) { + return false; + } + if ($resource->getStorage()->isFallbackStorage()) { + return false; + } + return true; + } + + /** + * @return string[] Array of body-tag attributes + */ + public function getBodyTagAttributes(): array + { + $resource = $this->linkParts['url'][$this->type->value] ?? null; + if (!$resource instanceof ($this->type->getResourceType())) { + return []; + } + if (!$resource->checkActionPermission('read')) { + return []; + } + if ($resource->getStorage()->isFallbackStorage()) { + return []; + } + return [ + 'data-linkbrowser-current-link' => GeneralUtility::makeInstance(LinkService::class)->asString([ + 'type' => $this->type->getLinkServiceType(), + $this->type->value => $resource, + ]), + ]; + } + + protected function createUri(ServerRequestInterface $request, array $parameters = []): string + { + return (string)$this->uriBuilder->buildUriFromRequest($request, $this->getUrlParameters($parameters)); + } + + protected function getSortingModeButtons(ServerRequestInterface $request): ButtonInterface + { + $sortingButton = $this->componentFactory->createDropDownButton() + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting')) + ->setIcon($this->iconFactory->getIcon($this->sortDirection->getIconIdentifier())); + + $sortingModeButtons = []; + $sortableFields = $this->filelist->getSortableFields(); + if (count($sortableFields) > 1) { + foreach ($sortableFields as $field) { + $label = $this->filelist->getFieldLabel($field); + + $sortingModeButtons[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->sortField === $field) + ->setHref($this->createUri($request, [ + 'sortField' => $field, + 'sortDirection' => SortDirection::ASCENDING->value, + 'currentPage' => 1, + ])) + ->setLabel($label); + } + + $sortingModeButtons[] = $this->componentFactory->createDropDownDivider(); + } + $defaultSortingDirectionParams = ['sortField' => $this->sortField, 'currentPage' => 1]; + $sortingModeButtons[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->sortDirection === SortDirection::ASCENDING) + ->setHref($this->createUri($request, array_merge($defaultSortingDirectionParams, ['sortDirection' => SortDirection::ASCENDING->value]))) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting.asc')); + $sortingModeButtons[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->sortDirection === SortDirection::DESCENDING) + ->setHref($this->createUri($request, array_merge($defaultSortingDirectionParams, ['sortDirection' => SortDirection::DESCENDING->value]))) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting.desc')); + + foreach ($sortingModeButtons as $sortingModeButton) { + $sortingButton->addItem($sortingModeButton); + } + + return $sortingButton; + } + + protected function getViewModeButton(ServerRequestInterface $request): ButtonInterface + { + $viewModeItems = []; + $viewModeItems[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->viewMode === ViewMode::TILES) + ->setHref($this->createUri($request, ['viewMode' => ViewMode::TILES->value])) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.tiles')) + ->setIcon($this->iconFactory->getIcon('actions-viewmode-tiles')); + $viewModeItems[] = $this->componentFactory->createDropDownRadio() + ->setActive($this->viewMode === ViewMode::LIST) + ->setHref($this->createUri($request, ['viewMode' => ViewMode::LIST->value])) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.list')) + ->setIcon($this->iconFactory->getIcon('actions-viewmode-list')); + if (!($this->getBackendUser()->getTSConfig()['options.']['noThumbsInEB'] ?? false)) { + $viewModeItems[] = $this->componentFactory->createDropDownDivider(); + $viewModeItems[] = $this->componentFactory->createDropDownToggle() + ->setActive($this->displayThumbs) + ->setHref($this->createUri($request, ['displayThumbs' => $this->displayThumbs ? 0 : 1])) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.showThumbnails')) + ->setIcon($this->iconFactory->getIcon('actions-image')); + } + if ( + ($this->getBackendUser()->getTSConfig()['options.']['file_list.']['displayColumnSelector'] ?? true) + && $this->viewMode === ViewMode::LIST + && ($request->getQueryParams()['act'] ?? '') === 'file' + ) { + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/column-selector-button.js'); + $viewModeItems[] = $this->componentFactory->createDropDownDivider(); + $viewModeItems[] = $this->componentFactory->createDropDownItem() + ->setTag('typo3-backend-column-selector-button') + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.selectColumns')) + ->setAttributes([ + 'data-url' => (string)$this->uriBuilder->buildUriFromRoute( + 'ajax_show_columns_selector', + ['table' => '_FILE'] + ), + 'data-target' => (string)$this->filelist->createModuleUri(), + 'data-title' => sprintf( + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:showColumnsSelection'), + $this->tcaSchemaFactory->get('sys_file')->getTitle($this->getLanguageService()->sL(...)), + ), + 'data-button-ok' => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView'), + 'data-button-close' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel'), + 'data-error-message' => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView.error'), + ]) + ->setIcon($this->iconFactory->getIcon('actions-options')); + } + + $viewModeButton = $this->componentFactory->createDropDownButton() + ->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view')) + ->setIcon($this->iconFactory->getIcon('actions-cog')); + foreach ($viewModeItems as $viewModeItem) { + $viewModeButton->addItem($viewModeItem); + } + + return $viewModeButton; + } + + public function getUrlParameters(array $values): array + { + $values = array_replace_recursive([ + 'expandFolder' => $values['identifier'] ?? $this->expandFolder, + ], $values); + + return array_merge($this->linkBrowser->getUrlParameters($values), $values); + } + + protected function getLanguageService(): LanguageService + { + return $this->languageService; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/LinkHandler/FileLinkHandler.php b/Classes/LinkHandler/FileLinkHandler.php new file mode 100644 index 0000000..c07cda2 --- /dev/null +++ b/Classes/LinkHandler/FileLinkHandler.php @@ -0,0 +1,140 @@ +pageRenderer->loadJavaScriptModule('@typo3/filelist/linkbrowser-file-handler.js'); + + $this->resourceDisplayMatcher = GeneralUtility::makeInstance(Matcher::class); + $this->resourceDisplayMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + + // @todo Deprecate "allowedExtensions", see LinkPopup for further information + $allowedExtensions = GeneralUtility::trimExplode(',', (string)($this->linkBrowser->getParameters()['params']['allowedExtensions'] ?? ''), true); + $allowedFileExtensions = GeneralUtility::trimExplode(',', (string)($this->linkBrowser->getParameters()['params']['allowedFileExtensions'] ?? ''), true); + $allowedFileExtensions = array_unique(array_merge($allowedExtensions, $allowedFileExtensions)); + if (count($allowedFileExtensions) >= 1) { + $fileExtensionMatcher = GeneralUtility::makeInstance(ResourceFileExtensionMatcher::class); + $fileExtensionMatcher->setExtensions($allowedFileExtensions); + } else { + $fileExtensionMatcher = GeneralUtility::makeInstance(ResourceFileTypeMatcher::class); + } + $this->resourceDisplayMatcher->addMatcher($fileExtensionMatcher); + } + + public function render(ServerRequestInterface $request): string + { + $contentHtml = ''; + if ($this->selectedFolder !== null) { + + // store the selected folder + $backendUser = $this->getBackendUser(); + $modData = $backendUser->getModuleData('browse_links.php', 'ses'); + $modData['expandFolder'] = $this->selectedFolder->getCombinedIdentifier(); + $backendUser->pushModuleData('browse_links.php', $modData); + + // Create the filelist + $this->filelist->start( + $this->selectedFolder, + MathUtility::forceIntegerInRange($this->currentPage, 1, 100000), + $this->sortField, + $this->sortDirection, + Mode::BROWSE + ); + $this->filelist->setResourceDisplayMatcher($this->resourceDisplayMatcher); + $this->filelist->setResourceSelectableMatcher($this->resourceSelectableMatcher); + + // Only add selected columns if the feature is enabled + if ($this->getBackendUser()->getTSConfig()['options.']['file_list.']['displayColumnSelector'] ?? true) { + $this->filelist->setColumnsToRender($this->getBackendUser()->getModuleData('list/displayFields')['_FILE'] ?? []); + } + + $searchWord = trim((string)($request->getParsedBody()['searchTerm'] ?? $request->getQueryParams()['searchTerm'] ?? '')); + $searchDemand = $searchWord !== '' ? FileSearchDemand::createForSearchTerm($searchWord)->withFolder($this->selectedFolder)->withRecursive() : null; + + $resource = $this->linkParts['url']['file'] ?? null; + if ($resource instanceof ResourceInterface) { + $resourceSelectedMatcher = GeneralUtility::makeInstance(Matcher::class); + $resourceMatcher = GeneralUtility::makeInstance(ResourceMatcher::class); + $resourceMatcher->addResource($resource); + $resourceSelectedMatcher->addMatcher($resourceMatcher); + $this->filelist->setResourceSelectedMatcher($resourceSelectedMatcher); + } + + $markup = []; + + // Render the filelist search box + $markup[] = '
    '; + $markup[] = GeneralUtility::makeInstance(RecordSearchBoxComponent::class) + ->setSearchWord($searchWord) + ->render($request, $this->filelist->createModuleUri($this->getUrlParameters([]))); + $markup[] = '
    '; + + // Render the filelist header bar + $markup[] = '
    '; + $markup[] = '
    '; + $markup[] = '
    '; + $markup[] = ' ' . $this->getSortingModeButtons($request); + $markup[] = ' ' . $this->getViewModeButton($request); + $markup[] = '
    '; + $markup[] = '
    '; + + // Render the filelist + $markup[] = $this->filelist->render($searchDemand, $this->view); + + // Render the file upload and folder creation form + $resourceUtilityRenderer = GeneralUtility::makeInstance(ResourceUtilityRenderer::class, $this); + $markup[] = $resourceUtilityRenderer->uploadForm($request, $this->selectedFolder); + $markup[] = $resourceUtilityRenderer->addOnlineMedia($request, $this->selectedFolder); + $markup[] = $resourceUtilityRenderer->createFolder($request, $this->selectedFolder); + + $contentHtml = implode(PHP_EOL, $markup); + } + + $this->view->assign('selectedFolder', $this->selectedFolder); + $this->view->assign('content', $contentHtml); + $this->view->assign('contentOnly', (bool)($request->getQueryParams()['contentOnly'] ?? false)); + $this->view->assign('treeActions', ($this->type === LinkType::FOLDER) ? ['link'] : []); + $this->view->assign('currentIdentifier', !empty($this->linkParts) ? $this->linkParts['url']['file']->getUid() : ''); + + return $this->view->render('LinkHandler/File'); + } +} diff --git a/Classes/LinkHandler/FolderLinkHandler.php b/Classes/LinkHandler/FolderLinkHandler.php new file mode 100644 index 0000000..849c37c --- /dev/null +++ b/Classes/LinkHandler/FolderLinkHandler.php @@ -0,0 +1,102 @@ +pageRenderer->loadJavaScriptModule('@typo3/filelist/linkbrowser-folder-handler.js'); + + $this->resourceDisplayMatcher = GeneralUtility::makeInstance(Matcher::class); + $this->resourceDisplayMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class)); + } + + public function render(ServerRequestInterface $request): string + { + $contentHtml = ''; + if ($this->selectedFolder !== null) { + // Create the filelist + $this->filelist->start( + $this->selectedFolder, + MathUtility::forceIntegerInRange($this->currentPage, 1, 100000), + $this->sortField, + $this->sortDirection, + Mode::BROWSE + ); + + $markup = []; + + // Create the filelist header bar + $markup[] = '
    '; + $markup[] = '
    '; + $markup[] = '
    '; + $markup[] = ' ' . $this->getSortingModeButtons($request); + $markup[] = ' ' . $this->getViewModeButton($request); + $markup[] = '
    '; + $markup[] = '
    '; + + $this->filelist->setResourceDisplayMatcher($this->resourceDisplayMatcher); + $this->filelist->setResourceSelectableMatcher($this->resourceSelectableMatcher); + + $resource = $this->linkParts['url']['folder'] ?? null; + if ($resource instanceof ResourceInterface) { + $resourceSelectedMatcher = GeneralUtility::makeInstance(Matcher::class); + $resourceMatcher = GeneralUtility::makeInstance(ResourceMatcher::class); + $resourceMatcher->addResource($resource); + $resourceSelectedMatcher->addMatcher($resourceMatcher); + $this->filelist->setResourceSelectedMatcher($resourceSelectedMatcher); + } + + $markup[] = $this->filelist->render(null, $this->view); + + // Build the file upload and folder creation form + $resourceUtilityRenderer = GeneralUtility::makeInstance(ResourceUtilityRenderer::class, $this); + $markup[] = $resourceUtilityRenderer->createFolder($request, $this->selectedFolder); + + $contentHtml = implode(PHP_EOL, $markup); + } + + $this->view->assign('selectedFolder', $this->selectedFolder); + $this->view->assign('selectedFolderLink', (GeneralUtility::makeInstance(LinkService::class))->asString(['type' => LinkService::TYPE_FOLDER, 'folder' => $this->selectedFolder])); + $this->view->assign('content', $contentHtml); + $this->view->assign('contentOnly', (bool)($request->getQueryParams()['contentOnly'] ?? false)); + $this->view->assign('treeActions', ['link']); + $this->view->assign('currentIdentifier', !empty($this->linkParts) ? $this->linkParts['url']['folder']->getCombinedIdentifier() : ''); + + return $this->view->render('LinkHandler/Folder'); + } +} diff --git a/Classes/Matcher/AndMatcher.php b/Classes/Matcher/AndMatcher.php new file mode 100644 index 0000000..a34812e --- /dev/null +++ b/Classes/Matcher/AndMatcher.php @@ -0,0 +1,64 @@ +matchers = $matchers; + } + + public function supports(mixed $item): bool + { + if ($this->matchers === []) { + return false; + } + + foreach ($this->matchers as $matcher) { + if (!$matcher->supports($item)) { + return false; + } + } + + return true; + } + + public function match(mixed $item): bool + { + if ($this->matchers === []) { + return false; + } + + foreach ($this->matchers as $matcher) { + if (!$matcher->match($item)) { + return false; + } + } + + return true; + } +} diff --git a/Classes/Matcher/Matcher.php b/Classes/Matcher/Matcher.php new file mode 100644 index 0000000..08c4873 --- /dev/null +++ b/Classes/Matcher/Matcher.php @@ -0,0 +1,47 @@ +matchers[] = $matcher; + + return $this; + } + + public function match(mixed $item): bool + { + foreach ($this->matchers as $matcher) { + if ($matcher->supports($item) && $matcher->match($item)) { + return true; + } + } + + return false; + } +} diff --git a/Classes/Matcher/MatcherInterface.php b/Classes/Matcher/MatcherInterface.php new file mode 100644 index 0000000..46f3141 --- /dev/null +++ b/Classes/Matcher/MatcherInterface.php @@ -0,0 +1,27 @@ +extensions = array_map(strtolower(...), $extensions); + + return $this; + } + + public function addExtension(string $extension): self + { + $this->extensions[] = strtolower($extension); + + return $this; + } + + /** + * @param string[] $ignoredExtensions + */ + public function setIgnoredExtensions(array $ignoredExtensions): self + { + $this->ignoredExtensions = array_map(strtolower(...), $ignoredExtensions); + + return $this; + } + + public function addIgnoredExtension(string $ignoredExtension): self + { + $this->ignoredExtensions[] = strtolower($ignoredExtension); + + return $this; + } + + public function supports(mixed $item): bool + { + return $item instanceof File; + } + + public function match(mixed $item): bool + { + if (!$item instanceof File) { + return false; + } + + if (in_array($item->getExtension(), $this->ignoredExtensions, true)) { + return false; + } + + if (in_array('*', $this->extensions, true) || in_array($item->getExtension(), $this->extensions, true)) { + return true; + } + + return false; + } +} diff --git a/Classes/Matcher/ResourceFileTypeMatcher.php b/Classes/Matcher/ResourceFileTypeMatcher.php new file mode 100644 index 0000000..9dc50ef --- /dev/null +++ b/Classes/Matcher/ResourceFileTypeMatcher.php @@ -0,0 +1,37 @@ +resources = $resources; + + return $this; + } + + public function addResource(ResourceInterface $resource): self + { + $this->resources[] = $resource; + + return $this; + } + + public function supports(mixed $item): bool + { + return $item instanceof ResourceInterface; + } + + public function match(mixed $item): bool + { + return in_array($item, $this->resources); + } +} diff --git a/Classes/Pagination/ResourceCollectionPaginator.php b/Classes/Pagination/ResourceCollectionPaginator.php new file mode 100644 index 0000000..0961f62 --- /dev/null +++ b/Classes/Pagination/ResourceCollectionPaginator.php @@ -0,0 +1,61 @@ +paginatedItems = new ResourceCollection(); + $this->setCurrentPageNumber($currentPageNumber); + $this->setItemsPerPage($itemsPerPage); + + $this->updateInternalState(); + } + + public function getPaginatedItems(): ResourceCollection + { + return $this->paginatedItems; + } + + protected function updatePaginatedItems(int $itemsPerPage, int $offset): void + { + $this->paginatedItems = new ResourceCollection(array_slice($this->items->getResources(), $offset, $itemsPerPage)); + } + + protected function getTotalAmountOfItems(): int + { + return count($this->items); + } + + protected function getAmountOfItemsOnCurrentPage(): int + { + return count($this->paginatedItems); + } +} diff --git a/Classes/Search/LiveSearch/FileProvider.php b/Classes/Search/LiveSearch/FileProvider.php new file mode 100644 index 0000000..84e9a83 --- /dev/null +++ b/Classes/Search/LiveSearch/FileProvider.php @@ -0,0 +1,211 @@ +languageService = $languageServiceFactory->createFromUserPreferences($this->getBackendUser()); + } + + public function getFilterLabel(): string + { + return $this->languageService->sL('filelist.messages:live_search.file_provider.filter_label'); + } + + public function count(SearchDemand $searchDemand): int + { + $query = FileSearchQuery::createCountForSearchDemand($this->buildFileSearchDemand($searchDemand)); + $query->additionalRestriction(new FolderMountsRestriction($this->getBackendUser())); + return (int)$query->execute()->fetchOne(); + } + + /** + * @return ResultItem[] + */ + public function find(SearchDemand $searchDemand): array + { + $fileSearchDemand = $this->buildFileSearchDemand($searchDemand) + ->withStartResult($searchDemand->getOffset()) + ->withMaxResults($searchDemand->getLimit()); + + $query = FileSearchQuery::createForSearchDemand($fileSearchDemand); + $result = $query->execute(); + + $items = []; + while ($row = $result->fetchAssociative()) { + try { + $file = $this->resourceFactory->getFileObject((int)$row['uid'], $row); + } catch (Exception) { + continue; + } + + try { + $parentFolder = $file->getParentFolder(); + $parentFolderIdentifier = $parentFolder->getCombinedIdentifier(); + } catch (Exception) { + // Orphaned sys_file row referring to a folder that no longer exists on disk + continue; + } + + $actions = []; + $editAction = $this->buildEditMetadataAction($file); + if ($editAction !== null) { + $actions['edit'] = $editAction; + } + $actions['show'] = $this->buildShowInListAction($parentFolderIdentifier, $searchDemand->getQuery()); + + $resultItem = (new ResultItem(self::class)) + ->setItemTitle($file->getName()) + ->setTypeLabel($this->languageService->sL('filelist.messages:live_search.file_provider.type_label')) + ->setIcon($this->iconFactory->getIconForResource($file, IconSize::SMALL)) + ->setThumbnailUrl($this->buildThumbnailUrl($file)) + ->setActions(...array_values($actions)) + ->setDefaultAction($actions['edit'] ?? $actions['show']) + ->setExtraData([ + 'breadcrumb' => $this->buildBreadcrumb($parentFolder), + ]); + + foreach ($this->buildProperties($file, $parentFolder) as $label => $value) { + $resultItem->addProperty($label, $value); + } + + $items[] = $resultItem; + } + + return $items; + } + + private function buildFileSearchDemand(SearchDemand $searchDemand): FileSearchDemand + { + return FileSearchDemand::createForSearchTerm($searchDemand->getQuery())->withRecursive(); + } + + private function buildEditMetadataAction(File $file): ?ResultItemAction + { + if (!$file->isIndexed() || !$file->checkActionPermission('editMeta')) { + return null; + } + $metaDataUid = $file->getMetaData()->offsetGet('uid'); + if (!$metaDataUid) { + return null; + } + $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ + 'edit' => ['sys_file_metadata' => [$metaDataUid => 'edit']], + 'module' => 'media_management', + ]); + + return (new ResultItemAction('edit')) + ->setLabel($this->languageService->sL('core.core:cm.editMetadata')) + ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL)) + ->setUrl($url); + } + + private function buildShowInListAction(string $parentFolderIdentifier, string $query): ResultItemAction + { + $url = (string)$this->uriBuilder->buildUriFromRoute('media_management', [ + 'id' => $parentFolderIdentifier, + 'searchTerm' => $query, + ]); + + return (new ResultItemAction('show')) + ->setLabel($this->languageService->sL('filelist.messages:live_search.file_provider.show_in_list')) + ->setIcon($this->iconFactory->getIcon('actions-list', IconSize::SMALL)) + ->setUrl($url); + } + + private function buildBreadcrumb(Folder $parentFolder): string + { + return $parentFolder->getStorage()->getName() . $parentFolder->getReadablePath(); + } + + /** + * @return array + */ + private function buildProperties(File $file, Folder $parentFolder): array + { + $items = [ + $this->languageService->sL('filelist.messages:live_search.file_provider.property.location') => $this->buildBreadcrumb($parentFolder), + $this->languageService->sL('filelist.messages:live_search.file_provider.property.size') => GeneralUtility::formatSize( + (int)$file->getSize(), + $this->languageService->sL('core.common:byteSizeUnits') + ), + ]; + if ($file->getModificationTime() > 0) { + $items[$this->languageService->sL('filelist.messages:live_search.file_provider.property.modified')] = BackendUtility::datetime($file->getModificationTime()); + } + return $items; + } + + private function buildThumbnailUrl(File $file): ?string + { + if (!$file->isImage() && !$file->isMediaFile()) { + return null; + } + try { + $processedFile = $file->process( + ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, + ['maxWidth' => 166, 'maxHeight' => 115] + ); + } catch (\Throwable) { + return null; + } + + return $processedFile->getPublicUrl() ?: null; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Type/LinkType.php b/Classes/Type/LinkType.php new file mode 100644 index 0000000..82607d7 --- /dev/null +++ b/Classes/Type/LinkType.php @@ -0,0 +1,47 @@ + File::class, + LinkType::FOLDER => Folder::class, + }; + } + + public function getLinkServiceType(): string + { + return match ($this) { + LinkType::FILE => LinkService::TYPE_FILE, + LinkType::FOLDER => LinkService::TYPE_FOLDER, + }; + } +} diff --git a/Classes/Type/Mode.php b/Classes/Type/Mode.php new file mode 100644 index 0000000..99adc81 --- /dev/null +++ b/Classes/Type/Mode.php @@ -0,0 +1,35 @@ + ['_SELECTOR_', 'icon', 'name', '_CONTROL_', 'record_type', 'size', 'rw', '_REF_'], + Mode::BROWSE => ['_SELECTOR_', 'icon', 'name', '_CONTROL_', 'record_type', 'size'], + }; + } +} diff --git a/Classes/Type/SortDirection.php b/Classes/Type/SortDirection.php new file mode 100644 index 0000000..c2f5456 --- /dev/null +++ b/Classes/Type/SortDirection.php @@ -0,0 +1,35 @@ + 'actions-sort-amount-up', + self::DESCENDING => 'actions-sort-amount-down', + }; + } +} diff --git a/Classes/Type/ViewMode.php b/Classes/Type/ViewMode.php new file mode 100644 index 0000000..79cdabc --- /dev/null +++ b/Classes/Type/ViewMode.php @@ -0,0 +1,27 @@ + [ + 'parent' => 'media', + 'access' => 'user', + 'path' => '/module/file/list', + 'iconIdentifier' => 'module-file', + 'labels' => 'filelist.module', + 'aliases' => ['file_FilelistList'], + 'routes' => [ + '_default' => [ + 'target' => FileListController::class . '::handleRequest', + ], + ], + 'moduleData' => [ + 'displayThumbs' => true, + 'clipBoard' => true, + 'sortField' => 'name', + 'sortDirection' => \TYPO3\CMS\Filelist\Type\SortDirection::ASCENDING->value, + 'viewMode' => null, + ], + ], +]; diff --git a/Configuration/Backend/Routes.php b/Configuration/Backend/Routes.php new file mode 100644 index 0000000..1775fc6 --- /dev/null +++ b/Configuration/Backend/Routes.php @@ -0,0 +1,32 @@ + [ + 'path' => '/file/editcontent', + 'target' => \TYPO3\CMS\Filelist\Controller\File\EditFileController::class . '::mainAction', + ], + + 'file_download' => [ + 'path' => '/file/download', + 'methods' => ['POST'], + 'target' => \TYPO3\CMS\Filelist\Controller\FileDownloadController::class . '::handleRequest', + ], + + 'file_update_online_media' => [ + 'path' => '/file/update-online-media', + 'methods' => ['POST'], + 'target' => \TYPO3\CMS\Filelist\Controller\FileUpdateOnlineMediaController::class . '::handleRequest', + ], +]; diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 0000000..4a593cc --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,11 @@ + [ + 'backend', + 'core', + ], + 'imports' => [ + '@typo3/filelist/' => 'EXT:filelist/Resources/Public/JavaScript/', + ], +]; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..c95c992 --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,18 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + TYPO3\CMS\Filelist\: + resource: '../Classes/*' + + TYPO3\CMS\Filelist\ElementBrowser\FileBrowser: + shared: false + + TYPO3\CMS\Filelist\ElementBrowser\FolderBrowser: + shared: false + + TYPO3\CMS\Filelist\Search\LiveSearch\FileProvider: + tags: + - { name: 'livesearch.provider', priority: 40 } diff --git a/Configuration/page.tsconfig b/Configuration/page.tsconfig new file mode 100644 index 0000000..1d4dcd6 --- /dev/null +++ b/Configuration/page.tsconfig @@ -0,0 +1,15 @@ +# Register link handlers +TCEMAIN.linkHandler { + file { + handler = TYPO3\CMS\Filelist\LinkHandler\FileLinkHandler + label = LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:file + displayAfter = page + scanAfter = page + } + folder { + handler = TYPO3\CMS\Filelist\LinkHandler\FolderLinkHandler + label = LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:folder + displayAfter = page,file + scanAfter = page,file + } +} diff --git a/Configuration/user.tsconfig b/Configuration/user.tsconfig new file mode 100644 index 0000000..c566aa8 --- /dev/null +++ b/Configuration/user.tsconfig @@ -0,0 +1,8 @@ +options.file_list { + enableDisplayThumbnails = selectable + enableClipBoard = selectable + thumbnail { + width = 64 + height = 64 + } +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..a548fd3 --- /dev/null +++ b/README.rst @@ -0,0 +1,13 @@ +============================ +TYPO3 extension ``filelist`` +============================ + +This TYPO3 backend module "Media" is used for managing files. + +It makes files in the defined storages available in the backend (upload, delete, +copy etc.). The default storage is fileadmin/. + +:Repository: https://github.com/typo3/typo3 +:Issues: https://forge.typo3.org/ +:Read online: https://docs.typo3.org/ +:Packagist: https://packagist.org/packages/typo3/cms-filelist diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf new file mode 100644 index 0000000..52039ab --- /dev/null +++ b/Resources/Private/Language/locallang.xlf @@ -0,0 +1,146 @@ + + + +
    + + + Search "%s" in %s + + + Files + + + File + + + Show in file list + + + Location + + + Size + + + Last modified + + + Reset search + + + No results found + + + This folder does not contain any files for "%s" + + + This folder is empty + + + Drag files here to upload them + + + Upload files + + + Upload files + + + files + + + File exists already + + + You want to rename the file "{0}" to "{1}", but the file "{1}" already exists. How do you want to proceed? + + + Cancel + + + Rename with unique name + + + Overwrite + + + Rename + + + Rename + This label is not used since TYPO3 v9. + + + New file name + + + New folder name + + + Replace file "%s" + + + You're about to replace the file "%s". + + + Select new file + + + Keep current filename "%s"? + + + Replace + + + Edit + + + Save + + + Save and Close + + + This filetype cannot be edited.<br />The file must have an extension like:<br /><br + /> <b>%s</b> + + + + New file or folder + + + Folder + + + Create folders + + + folders + + + Create file + + + Create new textfile + + + File name + + + Number of folders + + + Download + + + New Folder + + + New File + + + Translations + + + + diff --git a/Resources/Private/Language/locallang_mod_file_list.xlf b/Resources/Private/Language/locallang_mod_file_list.xlf new file mode 100644 index 0000000..02ba470 --- /dev/null +++ b/Resources/Private/Language/locallang_mod_file_list.xlf @@ -0,0 +1,107 @@ + + + +
    + + + Show clipboard + + + Paste in clipboard content + + + Paste into: Clipboard content is inserted into this folder + + + Edit Metadata + + + Edit specific Metadata + + + Delete marked items + + + Transfer to clipboard + + + Remove from clipboard + + + Are you sure you want to delete all marked items from this folder? + + + Name + + + Size + + + Type + + + RW + + + files + + + Temporary files + + + R + + + Recycler + + + W + + + Access denied. + + + You are trying to access a folder in a storage that is not browsable. + + + Missing folder permissions + + + You have no access to the folder "%s". + + + Base folder for local storage missing or not allowed + + + Verify that the base folder for the storage "%s" exists and is allowed to be accessed. + + + Folder not found. + + + The folder "%s" cannot be accessed. Trying to use parent folder(s). + + + Translate metadata + + + Create metadata of this file for %s + + + Edit metadata of this file for %s + + + Parameter Error + + + Target was not a directory! + + + Reload Metadata + + + Media %1$d - %2$d + + + + diff --git a/Resources/Private/Language/locallang_transfer_handler.xlf b/Resources/Private/Language/locallang_transfer_handler.xlf new file mode 100644 index 0000000..68b82af --- /dev/null +++ b/Resources/Private/Language/locallang_transfer_handler.xlf @@ -0,0 +1,29 @@ + + + +
    + + + Transfer Resource + + + Transfer "%s" to "%s"? + + + Transfer Resources + + + Transfer %d resources to "%s"? + + + Cancel + + + Copy + + + Move + + + + diff --git a/Resources/Private/Language/module.xlf b/Resources/Private/Language/module.xlf new file mode 100644 index 0000000..2adac0f --- /dev/null +++ b/Resources/Private/Language/module.xlf @@ -0,0 +1,17 @@ + + + +
    + + + Listing of media resources in registered storages + + + This is the media administration system. Through this module you can upload, copy, move and delete files on the system. + + + Media + + + + diff --git a/Resources/Private/Partials/Pagination.fluid.html b/Resources/Private/Partials/Pagination.fluid.html new file mode 100644 index 0000000..f798329 --- /dev/null +++ b/Resources/Private/Partials/Pagination.fluid.html @@ -0,0 +1,51 @@ + + + + + diff --git a/Resources/Private/Templates/ElementBrowser/Files.fluid.html b/Resources/Private/Templates/ElementBrowser/Files.fluid.html new file mode 100644 index 0000000..c0166a3 --- /dev/null +++ b/Resources/Private/Templates/ElementBrowser/Files.fluid.html @@ -0,0 +1,27 @@ + + + + + + + + + + + +

    + + {selectedFolder.storage.name}: {selectedFolder.identifier} +

    +
    + + {content} +
    + + diff --git a/Resources/Private/Templates/ElementBrowser/Folder.fluid.html b/Resources/Private/Templates/ElementBrowser/Folder.fluid.html new file mode 100644 index 0000000..5a76990 --- /dev/null +++ b/Resources/Private/Templates/ElementBrowser/Folder.fluid.html @@ -0,0 +1,42 @@ + + + + + + + + + + + +

    + + {selectedFolder.storage.name}: {selectedFolder.identifier} +

    +
    + + + +
    + +
    +
    + + {content} +
    + + diff --git a/Resources/Private/Templates/ElementBrowser/ResourceCreation.fluid.html b/Resources/Private/Templates/ElementBrowser/ResourceCreation.fluid.html new file mode 100644 index 0000000..6feeacd --- /dev/null +++ b/Resources/Private/Templates/ElementBrowser/ResourceCreation.fluid.html @@ -0,0 +1,34 @@ + + + + + + + + + + + +
    +
    + +

    + + {selectedFolder.storage.name}: {selectedFolder.identifier} +

    +
    + + {content} +
    + + diff --git a/Resources/Private/Templates/File/EditFile.fluid.html b/Resources/Private/Templates/File/EditFile.fluid.html new file mode 100644 index 0000000..3dd608a --- /dev/null +++ b/Resources/Private/Templates/File/EditFile.fluid.html @@ -0,0 +1,18 @@ + + + + + + +
    +

    {fileName}

    + {hookContent} + {form} +
    + +
    + + diff --git a/Resources/Private/Templates/File/List.fluid.html b/Resources/Private/Templates/File/List.fluid.html new file mode 100644 index 0000000..4a88fc1 --- /dev/null +++ b/Resources/Private/Templates/File/List.fluid.html @@ -0,0 +1,168 @@ + + + + + + + +
    + + + + +
    + + +
    +
    + +
    + + + +

    + + + + + + {headline} + + +

    +
    + + +
    + + +
    +
    +
    + + + + +
    +
    +
    +
    + +
    + + + +
    + {f:translate(key: 'LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:flashmessage.no_results')} +
    +

    + +

    + +
    + + +
    + {f:translate(key: 'LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:flashmessage.no_items')} +
    +

    + +

    +
    +
    +
    +
    +
    + +
    + +
    +
    + {listHtml -> f:format.raw() -> f:if(condition: totalItems)} +
    +
    + + + +
    + +
    +
    +
    + + diff --git a/Resources/Private/Templates/Filelist/List.fluid.html b/Resources/Private/Templates/Filelist/List.fluid.html new file mode 100644 index 0000000..9fbd1aa --- /dev/null +++ b/Resources/Private/Templates/Filelist/List.fluid.html @@ -0,0 +1,26 @@ + + +
    + + + {tableHeader -> f:format.raw()} + + + {tableBody -> f:format.raw()} + +
    +
    + + + diff --git a/Resources/Private/Templates/Filelist/Tiles.fluid.html b/Resources/Private/Templates/Filelist/Tiles.fluid.html new file mode 100644 index 0000000..55b4f41 --- /dev/null +++ b/Resources/Private/Templates/Filelist/Tiles.fluid.html @@ -0,0 +1,86 @@ + +
    +
    + + + +
    +
    + + + + +
    + + + +
    + +
    +
    +
    +
    +
    + diff --git a/Resources/Private/Templates/LinkHandler/File.fluid.html b/Resources/Private/Templates/LinkHandler/File.fluid.html new file mode 100644 index 0000000..b9b96a0 --- /dev/null +++ b/Resources/Private/Templates/LinkHandler/File.fluid.html @@ -0,0 +1,25 @@ + + + + + + + + + + + +

    + + {selectedFolder.storage.name}: {selectedFolder.identifier} +

    +
    + {content} +
    + + diff --git a/Resources/Private/Templates/LinkHandler/Folder.fluid.html b/Resources/Private/Templates/LinkHandler/Folder.fluid.html new file mode 100644 index 0000000..a28747d --- /dev/null +++ b/Resources/Private/Templates/LinkHandler/Folder.fluid.html @@ -0,0 +1,41 @@ + + + + + + + + + + + +

    + + + {selectedFolder.storage.name}: {selectedFolder.identifier} + +

    +
    + {content} +
    + + + + + + {linkText -> f:format.raw()} + + + + {linkText -> f:format.raw()} + + + + + diff --git a/Resources/Public/Icons/Extension.png b/Resources/Public/Icons/Extension.png new file mode 100644 index 0000000000000000000000000000000000000000..7eb8d57315d866701a7c619a1bc4b904be4747af GIT binary patch literal 350 zcmV-k0iphhP)Xd+05DD*W>Bo?C|UM`1Sw)|5;;rQUCw|#7RU!R2UiU!98jMQ5Xf# z3(7#0sHK%_uoQw=TbU(HohB|pvjIDMSCI5U;!Z3q1RIAW1VSF~Dl(Ao$JblTIjB@p z2J63N2WbyH+aoFC#R@6Uya-6Bu{6VVO_n~#?=QGw!crh(4sSu>3*HBXUwB6f5grvd zT}lB~ZA<9~R*y=Y!+50@AEDG~eM7mU6+4jb=}aND>1-gi=@gJ!bP7lfIt8SR&Ks01 wtsKgTMu3*l*ufZ534K^kNB%qHs8nv$3G*P2LY4{PDF6Tf07*qoM6N<$f;ondeE{e.preventDefault();const t=e.target,n=e.detail.checkboxes;if(!n.length)return;const o=[];n.forEach(i=>{if(i.checked){const s=i.closest(g.elementSelector),c=h.getResourceForElement(s);c.type==="file"&&c.uid&&o.unshift(c)}}),o.length&&(m.getIcon("spinner-circle",m.sizes.small,null,null,m.markupIdentifiers.inline).then(i=>{t.classList.add("disabled"),t.innerHTML=i}),l.handleNext(o),new a("message",i=>{if(!u.verifyOrigin(i.origin))throw"Denied message sent by "+i.origin;i.data.actionName==="typo3:foreignRelation:inserted"&&(o.length>0?l.handleNext(o):f.focusOpenerAndClose())}).bindTo(window))},new a(r.primary,e=>{e.preventDefault();const t=e.detail;t.originalAction=r.primary,t.action=r.select,document.dispatchEvent(new CustomEvent(r.select,{detail:t}))}).bindTo(document),new a(r.select,e=>{e.preventDefault();const t=e.detail,n=t.resources[0];n.type==="file"&&l.insertElement(n.name,n.uid,t.originalAction===r.primary),n.type==="folder"&&this.loadContent(n)}).bindTo(document),new a(r.show,e=>{e.preventDefault();const n=e.detail.resources[0];y.showItem("_"+n.type.toUpperCase(),n.identifier)}).bindTo(document),new a("multiRecordSelection:action:import",this.importSelection).bindTo(document)}static insertElement(e,t,n){return f.insertElement("sys_file",String(t),e,String(t),n)}static handleNext(e){if(e.length>0){const t=e.pop();l.insertElement(t.name,Number(t.uid))}}async loadContent(e){if(e.type!=="folder")return;const t=document.location.href+"&contentOnly=1&expandFolder="+e.identifier,o=await(await new w(t).get()).resolve(),i=document.querySelector(".element-browser-main-content .element-browser-body");i.innerHTML=o;const s=document.querySelector("typo3-backend-component-filestorage-browser-tree");if(s){const c=encodeURIComponent(e.identifier),d=s.nodes.find(p=>p.identifier===c);d&&(await s.expandNodeParents(d),s.selectNode(d,!1))}}}var b=new l;export{b as default}; diff --git a/Resources/Public/JavaScript/browse-folders.js b/Resources/Public/JavaScript/browse-folders.js new file mode 100644 index 0000000..150b615 --- /dev/null +++ b/Resources/Public/JavaScript/browse-folders.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import l from"@typo3/backend/element-browser.js";import r from"@typo3/core/event/regular-event.js";import{FileListActionSelector as a,FileListActionUtility as d,FileListActionEvent as o}from"@typo3/filelist/file-list-actions.js";import u from"@typo3/backend/info-window.js";class c{constructor(){this.importSelection=e=>{e.preventDefault();const t=e.detail.checkboxes;if(!t.length)return;const i=[];t.forEach(n=>{if(n.checked){const m=n.closest(a.elementSelector),s=d.getResourceForElement(m);s.type==="folder"&&s.identifier&&i.unshift(s)}}),i.length&&(i.forEach(function(n){c.insertElement(n.identifier)}),l.focusOpenerAndClose())},new r(o.primary,e=>{e.preventDefault();const t=e.detail;t.originalAction=o.primary,t.action=o.select,document.dispatchEvent(new CustomEvent(o.select,{detail:t}))}).bindTo(document),new r(o.select,e=>{e.preventDefault();const t=e.detail,i=t.resources[0];i.type==="folder"&&c.insertElement(i.identifier,t.originalAction===o.primary)}).bindTo(document),new r(o.show,e=>{e.preventDefault();const i=e.detail.resources[0];u.showItem("_"+i.type.toUpperCase(),i.identifier)}).bindTo(document),new r("multiRecordSelection:action:import",this.importSelection).bindTo(document)}static insertElement(e,t){return l.insertElement("",e,e,e,t)}}var f=new c;export{f as default}; diff --git a/Resources/Public/JavaScript/context-menu-actions.js b/Resources/Public/JavaScript/context-menu-actions.js new file mode 100644 index 0000000..39f3811 --- /dev/null +++ b/Resources/Public/JavaScript/context-menu-actions.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import{SeverityEnum as C}from"@typo3/backend/enum/severity.js";import c from"@typo3/core/ajax/ajax-request.js";import s from"@typo3/backend/notification.js";import U from"@typo3/backend/modal.js";import d from"@typo3/backend/hashing/md5.js";import{fileListOpenElementBrowser as w}from"@typo3/filelist/file-list.js";import{FileListActionUtility as f,FileListActionEvent as m}from"@typo3/filelist/file-list-actions.js";import a from"~labels/core.core";import u from"~labels/core.common";class l{static getReturnUrl(){return encodeURIComponent(top.list_frame.document.location.pathname+top.list_frame.document.location.search)}static triggerFileDownload(r,t,e=!1){const n=document.createElement("a");n.href=r,n.download=t,document.body.appendChild(n),n.click(),e&&URL.revokeObjectURL(r),document.body.removeChild(n),s.success(a.get("file_download.success"),"",2)}static renameFile(r,t,e){(async()=>{await import("@typo3/filelist/file-list-rename-handler.js");const n=f.createResourceFromContextDataset(e),o={event:null,trigger:null,action:m.rename,resources:[n],url:null,originalAction:null};document.dispatchEvent(new CustomEvent(m.rename,{detail:o}))})()}static replaceFile(r,t,e){(async()=>{await import("@typo3/filelist/file-list-replace-handler.js");const n=f.createResourceFromContextDataset(e),o={event:null,trigger:null,action:m.rename,resources:[n],url:null,originalAction:null};document.dispatchEvent(new CustomEvent(m.replace,{detail:o}))})()}static editFile(r,t,e){const n=e.actionUrl;top.TYPO3.Backend.ContentContainer.setUrl(n+"&target="+encodeURIComponent(t)+"&returnUrl="+l.getReturnUrl())}static editMetadata(r,t,e){const n=f.createResourceFromContextDataset(e);n.metaUid&&top.TYPO3.Backend.ContentContainer.setUrl(top.TYPO3.settings.FormEngine.moduleUrl+"&edit[sys_file_metadata]["+n.metaUid+"]=edit&module="+encodeURIComponent(top.TYPO3.ModuleMenu.App.getCurrentModule())+"&returnUrl="+l.getReturnUrl())}static openInfoPopUp(r,t){r==="sys_file_storage"?top.TYPO3.InfoWindow.showItem(r,t):top.TYPO3.InfoWindow.showItem("_FILE",t)}static createFolder(r,t,e){top.TYPO3.Backend.ContentContainer.get().document.dispatchEvent(new CustomEvent(w,{detail:{actionUrl:e.actionUrl,identifier:e.identifier,mode:e.mode}}))}static createFile(r,t,e){top.TYPO3.Backend.ContentContainer.get().document.dispatchEvent(new CustomEvent(w,{detail:{actionUrl:e.actionUrl,identifier:e.identifier,mode:e.mode}}))}static downloadFile(r,t,e){l.triggerFileDownload(e.url,e.name)}static downloadFolder(r,t,e){s.info(a.get("file_download.prepare"),"",2);const n=e.actionUrl;new c(n).post({items:[t]}).then(async o=>{let i=o.response.headers.get("Content-Disposition");if(!i){const g=await o.resolve();g.success===!1&&g.status==="noFiles"?s.warning(a.get("file_download.noFiles"),a.get("file_download.noFiles.message"),10):s.error(a.get("file_download.error"));return}i=i.substring(i.indexOf(" filename=")+10);const p=await o.raw().arrayBuffer(),b=new Blob([p],{type:o.raw().headers.get("Content-Type")});l.triggerFileDownload(URL.createObjectURL(b),i,!0)}).catch(()=>{s.error(a.get("file_download.error"))})}static createFilemount(r,t){t.split(":").length===2&&top.TYPO3.Backend.ContentContainer.setUrl(top.TYPO3.settings.FormEngine.moduleUrl+"&edit[sys_filemounts][0]=new&defVals[sys_filemounts][identifier]="+encodeURIComponent(t)+"&returnUrl="+l.getReturnUrl())}static deleteFile(r,t,e){const n=()=>{top.TYPO3.Backend.ContentContainer.setUrl(top.TYPO3.settings.FileCommit.moduleUrl+"&data[delete][0][data]="+encodeURIComponent(t)+"&data[delete][0][redirect]="+l.getReturnUrl())};if(!e.title){n();return}const o=U.confirm(e.title,e.message,C.warning,[{text:e.buttonCloseText||u.get("cancel"),active:!0,btnClass:"btn-default",name:"cancel"},{text:e.buttonOkText||u.get("delete")||"Delete",btnClass:"btn-warning",name:"delete"}]);o.addEventListener("button.clicked",i=>{i.target.name==="delete"&&n(),o.hideModal()})}static copyFile(r,t){const e=d.hash(t),n=TYPO3.settings.ajaxUrls.contextmenu_clipboard,o={CB:{el:{["_FILE%7C"+e]:t},setCopyMode:"1"}};new c(n).withQueryArguments(o).get().finally(()=>{top.TYPO3.Backend.ContentContainer.refresh()})}static copyReleaseFile(r,t){const e=d.hash(t),n=TYPO3.settings.ajaxUrls.contextmenu_clipboard,o={CB:{el:{["_FILE%7C"+e]:"0"},setCopyMode:"1"}};new c(n).withQueryArguments(o).get().finally(()=>{top.TYPO3.Backend.ContentContainer.refresh()})}static cutFile(r,t){const e=d.hash(t),n=TYPO3.settings.ajaxUrls.contextmenu_clipboard,o={CB:{el:{["_FILE%7C"+e]:t}}};new c(n).withQueryArguments(o).get().finally(()=>{top.TYPO3.Backend.ContentContainer.refresh()})}static cutReleaseFile(r,t){const e=d.hash(t),n=TYPO3.settings.ajaxUrls.contextmenu_clipboard,o={CB:{el:{["_FILE%7C"+e]:"0"}}};new c(n).withQueryArguments(o).get().finally(()=>{top.TYPO3.Backend.ContentContainer.refresh()})}static pasteFileInto(r,t,e){const n=()=>{top.TYPO3.Backend.ContentContainer.setUrl(top.TYPO3.settings.FileCommit.moduleUrl+"&CB[paste]=FILE|"+encodeURIComponent(t)+"&CB[pad]=normal&redirect="+l.getReturnUrl())};if(!e.title){n();return}const o=U.confirm(e.title,e.message,C.warning,[{text:e.buttonCloseText||u.get("cancel"),active:!0,btnClass:"btn-default",name:"cancel"},{text:e.buttonOkText||u.get("ok"),btnClass:"btn-warning",name:"ok"}]);o.addEventListener("button.clicked",i=>{i.target.name==="ok"&&n(),o.hideModal()})}static updateOnlineMedia(r,t,e){if(!e.actionUrl||!e.filecontextUid||e.filecontextType!=="file")return;const n={resource:{type:e.filecontextType,uid:e.filecontextUid}};new c(e.actionUrl).post(n).then(()=>{s.success(a.get("online_media.update.success"))}).catch(()=>{s.error(a.get("online_media.update.error"))}).finally(()=>{window.location.reload()})}}export{l as default}; diff --git a/Resources/Public/JavaScript/file-delete.js b/Resources/Public/JavaScript/file-delete.js new file mode 100644 index 0000000..4c10117 --- /dev/null +++ b/Resources/Public/JavaScript/file-delete.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import{SeverityEnum as c}from"@typo3/backend/enum/severity.js";import m from"@typo3/core/event/regular-event.js";import f from"@typo3/core/document-service.js";import p from"@typo3/backend/modal.js";import o from"~labels/backend.alt_doc";class u{constructor(){f.ready().then(()=>{new m("click",(i,e)=>{i.preventDefault();let t=e.dataset.redirectUrl;t=encodeURIComponent(t||top.list_frame.document.location.pathname+top.list_frame.document.location.search);const d=e.dataset.filelistDeleteIdentifier,r=e.dataset.filelistDeleteType,n=e.dataset.filelistDeleteUrl+"&data[delete][0][data]="+encodeURIComponent(d)+"&data[delete][0][redirect]="+t;if(e.dataset.filelistDeleteCheck){const l=p.confirm(e.dataset.title,e.dataset.content,c.warning,[{text:o.get("buttons.confirm.delete_file.no"),active:!0,btnClass:"btn-default",name:"no"},{text:r==="delete_folder"?o.get("buttons.confirm.delete_folder.yes"):o.get("buttons.confirm.delete_file.yes"),btnClass:"btn-warning",name:"yes"}]);l.addEventListener("button.clicked",s=>{const a=s.target.name;a==="no"?l.hideModal():a==="yes"&&(l.hideModal(),top.list_frame.location.href=n)})}else top.list_frame.location.href=n}).delegateTo(document,'[data-filelist-delete="true"]')})}}var b=new u;export{b as default}; diff --git a/Resources/Public/JavaScript/file-list-actions.js b/Resources/Public/JavaScript/file-list-actions.js new file mode 100644 index 0000000..0c31c86 --- /dev/null +++ b/Resources/Public/JavaScript/file-list-actions.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import r from"@typo3/core/event/regular-event.js";var a;(function(t){t.primary="typo3:filelist:resource:action:primary",t.primaryContextmenu="typo3:filelist:resource:action:primaryContextmenu",t.show="typo3:filelist:resource:action:show",t.rename="typo3:filelist:resource:action:rename",t.replace="typo3:filelist:resource:action:replace",t.select="typo3:filelist:resource:action:select",t.download="typo3:filelist:resource:action:download",t.updateOnlineMedia="typo3:filelist:resource:action:updateOnlineMedia"})(a||(a={}));var n;(function(t){t.elementSelector="[data-filelist-element]",t.actionSelector="[data-filelist-action]"})(n||(n={}));class o{static createResourceFromContextDataset(e){return{type:e.filecontextType,identifier:e.filecontextIdentifier,name:e.filecontextName,hasPreview:!1,uid:e.filecontextUid?parseInt(e.filecontextUid,10):null,metaUid:e.filecontextMetaUid?parseInt(e.filecontextMetaUid,10):null,url:e.filecontextUid?e.url:null,createdAt:e.filecontextCreatedAt?parseInt(e.filecontextCreatedAt,10):null,size:e.filecontextSize?parseInt(e.filecontextSize,10):null}}static getResourceForElement(e){return{type:e.dataset.filelistType,identifier:e.dataset.filelistIdentifier,name:e.dataset.filelistName,hasPreview:"filelistPreview"in e.dataset&&e.dataset.filelistPreview.trim()==="true",uid:e.dataset.filelistUid?parseInt(e.dataset.filelistUid,10):null,metaUid:e.dataset.filelistMetaUid?parseInt(e.dataset.filelistMetaUid,10):null,url:e.dataset.filelistUrl?e.dataset.filelistUrl:null,createdAt:e.dataset.filelistCreatedAt?parseInt(e.dataset.filelistCreatedAt,10):null,size:e.dataset.filelistSize?parseInt(e.dataset.filelistSize,10):null}}}class d{constructor(){new r("contextmenu",(e,l)=>{e.preventDefault(),e.stopImmediatePropagation();const i=this.getActionDetail(e,l);switch(i.action){case"primary":document.dispatchEvent(new CustomEvent(a.primaryContextmenu,{detail:i}));break;default:break}}).delegateTo(document,n.actionSelector),new r("click",(e,l)=>{e.preventDefault();const i=this.getActionDetail(e,l);switch(i.action){case"primary":document.dispatchEvent(new CustomEvent(a.primary,{detail:i}));break;case"show":document.dispatchEvent(new CustomEvent(a.show,{detail:i}));break;case"select":document.dispatchEvent(new CustomEvent(a.select,{detail:i}));break;case"rename":document.dispatchEvent(new CustomEvent(a.rename,{detail:i}));break;case"replace":document.dispatchEvent(new CustomEvent(a.replace,{detail:i}));break;case"download":document.dispatchEvent(new CustomEvent(a.download,{detail:i}));break;case"updateOnlineMedia":document.dispatchEvent(new CustomEvent(a.updateOnlineMedia,{detail:i}));break;default:break}}).delegateTo(document,n.actionSelector)}getActionDetail(e,l){const i=l.dataset.filelistAction,s=l.closest(n.elementSelector),c=o.getResourceForElement(s);return{event:e,trigger:l,action:i,resources:[c],url:l.dataset.filelistActionUrl??null,originalAction:null}}}var u=new d;export{a as FileListActionEvent,n as FileListActionSelector,o as FileListActionUtility,u as default}; diff --git a/Resources/Public/JavaScript/file-list-dragdrop.js b/Resources/Public/JavaScript/file-list-dragdrop.js new file mode 100644 index 0000000..7a165de --- /dev/null +++ b/Resources/Public/JavaScript/file-list-dragdrop.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import i from"@typo3/core/event/regular-event.js";import{MultiRecordSelectionSelectors as v}from"@typo3/backend/multi-record-selection.js";import{FileListActionSelector as l,FileListActionUtility as n}from"@typo3/filelist/file-list-actions.js";import{DataTransferTypes as f}from"@typo3/backend/enum/data-transfer-types.js";var c;(function(m){m.transfer="typo3:filelist:resource:dragdrop:transfer"})(c||(c={}));class w{constructor(){this.previewSize=32;const t=l.elementSelector+'[draggable="true"]';new i("dragstart",(e,r)=>{const s=[];let d="",u="";const g=document.querySelectorAll(v.checkboxSelector+":checked");if(g.length)g.forEach(a=>{if(a.checked){const o=a.closest(l.elementSelector);o.dataset.filelistDragdropTransferItem="true";const h=n.getResourceForElement(o);s.push(h),u=o.dataset.filelistName,d=o.dataset.filelistIcon}});else{const a=r.closest(l.elementSelector);a.dataset.filelistDragdropTransferItem="true";const o=n.getResourceForElement(a);s.push(o),u=a.dataset.filelistName,d=a.dataset.filelistIcon}e.dataTransfer.effectAllowed="move",e.dataTransfer.setData(f.falResources,JSON.stringify(s));const p={tooltipIconIdentifier:s.length>1?"apps-clipboard-images":d,tooltipLabel:s.length>1?this.getPreviewLabel(s):u,thumbnails:this.getPreviewItems(s)};e.dataTransfer.setData(f.dragTooltip,JSON.stringify(p))}).delegateTo(document,t),new i("dragover",(e,r)=>{const s=n.getResourceForElement(r);this.isDropAllowedOnResoruce(s)&&(e.dataTransfer.dropEffect="move",e.preventDefault(),r.classList.add("success"))},{capture:!0}).delegateTo(document,t),new i("drop",(e,r)=>{const s={action:"transfer",resources:JSON.parse(e.dataTransfer.getData(f.falResources)??"{}"),target:n.getResourceForElement(r)};top.document.dispatchEvent(new CustomEvent(c.transfer,{detail:s}))},{capture:!0,passive:!0}).delegateTo(document,t),new i("dragend",()=>{this.reset()},{capture:!0,passive:!0}).delegateTo(document,t),new i("dragleave",(e,r)=>{r.classList.remove("success")},{capture:!0,passive:!0}).delegateTo(document,t)}getPreviewItems(t){return t.filter(e=>e.hasPreview).map(e=>{const r=new URL(top.TYPO3.settings.Resource.thumbnailUrl,window.origin);return r.searchParams.set("identifier",e.uid.toString(10)),{src:r.toString(),width:this.previewSize,height:this.previewSize}})}getPreviewLabel(t){const e=t.filter(s=>s.hasPreview),r=t.length-e.length;return r>0?(e.length>0?"+":"")+r.toString():""}reset(){document.querySelectorAll(l.elementSelector).forEach(t=>{delete t.dataset.filelistDragdropTransferItem,t.classList.remove("success")})}isDropAllowedOnResoruce(t){return!("filelistDragdropTransferItem"in document.querySelector(l.elementSelector+'[data-filelist-identifier="'+t.identifier+'"]').dataset)&&t.type==="folder"}}var S=new w;export{c as FileListDragDropEvent,S as default}; diff --git a/Resources/Public/JavaScript/file-list-rename-handler.js b/Resources/Public/JavaScript/file-list-rename-handler.js new file mode 100644 index 0000000..9991890 --- /dev/null +++ b/Resources/Public/JavaScript/file-list-rename-handler.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import g from"@typo3/core/event/regular-event.js";import{html as h}from"lit";import{FileListActionEvent as w}from"@typo3/filelist/file-list-actions.js";import l from"@typo3/backend/modal.js";import y from"@typo3/core/ajax/ajax-request.js";import d from"@typo3/backend/notification.js";import s from"@typo3/backend/viewport.js";import n from"~labels/core.core";class C{constructor(){new g(w.rename,o=>{const a=o.detail.resources[0],c=l.advanced({title:n.get("file_rename.title"),type:l.types.default,size:l.sizes.small,content:this.composeEditForm(a),buttons:[{text:n.get("file_rename.button.cancel"),btnClass:"btn-default",name:"cancel",trigger:()=>{c.hideModal()}},{text:n.get("file_rename.button.rename"),btnClass:"btn-primary",name:"rename",trigger:()=>{c.querySelector("form")?.requestSubmit()}}],callback:function(i){const f=i.querySelector("form");f.addEventListener("submit",t=>{t.preventDefault();const p=new FormData(t.target),u=Object.fromEntries(p).name.toString();a.name!==u&&new y(TYPO3.settings.ajaxUrls.resource_rename).post({identifier:a.identifier,resourceName:u}).then(async b=>{const r=await b.resolve();if(r.status.length>0&&r.status.forEach(e=>{r.success?d.success(e.title,e.message):d.error(e.title,e.message)}),r.resource?.type==="folder"){const e=s.ContentContainer.getUrl();new URL(e,window.location.origin).searchParams.get("id")===r.origin.identifier?s.ContentContainer.setUrl(e+"&id="+r.resource.identifier):s.ContentContainer.refresh()}else s.ContentContainer.refresh();top.document.dispatchEvent(new CustomEvent("typo3:filestoragetree:refresh")),i.hideModal()})}),i.addEventListener("typo3-modal-shown",()=>{const t=f.querySelector("input");t!==null&&(t.focus(),t.setSelectionRange(0,a.name.lastIndexOf(".")))})}})}).bindTo(document)}composeEditForm(o){const m=o?.type==="folder"?n.get("folder_rename.label"):n.get("file_rename.label");return h`
    `}}var v=new C;export{v as default}; diff --git a/Resources/Public/JavaScript/file-list-replace-handler.js b/Resources/Public/JavaScript/file-list-replace-handler.js new file mode 100644 index 0000000..07a5008 --- /dev/null +++ b/Resources/Public/JavaScript/file-list-replace-handler.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import{html as l,nothing as h}from"lit";import{until as v}from"lit/directives/until.js";import p from"@typo3/core/event/regular-event.js";import{FileListActionEvent as y}from"@typo3/filelist/file-list-actions.js";import n from"@typo3/backend/modal.js";import f from"@typo3/core/ajax/ajax-request.js";import"@typo3/core/ajax/ajax-response.js";import u from"@typo3/backend/notification.js";import k from"@typo3/backend/viewport.js";import{FormatUtility as w}from"@typo3/backend/utility/format-utility.js";import{ThumbnailSize as _}from"@typo3/backend/element/thumbnail-element.js";import{topLevelModuleImport as $}from"@typo3/backend/utility/top-level-module-import.js";import a from"~labels/filelist.messages";import F from"~labels/core.common";import L from"~labels/core.core";import b from"~labels/filelist.mod_file_list";class q{constructor(){new p(y.replace,e=>{const t=e.detail.resources[0],m=n.advanced({title:a.get("file_replace.title",[t.name]),type:n.types.default,size:n.sizes.small,content:l`${v(this.loadEditor(t.identifier),l``)}`,buttons:[{text:F.get("cancel"),btnClass:"btn-default",name:"cancel",trigger:()=>{m.hideModal()}},{text:a.get("file_replace.button.replace"),btnClass:"btn-primary",name:"rename",trigger:()=>{m.querySelector("form")?.requestSubmit()}}],callback:function(o){new p("submit",c=>{c.preventDefault();const d=new FormData(c.target);d.set("uid",t.uid.toString()),new f(TYPO3.settings.ajaxUrls.resource_replace).post(d).then(async g=>{const s=await g.resolve();s.status.length>0&&s.status.forEach(r=>{s.success?u.success(r.title,r.message):u.error(r.title,r.message)}),k.ContentContainer.refresh(),o.hideModal()})}).delegateTo(o,"form")}})}).bindTo(document)}async loadEditor(e){const t=await(await new f(TYPO3.settings.ajaxUrls.resource_gather).withQueryArguments({identifier:e}).get()).resolve();return await $("@typo3/backend/element/datetime-element.js"),this.composeEditForm(t)}composeEditForm(e){const i=new URL(top.TYPO3.settings.Resource.thumbnailUrl,window.origin);return i.searchParams.set("identifier",e.uid.toString(10)),l`
    ${a.get("file_replace.intro",[e.name])}
    ${e.hasPreview?l`
    `:h}
    ${b.get("c_name")}
    ${e.name}
    ${b.get("c_size")}
    ${w.fileSizeAsString(e.size)}
    ${L.get("labels.crdate")}
    `}}var x=new q;export{x as default}; diff --git a/Resources/Public/JavaScript/file-list-transfer-handler.js b/Resources/Public/JavaScript/file-list-transfer-handler.js new file mode 100644 index 0000000..faebb1c --- /dev/null +++ b/Resources/Public/JavaScript/file-list-transfer-handler.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import{SeverityEnum as g}from"@typo3/backend/enum/severity.js";import d from"@typo3/backend/modal.js";import p from"@typo3/core/ajax/ajax-request.js";import u from"@typo3/core/event/regular-event.js";import h from"@typo3/backend/notification.js";import l from"@typo3/backend/viewport.js";import r from"~labels/filelist.transfer_handler";import{FileListDragDropEvent as v}from"@typo3/filelist/file-list-dragdrop.js";var c;(function(m){m.move="move",m.copy="copy"})(c||(c={}));class y{constructor(){new u(v.transfer,n=>{const e=n.detail,a=e.target,o=e.resources;let i,t;if(e.resources.length===1){const f=e.resources[0];i=r.get("message.transfer_resource.title"),t=r.get("message.transfer_resource.text",[f.name,a.name])}else i=r.get("message.transfer_resources.title"),t=r.get("message.transfer_resources.text",[o.length,a.name]);const s=d.confirm(i,t,g.notice,[{text:r.get("message.button.cancel"),active:!0,btnClass:"btn-default",name:"cancel",trigger:()=>{s.hideModal()}},{text:r.get("message.button.copy"),btnClass:"btn-primary",name:"copy",trigger:()=>{this.transfer(c.copy,o,a),s.hideModal()}},{text:r.get("message.button.move"),btnClass:"btn-primary",name:"move",trigger:()=>{this.transfer(c.move,o,a),s.hideModal()}}])}).bindTo(top.document)}transfer(n,e,a){const o=[];e.forEach(t=>{const s={data:t.identifier,target:a.identifier};o.push(s)});const i={data:{[n]:o}};new p(top.TYPO3.settings.ajaxUrls.file_process).post(i).then(async t=>{const s=await t.resolve();this.handleMessages(s.messages??[]),l.ContentContainer.refresh(),top.document.dispatchEvent(new CustomEvent("typo3:filestoragetree:refresh"))}).catch(async t=>{const s=await t.resolve();this.handleMessages(s.messages??[]),l.ContentContainer.refresh(),top.document.dispatchEvent(new CustomEvent("typo3:filestoragetree:refresh"))})}handleMessages(n){n.forEach(e=>{h.showMessage(e.title||"",e.message||"",e.severity)})}}var b=new y;export{b as default}; diff --git a/Resources/Public/JavaScript/file-list.js b/Resources/Public/JavaScript/file-list.js new file mode 100644 index 0000000..9d2757a --- /dev/null +++ b/Resources/Public/JavaScript/file-list.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import C from"@typo3/core/document-service.js";import d from"@typo3/backend/notification.js";import E from"@typo3/backend/info-window.js";import{FileListActionSelector as M,FileListActionUtility as T,FileListActionEvent as p}from"@typo3/filelist/file-list-actions.js";import y from"@typo3/backend/icons.js";import v from"@typo3/core/ajax/ajax-request.js";import l from"@typo3/core/event/regular-event.js";import{ModuleStateStorage as U}from"@typo3/backend/storage/module-state-storage.js";import h from"@typo3/backend/modal.js";import{SeverityEnum as S}from"@typo3/backend/enum/severity.js";import R from"@typo3/backend/severity.js";import{MultiRecordSelectionSelectors as k}from"@typo3/backend/multi-record-selection.js";import D from"@typo3/backend/context-menu.js";import m from"~labels/core.core";import F from"~labels/core.common";import"@typo3/backend/element/progress-bar-element.js";var u;(function(g){g.fileListFormSelector='form[name="fileListForm"]',g.commandSelector='input[name="cmd"]',g.searchFieldSelector='input[name="searchTerm"]',g.pointerFieldSelector='input[name="pointer"]'})(u||(u={}));const w="typo3:filelist:openElementBrowser";class c{constructor(){this.downloadFilesAndFolders=t=>{t.preventDefault();const e=t.target,o=t.detail,i=o.configuration,r=[];o.checkboxes.forEach(a=>{if(a.checked){const s=a.closest(M.elementSelector),f=T.getResourceForElement(s);r.unshift(f)}}),r.length?this.triggerDownload(r,i.downloadUrl,e):d.warning(m.get("file_download.invalidSelection"))},this.registerPaginationEvents=()=>{document.querySelectorAll(".t3js-filelist-paging").forEach(t=>{t.addEventListener("keyup",e=>{e.preventDefault();let o=Number(t.value);const i=Number(t.min),r=Number(t.max);if(i&&or&&(o=r),t.value=o.toString(10),e.key==="Enter"&&o!==Number(t.dataset.currentpage)){const a=t.closest('form[name="fileListForm"]'),s=new URL(a.action,window.origin);s.searchParams.set("currentPage",o.toString()),window.location.href=s.toString()}})})},new l(w,t=>{const e=new URL(t.detail.actionUrl,window.location.origin);e.searchParams.set("expandFolder",t.detail.identifier),e.searchParams.set("mode",t.detail.mode),h.advanced({type:h.types.iframe,content:e.toString(),size:h.sizes.large}).addEventListener("typo3-modal-hidden",()=>{top.list_frame.document.location.reload()})}).bindTo(document),new l(p.primary,t=>{const e=t.detail,o=e.resources[0],i=e.trigger.closest("[data-default-language-access]");if(o.type==="file"&&i!==null){const r=new URL(top.TYPO3.settings.FormEngine.moduleUrl,window.location.origin);o.metaUid>0?r.searchParams.set("edit[sys_file_metadata]["+o.metaUid+"]","edit"):(r.searchParams.set("edit[sys_file_metadata][0]","new"),r.searchParams.set("defVals[sys_file_metadata][file]",o.uid.toString(10))),r.searchParams.set("module",top.TYPO3.ModuleMenu.App.getCurrentModule()),r.searchParams.set("returnUrl",c.getReturnUrl("")),window.location.href=r.toString()}if(o.type==="folder"){const r=c.parseQueryParameters(document.location);r.id=o.identifier;const a=new URL(window.location.pathname,window.location.origin);for(const[s,f]of Object.entries(r))a.searchParams.set(s,f);window.location.href=a.toString()}}).bindTo(document),new l(p.primaryContextmenu,t=>{const e=t.detail,o=e.resources[0];D.show("sys_file",o.identifier,"","","",e.trigger,e.event)}).bindTo(document),new l(p.show,t=>{const o=t.detail.resources[0];c.openInfoPopup("_"+o.type.toUpperCase(),o.identifier)}).bindTo(document),new l(p.download,t=>{const e=t.detail,o=e.resources[0];this.triggerDownload([o],e.url,e.trigger)}).bindTo(document),new l(p.updateOnlineMedia,t=>{const e=t.detail,o=e.resources[0];this.updateOnlineMedia(o,e.url)}).bindTo(document),C.ready().then(()=>{c.processTriggers(),this.registerPaginationEvents(),new l("click",(t,e)=>{t.preventDefault(),document.dispatchEvent(new CustomEvent(w,{detail:{actionUrl:e.href,identifier:e.dataset.identifier,mode:e.dataset.mode}}))}).delegateTo(document,".t3js-element-browser")}),new l("multiRecordSelection:action:edit",this.editFileMetadata).bindTo(document),new l("multiRecordSelection:action:delete",this.deleteMultiple).bindTo(document),new l("multiRecordSelection:action:download",this.downloadFilesAndFolders).bindTo(document),new l("multiRecordSelection:action:copyMarked",t=>{c.submitClipboardFormWithCommand("copyMarked",t.target)}).bindTo(document),new l("multiRecordSelection:action:removeMarked",t=>{c.submitClipboardFormWithCommand("removeMarked",t.target)}).bindTo(document);const n=document.querySelector([u.fileListFormSelector,u.searchFieldSelector].join(" "))?.value!=="";new l("search",t=>{const e=t.target;e.value===""&&n&&e.closest(u.fileListFormSelector)?.submit()}).delegateTo(document,u.searchFieldSelector)}static submitClipboardFormWithCommand(n,t){const e=t.closest(u.fileListFormSelector);if(!e)return;const o=e.querySelector(u.commandSelector);if(o){if(o.value=n,n==="copyMarked"||n==="removeMarked"){const i=e.querySelector(u.pointerFieldSelector),r=c.parseQueryParameters(document.location).pointer;i&&r&&(i.value=r)}e.submit()}}static openInfoPopup(n,t){E.showItem(n,t)}static processTriggers(){const n=document.querySelector(".filelist-main");n!==null&&U.update("media",n.dataset.filelistCurrentIdentifier)}static parseQueryParameters(n){const t=new URLSearchParams(n.search);return Object.fromEntries(t.entries())}static getReturnUrl(n){if(n===""){const t=top.list_frame.document.forms.namedItem("fileListForm");t!==null?n=t.action:n=top.list_frame.document.location.pathname+top.list_frame.document.location.search}return n}deleteMultiple(n){n.preventDefault();const e=n.detail.configuration;h.advanced({title:e.title||"Delete",content:e.content||"Are you sure you want to delete those files and folders?",severity:S.warning,buttons:[{text:F.get("close"),active:!0,btnClass:"btn-default",trigger:(o,i)=>i.hideModal()},{text:e.ok||F.get("ok"),btnClass:"btn-"+R.getCssClass(S.warning),trigger:(o,i)=>{c.submitClipboardFormWithCommand("delete",n.target),i.hideModal()}}]})}editFileMetadata(n){n.preventDefault();const t=n.detail,e=t.configuration;if(!e||!e.idField||!e.table)return;const o=[];if(t.checkboxes.forEach(i=>{const r=i.closest(k.elementSelector);r!==null&&r.dataset[e.idField]&&o.push(r.dataset[e.idField])}),o.length){const i=new URL(top.TYPO3.settings.FormEngine.moduleUrl,window.location.origin);i.searchParams.set("edit["+e.table+"]["+o.join(",")+"]","edit"),i.searchParams.set("returnUrl",c.getReturnUrl(e.returnUrl||"")),(e.columnsOnly||[]).forEach((a,s)=>{i.searchParams.set("columnsOnly["+e.table+"]["+s+"]",a)}),window.location.href=i.toString()}else d.warning("The selected elements can not be edited.")}triggerDownload(n,t,e){if(n.length===1){const a=n.at(0);if(a.type==="file"){this.invokeDownload(a.url,a.name);return}}d.info(m.get("file_download.prepare"),"",2);const o=e?.innerHTML;e&&(e.setAttribute("disabled","disabled"),y.getIcon("spinner-circle",y.sizes.small).then(a=>{e.innerHTML=a}));const i=this.getProgress();i.start();const r=n.map(a=>a.identifier);new v(t).post({items:r}).then(async a=>{let s=a.response.headers.get("Content-Disposition");if(!s){const b=await a.resolve();b.success===!1&&b.status==="noFiles"?d.warning(m.get("file_download.noFiles"),m.get("file_download.noFiles.message"),10):d.error(m.get("file_download.error"));return}s=s.substring(s.indexOf(" filename=")+10);const f=await a.raw().arrayBuffer(),L=new Blob([f],{type:a.raw().headers.get("Content-Type")}),P=URL.createObjectURL(L);this.invokeDownload(P,s),d.success(m.get("file_download.success"),"",2)}).catch(()=>{d.error(m.get("file_download.error"))}).finally(()=>{i.done(),e&&(e.removeAttribute("disabled"),e.innerHTML=o)})}updateOnlineMedia(n,t){if(!t||!n.uid||n.type!=="file")return;const e=this.getProgress();e.start(),new v(t).post({resource:n}).then(()=>{d.success(m.get("online_media.update.success"))}).catch(()=>{d.error(m.get("online_media.update.error"))}).finally(()=>{e.done(),window.location.reload()})}invokeDownload(n,t){const e=document.createElement("a");e.href=n,e.download=t,document.body.appendChild(e),e.click(),URL.revokeObjectURL(n),document.body.removeChild(e)}getProgress(){return(!this.progressBar||!this.progressBar.isConnected)&&(this.progressBar=document.createElement("typo3-backend-progress-bar"),document.querySelector(".module-loading-indicator").appendChild(this.progressBar)),this.progressBar}}export{c as default,w as fileListOpenElementBrowser}; diff --git a/Resources/Public/JavaScript/linkbrowser-file-handler.js b/Resources/Public/JavaScript/linkbrowser-file-handler.js new file mode 100644 index 0000000..1c4be22 --- /dev/null +++ b/Resources/Public/JavaScript/linkbrowser-file-handler.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import m from"@typo3/backend/link-browser.js";import c from"@typo3/core/event/regular-event.js";import{FileListActionEvent as r}from"@typo3/filelist/file-list-actions.js";import d from"@typo3/core/ajax/ajax-request.js";import p from"@typo3/backend/info-window.js";import u from"@typo3/backend/notification.js";class w{constructor(){new c(r.primary,e=>{e.preventDefault();const n=e.detail;n.action=r.select,document.dispatchEvent(new CustomEvent(r.select,{detail:n}))}).bindTo(document),new c(r.select,e=>{e.preventDefault();const t=e.detail.resources[0];t.type==="file"&&this.insertLink(t),t.type==="folder"&&this.loadContent(t)}).bindTo(document),new c(r.show,e=>{e.preventDefault();const t=e.detail.resources[0];p.showItem("_"+t.type.toUpperCase(),t.identifier)}).bindTo(document)}insertLink(e){new d(TYPO3.settings.ajaxUrls.link_resource).post({identifier:e.identifier}).then(async t=>{const o=await t.resolve();o.status.forEach(i=>{u.showMessage(i.title,i.message,i.severity)}),o.success&&m.finalizeFunction(o.link)})}async loadContent(e){if(e.type!=="folder")return;const n=document.location.href+"&contentOnly=1&expandFolder="+e.identifier,o=await(await new d(n).get()).resolve(),i=document.querySelector(".element-browser-main-content .element-browser-body");i.innerHTML=o;const s=document.querySelector("typo3-backend-component-filestorage-browser-tree");if(s){const l=encodeURIComponent(e.identifier),a=s.nodes.find(f=>f.identifier===l);a&&(await s.expandNodeParents(a),s.selectNode(a,!1))}}}var y=new w;export{y as default}; diff --git a/Resources/Public/JavaScript/linkbrowser-folder-handler.js b/Resources/Public/JavaScript/linkbrowser-folder-handler.js new file mode 100644 index 0000000..efb07c4 --- /dev/null +++ b/Resources/Public/JavaScript/linkbrowser-folder-handler.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import a from"@typo3/backend/link-browser.js";import o from"@typo3/core/event/regular-event.js";import{FileListActionEvent as n}from"@typo3/filelist/file-list-actions.js";import l from"@typo3/backend/info-window.js";import c from"@typo3/core/ajax/ajax-request.js";import d from"@typo3/backend/notification.js";class u{constructor(){new o("click",(e,t)=>{e.preventDefault(),a.finalizeFunction(t.dataset.linkbrowserLink)}).delegateTo(document,"[data-linkbrowser-link]"),new o(n.primary,e=>{e.preventDefault();const t=e.detail;t.action=n.select,document.dispatchEvent(new CustomEvent(n.select,{detail:t}))}).bindTo(document),new o(n.select,e=>{e.preventDefault();const i=e.detail.resources[0];i.type==="folder"&&this.insertLink(i)}).bindTo(document),new o(n.show,e=>{e.preventDefault();const i=e.detail.resources[0];l.showItem("_"+i.type.toUpperCase(),i.identifier)}).bindTo(document)}insertLink(e){new c(TYPO3.settings.ajaxUrls.link_resource).post({identifier:e.identifier}).then(async i=>{const r=await i.resolve();r.status.forEach(s=>{d.showMessage(s.title,s.message,s.severity)}),r.success&&a.finalizeFunction(r.link)})}}var f=new u;export{f as default}; diff --git a/Resources/Public/JavaScript/rename-file.js b/Resources/Public/JavaScript/rename-file.js new file mode 100644 index 0000000..28d3db0 --- /dev/null +++ b/Resources/Public/JavaScript/rename-file.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import{SeverityEnum as g}from"@typo3/backend/enum/severity.js";import v from"@typo3/core/ajax/ajax-request.js";import b from"@typo3/backend/modal.js";import h from"@typo3/core/document-service.js";import n from"~labels/filelist.messages";class x{constructor(){h.ready().then(()=>{this.initialize()})}initialize(){const e=document.querySelector(".t3js-submit-file-rename");e!==null&&e.addEventListener("click",this.checkForDuplicate)}checkForDuplicate(e){e.preventDefault();const t=e.currentTarget.form,a=t.querySelector('input[name="data[rename][0][target]"]'),i=t.querySelector('input[name="data[rename][0][destination]"]'),r=t.querySelector('input[name="data[rename][0][conflictMode]"]'),l={fileName:a.value};i!==null&&(l.fileTarget=i.value),new v(TYPO3.settings.ajaxUrls.file_exists).withQueryArguments(l).get({cache:"no-cache"}).then(async u=>{const d=typeof(await u.resolve()).uid<"u",o=a.dataset.original,s=a.value;if(d&&o!==s){const f=n.get("file_rename.exists.description",{0:o,1:s}),c=b.confirm(n.get("file_rename.exists.title"),f,g.warning,[{active:!0,btnClass:"btn-default",name:"cancel",text:n.get("file_rename.actions.cancel")},{btnClass:"btn-primary",name:"rename",text:n.get("file_rename.actions.rename")},{btnClass:"btn-default",name:"replace",text:n.get("file_rename.actions.override")}]);c.addEventListener("button.clicked",p=>{const m=p.target;m.name!=="cancel"&&(r!==null&&(r.value=m.name),t.submit()),c.hideModal()})}else t.submit()})}}var y=new x;export{y as default}; diff --git a/Resources/Public/JavaScript/resource-creation.js b/Resources/Public/JavaScript/resource-creation.js new file mode 100644 index 0000000..534da9a --- /dev/null +++ b/Resources/Public/JavaScript/resource-creation.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import s from"@typo3/core/event/regular-event.js";import f from"@typo3/core/ajax/ajax-request.js";import{FileListActionEvent as n}from"@typo3/filelist/file-list-actions.js";import m from"@typo3/backend/info-window.js";class p{constructor(){new s(n.primary,e=>{e.preventDefault();const t=e.detail;t.action=n.select,document.dispatchEvent(new CustomEvent(n.select,{detail:t}))}).bindTo(document),new s(n.select,e=>{e.preventDefault();const o=e.detail.resources[0];o.type==="folder"&&this.loadContent(o)}).bindTo(document),new s(n.show,e=>{e.preventDefault();const o=e.detail.resources[0];m.showItem("_"+o.type.toUpperCase(),o.identifier)}).bindTo(document)}async loadContent(e){if(e.type!=="folder")return;const t=document.location.href+"&contentOnly=1&expandFolder="+e.identifier,c=await(await new f(t).get()).resolve(),d=document.querySelector(".element-browser-main-content .element-browser-body");d.innerHTML=c;const r=document.querySelector("typo3-backend-component-filestorage-browser-tree");if(r){const a=encodeURIComponent(e.identifier),i=r.nodes.find(l=>l.identifier===a);i&&(await r.expandNodeParents(i),r.selectNode(i,!1))}}}var u=new p;export{u as default}; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..74e9cd6 --- /dev/null +++ b/composer.json @@ -0,0 +1,58 @@ +{ + "name": "typo3/cms-filelist", + "type": "typo3-cms-framework", + "description": "TYPO3 CMS Filelist - TYPO3 backend module 'Media' used for managing files.", + "homepage": "https://typo3.community/", + "funding": [ + { + "type": "membership", + "url": "https://typo3.org/membership" + } + ], + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "support": { + "issues": "https://forge.typo3.org/issues/", + "forum": "https://talk.typo3.org/", + "source": "https://github.com/TYPO3/typo3/", + "docs": "https://docs.typo3.org/", + "rss": "https://news.typo3.com/rss/", + "chat": "https://typo3.community/meet/slack/", + "security": "https://typo3.org/security/" + }, + "config": { + "sort-packages": true + }, + "require": { + "typo3/cms-core": "15.0.*@dev" + }, + "conflict": { + "typo3/cms": "*" + }, + "extra": { + "branch-alias": { + "dev-main": "15.0.x-dev" + }, + "typo3/cms": { + "Package": { + "protected": true, + "partOfFactoryDefault": true, + "partOfMinimalUsableSystem": true + }, + "extension-key": "filelist" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Filelist\\": "Classes/" + } + } +}