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