TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,144 @@
<?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\Backend\ElementBrowser;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\PageRendererBackendSetupTrait;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewInterface;
/**
* Base class for element browsers
* This class should only be used internally. Extensions must implement the ElementBrowserInterface.
*
* @internal This class is a specific LinkBrowser implementation and is not part of the TYPO3's Core API.
*/
abstract class AbstractElementBrowser
{
use PageRendererBackendSetupTrait;
/**
* The element browsers unique identifier
*/
protected string $identifier = '';
/**
* Typed DTO containing all browser parameters.
*/
protected ElementBrowserParameters $browserParameters;
protected ?ServerRequestInterface $request = null;
protected ViewInterface $view;
public function __construct(
protected readonly IconFactory $iconFactory,
protected readonly PageRenderer $pageRenderer,
protected readonly UriBuilder $uriBuilder,
protected readonly ExtensionConfiguration $extensionConfiguration,
protected readonly BackendViewFactory $backendViewFactory,
protected readonly TcaSchemaFactory $tcaSchemaFactory,
protected readonly ComponentFactory $componentFactory,
) {}
/**
* Main initialization
*/
protected function initialize(ServerRequestInterface $request)
{
$this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $this->getRequest(), $this->getLanguageService());
$view = $this->backendViewFactory->create($request);
$this->view = $view;
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/element-browser.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/hotkeys.js');
$this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
$this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf');
$this->initVariables($request);
}
/**
* Returns the identifier for the browser
*/
public function getIdentifier(): string
{
return $this->identifier;
}
protected function initVariables(ServerRequestInterface $request)
{
$this->browserParameters = ElementBrowserParameters::fromRequest($request);
}
protected function getBodyTagParameters(): string
{
$bodyDataAttributes = array_merge(
$this->getBParamDataAttributes(),
$this->getBodyTagAttributes()
);
return GeneralUtility::implodeAttributes($bodyDataAttributes, true, true);
}
/**
* @return array<string, string> Array of body-tag attributes
*/
protected function getBodyTagAttributes()
{
return [];
}
/**
* Returns data attributes for the body tag, used by the Javascript.
*
* @return array<string, string|null> Data attributes for Javascript
*/
protected function getBParamDataAttributes()
{
return $this->browserParameters->toDataAttributes();
}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
// initialize here, this is a dirty hack as long as the interface does not support setting a request object properly
// see ElementBrowserController.php for the process on how the program code flow is used
$this->initialize($request);
}
protected function getRequest(): ServerRequestInterface
{
return $this->request ?? $GLOBALS['TYPO3_REQUEST'];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
+248
View File
@@ -0,0 +1,248 @@
<?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\Backend\ElementBrowser;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Module\ModuleData;
use TYPO3\CMS\Backend\RecordList\ElementBrowserRecordList;
use TYPO3\CMS\Backend\Tree\View\LinkParameterProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\RecordSearchBoxComponent;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Showing a page tree and allows you to browse for records. This is the modal rendered
* for type=group to add db relations to a group field.
*
* @internal This class is a specific LinkBrowser implementation and is not part of the TYPO3's Core API.
*/
class DatabaseBrowser extends AbstractElementBrowser implements ElementBrowserInterface, LinkParameterProviderInterface
{
protected string $identifier = 'db';
/**
* When you click a page title/expand icon to see the content of a certain page, this
* value will contain the ID of the expanded page.
* If the value is NOT set by GET parameter, then it will be restored from the module session data.
*
* @var int|null
*/
protected $expandPage;
protected array $modTSconfig = [];
protected function initialize(ServerRequestInterface $request)
{
parent::initialize($request);
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/browse-database.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/tree/page-browser.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/column-selector-button.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/recordlist.js');
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/record-search.js');
}
protected function initVariables(ServerRequestInterface $request)
{
parent::initVariables($request);
$this->expandPage = $request->getParsedBody()['expandPage'] ?? $request->getQueryParams()['expandPage'] ?? null;
}
/**
* Session data for this class can be set from outside with this method.
*
* @param mixed[] $data Session data array
* @return array<int, array|bool> Session data and boolean which indicates that data needs to be stored in session because it's changed
*/
public function processSessionData($data)
{
if ($this->expandPage !== null) {
$data['expandPage'] = $this->expandPage;
$store = true;
} else {
$this->expandPage = (int)($data['expandPage'] ?? 0);
$store = false;
}
return [$data, $store];
}
/**
* @return string HTML content
*/
public function render()
{
$this->getBackendUser()->initializeWebmountsForElementBrowser();
$this->modTSconfig = BackendUtility::getPagesTSconfig((int)$this->expandPage)['mod.']['web_list.'] ?? [];
$allowedTables = $this->browserParameters->allowedTypes;
$withTree = true;
if ($allowedTables !== '' && $allowedTables !== '*') {
$tablesArr = GeneralUtility::trimExplode(',', $allowedTables, true);
$onlyRootLevel = true;
foreach ($tablesArr as $currentTable) {
if ($this->tcaSchemaFactory->has($currentTable)) {
$schema = $this->tcaSchemaFactory->get($currentTable);
if ($schema->getCapability(TcaSchemaCapability::RestrictionRootLevel)->canExistOnPages()) {
$onlyRootLevel = false;
break;
}
}
}
if ($onlyRootLevel) {
$withTree = false;
// page to work on is root
$this->expandPage = 0;
}
}
$contentOnly = (bool)($this->getRequest()->getQueryParams()['contentOnly'] ?? false);
$renderedRecordList = $this->renderTableRecords($allowedTables);
$this->pageRenderer->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:recordSelector'));
$view = $this->view;
$view->assignMultiple([
'treeEnabled' => $withTree,
'treeActions' => $allowedTables === 'pages' ? ['select'] : [],
'activePage' => $this->expandPage,
'initialNavigationWidth' => $this->getBackendUser()->uc['selector']['navigation']['width'] ?? 250,
'content' => $renderedRecordList,
'contentOnly' => $contentOnly,
]);
$content = $this->view->render('ElementBrowser/Page');
if ($contentOnly) {
return $content;
}
$this->pageRenderer->setBodyContent('<body ' . $this->getBodyTagParameters() . '>' . $content);
return $this->pageRenderer->render($this->getRequest());
}
/**
* This lists all content elements for the given list of tables
*
* @param string $tables Comma separated list of tables. Set to "*" if you want all tables.
* @return string HTML code
*/
protected function renderTableRecords($tables)
{
$request = $this->getRequest();
$backendUser = $this->getBackendUser();
if ($this->expandPage === null || $this->expandPage < 0 || !$backendUser->isInWebMount($this->expandPage)) {
return '';
}
// Set array with table names to list:
if (trim($tables) === '*') {
$tablesArr = $this->tcaSchemaFactory->all()->getNames();
} else {
$tablesArr = GeneralUtility::trimExplode(',', $tables, true);
}
$out = '';
// Create the header, showing the current page for which the listing is.
// Includes link to the page itself, if pages are amount allowed tables.
$mainPageRecord = BackendUtility::getRecordWSOL('pages', $this->expandPage);
if (is_array($mainPageRecord)) {
$pText = htmlspecialchars(BackendUtility::cropToTitleLength($mainPageRecord['title']));
$out .= '<p>' . $this->iconFactory->getIconForRecord('pages', $mainPageRecord, IconSize::SMALL)->render() . '&nbsp;';
if (in_array('pages', $tablesArr, true)) {
$out .= '<span data-uid="' . htmlspecialchars((string)$mainPageRecord['uid']) . '" data-table="pages" data-title="' . htmlspecialchars($mainPageRecord['title']) . '">';
$out .= '<a href="#" data-close="0">'
. $this->iconFactory->getIcon('actions-plus', IconSize::SMALL)->render()
. '</a>'
. '<a href="#" data-close="1">'
. $pText
. '</a>';
$out .= '</span>';
} else {
$out .= $pText;
}
$out .= '</p>';
}
$permsClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
$pageInfo = BackendUtility::readPageAccess($this->expandPage, $permsClause);
$existingModuleData = $backendUser->getModuleData('records');
$moduleData = new ModuleData('records', is_array($existingModuleData) ? $existingModuleData : []);
$dbList = GeneralUtility::makeInstance(ElementBrowserRecordList::class);
$dbList->setRequest($request);
$dbList->setModuleData($moduleData);
$dbList->setOverrideUrlParameters($this->getUrlParameters([]), $request);
$dbList->setIsEditable(false);
$dbList->calcPerms = new Permission($backendUser->calcPerms($pageInfo));
$dbList->noControlPanels = true;
$dbList->clickMenuEnabled = false;
$dbList->displayRecordDownload = false;
$dbList->tableList = implode(',', $tablesArr);
// Extract relating table and field from field reference (e.g., "data[pages][79][storage_pid]")
$fieldReferenceParts = $this->browserParameters->getFieldReferenceParts();
if ($fieldReferenceParts['tableName'] !== '' && $fieldReferenceParts['fieldName'] !== '') {
$dbList->setRelatingTableAndField($fieldReferenceParts['tableName'], $fieldReferenceParts['fieldName']);
}
$selectedTable = (string)($request->getParsedBody()['table'] ?? $request->getQueryParams()['table'] ?? '');
$searchWord = (string)($request->getParsedBody()['searchTerm'] ?? $request->getQueryParams()['searchTerm'] ?? '');
$searchLevels = (int)($request->getParsedBody()['search_levels'] ?? $request->getQueryParams()['search_levels'] ?? $this->modTSconfig['searchLevel.']['default'] ?? 0);
$pointer = (int)($request->getParsedBody()['pointer'] ?? $request->getQueryParams()['pointer'] ?? 0);
$dbList->start(
(int)$this->expandPage,
$selectedTable,
MathUtility::forceIntegerInRange($pointer, 0, 100000),
$searchWord,
$searchLevels
);
$tableList = $dbList->generateList();
$out .= $this->renderSearchBox($request, $dbList, $searchWord, $searchLevels);
// Add the HTML for the record list to output variable:
$out .= $tableList;
return $out;
}
protected function renderSearchBox(ServerRequestInterface $request, ElementBrowserRecordList $dblist, string $searchWord, int $searchLevels): string
{
return GeneralUtility::makeInstance(RecordSearchBoxComponent::class)
->setAllowedSearchLevels((array)($this->modTSconfig['searchLevel.']['items.'] ?? []))
->setSearchWord($searchWord)
->setSearchLevel($searchLevels)
->render($request, $dblist->listURL('', null, 'pointer,searchTerm'));
}
/**
* @param array $values Array of values to include into the parameters
* @return array<string,mixed> Array of parameters which have to be added to URLs
*/
public function getUrlParameters(array $values): array
{
$pid = $values['pid'] ?? $this->expandPage;
return array_merge(
[
'mode' => 'db',
'expandPage' => $pid,
],
$this->browserParameters->toQueryParameters()
);
}
}
@@ -0,0 +1,46 @@
<?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\Backend\ElementBrowser;
/**
* Element browsers are modals rendered when records are attached to FormEngine elements.
* Core usages:
* * Managing TCA type=file relations
* * Managing FAL folder relations a TCA type=folder
* * Managing various target relations of a TCA type=group
*/
interface ElementBrowserInterface
{
/**
* Returns the unique identifier of the element browser
*/
public function getIdentifier(): string;
/**
* @return string HTML content
*/
public function render();
/**
* Session data for this class can be set from outside with this method.
*
* @param mixed[] $data Session data array
* @return array[] Session data and boolean which indicates that data needs to be stored in session because it's changed
*/
public function processSessionData($data);
}
@@ -0,0 +1,220 @@
<?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\Backend\ElementBrowser;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Data Transfer Object for Element Browser parameters.
*
* Provides type-safe access to the parameters passed between FormEngine and
* the Element Browser.
*
* @internal This class is not part of the TYPO3 Core API.
*/
final readonly class ElementBrowserParameters implements \JsonSerializable
{
/**
* @param string $fieldReference Form field name reference, e.g., "data[tt_content][123][image]"
* @param string $allowedTypes Allowed types: tables (comma-separated) for db mode, or file extensions for file mode
* @param string $disallowedFileExtensions Disallowed file extensions (comma-separated) for file mode
* @param string $irreObjectId IRRE uniqueness target, e.g., "data-4-pages-4-nav_icon-sys_file_reference"
*/
public function __construct(
public string $fieldReference = '',
public string $allowedTypes = '',
public string $disallowedFileExtensions = '',
public string $irreObjectId = '',
public bool $useEvents = false,
) {}
/**
* Creates an instance from the current HTTP request.
*/
public static function fromRequest(ServerRequestInterface $request): self
{
$queryParams = $request->getQueryParams();
$parsedBody = $request->getParsedBody() ?? [];
return new self(
fieldReference: (string)($parsedBody['fieldReference'] ?? $queryParams['fieldReference'] ?? ''),
allowedTypes: (string)($parsedBody['allowedTypes'] ?? $queryParams['allowedTypes'] ?? ''),
disallowedFileExtensions: (string)($parsedBody['disallowedFileExtensions'] ?? $queryParams['disallowedFileExtensions'] ?? ''),
irreObjectId: (string)($parsedBody['irreObjectId'] ?? $queryParams['irreObjectId'] ?? ''),
useEvents: (bool)(int)($parsedBody['useEvents'] ?? $queryParams['useEvents'] ?? 0),
);
}
/**
* Returns the allowed file extensions as an array.
*
* @return string[] List of allowed file extensions
*/
public function getAllowedFileExtensions(): array
{
if ($this->allowedTypes === '' || $this->allowedTypes === '*') {
return [];
}
// Skip if it looks like a table name (contains underscore typical for TYPO3 tables)
if (str_contains($this->allowedTypes, 'sys_file')) {
return [];
}
return GeneralUtility::trimExplode(',', $this->allowedTypes, true);
}
/**
* Returns the disallowed file extensions as an array.
*
* @return string[] List of disallowed file extensions
*/
public function getDisallowedFileExtensions(): array
{
if ($this->disallowedFileExtensions === '') {
return [];
}
return GeneralUtility::trimExplode(',', $this->disallowedFileExtensions, true);
}
/**
* Parses the allowed file extensions from the allowedTypes field.
*
* @return array{allowed: string[], disallowed: string[]}
*/
public function getFileExtensions(): array
{
return [
'allowed' => $this->getAllowedFileExtensions(),
'disallowed' => $this->getDisallowedFileExtensions(),
];
}
/**
* Parses the allowed tables from the allowedTypes field.
*
* @return string[] List of allowed table names
*/
public function getAllowedTables(): array
{
if ($this->allowedTypes === '' || $this->allowedTypes === '*') {
return [];
}
return GeneralUtility::trimExplode(',', $this->allowedTypes, true);
}
/**
* Returns the field reference parsed into table name and field name.
*
* Parses format like "data[tt_content][123][image]" to extract
* table name ("tt_content") and field name ("image").
*
* @return array{tableName: string, fieldName: string}
*/
public function getFieldReferenceParts(): array
{
$result = [
'tableName' => '',
'fieldName' => '',
];
if ($this->fieldReference === '') {
return $result;
}
// Parse "data[table][uid][field]" format
$parts = explode('[', $this->fieldReference);
if (count($parts) >= 4) {
// parts[1] = "table]", parts[3] = "field]"
$result['tableName'] = rtrim($parts[1], ']');
$result['fieldName'] = rtrim($parts[3], ']');
}
return $result;
}
/**
* Returns data attributes for use in HTML elements (body tag).
*
* @return array<string, string|null>
*/
public function toDataAttributes(): array
{
return [
'data-field-reference' => $this->fieldReference,
'data-irre-object-id' => $this->irreObjectId ?: null,
'data-use-events' => $this->useEvents ? 'true' : null,
];
}
/**
* Returns array representation of the parameters.
*
* @return array{
* fieldReference: string,
* allowedTypes: string,
* disallowedFileExtensions: string,
* irreObjectId: string,
* useEvents: bool
* }
*/
public function toArray(): array
{
return [
'fieldReference' => $this->fieldReference,
'allowedTypes' => $this->allowedTypes,
'disallowedFileExtensions' => $this->disallowedFileExtensions,
'irreObjectId' => $this->irreObjectId,
'useEvents' => $this->useEvents,
];
}
/**
* Returns URL query parameters array (new format).
*
* @return array<string, string>
*/
public function toQueryParameters(): array
{
$params = [];
if ($this->fieldReference !== '') {
$params['fieldReference'] = $this->fieldReference;
}
if ($this->allowedTypes !== '') {
$params['allowedTypes'] = $this->allowedTypes;
}
if ($this->disallowedFileExtensions !== '') {
$params['disallowedFileExtensions'] = $this->disallowedFileExtensions;
}
if ($this->irreObjectId !== '') {
$params['irreObjectId'] = $this->irreObjectId;
}
if ($this->useEvents) {
$params['useEvents'] = '1';
}
return $params;
}
public function jsonSerialize(): array
{
return $this->toArray();
}
}
@@ -0,0 +1,79 @@
<?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\Backend\ElementBrowser;
/**
* Registry for element browsers. The registry receives all services, tagged with "recordlist.elementbrowser".
* The tagging of element browsers is automatically done based on the implemented ElementBrowserInterface.
*
* @internal
*/
class ElementBrowserRegistry
{
/**
* @var ElementBrowserInterface[]
*/
private array $elementBrowsers = [];
public function __construct(iterable $elementBrowsers)
{
foreach ($elementBrowsers as $elementBrowser) {
if (!($elementBrowser instanceof ElementBrowserInterface)) {
continue;
}
$identifier = $elementBrowser->getIdentifier();
if ($identifier === '') {
throw new \InvalidArgumentException('Identifier for element browser ' . get_class($elementBrowser) . ' is empty.', 1647241084);
}
if (isset($this->elementBrowsers[$identifier])) {
throw new \InvalidArgumentException('Element browser with identifier ' . $identifier . ' is already registered.', 1647241085);
}
$this->elementBrowsers[$identifier] = $elementBrowser;
}
}
/**
* Whether a registered element browser exists for the identifier
*/
public function hasElementBrowser(string $identifier): bool
{
return isset($this->elementBrowsers[$identifier]);
}
/**
* Get registered element browser by identifier
*/
public function getElementBrowser(string $identifier): ElementBrowserInterface
{
if (!$this->hasElementBrowser($identifier)) {
throw new \UnexpectedValueException('Element browser with identifier ' . $identifier . ' is not registered.', 1647241086);
}
return $this->elementBrowsers[$identifier];
}
/**
* Get all registered element browsers
*
* @return ElementBrowserInterface[]
*/
public function getElementBrowsers(): array
{
return $this->elementBrowsers;
}
}
@@ -0,0 +1,52 @@
<?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\Backend\ElementBrowser\Event;
use TYPO3\CMS\Core\Resource\FileInterface;
/**
* Listeners to this event are able to define whether a file can be selected in the file browser
*/
final class IsFileSelectableEvent
{
private bool $isFileSelectable = true;
public function __construct(
private readonly FileInterface $file,
) {}
public function getFile(): FileInterface
{
return $this->file;
}
public function isFileSelectable(): bool
{
return $this->isFileSelectable;
}
public function allowFileSelection(): void
{
$this->isFileSelectable = true;
}
public function denyFileSelection(): void
{
$this->isFileSelectable = false;
}
}