Files
cms-filelist/Classes/FileList.php
T

1774 lines
74 KiB
PHP

<?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;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonSize;
use TYPO3\CMS\Backend\Template\Components\Buttons\GenericButton;
use TYPO3\CMS\Backend\Template\Components\Buttons\LinkButton;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\Components\ComponentGroup;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\JsConfirmation;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Pagination\SimplePagination;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\FolderInterface;
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Schema\Capability\LanguageAwareSchemaCapability;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\View\ViewInterface;
use TYPO3\CMS\Filelist\Dto\ResourceCollection;
use TYPO3\CMS\Filelist\Dto\ResourceView;
use TYPO3\CMS\Filelist\Dto\UserPermissions;
use TYPO3\CMS\Filelist\Event\AfterFileListRowPreparedEvent;
use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;
use TYPO3\CMS\Filelist\Matcher\Matcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFileExtensionMatcher;
use TYPO3\CMS\Filelist\Matcher\ResourceFolderTypeMatcher;
use TYPO3\CMS\Filelist\Pagination\ResourceCollectionPaginator;
use TYPO3\CMS\Filelist\Type\Mode;
use TYPO3\CMS\Filelist\Type\SortDirection;
use TYPO3\CMS\Filelist\Type\ViewMode;
/**
* Class for rendering of File>Filelist (basically used in FileListController)
* @see \TYPO3\CMS\Filelist\Controller\FileListController
* @internal this is a concrete TYPO3 controller implementation and solely used for EXT:filelist and not part of TYPO3's Core API.
*/
class FileList
{
public Mode $mode = Mode::MANAGE;
public ViewMode $viewMode = ViewMode::TILES;
/**
* Default Max items shown
*/
public int $itemsPerPage = 40;
/**
* Current Page
*/
public int $currentPage = 1;
/**
* Total file size of the current selection
*/
public int $totalbytes = 0;
/**
* Total count of folders and files
*/
public int $totalItems = 0;
/**
* The field to sort by
*/
public string $sortField = '';
public SortDirection $sortDirection = SortDirection::DESCENDING;
/**
* Thumbnails on records containing files (pictures)
*/
public bool $thumbs = false;
/**
* Max length of strings
*/
public int $maxTitleLength = 30;
/**
* Decides the columns shown. Filled with values that refers to the keys of the data-array. $this->fieldArray[0] is the title column.
*/
public array $fieldArray = [];
/**
* Keys are fieldnames and values are td-css-classes to add in addElement();
*
* @var array<string, string>
*/
public array $addElement_tdCssClass = [
'_CONTROL_' => 'col-control',
'_SELECTOR_' => 'col-checkbox',
'icon' => 'col-icon',
'name' => 'col-title col-responsive',
];
/**
* @var Folder
*/
protected $folderObject;
public Clipboard $clipObj;
// Evaluates if a resource can be downloaded
protected ?Matcher $resourceDownloadMatcher = null;
// Evaluates if a resource can be displayed
protected ?Matcher $resourceDisplayMatcher = null;
// Evaluates if a resource can be selected
protected ?Matcher $resourceSelectableMatcher = null;
// Evaluates if a resource is currently selected
protected ?Matcher $resourceSelectedMatcher = null;
protected ?FileSearchDemand $searchDemand = null;
protected EventDispatcherInterface $eventDispatcher;
protected ServerRequestInterface $request;
protected IconFactory $iconFactory;
protected ResourceFactory $resourceFactory;
protected UriBuilder $uriBuilder;
protected TranslationConfigurationProvider $translateTools;
protected OnlineMediaHelperRegistry $onlineMediaHelperRegistry;
protected TcaSchemaFactory $tcaSchemaFactory;
protected ComponentFactory $componentFactory;
public function __construct(ServerRequestInterface $request)
{
$this->request = $request;
// Setting the maximum length of the filenames to the user's settings or minimum 30 (= $this->maxTitleLength)
$this->maxTitleLength = max($this->maxTitleLength, (int)($this->getBackendUser()->uc['titleLen'] ?? 1));
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
$this->translateTools = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
$this->tcaSchemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
$this->itemsPerPage = MathUtility::forceIntegerInRange(
$this->getBackendUser()->getTSConfig()['options.']['file_list.']['filesPerPage'] ?? $this->itemsPerPage,
1
);
// Create clipboard object and initialize that
$this->clipObj = GeneralUtility::makeInstance(Clipboard::class);
$this->clipObj->initializeClipboard($request);
$this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
$this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$this->onlineMediaHelperRegistry = GeneralUtility::makeInstance(OnlineMediaHelperRegistry::class);
// Initialize Resource Download
$this->resourceDownloadMatcher = GeneralUtility::makeInstance(Matcher::class);
$this->resourceDownloadMatcher->addMatcher(GeneralUtility::makeInstance(ResourceFolderTypeMatcher::class));
// Create filter for file extensions
$fileExtensionMatcher = GeneralUtility::makeInstance(ResourceFileExtensionMatcher::class);
$fileDownloadConfiguration = (array)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['fileDownload.'] ?? []);
if ($fileDownloadConfiguration !== []) {
$allowedExtensions = GeneralUtility::trimExplode(',', (string)($fileDownloadConfiguration['allowedFileExtensions'] ?? ''), true);
$disallowedExtensions = GeneralUtility::trimExplode(',', (string)($fileDownloadConfiguration['disallowedFileExtensions'] ?? ''), true);
$fileExtensionMatcher = GeneralUtility::makeInstance(ResourceFileExtensionMatcher::class);
$fileExtensionMatcher->setExtensions($allowedExtensions);
$fileExtensionMatcher->setIgnoredExtensions($disallowedExtensions);
} else {
$fileExtensionMatcher->addExtension('*');
}
$this->resourceDownloadMatcher->addMatcher($fileExtensionMatcher);
$this->componentFactory = GeneralUtility::makeInstance(ComponentFactory::class);
}
public function setResourceDownloadMatcher(?Matcher $matcher): self
{
$this->resourceDownloadMatcher = $matcher;
return $this;
}
public function setResourceDisplayMatcher(?Matcher $matcher): self
{
$this->resourceDisplayMatcher = $matcher;
return $this;
}
public function setResourceSelectableMatcher(?Matcher $matcher): self
{
$this->resourceSelectableMatcher = $matcher;
return $this;
}
public function setResourceSelectedMatcher(?Matcher $matcher): self
{
$this->resourceSelectedMatcher = $matcher;
return $this;
}
/**
* Initialization of class
*
* @param Folder $folderObject The folder to work on
* @param int $currentPage The current page to render
* @param string $sortField Sorting column
* @param Mode $mode Mode of the file list
*/
public function start(Folder $folderObject, int $currentPage, string $sortField, SortDirection $sortDirection, Mode $mode = Mode::MANAGE): void
{
$this->folderObject = $folderObject;
$this->currentPage = MathUtility::forceIntegerInRange($currentPage, 1, 100000);
$this->sortField = $sortField;
$this->sortDirection = $sortDirection;
$this->totalbytes = 0;
$this->resourceDownloadMatcher = null;
$this->resourceDisplayMatcher = null;
$this->resourceSelectableMatcher = null;
$this->setMode($mode);
}
public function setMode(Mode $mode)
{
$this->mode = $mode;
$this->fieldArray = $mode->fieldArray();
}
public function setColumnsToRender(array $additionalFields = []): void
{
// Passed fields might have fields utilized that are no longer / not yet part of TCA Schema.
// For example, EXT:filemetadata might not be available, so fields that this extension provide
// must only be allowed when the extension is active.
$allowedAdditionalFields = [];
foreach ($additionalFields as $field) {
if (!$this->tcaSchemaFactory->get('sys_file')->hasField($field)
&& (!$this->tcaSchemaFactory->has('sys_file_metadata') || !$this->tcaSchemaFactory->get('sys_file_metadata')->hasField($field))
) {
continue;
}
$allowedAdditionalFields[] = $field;
}
$this->fieldArray = array_unique(array_merge($this->fieldArray, $allowedAdditionalFields));
}
/**
* @param ResourceView[] $resourceViews
*/
protected function renderTiles(array $resourceViews, ViewInterface $view): string
{
$view->assignMultiple([
'displayThumbs' => $this->thumbs,
'displayCheckbox' => (bool)$this->resourceSelectableMatcher,
'defaultLanguageAccess' => $this->getBackendUser()->checkLanguageAccess(0),
'resources' => $resourceViews,
]);
return $view->render('Filelist/Tiles');
}
/**
* @param ResourceView[] $resourceViews
*/
protected function renderList(array $resourceViews, ViewInterface $view): string
{
$view->assignMultiple([
'mode' => $this->mode->value,
'tableHeader' => $this->renderListTableHeader(),
'tableBody' => $this->renderListTableBody($resourceViews),
]);
return $view->render('Filelist/List');
}
public function render(?FileSearchDemand $searchDemand, ViewInterface $view): string
{
$storage = $this->folderObject->getStorage();
$storage->resetFileAndFolderNameFiltersToDefault();
if (!$this->folderObject->getStorage()->isBrowsable()) {
return '';
}
if ($searchDemand !== null) {
$this->searchDemand = $searchDemand;
if ($searchDemand->hasSearchTerm()) {
$folders = [];
// Add special "Path" field for the search result
array_splice($this->fieldArray, 3, 0, '_PATH_');
} else {
$folders = $storage->getFoldersInFolder($this->folderObject);
}
$files = iterator_to_array($this->folderObject->searchFiles($searchDemand));
} else {
$folders = $storage->getFoldersInFolder($this->folderObject);
$files = $this->folderObject->getFiles();
}
// Cleanup field array
$this->fieldArray = array_filter($this->fieldArray, function (string $fieldName) {
if ($fieldName === '_SELECTOR_' && $this->resourceSelectableMatcher === null) {
return false;
}
return true;
});
// Remove processing folders
$folders = array_filter($folders, function (Folder $folder) {
return $folder->getRole() !== FolderInterface::ROLE_PROCESSING;
});
// Apply filter
$resources = array_filter($folders + $files, function (ResourceInterface $resource) {
return $this->resourceDisplayMatcher === null || $this->resourceDisplayMatcher->match($resource);
});
$resourceCollection = new ResourceCollection($resources);
$this->totalItems = $resourceCollection->getTotalCount();
$this->totalbytes = $resourceCollection->getTotalBytes();
// Sort the files before sending it to the renderer
if (trim($this->sortField) !== '') {
$resourceCollection->setResources($this->sortResources($resourceCollection->getResources(), $this->sortField));
}
$paginator = new ResourceCollectionPaginator($resourceCollection, $this->currentPage, $this->itemsPerPage);
// Prepare Resources for View
$resourceViews = [];
$userPermissions = $this->getUserPermissions();
foreach ($paginator->getPaginatedItems() as $resource) {
$resourceView = new ResourceView(
$resource,
$userPermissions,
$this->iconFactory->getIconForResource($resource, IconSize::SMALL)
);
$resourceView->moduleUri = $this->createModuleUriForResource($resource);
$resourceView->editDataUri = $this->createEditDataUriForResource($resource);
$resourceView->editContentUri = $this->createEditContentUriForResource($resource);
$resourceView->isDownloadable = $this->resourceDownloadMatcher !== null && $this->resourceDownloadMatcher->match($resource);
$resourceView->isSelectable = $this->resourceSelectableMatcher !== null && $this->resourceSelectableMatcher->match($resource);
$resourceView->isSelected = $this->resourceSelectedMatcher !== null && $this->resourceSelectedMatcher->match($resource);
$resourceViews[] = $resourceView;
}
$pagination = new SimplePagination($paginator);
$currentPage = $paginator->getCurrentPageNumber();
$totalItems = $this->totalItems;
$itemsPerPage = $this->itemsPerPage;
if ($totalItems > $currentPage * $itemsPerPage) {
$lastElementNumber = $currentPage * $itemsPerPage;
} else {
$lastElementNumber = $totalItems;
}
$view->assignMultiple([
'currentUrl' => $this->getListURL(),
'paginator' => $paginator,
'pagination' => $pagination,
'currentPage' => $currentPage,
'totalPages' => $paginator->getNumberOfPages(),
'firstElement' => ((($currentPage - 1) * $itemsPerPage) + 1),
'lastElement' => $lastElementNumber,
]);
if ($this->viewMode === ViewMode::TILES) {
return $this->renderTiles($resourceViews, $view);
}
return $this->renderList($resourceViews, $view);
}
protected function getListURL(): UriInterface
{
$uri = new Uri($this->request->getAttribute('normalizedParams')->getRequestUri());
parse_str($uri->getQuery(), $queryParameters);
unset($queryParameters['contentOnly'], $queryParameters['currentPage']);
if ($this->searchDemand) {
$queryParameters['searchTerm'] = $this->searchDemand->getSearchTerm() ?? '';
}
return $uri->withQuery(HttpUtility::buildQueryString($queryParameters, '&'));
}
/**
* Returns a table-row with the content from the fields in the input data array.
* OBS: $this->fieldArray MUST be set! (represents the list of fields to display)
*
* @param array $data Is the data array, record with the fields. Notice: These fields are (currently) NOT htmlspecialchar'ed before being wrapped in <td>-tags
* @param array $attributes Attributes for the table row. Values will be htmlspecialchar'ed!
* @param bool $isTableHeader Whether the element to be added is a table header
*
* @return string HTML content for the table row
*/
public function addElement(array $data, array $attributes = [], bool $isTableHeader = false): string
{
// Initialize rendering.
$cols = [];
$colType = $isTableHeader ? 'th' : 'td';
// Traverse field array which contains the data to present:
foreach ($this->fieldArray as $fieldName) {
$cellAttributes = [];
$cellAttributes['class'] = $this->addElement_tdCssClass[$fieldName] ?? 'col-nowrap';
// Special handling to combine icon and name column
if ($isTableHeader && $fieldName === 'icon') {
continue;
}
if ($isTableHeader && $fieldName === 'name') {
$cellAttributes['colspan'] = 2;
}
$cols[] = '<' . $colType . ' ' . GeneralUtility::implodeAttributes($cellAttributes, true) . '>' . ($data[$fieldName] ?? '') . '</' . $colType . '>';
}
// Add the table row
return '
<tr ' . GeneralUtility::implodeAttributes($attributes, true) . '>
' . implode(PHP_EOL, $cols) . '
</tr>';
}
/**
* Gets the number of files and total size of a folder
*/
public function getFolderInfo(): string
{
if ($this->totalItems == 1) {
$fileLabel = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:file');
} else {
$fileLabel = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:files');
}
return $this->totalItems . ' ' . htmlspecialchars($fileLabel) . ', ' . GeneralUtility::formatSize(
$this->totalbytes,
htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:byteSizeUnits'))
);
}
/**
* @return array<string>
*/
public function getSortableFields(): array
{
return array_filter($this->fieldArray, $this->isFieldSortable(...));
}
protected function renderListTableHeader(): string
{
$data = [];
foreach ($this->fieldArray as $field) {
$data[$field] = match ($field) {
'icon' => '',
'_SELECTOR_' => $this->renderCheckboxActions(),
default => $this->renderListTableFieldHeader($field),
};
}
return $this->addElement($data, [], true);
}
protected function isFieldSortable(string $field): bool
{
return !in_array($field, ['icon', 'rw', '_SELECTOR_', '_CONTROL_', '_PATH_'], true);
}
protected function renderListTableFieldHeader(string $field): string
{
$label = $this->getFieldLabel($field);
if (!$this->isFieldSortable($field)) {
return $label;
}
$params = ['sortField' => $field, 'currentPage' => 0];
$paramsAsc = $params;
$paramsAsc['sortDirection'] = SortDirection::ASCENDING->value;
$paramsDesc = $params;
$paramsDesc['sortDirection'] = SortDirection::DESCENDING->value;
$icon = $this->sortField === $field
? $this->iconFactory->getIcon($this->sortDirection->getIconIdentifier(), IconSize::SMALL)->render()
: $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render();
return '
<div class="dropdown dropdown-static">
<button class="dropdown-toggle dropdown-toggle-link" type="button" data-bs-toggle="dropdown" aria-expanded="false">
' . htmlspecialchars($label) . ' <div class="' . ($this->sortField === $field ? 'text-primary' : '') . '">' . $icon . '</div>
</button>
<ul class="dropdown-menu">
<li>
<a href="' . $this->createModuleUri($paramsAsc) . '" class="dropdown-item">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon text-primary">
' . ($this->sortField === $field && $this->sortDirection === SortDirection::ASCENDING ? $this->iconFactory->getIcon('actions-dot', IconSize::SMALL)->render() : '') . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting.asc') . '
</span>
</span>
</a>
</li>
<li>
<a href="' . $this->createModuleUri($paramsDesc) . '" class="dropdown-item">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon text-primary">
' . ($this->sortField === $field && $this->sortDirection === SortDirection::DESCENDING ? $this->iconFactory->getIcon('actions-dot', IconSize::SMALL)->render() : '') . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.sorting.desc') . '
</span>
</span>
</a>
</li>
</ul>
</div>
';
}
/**
* @param ResourceView[] $resourceViews
*/
protected function renderListTableBody(array $resourceViews): string
{
$output = '';
foreach ($resourceViews as $resourceView) {
$data = [];
$attributes = [
'class' => $resourceView->isSelected ? 'selected' : '',
'data-filelist-element' => 'true',
'data-filelist-type' => $resourceView->getType(),
'data-filelist-identifier' => $resourceView->getIdentifier(),
'data-filelist-name' => htmlspecialchars($resourceView->getName()),
'data-filelist-icon' => $resourceView->getIconIdentifier(),
'data-filelist-preview' => $resourceView->getPreview() !== null ? 'true' : 'false',
'data-filelist-uid' => $resourceView->getUid(),
'data-filelist-meta-uid' => $resourceView->getMetaDataUid(),
'data-filelist-url' => $resourceView->getPublicUrl(),
'data-filelist-selectable' => $resourceView->isSelectable ? 'true' : 'false',
'data-filelist-selected' => $resourceView->isSelected ? 'true' : 'false',
'data-multi-record-selection-element' => 'true',
'draggable' => $resourceView->canMove() ? 'true' : 'false',
];
if ($this->getBackendUser()->checkLanguageAccess(0)) {
$attributes['data-default-language-access'] = 'true';
}
foreach ($this->fieldArray as $field) {
switch ($field) {
case 'icon':
$data[$field] = $this->renderIcon($resourceView);
break;
case 'name':
$data[$field] = $this->renderName($resourceView)
. $this->renderThumbnail($resourceView);
break;
case 'size':
$data[$field] = $this->renderSize($resourceView);
break;
case 'rw':
$data[$field] = $this->renderPermission($resourceView);
break;
case 'record_type':
$data[$field] = $this->renderType($resourceView);
break;
case 'crdate':
$data[$field] = $this->renderCreationTime($resourceView);
break;
case 'tstamp':
$data[$field] = $this->renderModificationTime($resourceView);
break;
case '_SELECTOR_':
$data[$field] = $this->renderSelector($resourceView);
break;
case '_PATH_':
$data[$field] = $this->renderPath($resourceView);
break;
case '_REF_':
$data[$field] = $this->renderReferenceCount($resourceView);
break;
case '_CONTROL_':
$data[$field] = $this->renderControl($resourceView);
break;
default:
$data[$field] = $this->renderField($resourceView, $field);
}
}
$event = $this->eventDispatcher->dispatch(
new AfterFileListRowPreparedEvent($resourceView->resource, $data, $this, $attributes)
);
$output .= $this->addElement($event->getData(), $event->getAttributes());
}
return $output;
}
/**
* Fetch the translations for a sys_file_metadata record
*
* @param array $metaDataRecord
* @return array<int, array<string, mixed>> keys are the site language ids, values are the $rows
*/
protected function getTranslationsForMetaData(array $metaDataRecord): array
{
$schema = $this->tcaSchemaFactory->get('sys_file_metadata');
if (!$schema->isLanguageAware()) {
return [];
}
/** @var LanguageAwareSchemaCapability $languageCapability */
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_metadata');
$queryBuilder->getRestrictions()->removeAll();
$translationRecords = $queryBuilder->select('*')
->from('sys_file_metadata')
->where(
$queryBuilder->expr()->eq(
$languageCapability->getTranslationOriginPointerField()->getName(),
$queryBuilder->createNamedParameter($metaDataRecord['uid'] ?? 0, Connection::PARAM_INT)
),
$queryBuilder->expr()->gt(
$languageCapability->getLanguageField()->getName(),
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchAllAssociative();
$translations = [];
foreach ($translationRecords as $record) {
$languageId = $record[$languageCapability->getLanguageField()->getName()];
$translations[$languageId] = $record;
}
return $translations;
}
/**
* Render icon
*/
protected function renderIcon(ResourceView $resourceView): string
{
return $this->mode === Mode::BROWSE
? $resourceView->getIconSmall()->render()
: BackendUtility::wrapClickMenuOnIcon($resourceView->getIconSmall()->render(), 'sys_file', $resourceView->getIdentifier());
}
/**
* Render name
*/
protected function renderName(ResourceView $resourceView): string
{
$resourceName = htmlspecialchars($resourceView->getName());
if ($resourceView->resource instanceof Folder
&& $resourceView->resource->getRole() !== FolderInterface::ROLE_DEFAULT) {
$resourceName = '<strong>' . $resourceName . '</strong>';
}
$attributes = [];
$attributes['title'] = $resourceView->getName();
$attributes['type'] = 'button';
$attributes['class'] = 'btn btn-link';
$attributes['data-filelist-action'] = 'primary';
$output = '<button ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $resourceName . '</button>';
if ($resourceView->isMissing()) {
$label = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'));
$output = '<span class="badge badge-danger">' . $label . '</span> ' . $output;
}
return $output;
}
/**
* Render thumbnail
*/
protected function renderThumbnail(ResourceView $resourceView): string
{
if ($this->thumbs === false
|| $resourceView->getPreview() === null
|| !($resourceView->getPreview()->isImage() || $resourceView->getPreview()->isMediaFile())
) {
return '';
}
$processedFile = $resourceView->getPreview()->process(
ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
[
'maxWidth' => (int)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['thumbnail.']['width'] ?? 64),
'maxHeight' => (int)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['thumbnail.']['height'] ?? 64),
]
);
if (($thumbnailUrl = ($processedFile->getPublicUrl() ?? '')) === '') {
// Prevent rendering of a "img" tag with an empty "src" attribute
return '';
}
if (!str_contains($thumbnailUrl, '?') && !PathUtility::hasProtocolAndScheme($thumbnailUrl)) {
$thumbnailUrl .= '?' . $processedFile->getModificationTime();
}
return '<br><img src="' . htmlspecialchars($thumbnailUrl) . '" '
. 'width="' . htmlspecialchars($processedFile->getProperty('width')) . '" '
. 'height="' . htmlspecialchars($processedFile->getProperty('height')) . '" '
. 'loading="lazy" '
. 'alt="" />';
}
/**
* Render type
*/
protected function renderType(ResourceView $resourceView): string
{
$type = $resourceView->getType();
$content = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:' . $type);
if ($resourceView->resource instanceof File && $resourceView->resource->getExtension() !== '') {
$content .= ' (' . strtoupper($resourceView->resource->getExtension()) . ')';
}
return htmlspecialchars($content);
}
/**
* Render creation time
*/
protected function renderCreationTime(ResourceView $resourceView): string
{
if ($resourceView->resource instanceof File) {
$timestamp = $resourceView->getCreatedAt();
} elseif ($resourceView->resource instanceof Folder) {
$timestamp = $resourceView->resource->getCreationTime();
} else {
$timestamp = null;
}
return $timestamp ? BackendUtility::datetime($timestamp) : '';
}
/**
* Render modification time
*/
protected function renderModificationTime(ResourceView $resourceView): string
{
if ($resourceView->resource instanceof File) {
$timestamp = $resourceView->getUpdatedAt();
} elseif ($resourceView->resource instanceof Folder) {
$timestamp = $resourceView->resource->getModificationTime();
} else {
$timestamp = null;
}
return $timestamp ? BackendUtility::datetime($timestamp) : '';
}
/**
* Render size
*/
protected function renderSize(ResourceView $resourceView): string
{
if ($resourceView->resource instanceof File) {
return GeneralUtility::formatSize((int)$resourceView->resource->getSize(), htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:byteSizeUnits')));
}
if ($resourceView->resource instanceof Folder) {
try {
$numFiles = $resourceView->resource->getFileCount();
} catch (InsufficientFolderAccessPermissionsException $e) {
$numFiles = 0;
}
if ($numFiles === 1) {
return $numFiles . ' ' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:file'));
}
return $numFiles . ' ' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:files'));
}
return '';
}
/**
* Render resource permission
*/
protected function renderPermission(ResourceView $resourceView): string
{
return '<strong class="text-danger">'
. htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:read'))
. ($resourceView->canWrite() ? htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:write')) : '')
. '</strong>';
}
/**
* Render any resource field
*/
protected function renderField(ResourceView $resourceView, string $field): string
{
if ($resourceView->resource instanceof File && $resourceView->resource->hasProperty($field)) {
if ($field === 'storage') {
// Fetch storage name of the current file
$storage = GeneralUtility::makeInstance(StorageRepository::class)->findByUid((int)$resourceView->resource->getProperty($field));
if ($storage !== null) {
return htmlspecialchars($storage->getName());
}
} else {
$metaData = $resourceView->resource->getMetaData()->get();
return htmlspecialchars(
(string)BackendUtility::getProcessedValueExtra(
$this->getConcreteTableName($field),
$field,
$resourceView->resource->getProperty($field),
$this->maxTitleLength,
$metaData['uid'],
false,
0,
$metaData,
)
);
}
}
return '';
}
/**
* Renders the checkbox to select a resource in the listing
*/
protected function renderSelector(ResourceView $resourceView): string
{
$checkboxConfig = $resourceView->getCheckboxConfig();
if ($checkboxConfig === null) {
return '';
}
if (!$resourceView->isSelectable) {
return '';
}
$attributes = [
'class' => 'form-check-input ' . $checkboxConfig['class'],
'type' => 'checkbox',
'name' => $checkboxConfig['name'],
'value' => $checkboxConfig['value'],
'checked' => $checkboxConfig['checked'],
];
return '<span class="form-check form-check-type-toggle">'
. '<input ' . GeneralUtility::implodeAttributes($attributes, true) . ' />'
. '</span>';
}
/**
* Render resource path
*/
protected function renderPath(ResourceView $resourceView): string
{
return htmlspecialchars($resourceView->getPath());
}
/**
* Render reference count. Wraps the count into a button to
* open the element information in case references exists.
*/
protected function renderReferenceCount(ResourceView $resourceView): string
{
if (!$resourceView->resource instanceof File) {
return '-';
}
$referenceCount = $this->getFileReferenceCount($resourceView->resource);
if (!$referenceCount) {
return '-';
}
$attributes = [
'type' => 'button',
'class' => 'btn btn-sm btn-link',
'data-filelist-action' => 'show',
'title' => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:show_references') . ' (' . $referenceCount . ')',
];
return '<button ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $referenceCount . '</button>';
}
/**
* Renders the control section
*/
protected function renderControl(ResourceView $resourceView): string
{
return match ($this->mode) {
Mode::MANAGE => $this->renderControlManage($resourceView),
Mode::BROWSE => $this->renderControlBrowse($resourceView),
};
}
/**
* Creates the control section for the file list module
*/
protected function renderControlManage(ResourceView $resourceView): string
{
if (!$resourceView->resource instanceof File && !$resourceView->resource instanceof Folder) {
return '';
}
// primary actions
$userTsConfig = $this->getBackendUser()->getTSConfig();
$primaryActions = GeneralUtility::trimExplode(',', $userTsConfig['options.']['file_list.']['primaryActions'] ?? 'view,metadata,translations,delete');
$primary = new ComponentGroup('primary');
$secondary = new ComponentGroup('secondary');
$actions = [
'edit' => $this->createControlEditContent($resourceView),
'metadata' => $this->createControlEditMetaData($resourceView),
'translations' => $this->createControlTranslation($resourceView),
'view' => $this->createControlView($resourceView),
'replace' => $this->createControlReplace($resourceView),
'rename' => $this->createControlRename($resourceView),
'download' => $this->createControlDownload($resourceView),
'info' => $this->createControlInfo($resourceView),
'delete' => $this->createControlDelete($resourceView),
'copy' => $this->createControlCopy($resourceView),
'cut' => $this->createControlCut($resourceView),
'paste' => $this->createControlPaste($resourceView),
'updateOnlineMedia' => $this->createControlUpdateOnlineMedia($resourceView),
];
foreach ($actions as $actionName => $action) {
if (in_array($actionName, $primaryActions, true)) {
$primary->add($actionName, $action);
} else {
$secondary->add($actionName, $action);
}
}
$event = new ProcessFileListActionsEvent($primary, $secondary, $resourceView->resource, $this->request);
$event = $this->eventDispatcher->dispatch($event);
if ($event->hasAction('translation')) {
// Always move "translations" to primary as this action has an own dropdown container and therefore cannot be a secondary action
$event->moveActionTo('translation', ActionGroup::primary);
}
$cellOutput = '';
$output = '';
foreach ($event->getActionGroup(ActionGroup::primary)->getItems() as $action) {
if (method_exists($action, 'setSize')) {
$action->setSize(ButtonSize::MEDIUM);
}
$output .= $action;
}
foreach ($event->getActionGroup(ActionGroup::secondary)->getItems() as $action) {
if ($action instanceof GenericButton) {
$action = $this->componentFactory
->createDropDownItem()
->setTag($action->getTag())
->setLabel($action->getLabel() ?: $action->getTitle())
->setIcon($action->getIcon())
->setHref($action->getHref())
->setAttributes($action->getAttributes());
$cellOutput .= '<li>' . $action->render() . '</li>';
continue;
}
if ($action instanceof LinkButton) {
$attributes = [];
foreach ($action->getDataAttributes() as $key => $value) {
$attributes['data-' . $key] = $value;
}
$action = $this->componentFactory
->createDropDownItem()
->setLabel($action->getTitle())
->setIcon($action->getIcon())
->setHref($action->getHref())
->setAttributes([
...$action->getAttributes(),
...$attributes,
'role' => $action->getRole(),
]);
$cellOutput .= '<li>' . $action->render() . '</li>';
continue;
}
$cellOutput .= '<li>' . $action->render() . '</li>';
}
if ($cellOutput !== '') {
$title = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more');
$output .= '<div class="dropdown">'
. '<a title="' . htmlspecialchars($title) . '" href="#actions_' . $resourceView->resource->getHashedIdentifier() . '" class="btn btn-default dropdown-toggle dropdown-toggle-no-chevron" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false">'
. $this->iconFactory->getIcon('actions-menu-alternative', IconSize::SMALL)->render()
. '</a>'
. '<ul id="actions_' . $resourceView->resource->getHashedIdentifier() . '" class="dropdown-menu">' . $cellOutput . '</ul>'
. '</div>';
}
return '<div class="btn-group">' . $output . '</div>';
}
/**
* Creates the control section for the element browser
*/
protected function renderControlBrowse(ResourceView $resourceView): string
{
$fileOrFolderObject = $resourceView->resource;
if (!$fileOrFolderObject instanceof File && !$fileOrFolderObject instanceof Folder) {
return '';
}
$actions = [
'select' => $this->createControlSelect($resourceView),
'info' => $this->createControlInfo($resourceView),
];
// Remove empty actions
$actions = array_filter($actions, static fn($action) => $action !== null && trim($action) !== '');
if (empty($actions)) {
return '';
}
foreach ($actions as $action) {
if (method_exists($action, 'setSize')) {
$action->setSize(ButtonSize::MEDIUM);
}
}
return '<div class="btn-group">' . implode(' ', $actions) . '</div>';
}
protected function createControlSelect(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->isSelectable) {
return null;
}
$title = sprintf(
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.selectFile'),
$resourceView->getName(),
);
$button = $this->componentFactory->createGenericButton();
$button->setTitle($title);
$button->setAttributes([
'type' => 'button',
'data-filelist-action' => 'select',
'aria-label' => $title,
]);
$button->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL));
return $button;
}
protected function createControlEditContent(ResourceView $resourceView): ?ButtonInterface
{
if (!($resourceView->resource instanceof File && $resourceView->resource->isTextFile())
|| !$resourceView->canWrite()) {
return null;
}
$button = $this->componentFactory->createLinkButton();
$button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editcontent'));
$button->setHref($resourceView->editContentUri);
$button->setIcon($this->iconFactory->getIcon('actions-page-open', IconSize::SMALL));
return $button;
}
protected function createControlEditMetaData(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->getMetaDataUid() || !$this->getBackendUser()->checkLanguageAccess(0)) {
return null;
}
$button = $this->componentFactory->createLinkButton();
$button->setSize(ButtonSize::MEDIUM);
$button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata'));
$button->setHref($resourceView->editDataUri);
$button->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL));
return $button;
}
protected function createControlView(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->getPublicUrl()) {
return null;
}
$button = $this->componentFactory->createLinkButton();
$button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view'));
$button->setHref($resourceView->getPublicUrl());
$button->setAttributes(['target' => '_blank']);
$button->setSize(ButtonSize::MEDIUM);
$button->setIcon($this->iconFactory->getIcon('actions-document-view', IconSize::SMALL));
return $button;
}
protected function createControlReplace(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->canReplace()) {
return null;
}
$button = $this->componentFactory->createGenericButton();
$button->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.replace'));
$button->setAttributes(['type' => 'button', 'data-filelist-action' => 'replace']);
$button->setIcon($this->iconFactory->getIcon('actions-edit-replace', IconSize::SMALL));
return $button;
}
protected function createControlRename(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->canRename()) {
return null;
}
$button = $this->componentFactory->createGenericButton();
$button->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.rename'));
$button->setAttributes(['type' => 'button', 'data-filelist-action' => 'rename']);
$button->setIcon($this->iconFactory->getIcon('actions-edit-rename', IconSize::SMALL));
return $button;
}
protected function createControlDownload(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->canRead() || !(bool)($this->getBackendUser()->getTSConfig()['options.']['file_list.']['fileDownload.']['enabled'] ?? true)) {
return null;
}
if (!$resourceView->isDownloadable) {
return null;
}
$button = $this->componentFactory->createGenericButton();
$button->setLabel($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:download'));
$button->setAttributes([
'type' => 'button',
'data-filelist-action' => 'download',
'data-filelist-action-url' => $this->uriBuilder->buildUriFromRoute('file_download'),
]);
$button->setIcon($this->iconFactory->getIcon('actions-download', IconSize::SMALL));
return $button;
}
protected function createControlInfo(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->canRead()) {
return null;
}
$button = $this->componentFactory->createGenericButton();
$button->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info'));
$button->setAttributes([
'type' => 'button',
'data-filelist-action' => 'show',
]);
$button->setIcon($this->iconFactory->getIcon('actions-document-info', IconSize::SMALL));
return $button;
}
protected function createControlDelete(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->canDelete()) {
return null;
}
$recordInfo = $resourceView->getName();
$referenceCountText = '';
if ($resourceView->resource instanceof Folder) {
$identifier = $resourceView->getIdentifier();
$deleteType = 'delete_folder';
if ($this->getBackendUser()->shallDisplayDebugInformation()) {
$recordInfo .= ' [' . $identifier . ']';
}
} else {
$referenceCountText = BackendUtility::referenceCount('sys_file', (int)$resourceView->getUid(), LF . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToFile'));
$deleteType = 'delete_file';
if ($this->getBackendUser()->shallDisplayDebugInformation()) {
$recordInfo .= ' [sys_file:' . $resourceView->getUid() . ']';
}
}
$title = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete');
$button = $this->componentFactory->createGenericButton();
$button->setLabel($title);
$button->setIcon($this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL));
$button->setAttributes([
'type' => 'button',
'data-title' => $title,
'data-content' => sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'), trim($recordInfo)) . $referenceCountText,
'data-filelist-action' => 'delete',
'data-filelist-delete' => 'true',
'data-filelist-delete-identifier' => $resourceView->getIdentifier(),
'data-filelist-delete-url' => $this->uriBuilder->buildUriFromRoute('tce_file'),
'data-filelist-delete-type' => $deleteType,
'data-filelist-delete-check' => $this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE) ? '1' : '0',
'data-redirect-url' => $this->createModuleUri(),
]);
$button->setSize(ButtonSize::MEDIUM);
return $button;
}
/**
* Creates the file metadata translation dropdown. Each item links
* to the corresponding metadata translation, while depending on
* the current state, either a new translation can be created or
* an existing translation can be edited.
*/
protected function createControlTranslation(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->resource instanceof File) {
return null;
}
$backendUser = $this->getBackendUser();
// Fetch all system languages except "default (0)" and "all languages (-1)"
$systemLanguages = array_filter(
$this->translateTools->getSystemLanguages(),
static fn(array $languageRecord): bool => $languageRecord['uid'] > 0 && $backendUser->checkLanguageAccess($languageRecord['uid'])
);
if ($systemLanguages === []
|| !$this->tcaSchemaFactory->get('sys_file_metadata')->isLanguageAware()
|| !$resourceView->resource->isIndexed()
|| !$resourceView->resource->checkActionPermission('editMeta')
|| !$backendUser->check('tables_modify', 'sys_file_metadata')
) {
// Early return in case no system languages exists or metadata
// of this file can not be created / edited by the current user.
return null;
}
$dropdownItems = [];
$metaDataRecord = $resourceView->resource->getMetaData()->get();
$existingTranslations = $this->getTranslationsForMetaData($metaDataRecord);
foreach ($systemLanguages as $languageId => $language) {
if (!isset($existingTranslations[$languageId]) && !($metaDataRecord['uid'] ?? false)) {
// Skip if neither a translation nor the metadata uid exists
continue;
}
if (isset($existingTranslations[$languageId])) {
// Set options for edit action of an existing translation
$title = sprintf($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:editMetadataForLanguage'), $language['title']);
$actionType = 'edit';
$url = (string)$this->uriBuilder->buildUriFromRoute(
'record_edit',
[
'edit' => [
'sys_file_metadata' => [
$existingTranslations[$languageId]['uid'] => 'edit',
],
],
'module' => 'media_management',
'returnUrl' => $this->createModuleUri(),
]
);
} else {
// Set options for "create new" action of a new translation using localization wizard
$title = sprintf($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:createMetadataForLanguage'), $language['title']);
$actionType = 'new';
$metaDataRecordId = (int)($metaDataRecord['uid'] ?? 0);
$dropdownItem = $this->componentFactory->createDropDownItem()
->setTag('typo3-backend-localization-button')
->setAttribute('record-type', 'sys_file_metadata')
->setAttribute('record-uid', (string)$metaDataRecordId)
->setAttribute('target-language', (string)$languageId)
->setLabel($title);
if (!empty($language['flagIcon'])) {
$dropdownItem->setIcon($this->iconFactory->getIcon($language['flagIcon'], IconSize::SMALL, 'overlay-' . $actionType));
}
$dropdownItems[] = $dropdownItem;
continue;
}
$dropdownItem = $this->componentFactory->createDropDownItem();
$dropdownItem->setLabel($title);
$dropdownItem->setHref($url);
$dropdownItem->setIcon($this->iconFactory->getIcon($language['flagIcon'], IconSize::SMALL, 'overlay-' . $actionType));
$dropdownItems[] = $dropdownItem;
}
if (empty($dropdownItems)) {
return null;
}
$dropdownButton = $this->componentFactory->createDropDownButton();
$dropdownButton->setLabel($this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:translations'));
$dropdownButton->setIcon($this->iconFactory->getIcon('actions-translate', IconSize::SMALL));
foreach ($dropdownItems as $dropdownItem) {
$dropdownButton->addItem($dropdownItem);
}
return $dropdownButton;
}
protected function createControlCopy(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->canRead() || !$resourceView->canCopy()) {
return null;
}
if ($this->clipObj->current === 'normal') {
$isSelected = $this->clipObj->isSelected('_FILE', md5($resourceView->getIdentifier()));
$button = $this->componentFactory->createLinkButton();
$button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . ($isSelected === 'copy' ? 'copyrelease' : 'copy')));
$button->setHref($this->clipObj->selUrlFile($resourceView->getIdentifier(), true, $isSelected === 'copy'));
$button->setIcon($this->iconFactory->getIcon($isSelected === 'copy' ? 'actions-edit-copy-release' : 'actions-edit-copy', IconSize::SMALL));
return $button;
}
return null;
}
protected function createControlCut(ResourceView $resourceView): ?ButtonInterface
{
if (!$resourceView->canRead() || !$resourceView->canMove()) {
return null;
}
if ($this->clipObj->current === 'normal') {
$isSelected = $this->clipObj->isSelected('_FILE', md5($resourceView->getIdentifier()));
$button = $this->componentFactory->createLinkButton();
$button->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . ($isSelected === 'cut' ? 'cutrelease' : 'cut')));
$button->setHref($this->clipObj->selUrlFile($resourceView->getIdentifier(), false, $isSelected === 'cut'));
$button->setIcon($this->iconFactory->getIcon($isSelected === 'cut' ? 'actions-edit-cut-release' : 'actions-edit-cut', IconSize::SMALL));
return $button;
}
return null;
}
protected function createControlPaste(ResourceView $resourceView): ?ButtonInterface
{
$permission = ($this->clipObj->clipData[$this->clipObj->current]['mode'] ?? '') === 'copy' ? 'copy' : 'move';
$addPasteButton = $this->folderObject->checkActionPermission($permission);
$elementFromTable = $this->clipObj->elFromTable('_FILE');
if ($elementFromTable === []
|| !$addPasteButton
|| !$resourceView->canRead()
|| !$resourceView->canWrite()
|| !$resourceView->resource instanceof Folder) {
return null;
}
foreach ($elementFromTable as $element) {
$clipBoardElement = $this->resourceFactory->retrieveFileOrFolderObject($element);
if ($clipBoardElement instanceof Folder
&& $clipBoardElement->getStorage()->isWithinFolder($clipBoardElement, $resourceView->resource)
) {
// In case folder is already present in the target folder, return actions without paste button
return null;
}
}
$pasteTitle = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:clip_pasteInto');
$button = $this->componentFactory->createLinkButton();
$button->setTitle($pasteTitle);
$button->setHref($this->clipObj->pasteUrl('_FILE', $resourceView->getIdentifier()));
$button->setDataAttributes([
'title' => $pasteTitle,
'bs-content' => $this->clipObj->confirmMsgText('_FILE', $resourceView->getName(), 'into'),
]);
$button->setIcon($this->iconFactory->getIcon('actions-document-paste-into', IconSize::SMALL));
return $button;
}
protected function createControlUpdateOnlineMedia(ResourceView $resourceView): ?ButtonInterface
{
if (!($resourceView->resource instanceof File)
|| !$resourceView->canEditMetadata()
|| !$this->getBackendUser()->checkLanguageAccess(0)
|| !$this->onlineMediaHelperRegistry->hasOnlineMediaHelper($resourceView->resource->getExtension())
) {
return null;
}
$title = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:reloadMetadata');
$button = $this->componentFactory->createGenericButton();
$button->setLabel($title);
$button->setIcon($this->iconFactory->getIcon('actions-refresh', IconSize::SMALL));
$button->setAttributes([
'type' => 'button',
'data-title' => $title,
'data-filelist-action' => 'updateOnlineMedia',
'data-filelist-action-url' => $this->uriBuilder->buildUriFromRoute('file_update_online_media'),
]);
return $button;
}
protected function isEditMetadataAllowed(File $file): bool
{
return $file->isIndexed()
&& $file->checkActionPermission('editMeta')
&& $this->getUserPermissions()->editMetaData;
}
/**
* Render convenience actions, such as "check all"
*
* @return string HTML markup for the checkbox actions
*/
protected function renderCheckboxActions(): string
{
// Early return in case there are no items
if (!$this->totalItems) {
return '';
}
$lang = $this->getLanguageService();
$dropdownItems['checkAll'] = '
<li>
<button type="button" class="dropdown-item" disabled data-multi-record-selection-check-action="check-all" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
' . $this->iconFactory->getIcon('actions-selection-elements-all', IconSize::SMALL)->render() . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '
</span>
</span>
</button>
</li>';
$dropdownItems['checkNone'] = '
<li>
<button type="button" class="dropdown-item" disabled data-multi-record-selection-check-action="check-none" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
' . $this->iconFactory->getIcon('actions-selection-elements-none', IconSize::SMALL)->render() . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '
</span>
</span>
</button>
</li>';
$dropdownItems['toggleSelection'] = '
<li>
<button type="button" class="dropdown-item" data-multi-record-selection-check-action="toggle" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
' . $this->iconFactory->getIcon('actions-selection-elements-invert', IconSize::SMALL)->render() . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '
</span>
</span>
</button>
</li>';
return '
<div class="dropdown">
<button type="button" class="dropdown-toggle dropdown-toggle-link t3js-multi-record-selection-check-actions-toggle" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false" aria-label="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openSelectionOptions')) . '">
' . $this->iconFactory->getIcon('actions-selection', IconSize::SMALL) . '
</button>
<ul class="dropdown-menu t3js-multi-record-selection-check-actions">
' . implode(PHP_EOL, $dropdownItems) . '
</ul>
</div>';
}
/**
* Determine the concrete table name by checking if
* the field exists, while sys_file takes precedence.
*/
protected function getConcreteTableName(string $fieldName): string
{
if ($this->tcaSchemaFactory->get('sys_file')->hasField($fieldName)) {
return 'sys_file';
}
return 'sys_file_metadata';
}
/**
* Returns list URL; This is the URL of the current script with id and imagemode parameters, that's all.
*/
public function createModuleUri(array $params = []): ?string
{
$request = $this->request;
$queryParams = $request->getQueryParams();
$parsedBody = $request->getParsedBody();
$route = $request->getAttribute('route');
if (!$route instanceof Route) {
return null;
}
$baseParams = [
'currentPage' => $this->currentPage,
'id' => $this->folderObject->getCombinedIdentifier(),
'searchTerm' => $this->searchDemand ? $this->searchDemand->getSearchTerm() : '',
];
// Keep ElementBrowser Settings
if ($mode = $parsedBody['mode'] ?? $queryParams['mode'] ?? null) {
$baseParams['mode'] = $mode;
}
foreach (['fieldReference', 'allowedTypes', 'disallowedFileExtensions', 'irreObjectId'] as $paramName) {
if ($value = $parsedBody[$paramName] ?? $queryParams[$paramName] ?? null) {
$baseParams[$paramName] = $value;
}
}
// Keep LinkHandler Settings
if ($act = ($parsedBody['act'] ?? $queryParams['act'] ?? null)) {
$baseParams['act'] = $act;
}
if ($linkHandlerParams = ($parsedBody['P'] ?? $queryParams['P'] ?? null)) {
$baseParams['P'] = $linkHandlerParams;
}
$params = array_replace_recursive($baseParams, $params);
// Expanded folder is used in the element browser.
// We always map it to the id here.
$params['expandFolder'] = $params['id'];
$params = array_filter($params, static function ($value) {
return (is_array($value) && $value !== []) || (trim((string)$value) !== '');
});
return (string)$this->uriBuilder->buildUriFromRequest($request, $params);
}
protected function createEditDataUriForResource(ResourceInterface $resource): ?string
{
if ($resource instanceof File
&& $this->isEditMetadataAllowed($resource)
&& ($metaDataUid = $resource->getMetaData()->offsetGet('uid'))
) {
$parameter = [
'edit' => ['sys_file_metadata' => [$metaDataUid => 'edit']],
'module' => 'media_management',
'returnUrl' => $this->createModuleUri(),
];
return (string)$this->uriBuilder->buildUriFromRoute('record_edit', $parameter);
}
return null;
}
protected function createEditContentUriForResource(ResourceInterface $resource): ?string
{
if ($resource instanceof File
&& $resource->checkActionPermission('write')
&& $resource->isTextFile()
) {
$parameter = [
'target' => $resource->getCombinedIdentifier(),
'returnUrl' => $this->createModuleUri(),
];
return (string)$this->uriBuilder->buildUriFromRoute('file_edit', $parameter);
}
return null;
}
protected function createModuleUriForResource(ResourceInterface $resource): ?string
{
if ($resource instanceof Folder) {
$parameter = [
'id' => $resource->getCombinedIdentifier(),
'searchTerm' => '',
'currentPage' => 1,
];
return (string)$this->createModuleUri($parameter);
}
if ($resource instanceof File) {
return $this->createEditDataUriForResource($resource);
}
return null;
}
/**
* @return ResourceInterface[]
*/
protected function sortResources(array $resources, string $sortField): array
{
$collator = new \Collator((string)($this->getLanguageService()->getLocale() ?? 'en'));
$collator->setAttribute(\Collator::NUMERIC_COLLATION, \Collator::ON);
$sortMultiplier = $this->sortDirection === SortDirection::DESCENDING ? -1 : 1;
uksort($resources, function (int $index1, int $index2) use ($sortField, $sortMultiplier, $resources, $collator) {
$resource1 = $resources[$index1];
$resource2 = $resources[$index2];
// Folders are always prioritized above files
if ($resource1 instanceof File && $resource2 instanceof Folder) {
return 1 * $sortMultiplier;
}
if ($resource1 instanceof Folder && $resource2 instanceof File) {
return -1 * $sortMultiplier;
}
// Sort by value first
$result = (int)$collator->compare(
$this->getSortingValue($resource1, $sortField),
$this->getSortingValue($resource2, $sortField)
);
// Use index as tiebreaker for stable sorting
if ($result === 0) {
$result = $index1 <=> $index2;
}
return $result * $sortMultiplier;
});
return $resources;
}
protected function getSortingValue(ResourceInterface $resource, string $sortField): string
{
if ($resource instanceof File) {
return $this->getSortingValueForFile($resource, $sortField);
}
if ($resource instanceof Folder) {
return $this->getSortingValueForFolder($resource, $sortField);
}
return '';
}
protected function getSortingValueForFile(File $resource, string $sortField): string
{
switch ($sortField) {
case 'fileext':
return $resource->getExtension();
case 'size':
return $resource->getSize() . 's';
case 'rw':
return ($resource->checkActionPermission('read') ? 'R' : '')
. ($resource->checkActionPermission('write') ? 'W' : '');
case '_REF_':
return $this->getFileReferenceCount($resource) . 'ref';
case 'tstamp':
return $resource->getModificationTime() . 't';
case 'crdate':
return $resource->getCreationTime() . 'c';
case 'file':
return $resource->getName();
default:
return $resource->hasProperty($sortField) ? (string)$resource->getProperty($sortField) : '';
}
}
protected function getSortingValueForFolder(Folder $resource, string $sortField): string
{
switch ($sortField) {
case 'size':
try {
$fileCount = $resource->getFileCount();
} catch (InsufficientFolderAccessPermissionsException $e) {
$fileCount = 0;
}
return '0' . $fileCount . 's';
case 'rw':
return ($resource->checkActionPermission('read') ? 'R' : '')
. ($resource->checkActionPermission('write') ? 'W' : '');
case 'name':
return $resource->getName();
case 'tstamp':
return $resource->getModificationTime() . 't';
case 'crdate':
return $resource->getCreationTime() . 'c';
default:
return '';
}
}
public function getFieldLabel(string $field): string
{
$lang = $this->getLanguageService();
if ($specialLabel = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $field)) {
return $specialLabel;
}
if ($customLabel = $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:c_' . $field)) {
return $customLabel;
}
$concreteTableName = $this->getConcreteTableName($field);
$schema = $this->tcaSchemaFactory->has($concreteTableName) ? $this->tcaSchemaFactory->get($concreteTableName) : null;
$label = ($schema?->hasField($field) ? $schema->getField($field)->getLabel() : '') ?: null;
// In case global TSconfig exists we have to check if the label is overridden there
$tsConfig = BackendUtility::getPagesTSconfig(0);
$label = $lang->translateLabel(
$tsConfig['TCEFORM.'][$concreteTableName . '.'][$field . '.']['label.'] ?? [],
$tsConfig['TCEFORM.'][$concreteTableName . '.'][$field . '.']['label']
?? $label
?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $field
);
return $label ?: $field;
}
/**
* Counts how often the given file is referenced. This is done by
* looking up the file in the "sys_refindex" table, while excluding
* sys_file_metadata relations as these are no such references.
*/
protected function getFileReferenceCount(File $file): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
return (int)$queryBuilder
->count('*')
->from('sys_refindex')
->where(
$queryBuilder->expr()->eq(
'ref_table',
$queryBuilder->createNamedParameter('sys_file')
),
$queryBuilder->expr()->eq(
'ref_uid',
$queryBuilder->createNamedParameter($file->getUid(), Connection::PARAM_INT)
),
$queryBuilder->expr()->neq(
'tablename',
$queryBuilder->createNamedParameter('sys_file_metadata')
)
)
->executeQuery()
->fetchOne();
}
protected function getUserPermissions(): UserPermissions
{
return new UserPermissions($this->getBackendUser()->check('tables_modify', 'sys_file_metadata'));
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}