TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:18 +02:00
commit 1da5e665e7
73 changed files with 8040 additions and 0 deletions
@@ -0,0 +1,522 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\ContextMenu\ItemProviders;
use TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\JsConfirmation;
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Filelist\ElementBrowser\CreateFileBrowser;
use TYPO3\CMS\Filelist\ElementBrowser\CreateFolderBrowser;
/**
* Provides click menu items for files and folders
*/
class FileProvider extends AbstractProvider
{
/**
* @var File|Folder|null
*/
protected $record;
/**
* @var array
*/
protected $itemsConfiguration = [
'edit' => [
'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();
}
}
@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Controller\File;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Form\FormResultFactory;
use TYPO3\CMS\Backend\Form\FormResultHandler;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Core\Http\ResponseFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
use TYPO3\CMS\Core\Resource\Exception\InvalidFileException;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Filelist\Event\ModifyEditFileFormDataEvent;
/**
* Edit text files via FormEngine. Reachable via FileList module "Edit content".
*
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
#[AsController]
class EditFileController
{
protected array $dataColumnTca = [
'label' => '',
'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'];
}
}
@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Controller;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Controller class to create a zip file for given items from a file or folder.
*
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
#[AsController]
class FileDownloadController
{
protected ResourceFactory $resourceFactory;
protected ResponseFactoryInterface $responseFactory;
protected StreamFactoryInterface $streamFactory;
protected Context $context;
public function __construct(
ResourceFactory $resourceFactory,
ResponseFactoryInterface $responseFactory,
StreamFactoryInterface $streamFactory,
Context $context
) {
$this->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'];
}
}
+762
View File
@@ -0,0 +1,762 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Controller;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Breadcrumb\BreadcrumbContext;
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Backend\Module\ModuleData;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior;
use TYPO3\CMS\Core\Resource\Exception;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Resource\Utility\ListUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Filelist\ElementBrowser\CreateFileBrowser;
use TYPO3\CMS\Filelist\ElementBrowser\CreateFolderBrowser;
use TYPO3\CMS\Filelist\FileList;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFileTypeMatcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFolderTypeMatcher;
use TYPO3\CMS\Filelist\Type\SortDirection;
use TYPO3\CMS\Filelist\Type\ViewMode;
/**
* Script Class for creating the list of files in the File > 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'];
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Controller;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
use TYPO3\CMS\Core\Resource\OnlineMedia\Service\PreviewService;
use TYPO3\CMS\Core\Resource\ResourceFactory;
/**
* Controller class to update an online media resource
*
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
#[AsController]
readonly class FileUpdateOnlineMediaController
{
public function __construct(
protected ResourceFactory $resourceFactory,
protected OnlineMediaHelperRegistry $onlineMediaHelperRegistry,
protected PreviewService $previewService,
protected ResponseFactoryInterface $responseFactory,
protected StreamFactoryInterface $streamFactory
) {}
public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$resource = $request->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'];
}
}
+180
View File
@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Dto;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceInterface;
/**
* @internal
*/
class ResourceCollection implements \Countable, \Iterator, \ArrayAccess
{
private int $position = 0;
/**
* @var ResourceInterface[]
*/
protected array $resources = [];
/**
* @param ResourceInterface[] $resources
*/
public function __construct(array $resources = [])
{
$this->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();
}
}
+291
View File
@@ -0,0 +1,291 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Dto;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\InaccessibleFolder;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Resource\Utility\ListUtility;
/**
* @internal
*/
class ResourceView
{
public ?string $moduleUri;
public ?string $editContentUri;
public ?string $editDataUri;
public ?string $replaceUri;
public bool $isDownloadable = true;
public bool $isSelectable = true;
public bool $isSelected = false;
public function __construct(
public readonly ResourceInterface $resource,
public readonly UserPermissions $userPermissions,
public readonly Icon $icon
) {}
public function getUid(): ?int
{
if ($this->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;
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Dto;
/**
* @internal
*/
readonly class UserPermissions
{
public function __construct(
public bool $editMetaData = false
) {}
}
@@ -0,0 +1,268 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\ElementBrowser;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\ElementBrowser\AbstractElementBrowser;
use TYPO3\CMS\Backend\ElementBrowser\ElementBrowserInterface;
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
use TYPO3\CMS\Backend\Tree\View\LinkParameterProviderInterface;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Filelist\FileList;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Type\SortDirection;
use TYPO3\CMS\Filelist\Type\ViewMode;
/**
* @internal
*/
abstract class AbstractResourceBrowser extends AbstractElementBrowser implements ElementBrowserInterface, LinkParameterProviderInterface
{
protected ?string $expandFolder = null;
protected int $currentPage = 1;
protected string $moduleStorageIdentifier = 'media_management';
protected ?FileList $filelist = null;
protected string $sortField = 'name';
protected ?SortDirection $sortDirection = null;
protected ?ViewMode $viewMode = null;
protected bool $displayThumbs = true;
protected ?Folder $selectedFolder = null;
protected ?Matcher $resourceDisplayMatcher = null;
protected ?Matcher $resourceSelectableMatcher = null;
/**
* Loads additional JavaScript
*/
protected function initialize(ServerRequestInterface $request): void
{
parent::initialize($request);
$this->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<int, array|bool> 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];
}
}
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\ElementBrowser;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\ResourceUtilityRenderer;
use TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFileTypeMatcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFolderTypeMatcher;
use TYPO3\CMS\Filelist\Type\Mode;
/**
* Browser to create new files. This is used with mode=create_file in the ElementBrowser.
*
* @internal
*/
class CreateFileBrowser extends AbstractResourceBrowser
{
public const IDENTIFIER = 'create_file';
protected string $identifier = self::IDENTIFIER;
protected function initialize(ServerRequestInterface $request): void
{
parent::initialize($request);
$this->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[] = '<div class="row justify-content-between mb-2">';
$markup[] = ' <div class="col-auto"></div>';
$markup[] = ' <div class="col-auto">';
$markup[] = ' ' . $this->getSortingModeButtons();
$markup[] = ' ' . $this->getViewModeButton();
$markup[] = ' </div>';
$markup[] = '</div>';
$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('<body ' . $this->getBodyTagParameters() . '>' . $content);
return $this->pageRenderer->render($this->getRequest());
}
}
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\ElementBrowser;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\ResourceUtilityRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFolderTypeMatcher;
use TYPO3\CMS\Filelist\Type\Mode;
/**
* Browser to create one or more folders. This is used with type=folder to select folders.
*
* @internal
*/
class CreateFolderBrowser extends AbstractResourceBrowser
{
public const IDENTIFIER = 'create_folder';
protected string $identifier = self::IDENTIFIER;
protected function initialize(ServerRequestInterface $request): void
{
parent::initialize($request);
$this->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[] = '<div class="row justify-content-between mb-2">';
$markup[] = ' <div class="col-auto"></div>';
$markup[] = ' <div class="col-auto">';
$markup[] = ' ' . $this->getSortingModeButtons();
$markup[] = ' ' . $this->getViewModeButton();
$markup[] = ' </div>';
$markup[] = '</div>';
$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('<body ' . $this->getBodyTagParameters() . '>' . $content);
return $this->pageRenderer->render($this->getRequest());
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\ElementBrowser;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\ElementBrowser\Event\IsFileSelectableEvent;
use TYPO3\CMS\Backend\View\RecordSearchBoxComponent;
use TYPO3\CMS\Backend\View\ResourceUtilityRenderer;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Filelist\Matcher\AndMatcher;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Matcher\MatcherInterface;
use TYPO3\CMS\Filelist\Matcher\ResourceFileExtensionMatcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFolderTypeMatcher;
use TYPO3\CMS\Filelist\Type\Mode;
/**
* Browser for files. This is used when adding a FAL inline image with the 'add image' button in FormEngine.
*
* @internal
*/
class FileBrowser extends AbstractResourceBrowser
{
public const IDENTIFIER = 'file';
protected string $identifier = self::IDENTIFIER;
protected ?string $searchWord = null;
protected ?FileExtensionFilter $fileExtensionFilter;
protected ?FileSearchDemand $searchDemand = null;
/**
* Loads additional JavaScript
*/
protected function initialize(ServerRequestInterface $request): void
{
parent::initialize($request);
$this->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[] = '<div class="mb-4">';
$markup[] = GeneralUtility::makeInstance(RecordSearchBoxComponent::class)
->setSearchWord($this->searchWord ?? '')
->render($this->getRequest(), $this->createUri());
$markup[] = '</div>';
// 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[] = '<div class="row justify-content-between mb-2">';
$markup[] = ' <div class="col-auto">';
$markup[] = ' <div class="hidden t3js-multi-record-selection-actions">';
$markup[] = ' <strong>' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selection')) . '</strong>';
$markup[] = ' <button type="button" class="btn btn-default btn-sm" data-multi-record-selection-action="import" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:importSelection')) . '">';
$markup[] = ' ' . $this->iconFactory->getIcon('actions-document-import-t3d', IconSize::SMALL);
$markup[] = ' ' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:importSelection'));
$markup[] = ' </button>';
$markup[] = ' </div>';
$markup[] = ' </div>';
$markup[] = ' <div class="col-auto">';
$markup[] = ' ' . $this->getSortingModeButtons();
$markup[] = ' ' . $this->getViewModeButton();
$markup[] = ' </div>';
$markup[] = '</div>';
$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('<body ' . $this->getBodyTagParameters() . '>' . $content);
return $this->pageRenderer->render($this->getRequest());
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\ElementBrowser;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\ResourceUtilityRenderer;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFolderTypeMatcher;
use TYPO3\CMS\Filelist\Type\Mode;
/**
* Browser for folders. This is used with type=folder to select folders.
*
* @internal
*/
class FolderBrowser extends AbstractResourceBrowser
{
public const IDENTIFIER = 'folder';
protected string $identifier = self::IDENTIFIER;
protected function initialize(ServerRequestInterface $request): void
{
parent::initialize($request);
$this->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[] = '<div class="row justify-content-between mb-2">';
$markup[] = ' <div class="col-auto">';
$markup[] = ' <div class="hidden t3js-multi-record-selection-actions">';
$markup[] = ' <strong>' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selection')) . '</strong>';
$markup[] = ' <button type="button" class="btn btn-default btn-sm" data-multi-record-selection-action="import" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:importSelection')) . '">';
$markup[] = ' ' . $this->iconFactory->getIcon('actions-document-import-t3d', IconSize::SMALL);
$markup[] = ' ' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:importSelection'));
$markup[] = ' </button>';
$markup[] = ' </div>';
$markup[] = ' </div>';
$markup[] = ' <div class="col-auto">';
$markup[] = ' ' . $this->getSortingModeButtons();
$markup[] = ' ' . $this->getViewModeButton();
$markup[] = ' </div>';
$markup[] = '</div>';
$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('<body ' . $this->getBodyTagParameters() . '>' . $content);
return $this->pageRenderer->render($this->getRequest());
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Event;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Filelist\FileList;
/**
* An event to modify the rendered row data for a file or folder in the File List.
*/
final class AfterFileListRowPreparedEvent
{
public function __construct(
private readonly ResourceInterface $resource,
private array $data,
private readonly FileList $fileList,
private array $attributes,
) {}
public function getResource(): ResourceInterface
{
return $this->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;
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Event;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\FileInterface;
/**
* Listeners to this event are be able to modify the form data,
* used to render the edit file form in the filelist module.
*/
final class ModifyEditFileFormDataEvent
{
public function __construct(
private array $formData,
private readonly FileInterface $file,
private readonly ServerRequestInterface $request
) {}
public function getFormData(): array
{
return $this->formData;
}
public function setFormData(array $formData): void
{
$this->formData = $formData;
}
public function getFile(): FileInterface
{
return $this->file;
}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
}
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Event;
use Psr\Http\Message\RequestInterface;
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Backend\Template\Components\ComponentGroup;
use TYPO3\CMS\Backend\Template\Components\ComponentInterface;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\ResourceInterface;
/**
* Event fired to modify icons rendered for the file listings
*/
final readonly class ProcessFileListActionsEvent
{
public function __construct(
private ComponentGroup $primary,
private ComponentGroup $secondary,
private ResourceInterface $fileOrFolder,
private RequestInterface $request,
) {}
public function getResource(): ResourceInterface
{
return $this->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;
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\EventListener;
use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\PageRenderer;
/**
* Adds locallang labels for file information used in a modal window (thus in "global" scope)
*/
final readonly class AfterBackendPageRenderEventListener
{
public function __construct(private PageRenderer $pageRenderer) {}
#[AsEventListener(event: AfterBackendPageRenderEvent::class)]
public function __invoke(): void
{
$this->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'];
}
}
+1773
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,368 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Filelist\LinkHandler;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Controller\AbstractLinkBrowserController;
use TYPO3\CMS\Backend\LinkHandler\LinkHandlerInterface;
use TYPO3\CMS\Backend\LinkHandler\LinkHandlerVariableProviderInterface;
use TYPO3\CMS\Backend\LinkHandler\LinkHandlerViewProviderInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Tree\View\LinkParameterProviderInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewInterface;
use TYPO3\CMS\Filelist\FileList;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Type\LinkType;
use TYPO3\CMS\Filelist\Type\SortDirection;
use TYPO3\CMS\Filelist\Type\ViewMode;
/**
* @internal
*/
abstract class AbstractResourceLinkHandler implements LinkHandlerInterface, LinkHandlerVariableProviderInterface, LinkHandlerViewProviderInterface, LinkParameterProviderInterface
{
protected ?string $expandFolder = null;
protected int $currentPage = 1;
protected string $moduleStorageIdentifier = 'media_management';
protected ?FileList $filelist = null;
protected string $sortField = 'name';
protected ?SortDirection $sortDirection = null;
protected ?ViewMode $viewMode = null;
protected bool $displayThumbs = true;
protected ?Folder $selectedFolder = null;
protected ?Matcher $resourceDisplayMatcher = null;
protected ?Matcher $resourceSelectableMatcher = null;
protected LinkType $type;
protected array $linkParts = [];
protected ViewInterface $view;
protected LanguageService $languageService;
protected AbstractLinkBrowserController $linkBrowser;
public function __construct(
protected readonly IconFactory $iconFactory,
protected readonly ResourceFactory $resourceFactory,
protected readonly PageRenderer $pageRenderer,
protected readonly UriBuilder $uriBuilder,
protected readonly TcaSchemaFactory $tcaSchemaFactory,
protected readonly LanguageServiceFactory $languageServiceFactory,
protected readonly ComponentFactory $componentFactory,
) {
$this->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'];
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Filelist\LinkHandler;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\View\RecordSearchBoxComponent;
use TYPO3\CMS\Backend\View\ResourceUtilityRenderer;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFileExtensionMatcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFileTypeMatcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFolderTypeMatcher;
use TYPO3\CMS\Filelist\Matcher\ResourceMatcher;
use TYPO3\CMS\Filelist\Type\LinkType;
use TYPO3\CMS\Filelist\Type\Mode;
/**
* @internal
*/
#[Autoconfigure(public: true, shared: false)]
class FileLinkHandler extends AbstractResourceLinkHandler
{
protected LinkType $type = LinkType::FILE;
public function initializeVariables(ServerRequestInterface $request): void
{
parent::initializeVariables($request);
$this->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[] = '<div class="mb-4">';
$markup[] = GeneralUtility::makeInstance(RecordSearchBoxComponent::class)
->setSearchWord($searchWord)
->render($request, $this->filelist->createModuleUri($this->getUrlParameters([])));
$markup[] = '</div>';
// Render the filelist header bar
$markup[] = '<div class="row justify-content-between mb-2">';
$markup[] = ' <div class="col-auto"></div>';
$markup[] = ' <div class="col-auto">';
$markup[] = ' ' . $this->getSortingModeButtons($request);
$markup[] = ' ' . $this->getViewModeButton($request);
$markup[] = ' </div>';
$markup[] = '</div>';
// 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');
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Filelist\LinkHandler;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\View\ResourceUtilityRenderer;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFolderTypeMatcher;
use TYPO3\CMS\Filelist\Matcher\ResourceMatcher;
use TYPO3\CMS\Filelist\Type\LinkType;
use TYPO3\CMS\Filelist\Type\Mode;
/**
* @internal
*/
#[Autoconfigure(public: true, shared: false)]
class FolderLinkHandler extends AbstractResourceLinkHandler
{
protected LinkType $type = LinkType::FOLDER;
public function initializeVariables(ServerRequestInterface $request): void
{
parent::initializeVariables($request);
$this->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[] = '<div class="row justify-content-between mb-2">';
$markup[] = ' <div class="col-auto"></div>';
$markup[] = ' <div class="col-auto">';
$markup[] = ' ' . $this->getSortingModeButtons($request);
$markup[] = ' ' . $this->getViewModeButton($request);
$markup[] = ' </div>';
$markup[] = '</div>';
$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');
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Matcher;
/**
* @internal
*/
final readonly class AndMatcher implements MatcherInterface
{
/**
* @var MatcherInterface[]
*/
private array $matchers;
public function __construct(MatcherInterface ...$matchers)
{
$this->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;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Matcher;
/**
* @internal
*/
class Matcher
{
/**
* @var MatcherInterface[]
*/
protected array $matchers = [];
public function addMatcher(MatcherInterface $matcher): self
{
$this->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;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Matcher;
/**
* @internal
*/
interface MatcherInterface
{
public function supports(mixed $item): bool;
public function match(mixed $item): bool;
}
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Matcher;
use TYPO3\CMS\Core\Resource\File;
/**
* @internal
*/
class ResourceFileExtensionMatcher implements MatcherInterface
{
/**
* @var string[]
*/
protected array $extensions = [];
/**
* @var string[]
*/
protected array $ignoredExtensions = [];
/**
* @param string[] $extensions
*/
public function setExtensions(array $extensions): self
{
$this->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;
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Matcher;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceInterface;
/**
* @internal
*/
class ResourceFileTypeMatcher implements MatcherInterface
{
public function supports(mixed $item): bool
{
return $item instanceof ResourceInterface;
}
public function match(mixed $item): bool
{
return $item instanceof File;
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Matcher;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceInterface;
/**
* @internal
*/
class ResourceFolderTypeMatcher implements MatcherInterface
{
public function supports(mixed $item): bool
{
return $item instanceof ResourceInterface;
}
public function match(mixed $item): bool
{
return $item instanceof Folder;
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Matcher;
use TYPO3\CMS\Core\Resource\ResourceInterface;
/**
* @internal
*/
class ResourceMatcher implements MatcherInterface
{
/**
* @var ResourceInterface[]
*/
protected array $resources = [];
/**
* @param ResourceInterface[] $resources
*/
public function setResources(array $resources): self
{
$this->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);
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Pagination;
use TYPO3\CMS\Core\Pagination\AbstractPaginator;
use TYPO3\CMS\Filelist\Dto\ResourceCollection;
/**
* @internal
*/
final class ResourceCollectionPaginator extends AbstractPaginator
{
private ResourceCollection $paginatedItems;
public function __construct(
private readonly ResourceCollection $items,
int $currentPageNumber = 1,
int $itemsPerPage = 10
) {
$this->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);
}
}
+211
View File
@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Search\LiveSearch;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Search\LiveSearch\ResultItem;
use TYPO3\CMS\Backend\Search\LiveSearch\ResultItemAction;
use TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand;
use TYPO3\CMS\Backend\Search\LiveSearch\SearchProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Resource\Exception;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
use TYPO3\CMS\Core\Resource\Search\FileSearchQuery;
use TYPO3\CMS\Core\Resource\Search\QueryRestrictions\FolderMountsRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Search provider to query files (sys_file + sys_file_metadata) for the
* backend live search. Results respect the user's file mounts and are
* deduplicated per file.
*
* @internal
*/
final class FileProvider implements SearchProviderInterface
{
private LanguageService $languageService;
public function __construct(
private readonly IconFactory $iconFactory,
private readonly UriBuilder $uriBuilder,
private readonly ResourceFactory $resourceFactory,
LanguageServiceFactory $languageServiceFactory,
) {
$this->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<string, string>
*/
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'];
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Type;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
/**
* @internal
*/
enum LinkType: string
{
case FILE = 'file';
case FOLDER = 'folder';
public function getResourceType(): string
{
return match ($this) {
LinkType::FILE => File::class,
LinkType::FOLDER => Folder::class,
};
}
public function getLinkServiceType(): string
{
return match ($this) {
LinkType::FILE => LinkService::TYPE_FILE,
LinkType::FOLDER => LinkService::TYPE_FOLDER,
};
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Type;
/**
* @internal
*/
enum Mode: string
{
case MANAGE = 'manage';
case BROWSE = 'browse';
public function fieldArray(): array
{
return match ($this) {
Mode::MANAGE => ['_SELECTOR_', 'icon', 'name', '_CONTROL_', 'record_type', 'size', 'rw', '_REF_'],
Mode::BROWSE => ['_SELECTOR_', 'icon', 'name', '_CONTROL_', 'record_type', 'size'],
};
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Type;
/**
* @internal
*/
enum SortDirection: string
{
case ASCENDING = 'asc';
case DESCENDING = 'desc';
public function getIconIdentifier(): string
{
return match ($this) {
self::ASCENDING => 'actions-sort-amount-up',
self::DESCENDING => 'actions-sort-amount-down',
};
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Filelist\Type;
/**
* @internal
*/
enum ViewMode: string
{
case LIST = 'list';
case TILES = 'tiles';
}