TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user