TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<?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\Backend\Tree;
|
||||
|
||||
use TYPO3\CMS\Backend\Tree\Renderer\AbstractTreeRenderer;
|
||||
|
||||
/**
|
||||
* Abstract Tree
|
||||
*/
|
||||
abstract class AbstractTree
|
||||
{
|
||||
/**
|
||||
* Data Provider
|
||||
*
|
||||
* @var \TYPO3\CMS\Backend\Tree\AbstractTreeDataProvider
|
||||
*/
|
||||
protected $dataProvider;
|
||||
|
||||
/**
|
||||
* Tree Node Decorator
|
||||
*
|
||||
* @var \TYPO3\CMS\Backend\Tree\Renderer\AbstractTreeRenderer
|
||||
*/
|
||||
protected $nodeRenderer;
|
||||
|
||||
public function setDataProvider(AbstractTreeDataProvider $dataProvider)
|
||||
{
|
||||
$this->dataProvider = $dataProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \TYPO3\CMS\Backend\Tree\AbstractTreeDataProvider
|
||||
*/
|
||||
public function getDataProvider()
|
||||
{
|
||||
return $this->dataProvider;
|
||||
}
|
||||
|
||||
public function setNodeRenderer(AbstractTreeRenderer $nodeRenderer)
|
||||
{
|
||||
$this->nodeRenderer = $nodeRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \TYPO3\CMS\Backend\Tree\Renderer\AbstractTreeRenderer
|
||||
*/
|
||||
public function getNodeRenderer()
|
||||
{
|
||||
return $this->nodeRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root node
|
||||
*
|
||||
* @return \TYPO3\CMS\Backend\Tree\TreeNode
|
||||
*/
|
||||
abstract public function getRoot();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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\Backend\Tree;
|
||||
|
||||
/**
|
||||
* Abstract Tree Data Provider
|
||||
*/
|
||||
abstract class AbstractTreeDataProvider
|
||||
{
|
||||
/**
|
||||
* Root Node
|
||||
*
|
||||
* @var TreeNode
|
||||
*/
|
||||
protected $rootNode;
|
||||
|
||||
/**
|
||||
* Returns the root node
|
||||
*
|
||||
* @return TreeNode
|
||||
*/
|
||||
abstract public function getRoot();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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\Backend\Tree;
|
||||
|
||||
/**
|
||||
* Interface that defines the comparison of nodes
|
||||
*/
|
||||
interface ComparableNodeInterface
|
||||
{
|
||||
/**
|
||||
* Compare Node against another one
|
||||
*
|
||||
* Returns:
|
||||
* 1 if the current node is greater than the $other,
|
||||
* -1 if $other is greater than the current node and
|
||||
* 0 if the nodes are equal
|
||||
*
|
||||
* <strong>Example</strong>
|
||||
* <pre>
|
||||
* if ($this->sortValue > $other->sortValue) {
|
||||
* return 1;
|
||||
* } elseif ($this->sortValue < $other->sortValue) {
|
||||
* return -1;
|
||||
* } else {
|
||||
* return 0;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param \TYPO3\CMS\Backend\Tree\TreeNode $other
|
||||
* @return int see description
|
||||
*/
|
||||
public function compareTo($other);
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?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\Tree;
|
||||
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Resource\Driver\DriverInterface;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderReadPermissionsException;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\FolderInterface;
|
||||
use TYPO3\CMS\Core\Resource\InaccessibleFolder;
|
||||
use TYPO3\CMS\Core\Resource\ResourceStorage;
|
||||
use TYPO3\CMS\Core\Resource\Utility\ListUtility;
|
||||
|
||||
/**
|
||||
* Responsible for fetching a tree-structure of folders.
|
||||
*
|
||||
* @internal not part of TYPO3 Core API due to the specific use case for the FileStorageTree component.
|
||||
*/
|
||||
readonly class FileStorageTreeProvider
|
||||
{
|
||||
public function prepareFolderInformation(Folder $folder, ?string $alternativeName = null, ?Folder $parentFolder = null, ?array $children = null): array
|
||||
{
|
||||
$name = $alternativeName ?? $folder->getName();
|
||||
$storage = $folder->getStorage();
|
||||
try {
|
||||
$parentFolder = $parentFolder ?? $folder->getParentFolder();
|
||||
} catch (InsufficientFolderAccessPermissionsException) {
|
||||
$parentFolder = null;
|
||||
}
|
||||
if (str_contains($folder->getRole(), FolderInterface::ROLE_MOUNT)) {
|
||||
$tableName = 'sys_filemount';
|
||||
$isStorage = true;
|
||||
} elseif ($parentFolder === null || $folder->getIdentifier() === $storage->getRootLevelFolder()->getIdentifier()) {
|
||||
$tableName = 'sys_file_storage';
|
||||
$isStorage = true;
|
||||
} else {
|
||||
$tableName = 'sys_file';
|
||||
$isStorage = false;
|
||||
}
|
||||
|
||||
try {
|
||||
$hasSubfolders = $storage->isBrowsable() && (is_array($children) ? $children !== [] : !empty($folder->getSubfolders()));
|
||||
} catch (\InvalidArgumentException|InsufficientFolderReadPermissionsException) {
|
||||
$hasSubfolders = false;
|
||||
}
|
||||
|
||||
return [
|
||||
'resource' => $folder,
|
||||
'identifier' => rawurlencode($folder->getCombinedIdentifier()),
|
||||
'name' => $name,
|
||||
'storage' => $storage->getUid(),
|
||||
'pathIdentifier' => rawurlencode($folder->getIdentifier()),
|
||||
'hasChildren' => $hasSubfolders,
|
||||
'parentIdentifier' => !$isStorage ? rawurlencode($parentFolder->getCombinedIdentifier()) : null,
|
||||
'recordType' => $tableName,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all file storages / file mounts visible for a user.
|
||||
*/
|
||||
public function getRootNodes(BackendUserAuthentication $user): array
|
||||
{
|
||||
$items = [];
|
||||
$storages = $user->getFileStorages();
|
||||
foreach ($storages as $storageObject) {
|
||||
$items = array_merge($items, $this->getFoldersInStorage($storageObject, $user));
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all folders recursively in a single store.
|
||||
*/
|
||||
protected function getFoldersInStorage(ResourceStorage $resourceStorage, BackendUserAuthentication $user): array
|
||||
{
|
||||
$rootLevelFolders = $this->getMountsInStorage($resourceStorage, $user);
|
||||
$items = [];
|
||||
foreach ($rootLevelFolders as $rootLevelFolderInfo) {
|
||||
/** @var Folder $rootLevelFolder */
|
||||
$rootLevelFolder = $rootLevelFolderInfo['folder'];
|
||||
$item = $this->prepareFolderInformation($rootLevelFolder, $rootLevelFolderInfo['name']);
|
||||
$item['depth'] = 0;
|
||||
$item['expanded'] = true;
|
||||
$item['loaded'] = true;
|
||||
$items[] = $item;
|
||||
if ($resourceStorage->isBrowsable()) {
|
||||
// Handle sub folders if the mount is browsable.
|
||||
$childItems = $this->getSubfolders($rootLevelFolder, 1);
|
||||
array_push($items, ...$childItems);
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a tree by a search word
|
||||
*
|
||||
* @return FolderInterface[]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getFilteredTree(BackendUserAuthentication $user, string $search): array
|
||||
{
|
||||
$foundFolders = [];
|
||||
$storages = $user->getFileStorages();
|
||||
foreach ($storages as $resourceStorage) {
|
||||
$processingFolders = $resourceStorage->getProcessingFolders();
|
||||
$processingFolderIdentifiers = array_map(static function (Folder $folder): string {
|
||||
return $folder->getIdentifier();
|
||||
}, $processingFolders);
|
||||
$resourceStorage->addFileAndFolderNameFilter(static function ($itemName, $itemIdentifier, $parentIdentifier, array $additionalInformation, DriverInterface $driver) use ($resourceStorage, $search, $processingFolderIdentifiers) {
|
||||
// Skip items in processing folders
|
||||
$isInProcessingFolder = array_filter($processingFolderIdentifiers, static function (string $processingFolderIdentifier) use ($parentIdentifier): bool {
|
||||
return stripos($parentIdentifier, $processingFolderIdentifier) !== false;
|
||||
});
|
||||
if (!empty($isInProcessingFolder)) {
|
||||
return -1;
|
||||
}
|
||||
if ($itemName instanceof Folder) {
|
||||
if ($resourceStorage->isProcessingFolder($itemName)) {
|
||||
return -1;
|
||||
}
|
||||
$name = $itemName->getName();
|
||||
} elseif (is_string($itemName)) {
|
||||
$name = $itemName;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
if (stripos($name, $search) !== false) {
|
||||
return true;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
try {
|
||||
$files = $folders = [];
|
||||
// Because $resourceStorage->getRootLevelFolder() does not return an actual root folder but
|
||||
// the first file mount, we first need to check if we have file mounts and then fetch them one by one.
|
||||
if (($fileMounts = $resourceStorage->getFileMounts()) !== []) {
|
||||
foreach ($fileMounts as $identifier => $configuration) {
|
||||
foreach ($resourceStorage->getFilesInFolder($resourceStorage->getFolder($identifier), 0, 0, true, true) as $file) {
|
||||
$files[] = $file;
|
||||
}
|
||||
foreach ($resourceStorage->getFolderIdentifiersInFolder($identifier, true, true) as $folder) {
|
||||
$folders[] = $folder;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$files = $resourceStorage->getFilesInFolder($resourceStorage->getRootLevelFolder(), 0, 0, true, true);
|
||||
$folders = $resourceStorage->getFolderIdentifiersInFolder($resourceStorage->getRootLevelFolder()->getIdentifier(), true, true);
|
||||
}
|
||||
foreach ($files as $file) {
|
||||
$folder = $file->getParentFolder();
|
||||
$foundFolders[$folder->getCombinedIdentifier()] = $folder;
|
||||
}
|
||||
foreach ($folders as $folder) {
|
||||
$folderObj = $resourceStorage->getFolder($folder);
|
||||
$foundFolders[$folderObj->getCombinedIdentifier()] = $folderObj;
|
||||
}
|
||||
} catch (InsufficientFolderAccessPermissionsException $e) {
|
||||
// do nothing
|
||||
}
|
||||
$resourceStorage->resetFileAndFolderNameFiltersToDefault();
|
||||
}
|
||||
return $foundFolders;
|
||||
}
|
||||
|
||||
public function getSubfolders(Folder $folderObject, int $currentDepth): array
|
||||
{
|
||||
$items = [];
|
||||
$subFolders = [];
|
||||
if (!$folderObject instanceof InaccessibleFolder) {
|
||||
$subFolders = $folderObject->getSubfolders();
|
||||
$subFolders = ListUtility::resolveSpecialFolderNames($subFolders);
|
||||
uksort($subFolders, strnatcasecmp(...));
|
||||
}
|
||||
foreach ($subFolders as $subFolderName => $subFolder) {
|
||||
$subFolderName = (string)$subFolderName; // Enforce string cast in case $subFolderName contains numeric chars only
|
||||
$children = [];
|
||||
if (!($subFolder instanceof InaccessibleFolder)) {
|
||||
// Get children to determine if that folder can be expanded again, 'hasChildren true' then renders expand triangle
|
||||
$children = $subFolder->getSubfolders();
|
||||
}
|
||||
$items[] = array_merge(
|
||||
$this->prepareFolderInformation($subFolder, $subFolderName, $folderObject, $children),
|
||||
[
|
||||
'depth' => $currentDepth,
|
||||
'expanded' => false,
|
||||
'loaded' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all "root level folders" of a storage. If a user has file mounts in this storage, they are properly resolved.
|
||||
*
|
||||
* @return array|array[]
|
||||
*/
|
||||
protected function getMountsInStorage(ResourceStorage $resourceStorage, BackendUserAuthentication $user): array
|
||||
{
|
||||
$fileMounts = $resourceStorage->getFileMounts();
|
||||
if (!empty($fileMounts)) {
|
||||
return array_map(static function (array $fileMountInfo): array {
|
||||
return [
|
||||
'folder' => $fileMountInfo['folder'],
|
||||
'name' => $fileMountInfo['title'],
|
||||
];
|
||||
}, $fileMounts);
|
||||
}
|
||||
|
||||
if ($user->isAdmin()) {
|
||||
return [
|
||||
[
|
||||
'folder' => $resourceStorage->getRootLevelFolder(),
|
||||
'name' => $resourceStorage->getName(),
|
||||
],
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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\Backend\Tree\Renderer;
|
||||
|
||||
use TYPO3\CMS\Backend\Tree\AbstractTree;
|
||||
use TYPO3\CMS\Backend\Tree\TreeNodeCollection;
|
||||
use TYPO3\CMS\Backend\Tree\TreeRepresentationNode;
|
||||
|
||||
/**
|
||||
* Abstract Renderer
|
||||
*/
|
||||
abstract class AbstractTreeRenderer
|
||||
{
|
||||
/**
|
||||
* Renders a node recursive or just a single instance
|
||||
*
|
||||
* @param bool $recursive
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function renderNode(TreeRepresentationNode $node, $recursive = true);
|
||||
|
||||
/**
|
||||
* Renders a node collection recursive or just a single instance
|
||||
*
|
||||
* @param bool $recursive
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function renderNodeCollection(TreeNodeCollection $collection, $recursive = true);
|
||||
|
||||
/**
|
||||
* Renders a tree recursively or just a single instance
|
||||
*
|
||||
* @param bool $recursive
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function renderTree(AbstractTree $tree, $recursive = true);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?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\Backend\Tree\Renderer;
|
||||
|
||||
use TYPO3\CMS\Backend\Tree\AbstractTree;
|
||||
use TYPO3\CMS\Backend\Tree\TreeNodeCollection;
|
||||
use TYPO3\CMS\Backend\Tree\TreeRepresentationNode;
|
||||
|
||||
/**
|
||||
* Renderer for unordered lists
|
||||
*/
|
||||
class UnorderedListTreeRenderer extends AbstractTreeRenderer
|
||||
{
|
||||
/**
|
||||
* recursion level
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $recursionLevel = 0;
|
||||
|
||||
/**
|
||||
* Renders a node recursive or just a single instance
|
||||
*
|
||||
* @param bool $recursive
|
||||
* @return string
|
||||
*/
|
||||
public function renderNode(TreeRepresentationNode $node, $recursive = true)
|
||||
{
|
||||
$code = '<li><span class="' . htmlspecialchars($node->getIcon()) . '"> </span>' . htmlspecialchars($node->getLabel());
|
||||
if ($recursive && $node->hasChildNodes()) {
|
||||
$this->recursionLevel++;
|
||||
$code .= $this->renderNodeCollection($node->getChildNodes());
|
||||
$this->recursionLevel--;
|
||||
}
|
||||
$code .= '</li>';
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a node collection recursive or just a single instance
|
||||
*
|
||||
* @param bool $recursive
|
||||
* @return string
|
||||
*/
|
||||
public function renderTree(AbstractTree $tree, $recursive = true)
|
||||
{
|
||||
$this->recursionLevel = 0;
|
||||
$code = '<ul class="level' . $this->recursionLevel . '" style="margin-left:10px">';
|
||||
// @todo: this doc block is a hack, as it needs to be a TreeReprsentationNode
|
||||
/** @var TreeRepresentationNode $rootNode */
|
||||
$rootNode = $tree->getRoot();
|
||||
$code .= $this->renderNode($rootNode, $recursive);
|
||||
$code .= '</ul>';
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a tree recursively or just a single instance
|
||||
*
|
||||
* @param bool $recursive
|
||||
* @return string
|
||||
*/
|
||||
public function renderNodeCollection(TreeNodeCollection $collection, $recursive = true)
|
||||
{
|
||||
$code = '<ul class="level' . $this->recursionLevel . '" style="margin-left:10px">';
|
||||
foreach ($collection as $node) {
|
||||
$code .= $this->renderNode($node, $recursive);
|
||||
}
|
||||
$code .= '</ul>';
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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\Tree\Repository;
|
||||
|
||||
/**
|
||||
* Listeners to this event will be able to modify a page with the special _children key,
|
||||
* or completely change e.g. a title.
|
||||
*/
|
||||
final class AfterRawPageRowPreparedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $rawPage,
|
||||
private readonly int $workspaceId
|
||||
) {}
|
||||
|
||||
public function getRawPage(): array
|
||||
{
|
||||
return $this->rawPage;
|
||||
}
|
||||
|
||||
public function setRawPage(array $rawPage): void
|
||||
{
|
||||
$this->rawPage = $rawPage;
|
||||
}
|
||||
|
||||
public function getWorkspaceId(): int
|
||||
{
|
||||
return $this->workspaceId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\Tree\Repository;
|
||||
|
||||
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
|
||||
/**
|
||||
* Listeners to this event will be able to modify the search parts, to be used to filter the page tree
|
||||
*/
|
||||
final class BeforePageTreeIsFilteredEvent
|
||||
{
|
||||
public function __construct(
|
||||
/** @param CompositeExpression $searchParts The search parts to be used for filtering */
|
||||
public CompositeExpression $searchParts,
|
||||
/** @param int[] $searchUids The UIDs to be used for filtering by a special search part, which is added by Core always after listener evaluation */
|
||||
public array $searchUids,
|
||||
/** @param string $searchPhrase The complete search phrase, as entered by the user */
|
||||
public readonly string $searchPhrase,
|
||||
/** @param QueryBuilder $queryBuilder This instance is provided for context and to simplify the creation of the search parts and must not be manipulated by listeners */
|
||||
public readonly QueryBuilder $queryBuilder,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
<?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\Tree\Repository;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent;
|
||||
use TYPO3\CMS\Backend\Dto\Tree\Label\Label;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Routing\SiteUrlResolver;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Page tree filter implementation providing search functionality for the backend page tree.
|
||||
*
|
||||
* This class implements page tree filtering through multiple event listeners that work together
|
||||
* to provide comprehensive search capabilities including:
|
||||
*
|
||||
* - Numeric UID search (direct page ID lookup)
|
||||
* - Wildcard text search in title/nav_title fields
|
||||
* - Search by Frontend URI
|
||||
* - Optional search in translated page titles
|
||||
* - Visual labels indicating how pages were matched
|
||||
*
|
||||
* Overview
|
||||
* ========
|
||||
*
|
||||
* The filtering process consists of two main phases:
|
||||
*
|
||||
* 1. Query Building Phase (BeforePageTreeIsFilteredEvent)
|
||||
* - addUidsFromSearchPhrase: Extracts numeric UIDs from search phrase
|
||||
* - addWildCardAliasFilter: Adds LIKE queries for title/nav_title
|
||||
* - addUidsFromSearchPhraseWithFrontendUri: Adds numeric UIDs from resolved frontend URIs
|
||||
* - addTranslatedPagesFilter: Queries translated pages (if enabled)
|
||||
*
|
||||
* 2. Label Attachment Phase (AfterPageTreeItemsPreparedEvent)
|
||||
* - attachSearchResultLabel: Adds "Search result" label to directly matched pages
|
||||
* - attachTranslationInfoLabel: Adds translation info labels
|
||||
*
|
||||
* Runtime Cache Usage
|
||||
* ===================
|
||||
*
|
||||
* Two runtime caches are utilized to speed up matching.
|
||||
*
|
||||
* One for translation matches with the structure:
|
||||
* [
|
||||
* pageUid => [languageUid1, languageUid2, ...]
|
||||
* ]
|
||||
*
|
||||
* This allows the label attachment phase to know which translations matched,
|
||||
* enabling informative labels like "Found in translation: German".
|
||||
*
|
||||
* The cache is populated during query building and consumed during label attachment.
|
||||
* Cache key: 'pageTree_translationMatches'
|
||||
*
|
||||
* The other is for frontend URI matches with the structure:
|
||||
* [
|
||||
* pageUid1 => true, pageUid2 => true, ...
|
||||
* ]
|
||||
*
|
||||
* This allows a simple array_key_exists lookup. The cache key is 'pageTree_uriMatches'
|
||||
* and also consumed for label attachment.
|
||||
*
|
||||
* User Configuration
|
||||
* ==================
|
||||
*
|
||||
* Translation search can be controlled via:
|
||||
* - TSConfig: options.pageTree.searchInTranslatedPages (default: true)
|
||||
* - User Preference: pageTree_searchInTranslatedPages
|
||||
*
|
||||
* Language restrictions from user groups are respected automatically.
|
||||
*
|
||||
* URI search can be controlled via:
|
||||
* - TSConfig: options.pageTree.searchByFrontendUri (default: true)
|
||||
* - User Preference: pageTree_searchByFrontendUri
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class PageTreeFilter
|
||||
{
|
||||
/**
|
||||
* Color for "Search result" labels on directly matched pages
|
||||
*/
|
||||
private const string SEARCH_RESULT_LABEL_COLOR = '#F5A770';
|
||||
|
||||
/**
|
||||
* Runtime cache identifier for storing translation match information
|
||||
*/
|
||||
private const string CACHE_IDENTIFIER_TRANSLATION = 'pageTree_translationMatches';
|
||||
|
||||
/**
|
||||
* Runtime cache identifier for storing URI match information
|
||||
*/
|
||||
private const string CACHE_IDENTIFIER_URI = 'pageTree_uriMatches';
|
||||
|
||||
public function __construct(
|
||||
private SiteFinder $siteFinder,
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
private FrontendInterface $runtimeCache,
|
||||
private SiteUrlResolver $siteUrlResolver,
|
||||
private ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Extracts numeric page UIDs from the search phrase and adds them to the query.
|
||||
*
|
||||
* When a user searches for "123", this method:
|
||||
* 1. Extracts the UID (123) and adds it to event->searchUids
|
||||
* 2. If translation search is enabled, checks if 123 is a translated page
|
||||
* 3. If yes, also adds the l10n_parent UID to show the default language page
|
||||
*
|
||||
* Example: Searching for UID 456 where 456 is a German translation of page 123
|
||||
* will result in page 123 being shown with a "Found in translation: German" label.
|
||||
*
|
||||
* Supports comma-separated UIDs: "123,456,789"
|
||||
*/
|
||||
#[AsEventListener('page-tree-uid-provider')]
|
||||
public function addUidsFromSearchPhrase(BeforePageTreeIsFilteredEvent $event): void
|
||||
{
|
||||
// Extract true integers from search string
|
||||
$searchPhrases = GeneralUtility::trimExplode(',', $event->searchPhrase, true);
|
||||
$numericUids = [];
|
||||
foreach ($searchPhrases as $searchPhrase) {
|
||||
if (MathUtility::canBeInterpretedAsInteger($searchPhrase) && $searchPhrase > 0) {
|
||||
$uid = (int)$searchPhrase;
|
||||
$event->searchUids[] = $uid;
|
||||
$numericUids[] = $uid;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners after this one may reset this array to clear unwanted UID restrictions.
|
||||
$event->searchUids = array_unique($event->searchUids);
|
||||
|
||||
// Check if any numeric UIDs match translated pages and add their l10n_parent
|
||||
if ($numericUids !== [] && $this->isTranslatedPagesSearchEnabled()) {
|
||||
$queryBuilder = $this->createPreparedPagesQueryBuilder();
|
||||
$whereConditions = [
|
||||
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($numericUids, Connection::PARAM_INT_ARRAY)),
|
||||
];
|
||||
$translatedPages = $this->fetchTranslatedPages($queryBuilder, $whereConditions);
|
||||
$this->processTranslatedPages($event, $translatedPages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds wildcard search conditions for title and nav_title fields.
|
||||
*
|
||||
* Creates a LIKE query that searches in both the 'title' and
|
||||
* 'nav_title' fields of default language pages.
|
||||
*
|
||||
* Example: Searching for "Home" will find pages with:
|
||||
* - title = "Homepage"
|
||||
* - nav_title = "Home Navigation"
|
||||
*/
|
||||
#[AsEventListener(identifier: 'page-tree-wildcard-alias-filter', after: 'page-tree-uid-provider')]
|
||||
public function addWildCardAliasFilter(BeforePageTreeIsFilteredEvent $event): void
|
||||
{
|
||||
$searchFilterWildcard = '%' . $event->queryBuilder->escapeLikeWildcards($event->searchPhrase) . '%';
|
||||
$searchWhereAlias = $event->queryBuilder->expr()->or(
|
||||
$event->queryBuilder->expr()->like(
|
||||
'nav_title',
|
||||
$event->queryBuilder->createNamedParameter($searchFilterWildcard)
|
||||
),
|
||||
$event->queryBuilder->expr()->like(
|
||||
'title',
|
||||
$event->queryBuilder->createNamedParameter($searchFilterWildcard)
|
||||
)
|
||||
);
|
||||
$event->searchParts = $event->searchParts->with($searchWhereAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches in translated page titles if translation search is enabled.
|
||||
*
|
||||
* Performs a separate query to find translated pages (sys_language_uid > 0)
|
||||
* whose `title` or `nav_title` matches the search phrase. When matches are found,
|
||||
* the `l10n_parent` pages are added to search results with language information
|
||||
* stored in runtime cache.
|
||||
*
|
||||
* This allows finding pages like:
|
||||
* - Default page "Products" with German translation "Produkte"
|
||||
* - Searching for "Produkte" shows "Products" with label "Found in translation: German"
|
||||
*
|
||||
* Respects:
|
||||
* - User's language permissions (allowed_languages from user groups)
|
||||
* - TSConfig setting options.pageTree.searchInTranslatedPages
|
||||
* - User preference pageTree_searchInTranslatedPages
|
||||
*/
|
||||
#[AsEventListener('page-tree-translated-pages-filter')]
|
||||
public function addTranslatedPagesFilter(BeforePageTreeIsFilteredEvent $event): void
|
||||
{
|
||||
if (!$this->isTranslatedPagesSearchEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder = $this->createPreparedPagesQueryBuilder();
|
||||
$searchFilterWildcard = '%' . $event->queryBuilder->escapeLikeWildcards($event->searchPhrase) . '%';
|
||||
|
||||
$whereConditions = [
|
||||
$queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->like('title', $queryBuilder->createNamedParameter($searchFilterWildcard)),
|
||||
$queryBuilder->expr()->like('nav_title', $queryBuilder->createNamedParameter($searchFilterWildcard))
|
||||
),
|
||||
];
|
||||
|
||||
$translatedPages = $this->fetchTranslatedPages($queryBuilder, $whereConditions);
|
||||
$this->processTranslatedPages($event, $translatedPages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches "Search result" labels to pages that directly matched the search.
|
||||
*
|
||||
* A page "directly matched" if its language is 0 and:
|
||||
* - Its UID equals the numeric search phrase
|
||||
* - Its title or nav_title contains the search phrase (case-insensitive)
|
||||
*
|
||||
* Pages that matched via translations do NOT get this label - they
|
||||
* get the translation info label instead.
|
||||
*/
|
||||
#[AsEventListener('page-tree-add-search-result-label')]
|
||||
public function attachSearchResultLabel(AfterPageTreeItemsPreparedEvent $event): void
|
||||
{
|
||||
$searchPhrase = $event->getRequest()->getQueryParams()['q'] ?? '';
|
||||
if (trim($searchPhrase) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pageTree.searchResult') ?: 'Search result';
|
||||
$items = $event->getItems();
|
||||
$searchPhraseLower = mb_strtolower($searchPhrase);
|
||||
|
||||
$uriMatches = $this->runtimeCache->get(self::CACHE_IDENTIFIER_URI) ?: [];
|
||||
|
||||
foreach ($items as &$item) {
|
||||
$page = $item['_page'] ?? [];
|
||||
if (!is_array($page)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matchedDirectly = false;
|
||||
|
||||
// Check if search phrase is numeric and matches the page UID
|
||||
if (MathUtility::canBeInterpretedAsInteger($searchPhrase) && (int)$searchPhrase === (int)($page['uid'] ?? 0)) {
|
||||
$matchedDirectly = true;
|
||||
}
|
||||
|
||||
// Check if page title or nav_title contains the search phrase
|
||||
if (!$matchedDirectly) {
|
||||
$title = mb_strtolower((string)($page['title'] ?? ''));
|
||||
$navTitle = mb_strtolower((string)($page['nav_title'] ?? ''));
|
||||
|
||||
if (str_contains($title, $searchPhraseLower) || str_contains($navTitle, $searchPhraseLower)) {
|
||||
$matchedDirectly = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($item['labels'])) {
|
||||
$item['labels'] = [];
|
||||
}
|
||||
|
||||
if ($matchedDirectly) {
|
||||
$item['labels'][] = new Label(
|
||||
label: $label,
|
||||
color: self::SEARCH_RESULT_LABEL_COLOR,
|
||||
inheritByChildren: false,
|
||||
);
|
||||
}
|
||||
|
||||
// Through the populated uriMatches runtime cache, we check if the current item
|
||||
// was matched by its frontend URI
|
||||
if (array_key_exists($page['uid'], $uriMatches)) {
|
||||
$label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pageTree.found_in_frontend_uri') ?: $label;
|
||||
$item['labels'][] = new Label(
|
||||
label: $label,
|
||||
color: self::SEARCH_RESULT_LABEL_COLOR,
|
||||
inheritByChildren: false,
|
||||
);
|
||||
|
||||
// Also attach a label to indicate a translated URI
|
||||
if ($uriMatches[$page['uid']]['languageUid'] !== 0) {
|
||||
if ($uriMatches[$page['uid']]['languageName'] === '') {
|
||||
$label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pageTree.found_translation') ?: 'Found translation';
|
||||
} else {
|
||||
$label = sprintf(
|
||||
$this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pageTree.found_in_translation') ?: 'Found in translation: %s',
|
||||
$uriMatches[$page['uid']]['languageName']
|
||||
);
|
||||
}
|
||||
$item['labels'][] = new Label(
|
||||
label: $label,
|
||||
color: self::SEARCH_RESULT_LABEL_COLOR,
|
||||
inheritByChildren: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
$event->setItems($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches translation info labels to pages found via translated uid / content.
|
||||
*
|
||||
* Reads translation match information from runtime cache (populated during
|
||||
* the query building phase) and creates informative labels:
|
||||
*
|
||||
* - Single translation: "Found in translation: German"
|
||||
* - Multiple translations: "Found in multiple translations"
|
||||
*
|
||||
* The language name is resolved from the site configuration when possible.
|
||||
*
|
||||
* Priority: 1 (shown before regular search result labels)
|
||||
*/
|
||||
#[AsEventListener('page-tree-add-translation-status')]
|
||||
public function attachTranslationInfoLabel(AfterPageTreeItemsPreparedEvent $event): void
|
||||
{
|
||||
$searchPhrase = $event->getRequest()->getQueryParams()['q'] ?? '';
|
||||
if (trim($searchPhrase) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$items = $event->getItems();
|
||||
foreach ($items as &$item) {
|
||||
$translationLanguageUids = $item['_translationLanguageUids'] ?? [];
|
||||
|
||||
if (empty($translationLanguageUids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$page = $item['_page'] ?? [];
|
||||
if (!is_array($page) || !isset($page['uid'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pageUid = (int)$page['uid'];
|
||||
|
||||
// Determine label based on number of translations found
|
||||
$translationCount = count($translationLanguageUids);
|
||||
if ($translationCount === 1) {
|
||||
// Single translation - show language name
|
||||
$languageTag = $translationLanguageUids[0];
|
||||
$languageName = $this->getLanguageName($pageUid, $languageTag);
|
||||
if ($languageName === '') {
|
||||
$label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pageTree.found_translation') ?: 'Found translation';
|
||||
} else {
|
||||
$label = sprintf(
|
||||
$this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pageTree.found_in_translation') ?: 'Found in translation: %s',
|
||||
$languageName
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Multiple translations
|
||||
$label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pageTree.found_in_multiple_translations') ?: 'Found in multiple translations';
|
||||
}
|
||||
|
||||
// Add a label to highlight pages found via translation
|
||||
if (!isset($item['labels'])) {
|
||||
$item['labels'] = [];
|
||||
}
|
||||
$item['labels'][] = new Label(
|
||||
label: $label,
|
||||
color: self::SEARCH_RESULT_LABEL_COLOR,
|
||||
priority: 1,
|
||||
inheritByChildren: false,
|
||||
);
|
||||
}
|
||||
unset($item);
|
||||
$event->setItems($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find pages via their frontend URI
|
||||
*/
|
||||
#[AsEventListener(identifier: 'page-tree-frontend-uri-provider', after: 'page-tree-uid-provider')]
|
||||
public function addUidsFromSearchPhraseWithFrontendUri(BeforePageTreeIsFilteredEvent $event): void
|
||||
{
|
||||
if (!$this->isFrontendUriSearchEnabled()) {
|
||||
return;
|
||||
}
|
||||
$uriMatches = $this->runtimeCache->get(self::CACHE_IDENTIFIER_URI) ?: [];
|
||||
|
||||
// Extract possible frontend URIs from search string
|
||||
$searchPhrases = GeneralUtility::trimExplode(',', $event->searchPhrase, true);
|
||||
foreach ($searchPhrases as $searchPhrase) {
|
||||
if (str_starts_with($searchPhrase, 'http://') || str_starts_with($searchPhrase, 'https://')) {
|
||||
// If a search pattern uses "http(s)://...." then a frontend URL will be resolved.
|
||||
$resolvedPage = $this->siteUrlResolver->resolvePageUidAndLanguageBySiteUrl($searchPhrase);
|
||||
if ($resolvedPage !== null) {
|
||||
$event->searchUids[] = $resolvedPage['uid'];
|
||||
$uriMatches[$resolvedPage['uid']] = $resolvedPage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->runtimeCache->set(self::CACHE_IDENTIFIER_URI, $uriMatches);
|
||||
|
||||
// Event listeners after this one may reset this array to clear unwanted UID restrictions.
|
||||
$event->searchUids = array_unique($event->searchUids);
|
||||
}
|
||||
|
||||
private function createPreparedPagesQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches translated pages with common base conditions plus additional WHERE clauses.
|
||||
*
|
||||
* Applies standard conditions that all translation queries need:
|
||||
* - sys_language_uid > 0 (only translated pages)
|
||||
* - l10n_parent > 0 (must have a parent page)
|
||||
* - Workspace conditions (respects current workspace)
|
||||
* - Language restrictions (from user group permissions)
|
||||
*
|
||||
* @param array $additionalConditions Extra WHERE conditions (e.g., UID match or title LIKE)
|
||||
*/
|
||||
private function fetchTranslatedPages(QueryBuilder $queryBuilder, array $additionalConditions): array
|
||||
{
|
||||
$allowedLanguages = $this->getAllowedLanguagesForCurrentUser();
|
||||
$workspace = $this->getBackendUser()->workspace;
|
||||
|
||||
$workspaceCondition = $workspace === 0
|
||||
? $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
|
||||
: $queryBuilder->expr()->in('t3ver_wsid', $queryBuilder->createNamedParameter([0, $workspace], Connection::PARAM_INT_ARRAY));
|
||||
|
||||
$whereConditions = [
|
||||
$queryBuilder->expr()->gt('sys_language_uid', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->gt('l10n_parent', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$workspaceCondition,
|
||||
...$additionalConditions,
|
||||
];
|
||||
|
||||
if ($allowedLanguages !== []) {
|
||||
$whereConditions[] = $queryBuilder->expr()->in(
|
||||
'sys_language_uid',
|
||||
$queryBuilder->createNamedParameter($allowedLanguages, Connection::PARAM_INT_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
return $queryBuilder
|
||||
->select('l10n_parent', 'sys_language_uid')
|
||||
->from('pages')
|
||||
->where(...$whereConditions)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes translated page query results: adds parent UIDs and updates cache.
|
||||
*
|
||||
* For each translated page found:
|
||||
* 1. Adds the l10n_parent UID to event->searchUids (avoiding duplicates)
|
||||
* 2. Stores the language UID in runtime cache for later label attachment
|
||||
*
|
||||
* The runtime cache structure is:
|
||||
* [pageUid => [languageUid1, languageUid2, ...]]
|
||||
*
|
||||
* @param array $translatedPages Query results with l10n_parent and sys_language_uid
|
||||
*/
|
||||
private function processTranslatedPages(BeforePageTreeIsFilteredEvent $event, array $translatedPages): void
|
||||
{
|
||||
$translationMatches = $this->runtimeCache->get(self::CACHE_IDENTIFIER_TRANSLATION) ?: [];
|
||||
$addedParents = [];
|
||||
|
||||
foreach ($translatedPages as $translatedPage) {
|
||||
$l10nParent = (int)$translatedPage['l10n_parent'];
|
||||
$languageTag = $translatedPage['language_tag'] ?? '';
|
||||
|
||||
// Add parent UID to search results (avoiding duplicates)
|
||||
if (!isset($addedParents[$l10nParent])) {
|
||||
$event->searchUids[] = $l10nParent;
|
||||
$addedParents[$l10nParent] = true;
|
||||
}
|
||||
|
||||
// Store translation match in runtime cache
|
||||
if (!isset($translationMatches[$l10nParent])) {
|
||||
$translationMatches[$l10nParent] = [];
|
||||
}
|
||||
if (!in_array($languageTag, $translationMatches[$l10nParent], true)) {
|
||||
$translationMatches[$l10nParent][] = $languageTag;
|
||||
}
|
||||
}
|
||||
|
||||
$this->runtimeCache->set(self::CACHE_IDENTIFIER_TRANSLATION, $translationMatches);
|
||||
}
|
||||
|
||||
private function getLanguageName(int $pageUid, string $languageTag): string
|
||||
{
|
||||
try {
|
||||
$site = $this->siteFinder->getSiteByPageId($pageUid);
|
||||
foreach ($site->getAllLanguages() as $language) {
|
||||
if ($language->getLanguageTag() === $languageTag) {
|
||||
return $language->getTitle();
|
||||
}
|
||||
}
|
||||
return $languageTag;
|
||||
} catch (\Exception) {
|
||||
return $languageTag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if translation search is enabled for the current user.
|
||||
*
|
||||
* Checks:
|
||||
* - TSConfig options.pageTree.searchInTranslatedPages
|
||||
* - User preference pageTree_searchInTranslatedPages
|
||||
*/
|
||||
private function isTranslatedPagesSearchEnabled(): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
|
||||
// If feature is disabled, always return false
|
||||
$translationSearchAvailable = (bool)($backendUser->getTSConfig()['options.']['pageTree.']['searchInTranslatedPages'] ?? true);
|
||||
if (!$translationSearchAvailable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If feature is available, check user preference
|
||||
if (isset($backendUser->uc['pageTree_searchInTranslatedPages'])) {
|
||||
return (bool)$backendUser->uc['pageTree_searchInTranslatedPages'];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if frontend URI search is enabled for the current user.
|
||||
*
|
||||
* Checks:
|
||||
* - TSConfig options.pageTree.searchByFrontendUri
|
||||
* - User preference pageTree_searchByFrontendUri
|
||||
*/
|
||||
private function isFrontendUriSearchEnabled(): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
|
||||
// If feature is disabled, always return false
|
||||
$frontendUriSearchAvailable = (bool)($backendUser->getTSConfig()['options.']['pageTree.']['searchByFrontendUri'] ?? true);
|
||||
if (!$frontendUriSearchAvailable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If feature is available, check user preference
|
||||
if (isset($backendUser->uc['pageTree_searchByFrontendUri'])) {
|
||||
return (bool)$backendUser->uc['pageTree_searchByFrontendUri'];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getAllowedLanguagesForCurrentUser(): array
|
||||
{
|
||||
$allowedLanguages = trim($this->getBackendUser()->groupData['allowed_languages'] ?? '');
|
||||
return $allowedLanguages !== '' ? GeneralUtility::intExplode(',', $allowedLanguages) : [];
|
||||
}
|
||||
|
||||
private function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
<?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\Tree\Repository;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\DataHandling\PlainDataResolver;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Core\Versioning\VersionState;
|
||||
|
||||
/**
|
||||
* Fetches ALL pages in the page tree, possibly overlaid with the workspace
|
||||
* in a sorted way.
|
||||
*
|
||||
* This works agnostic of the Backend User, allows to be used in FE as well in the future.
|
||||
*
|
||||
* @internal this class is not public API yet, as it needs to be proven stable enough first.
|
||||
*/
|
||||
class PageTreeRepository
|
||||
{
|
||||
/**
|
||||
* Fields to be queried from the database
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected readonly array $fields;
|
||||
|
||||
/**
|
||||
* The fields array, quoted for repeated use in recursive pages queries, to avoid the need to newly
|
||||
* quote the fields for each single query (which can get really expensive for a large amount of fields)
|
||||
* @var string[]
|
||||
*/
|
||||
protected readonly array $quotedFields;
|
||||
|
||||
/**
|
||||
* The workspace ID to operate on
|
||||
*/
|
||||
protected readonly int $currentWorkspace;
|
||||
|
||||
/**
|
||||
* Full page tree when selected without permissions applied.
|
||||
*/
|
||||
protected array $fullPageTree = [];
|
||||
|
||||
protected readonly array $additionalQueryRestrictions;
|
||||
|
||||
protected ?string $additionalWhereClause = null;
|
||||
|
||||
protected ?int $languageFilter = null;
|
||||
|
||||
protected string $defaultLanguageTag;
|
||||
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
protected readonly FrontendInterface $runtimeCache;
|
||||
|
||||
/**
|
||||
* @param int $workspaceId the workspace ID to be checked for.
|
||||
* @param array $additionalFieldsToQuery an array with more fields that should be accessed.
|
||||
* @param array $additionalQueryRestrictions an array with more restrictions to add
|
||||
*/
|
||||
public function __construct(int $workspaceId = 0, array $additionalFieldsToQuery = [], array $additionalQueryRestrictions = [])
|
||||
{
|
||||
$this->defaultLanguageTag = \Local\Multilanguage\Service\DefaultLanguageTagService::getTag();
|
||||
$this->currentWorkspace = $workspaceId;
|
||||
$this->fields = array_merge([
|
||||
'uid',
|
||||
'pid',
|
||||
'sorting',
|
||||
'starttime',
|
||||
'endtime',
|
||||
'hidden',
|
||||
'fe_group',
|
||||
'title',
|
||||
'nav_title',
|
||||
'nav_hide',
|
||||
'php_tree_stop',
|
||||
'doktype',
|
||||
'is_siteroot',
|
||||
'module',
|
||||
'extendToSubpages',
|
||||
'content_from_pid',
|
||||
't3ver_oid',
|
||||
't3ver_wsid',
|
||||
't3ver_state',
|
||||
't3ver_stage',
|
||||
'perms_userid',
|
||||
'perms_user',
|
||||
'perms_groupid',
|
||||
'perms_group',
|
||||
'perms_everybody',
|
||||
'mount_pid',
|
||||
'shortcut',
|
||||
'shortcut_mode',
|
||||
'mount_pid_ol',
|
||||
'link',
|
||||
'sys_language_uid',
|
||||
'l10n_parent',
|
||||
'language_tag',
|
||||
], $additionalFieldsToQuery);
|
||||
$this->additionalQueryRestrictions = $additionalQueryRestrictions;
|
||||
|
||||
// @todo: use DI in the future
|
||||
$this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
|
||||
$this->runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
|
||||
$this->quotedFields = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('pages')
|
||||
->quoteIdentifiersForSelect($this->fields);
|
||||
}
|
||||
|
||||
public function setLanguageFilter(?int $languageUid): void
|
||||
{
|
||||
$this->languageFilter = $languageUid;
|
||||
}
|
||||
|
||||
public function setAdditionalWhereClause(string $additionalWhereClause): void
|
||||
{
|
||||
$this->additionalWhereClause = $additionalWhereClause;
|
||||
}
|
||||
|
||||
private function getLanguageCondition($queryBuilder): void
|
||||
{
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter($this->defaultLanguageTag)),
|
||||
$queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter('')),
|
||||
$queryBuilder->expr()->isNull('language_tag')
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation language UIDs that matched for a specific page UID
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function getTranslationMatches(int $pageUid): array
|
||||
{
|
||||
$translationMatches = $this->runtimeCache->get('pageTree_translationMatches') ?: [];
|
||||
return $translationMatches[$pageUid] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for this repository, to fetch the tree data for a page.
|
||||
* Basically the page record, plus all child pages and their child pages recursively, stored within "_children" item.
|
||||
*
|
||||
* @param int $entryPoint the page ID to fetch the tree for
|
||||
* @param callable|null $callback a callback to be used to check for permissions and filter out pages not to be included.
|
||||
*/
|
||||
public function getTree(
|
||||
int $entryPoint,
|
||||
?callable $callback = null,
|
||||
array $dbMounts = []
|
||||
): array {
|
||||
$this->fetchAllPages($dbMounts);
|
||||
if ($entryPoint === 0) {
|
||||
$tree = $this->fullPageTree;
|
||||
} else {
|
||||
$tree = $this->findInPageTree($entryPoint, $this->fullPageTree);
|
||||
}
|
||||
if (!empty($tree) && $callback !== null) {
|
||||
$this->applyCallbackToChildren($tree, $callback);
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes items from a tree based on a callback, usually used for permission checks
|
||||
*/
|
||||
protected function applyCallbackToChildren(array &$tree, callable $callback): void
|
||||
{
|
||||
if (!isset($tree['_children'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($tree['_children'] as $k => &$childPage) {
|
||||
if (!$callback($childPage)) {
|
||||
unset($tree['_children'][$k]);
|
||||
continue;
|
||||
}
|
||||
$this->applyCallbackToChildren($childPage, $callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page tree based on a given page record and a given depth
|
||||
*
|
||||
* @param array $pageTree The page record of the top level page you want to get the page tree of
|
||||
* @param int $depth Number of levels to fetch
|
||||
* @param ?array $entryPointIds entryPointIds to include (null in case no entry-points were provided)
|
||||
* @return array An array with page records and their children
|
||||
*/
|
||||
public function getTreeLevels(array $pageTree, int $depth, ?array $entryPointIds = null): array
|
||||
{
|
||||
$groupedAndSortedPagesByPid = [];
|
||||
// the method was called without any entry-point information
|
||||
if ($entryPointIds === null) {
|
||||
$parentPageIds = [$pageTree['uid']];
|
||||
// the method was called with entry-point information, that is not empty
|
||||
} elseif ($entryPointIds !== []) {
|
||||
$pageRecords = $this->getPageRecords($entryPointIds);
|
||||
$groupedAndSortedPagesByPid[$pageTree['uid']] = $pageRecords;
|
||||
$parentPageIds = $entryPointIds;
|
||||
}
|
||||
for ($i = 0; $i < $depth; $i++) {
|
||||
// stop in case the initial or recursive query did not have any pages
|
||||
if (empty($parentPageIds)) {
|
||||
break;
|
||||
}
|
||||
$pageRecords = $this->getChildPageRecords($parentPageIds);
|
||||
$groupedAndSortedPagesByPid = $this->groupAndSortPages($pageRecords, $groupedAndSortedPagesByPid);
|
||||
$parentPageIds = array_column($pageRecords, 'uid');
|
||||
}
|
||||
$this->addChildrenToPage($pageTree, $groupedAndSortedPagesByPid);
|
||||
return $pageTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful to get a list of pages, with a specific depth, e.g. to limit
|
||||
* a query to another table to a list of page IDs.
|
||||
*
|
||||
* @param int[] $entryPointIds
|
||||
*/
|
||||
public function getFlattenedPages(array $entryPointIds, int $depth): array
|
||||
{
|
||||
$allPageRecords = $this->getPageRecords($entryPointIds);
|
||||
$parentPageIds = $entryPointIds;
|
||||
for ($i = 0; $i < $depth; $i++) {
|
||||
if (empty($parentPageIds)) {
|
||||
break;
|
||||
}
|
||||
$pageRecords = $this->getChildPageRecords($parentPageIds);
|
||||
$parentPageIds = array_column($pageRecords, 'uid');
|
||||
$allPageRecords = array_merge($allPageRecords, $pageRecords);
|
||||
}
|
||||
return $allPageRecords;
|
||||
}
|
||||
|
||||
protected function getChildPageRecords(array $parentPageIds): array
|
||||
{
|
||||
return $this->getPageRecords([], $parentPageIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the page records based on the given page or parent page ids
|
||||
*/
|
||||
protected function getPageRecords(array $pageIds = [], array $parentPageIds = []): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->currentWorkspace));
|
||||
|
||||
if (!empty($this->additionalQueryRestrictions)) {
|
||||
foreach ($this->additionalQueryRestrictions as $additionalQueryRestriction) {
|
||||
$queryBuilder->getRestrictions()->add($additionalQueryRestriction);
|
||||
}
|
||||
}
|
||||
|
||||
$queryBuilder->getConcreteQueryBuilder()->select(...$this->quotedFields);
|
||||
$queryBuilder
|
||||
->from('pages')
|
||||
->where('1=1');
|
||||
$this->getLanguageCondition($queryBuilder);
|
||||
$queryBuilder
|
||||
// ensure deterministic sorting
|
||||
->orderBy('sorting', 'ASC')
|
||||
->addOrderBy('uid', 'ASC');
|
||||
|
||||
if (!empty($this->additionalWhereClause)) {
|
||||
$queryBuilder->andWhere(
|
||||
QueryHelper::stripLogicalOperatorPrefix($this->additionalWhereClause)
|
||||
);
|
||||
}
|
||||
|
||||
if (count($pageIds) > 0) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY))
|
||||
);
|
||||
}
|
||||
|
||||
if (count($parentPageIds) > 0) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter($parentPageIds, Connection::PARAM_INT_ARRAY))
|
||||
);
|
||||
}
|
||||
|
||||
$pageRecords = $queryBuilder
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
// This is necessary to resolve all IDs in a workspace
|
||||
if ($this->currentWorkspace !== 0 && !empty($pageRecords)) {
|
||||
$livePageIds = [];
|
||||
$movedPages = [];
|
||||
foreach ($pageRecords as $pageRecord) {
|
||||
$livePageIds[] = (int)$pageRecord['uid'];
|
||||
if (VersionState::tryFrom($pageRecord['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER) {
|
||||
$movedPages[$pageRecord['t3ver_oid']] = [
|
||||
'pid' => (int)$pageRecord['pid'],
|
||||
'sorting' => (int)$pageRecord['sorting'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve placeholders of workspace versions
|
||||
$resolver = GeneralUtility::makeInstance(
|
||||
PlainDataResolver::class,
|
||||
'pages',
|
||||
$livePageIds
|
||||
);
|
||||
$resolver->setWorkspaceId($this->currentWorkspace);
|
||||
$resolver->setKeepDeletePlaceholder(false);
|
||||
$resolver->setKeepMovePlaceholder(false);
|
||||
$resolver->setKeepLiveIds(false);
|
||||
$recordIds = $resolver->get();
|
||||
|
||||
if (!empty($recordIds)) {
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder->getConcreteQueryBuilder()->select(...$this->quotedFields);
|
||||
$pageRecords = $queryBuilder
|
||||
->from('pages')
|
||||
->where(
|
||||
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($recordIds, Connection::PARAM_INT_ARRAY))
|
||||
)
|
||||
// ensure deterministic sorting
|
||||
->orderBy('sorting', 'ASC')
|
||||
->addOrderBy('uid', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
foreach ($pageRecords as &$pageRecord) {
|
||||
if (VersionState::tryFrom($pageRecord['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER && !empty($movedPages[$pageRecord['t3ver_oid']])) {
|
||||
$pageRecord['uid'] = $pageRecord['t3ver_oid'];
|
||||
$pageRecord['sorting'] = (int)$movedPages[$pageRecord['t3ver_oid']]['sorting'];
|
||||
$pageRecord['pid'] = (int)$movedPages[$pageRecord['t3ver_oid']]['pid'];
|
||||
} elseif ((int)$pageRecord['t3ver_oid'] > 0) {
|
||||
$liveRecord = BackendUtility::getRecord('pages', $pageRecord['t3ver_oid']);
|
||||
$pageRecord['sorting'] = (int)$liveRecord['sorting'];
|
||||
$pageRecord['uid'] = (int)$liveRecord['uid'];
|
||||
$pageRecord['pid'] = (int)$liveRecord['pid'];
|
||||
}
|
||||
}
|
||||
unset($pageRecord);
|
||||
} else {
|
||||
$pageRecords = [];
|
||||
}
|
||||
}
|
||||
foreach ($pageRecords as &$pageRecord) {
|
||||
$pageRecord['uid'] = (int)$pageRecord['uid'];
|
||||
}
|
||||
|
||||
return $pageRecords;
|
||||
}
|
||||
|
||||
public function hasChildren(int $pid): bool
|
||||
{
|
||||
$pageRecords = $this->getChildPageRecords([$pid]);
|
||||
return !empty($pageRecords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all non-deleted pages, regardless of permissions (however, considers additionalQueryRestrictions and additionalWhereClause).
|
||||
* That's why it's internal.
|
||||
*
|
||||
* @return array the full page tree of the whole installation
|
||||
*/
|
||||
protected function fetchAllPages(array $dbMounts): array
|
||||
{
|
||||
if (!empty($this->fullPageTree)) {
|
||||
return $this->fullPageTree;
|
||||
}
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->currentWorkspace));
|
||||
|
||||
if (!empty($this->additionalQueryRestrictions)) {
|
||||
foreach ($this->additionalQueryRestrictions as $additionalQueryRestriction) {
|
||||
$queryBuilder->getRestrictions()->add($additionalQueryRestriction);
|
||||
}
|
||||
}
|
||||
|
||||
$queryBuilder->getConcreteQueryBuilder()->select(...$this->quotedFields);
|
||||
$query = $queryBuilder
|
||||
->from('pages')
|
||||
->where('1=1');
|
||||
$this->getLanguageCondition($queryBuilder);
|
||||
|
||||
if (!empty($this->additionalWhereClause)) {
|
||||
$queryBuilder->andWhere(
|
||||
QueryHelper::stripLogicalOperatorPrefix($this->additionalWhereClause)
|
||||
);
|
||||
}
|
||||
|
||||
$pageRecords = $query->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$ids = array_column($pageRecords, 'uid');
|
||||
foreach ($dbMounts as $mount) {
|
||||
$entryPointRootLine = BackendUtility::BEgetRootLine($mount, '', false, $this->fields);
|
||||
foreach ($entryPointRootLine as $page) {
|
||||
$pageId = (int)$page['uid'];
|
||||
if (in_array($pageId, $ids) || $pageId === 0) {
|
||||
continue;
|
||||
}
|
||||
$pageRecords[] = $page;
|
||||
$ids[] = $pageId;
|
||||
}
|
||||
}
|
||||
|
||||
$livePagePids = [];
|
||||
$movedPages = [];
|
||||
// This is necessary to resolve all IDs in a workspace
|
||||
if ($this->currentWorkspace !== 0 && !empty($pageRecords)) {
|
||||
$livePageIds = [];
|
||||
foreach ($pageRecords as $pageRecord) {
|
||||
$livePageIds[] = (int)$pageRecord['uid'];
|
||||
$livePagePids[(int)$pageRecord['uid']] = (int)$pageRecord['pid'];
|
||||
if (VersionState::tryFrom($pageRecord['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER) {
|
||||
$movedPages[$pageRecord['t3ver_oid']] = [
|
||||
'pid' => (int)$pageRecord['pid'],
|
||||
'sorting' => (int)$pageRecord['sorting'],
|
||||
];
|
||||
}
|
||||
}
|
||||
// Resolve placeholders of workspace versions
|
||||
$resolver = GeneralUtility::makeInstance(
|
||||
PlainDataResolver::class,
|
||||
'pages',
|
||||
$livePageIds
|
||||
);
|
||||
$resolver->setWorkspaceId($this->currentWorkspace);
|
||||
$resolver->setKeepDeletePlaceholder(false);
|
||||
$resolver->setKeepMovePlaceholder(false);
|
||||
$resolver->setKeepLiveIds(false);
|
||||
$recordIds = $resolver->get();
|
||||
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder->getConcreteQueryBuilder()->select(...$this->quotedFields);
|
||||
$pageRecords = $queryBuilder
|
||||
->from('pages')
|
||||
->where(
|
||||
$queryBuilder->expr()->in('uid', $recordIds)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
|
||||
// Now set up sorting, nesting (tree-structure) for all pages based on pid+sorting fields
|
||||
$groupedAndSortedPagesByPid = [];
|
||||
foreach ($pageRecords as $pageRecord) {
|
||||
$parentPageId = (int)$pageRecord['pid'];
|
||||
// In case this is a record from a workspace
|
||||
// The uid+pid of the live-version record is fetched
|
||||
// This is done in order to avoid fetching records again (e.g. via BackendUtility::workspaceOL()
|
||||
if ((int)$pageRecord['t3ver_oid'] > 0) {
|
||||
// When a move pointer is found, the pid+sorting of the versioned record should be used
|
||||
if (VersionState::tryFrom($pageRecord['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER && !empty($movedPages[$pageRecord['t3ver_oid']])) {
|
||||
$parentPageId = (int)$movedPages[$pageRecord['t3ver_oid']]['pid'];
|
||||
$pageRecord['sorting'] = (int)$movedPages[$pageRecord['t3ver_oid']]['sorting'];
|
||||
} else {
|
||||
// Just a record in a workspace (not moved etc)
|
||||
$parentPageId = (int)($livePagePids[$pageRecord['t3ver_oid']] ?? $pageRecord['pid']);
|
||||
}
|
||||
// this is necessary so the links to the modules are still pointing to the live IDs
|
||||
$pageRecord['uid'] = (int)$pageRecord['t3ver_oid'];
|
||||
$pageRecord['pid'] = $parentPageId;
|
||||
}
|
||||
|
||||
$sorting = (int)$pageRecord['sorting'];
|
||||
while (isset($groupedAndSortedPagesByPid[$parentPageId][$sorting])) {
|
||||
$sorting++;
|
||||
}
|
||||
$groupedAndSortedPagesByPid[$parentPageId][$sorting] = $pageRecord;
|
||||
}
|
||||
|
||||
$this->fullPageTree = [
|
||||
'uid' => 0,
|
||||
'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3',
|
||||
];
|
||||
$this->addChildrenToPage($this->fullPageTree, $groupedAndSortedPagesByPid);
|
||||
return $this->fullPageTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the property "_children" to a page record with the child pages
|
||||
*
|
||||
* @param array[] $groupedAndSortedPagesByPid
|
||||
*/
|
||||
protected function addChildrenToPage(array &$page, array &$groupedAndSortedPagesByPid): void
|
||||
{
|
||||
$page['_children'] = $groupedAndSortedPagesByPid[(int)$page['uid']] ?? [];
|
||||
ksort($page['_children']);
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(new AfterRawPageRowPreparedEvent($page, $this->currentWorkspace));
|
||||
$page = $event->getRawPage();
|
||||
foreach ($page['_children'] as &$child) {
|
||||
$this->addChildrenToPage($child, $groupedAndSortedPagesByPid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Looking for a page by traversing the tree
|
||||
*
|
||||
* @param int $pageId the page ID to search for
|
||||
* @param array $pages the page tree to look for the page
|
||||
* @return array Array of the tree data, empty array if nothing was found
|
||||
*/
|
||||
protected function findInPageTree(int $pageId, array $pages): array
|
||||
{
|
||||
foreach ($pages['_children'] as $childPage) {
|
||||
if ((int)$childPage['uid'] === $pageId) {
|
||||
return $childPage;
|
||||
}
|
||||
$result = $this->findInPageTree($pageId, $childPage);
|
||||
if (!empty($result)) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the page tree based on the given search filter
|
||||
*/
|
||||
public function fetchFilteredTree(string $searchFilter, array $allowedMountPointPageIds, string $additionalWhereClause): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
|
||||
if (!empty($this->additionalQueryRestrictions)) {
|
||||
foreach ($this->additionalQueryRestrictions as $additionalQueryRestriction) {
|
||||
$queryBuilder->getRestrictions()->add($additionalQueryRestriction);
|
||||
}
|
||||
}
|
||||
|
||||
$expressionBuilder = $queryBuilder->expr();
|
||||
|
||||
if ($this->currentWorkspace === 0) {
|
||||
// Only include records from live workspace
|
||||
$workspaceIdExpression = $expressionBuilder->eq('t3ver_wsid', 0);
|
||||
} else {
|
||||
// Include live records PLUS records from the given workspace
|
||||
$workspaceIdExpression = $expressionBuilder->in(
|
||||
't3ver_wsid',
|
||||
$queryBuilder->createNamedParameter([0, $this->currentWorkspace], Connection::PARAM_INT_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
$queryBuilder->getConcreteQueryBuilder()->select(...$this->quotedFields);
|
||||
$queryBuilder = $queryBuilder
|
||||
->from('pages')
|
||||
->where(
|
||||
$workspaceIdExpression,
|
||||
QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
|
||||
);
|
||||
$this->getLanguageCondition($queryBuilder);
|
||||
|
||||
// Clear translation matches cache for new search
|
||||
$this->runtimeCache->remove('pageTree_translationMatches');
|
||||
|
||||
// Allow to extend search parts and search uids
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforePageTreeIsFilteredEvent($expressionBuilder->or(), [], $searchFilter, $queryBuilder)
|
||||
);
|
||||
$searchParts = $event->searchParts;
|
||||
$searchUids = $event->searchUids;
|
||||
|
||||
if (!empty($searchUids)) {
|
||||
// Ensure that the LIVE id is also found
|
||||
if ($this->currentWorkspace > 0) {
|
||||
$uidConditions = [
|
||||
// Check for UID of live record
|
||||
$expressionBuilder->and(
|
||||
$expressionBuilder->in('uid', $queryBuilder->createNamedParameter($searchUids, Connection::PARAM_INT_ARRAY)),
|
||||
$expressionBuilder->eq('t3ver_wsid', 0),
|
||||
),
|
||||
// Check for UID of live record in versioned record
|
||||
$expressionBuilder->and(
|
||||
$expressionBuilder->in('t3ver_oid', $queryBuilder->createNamedParameter($searchUids, Connection::PARAM_INT_ARRAY)),
|
||||
$expressionBuilder->eq('t3ver_wsid', $queryBuilder->createNamedParameter($this->currentWorkspace, Connection::PARAM_INT)),
|
||||
),
|
||||
];
|
||||
// Check for UID for new or moved versioned record (only if searchFilter is numeric)
|
||||
if (MathUtility::canBeInterpretedAsInteger($searchFilter)) {
|
||||
$uidConditions[] = $expressionBuilder->and(
|
||||
$expressionBuilder->eq('uid', $queryBuilder->createNamedParameter((int)$searchFilter, Connection::PARAM_INT)),
|
||||
$expressionBuilder->eq('t3ver_oid', 0),
|
||||
$expressionBuilder->eq('t3ver_wsid', $queryBuilder->createNamedParameter($this->currentWorkspace, Connection::PARAM_INT)),
|
||||
);
|
||||
}
|
||||
$uidFilter = $expressionBuilder->or(...$uidConditions);
|
||||
} else {
|
||||
$uidFilter = $expressionBuilder->in('uid', $queryBuilder->createNamedParameter($searchUids, Connection::PARAM_INT_ARRAY));
|
||||
}
|
||||
$searchParts = $searchParts->with($uidFilter);
|
||||
}
|
||||
|
||||
$queryBuilder->andWhere($searchParts);
|
||||
$pageRecords = $queryBuilder
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$livePagePids = [];
|
||||
if ($this->currentWorkspace !== 0 && !empty($pageRecords)) {
|
||||
$livePageIds = [];
|
||||
foreach ($pageRecords as $pageRecord) {
|
||||
$livePageIds[] = (int)$pageRecord['uid'];
|
||||
$livePagePids[(int)$pageRecord['uid']] = (int)$pageRecord['pid'];
|
||||
if ((int)$pageRecord['t3ver_oid'] > 0) {
|
||||
$livePagePids[(int)$pageRecord['t3ver_oid']] = (int)$pageRecord['pid'];
|
||||
}
|
||||
if (VersionState::tryFrom($pageRecord['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER) {
|
||||
$movedPages[$pageRecord['t3ver_oid']] = [
|
||||
'pid' => (int)$pageRecord['pid'],
|
||||
'sorting' => (int)$pageRecord['sorting'],
|
||||
];
|
||||
}
|
||||
}
|
||||
// Resolve placeholders of workspace versions
|
||||
$resolver = GeneralUtility::makeInstance(
|
||||
PlainDataResolver::class,
|
||||
'pages',
|
||||
$livePageIds
|
||||
);
|
||||
$resolver->setWorkspaceId($this->currentWorkspace);
|
||||
$resolver->setKeepDeletePlaceholder(false);
|
||||
$resolver->setKeepMovePlaceholder(false);
|
||||
$resolver->setKeepLiveIds(false);
|
||||
$recordIds = $resolver->get();
|
||||
|
||||
$pageRecords = [];
|
||||
if (!empty($recordIds)) {
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder->getConcreteQueryBuilder()->select(...$this->quotedFields);
|
||||
$queryBuilder
|
||||
->from('pages')
|
||||
->where(
|
||||
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($recordIds, Connection::PARAM_INT_ARRAY))
|
||||
);
|
||||
$queryBuilder->andWhere($searchParts);
|
||||
$pageRecords = $queryBuilder
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
|
||||
$pages = [];
|
||||
foreach ($pageRecords as $pageRecord) {
|
||||
// In case this is a record from a workspace
|
||||
// The uid+pid of the live-version record is fetched
|
||||
// This is done in order to avoid fetching records again (e.g. via BackendUtility::workspaceOL()
|
||||
if ((int)$pageRecord['t3ver_oid'] > 0) {
|
||||
// This probably should also remove the live version
|
||||
$versionState = VersionState::tryFrom($pageRecord['t3ver_state'] ?? 0);
|
||||
if ($versionState === VersionState::DELETE_PLACEHOLDER) {
|
||||
continue;
|
||||
}
|
||||
// When a move pointer is found, the pid+sorting of the versioned record be used
|
||||
if ($versionState === VersionState::MOVE_POINTER && !empty($movedPages[$pageRecord['t3ver_oid']])) {
|
||||
$parentPageId = (int)$movedPages[$pageRecord['t3ver_oid']]['pid'];
|
||||
$pageRecord['sorting'] = (int)$movedPages[$pageRecord['t3ver_oid']]['sorting'];
|
||||
} else {
|
||||
// Just a record in a workspace (not moved etc)
|
||||
$parentPageId = (int)$livePagePids[$pageRecord['t3ver_oid']];
|
||||
}
|
||||
// this is necessary so the links to the modules are still pointing to the live IDs
|
||||
$pageRecord['uid'] = (int)$pageRecord['t3ver_oid'];
|
||||
$pageRecord['pid'] = $parentPageId;
|
||||
}
|
||||
$pages[(int)$pageRecord['uid']] = $pageRecord;
|
||||
}
|
||||
unset($pageRecords);
|
||||
|
||||
$pages = $this->filterPagesOnMountPoints($pages, $allowedMountPointPageIds);
|
||||
|
||||
$groupedAndSortedPagesByPid = $this->groupAndSortPages($pages);
|
||||
|
||||
$this->fullPageTree = [
|
||||
'uid' => 0,
|
||||
'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3',
|
||||
];
|
||||
$this->addChildrenToPage($this->fullPageTree, $groupedAndSortedPagesByPid);
|
||||
|
||||
return $this->fullPageTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all records outside of the allowed mount points
|
||||
*/
|
||||
protected function filterPagesOnMountPoints(array $pages, array $mountPoints): array
|
||||
{
|
||||
foreach ($pages as $key => $pageRecord) {
|
||||
$rootline = BackendUtility::BEgetRootLine(
|
||||
$pageRecord['uid'],
|
||||
'',
|
||||
$this->currentWorkspace !== 0,
|
||||
$this->fields
|
||||
);
|
||||
$rootline = array_reverse($rootline);
|
||||
if (!in_array(0, $mountPoints, true)) {
|
||||
$isInsideMountPoints = false;
|
||||
foreach ($rootline as $rootlineElement) {
|
||||
if (in_array((int)$rootlineElement['uid'], $mountPoints, true)) {
|
||||
$isInsideMountPoints = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$isInsideMountPoints) {
|
||||
unset($pages[$key]);
|
||||
//skip records outside of the allowed mount points
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$inFilteredRootline = false;
|
||||
$amountOfRootlineElements = count($rootline);
|
||||
for ($i = 0; $i < $amountOfRootlineElements; ++$i) {
|
||||
$rootlineElement = $rootline[$i];
|
||||
$rootlineElement['uid'] = (int)$rootlineElement['uid'];
|
||||
$isInWebMount = false;
|
||||
if ($rootlineElement['uid'] > 0) {
|
||||
$isInWebMount = (int)$this->getBackendUser()->isInWebMount($rootlineElement);
|
||||
}
|
||||
|
||||
if (!$isInWebMount
|
||||
|| ($rootlineElement['uid'] === (int)$mountPoints[0]
|
||||
&& $rootlineElement['uid'] !== $isInWebMount)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if ($this->getBackendUser()->isAdmin() || ($rootlineElement['uid'] === $isInWebMount && in_array($rootlineElement['uid'], $mountPoints, true))) {
|
||||
$inFilteredRootline = true;
|
||||
}
|
||||
if (!$inFilteredRootline) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($pages[$rootlineElement['uid']])) {
|
||||
$pages[$rootlineElement['uid']] = $rootlineElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make sure the mountpoints show up in page tree even when parent pages are not accessible pages
|
||||
foreach ($mountPoints as $mountPoint) {
|
||||
if ($mountPoint !== 0) {
|
||||
if (!array_key_exists($mountPoint, $pages)) {
|
||||
$pages[$mountPoint] = BackendUtility::getRecordWSOL('pages', $mountPoint);
|
||||
$pages[$mountPoint]['uid'] = (int)$pages[$mountPoint]['uid'];
|
||||
}
|
||||
$pages[$mountPoint]['pid'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group pages by parent page and sort pages based on sorting property
|
||||
*/
|
||||
protected function groupAndSortPages(array $pages, array $groupedAndSortedPagesByPid = []): array
|
||||
{
|
||||
foreach ($pages as $pageRecord) {
|
||||
$parentPageId = (int)$pageRecord['pid'];
|
||||
$sorting = (int)$pageRecord['sorting'];
|
||||
// If the page record was already added in another depth level, don't add it another time.
|
||||
// This may happen, if entry points are intersecting each other (Entry point B is inside entry point A).
|
||||
if (($groupedAndSortedPagesByPid[$parentPageId][$sorting]['uid'] ?? 0) === $pageRecord['uid']) {
|
||||
continue;
|
||||
}
|
||||
while (isset($groupedAndSortedPagesByPid[$parentPageId][$sorting])) {
|
||||
$sorting++;
|
||||
}
|
||||
$groupedAndSortedPagesByPid[$parentPageId][$sorting] = $pageRecord;
|
||||
}
|
||||
|
||||
return $groupedAndSortedPagesByPid;
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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\Backend\Tree;
|
||||
|
||||
/**
|
||||
* Sorted Tree Node Collection
|
||||
*
|
||||
* Note: This collection works only with integers as offset keys and not
|
||||
* with much datasets. You have been warned!
|
||||
*/
|
||||
class SortedTreeNodeCollection extends TreeNodeCollection
|
||||
{
|
||||
/**
|
||||
* Checks if a specific node is inside the collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function contains(TreeNode $node)
|
||||
{
|
||||
return $this->offsetOf($node) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset key of given node
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function offsetOf(TreeNode $node)
|
||||
{
|
||||
return $this->binarySearch($node, 0, $this->count() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary search that returns the offset of a given node
|
||||
*
|
||||
* @param int $start
|
||||
* @param int $end
|
||||
* @return int
|
||||
*/
|
||||
protected function binarySearch(TreeNode $node, $start, $end)
|
||||
{
|
||||
if (!$start && $end - $start >= 2 || $end - $start > 2) {
|
||||
$divider = (int)ceil(($end - $start) / 2);
|
||||
if ($this->offsetGet($divider)->equals($node)) {
|
||||
return $divider;
|
||||
}
|
||||
if ($this->offsetGet($divider)->compareTo($node) > 0) {
|
||||
return $this->binarySearch($node, $start, $divider - 1);
|
||||
}
|
||||
return $this->binarySearch($node, $divider + 1, $end);
|
||||
}
|
||||
if ($this->offsetGet($start)->equals($node)) {
|
||||
return $start;
|
||||
}
|
||||
if ($this->offsetGet($end)->equals($node)) {
|
||||
return $end;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the array by reordering the keys
|
||||
*/
|
||||
protected function normalize()
|
||||
{
|
||||
$nodes = [];
|
||||
foreach ($this as $node) {
|
||||
$nodes[] = $node;
|
||||
}
|
||||
$this->exchangeArray($nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a node to the internal list in a sorted approach
|
||||
*
|
||||
* @param TreeNode $node
|
||||
*/
|
||||
public function append($node): void
|
||||
{
|
||||
parent::append($node);
|
||||
$this->asort();
|
||||
$this->normalize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?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\Backend\Tree;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Tree Node
|
||||
*/
|
||||
class TreeNode implements ComparableNodeInterface
|
||||
{
|
||||
/**
|
||||
* Node Identifier
|
||||
*
|
||||
* @var string|int
|
||||
*/
|
||||
protected $id = '';
|
||||
|
||||
/**
|
||||
* Parent Node
|
||||
*
|
||||
* @var TreeNode|null
|
||||
*/
|
||||
protected $parentNode;
|
||||
|
||||
/**
|
||||
* Child Nodes
|
||||
*
|
||||
* @var TreeNodeCollection|null
|
||||
*/
|
||||
protected $childNodes;
|
||||
|
||||
/**
|
||||
* @internal This is part of the category tree performance hack.
|
||||
*/
|
||||
protected array $additionalData = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* You can move an initial data array to initialize the instance and further objects.
|
||||
* This is useful for the deserialization.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
if ($data !== []) {
|
||||
$this->dataFromArray($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the child nodes collection
|
||||
*/
|
||||
public function setChildNodes(TreeNodeCollection $childNodes)
|
||||
{
|
||||
$this->childNodes = $childNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes child nodes collection
|
||||
*/
|
||||
public function removeChildNodes()
|
||||
{
|
||||
if ($this->childNodes !== null) {
|
||||
$this->childNodes = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns child nodes collection
|
||||
*
|
||||
* @return TreeNodeCollection
|
||||
*/
|
||||
public function getChildNodes()
|
||||
{
|
||||
return $this->childNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the node has child nodes attached
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildNodes()
|
||||
{
|
||||
if ($this->childNodes !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the identifier
|
||||
*
|
||||
* @param string|int $id
|
||||
*/
|
||||
public function setId($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier
|
||||
*
|
||||
* @return string|int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent node
|
||||
*
|
||||
* @param TreeNode|null $parentNode
|
||||
*/
|
||||
public function setParentNode(?TreeNode $parentNode = null)
|
||||
{
|
||||
$this->parentNode = $parentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent node
|
||||
*
|
||||
* @return TreeNode
|
||||
*/
|
||||
public function getParentNode()
|
||||
{
|
||||
return $this->parentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a node if it's identical to another node by the id property.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function equals(TreeNode $other)
|
||||
{
|
||||
return $this->id == $other->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a node to another one.
|
||||
*
|
||||
* Returns:
|
||||
* 1 if its greater than the other one
|
||||
* -1 if its smaller than the other one
|
||||
* 0 if its equal
|
||||
*
|
||||
* @param TreeNode $other
|
||||
* @return int See description above
|
||||
*/
|
||||
public function compareTo($other)
|
||||
{
|
||||
if ($this->equals($other)) {
|
||||
return 0;
|
||||
}
|
||||
return $this->id > $other->getId() ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal This is part of the category tree performance hack
|
||||
*/
|
||||
public function getAdditionalData(): array
|
||||
{
|
||||
return $this->additionalData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the node in an array representation that can be used for serialization
|
||||
*
|
||||
* @param bool $addChildNodes
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($addChildNodes = true)
|
||||
{
|
||||
$arrayRepresentation = [
|
||||
'serializeClassName' => static::class,
|
||||
'id' => $this->id,
|
||||
];
|
||||
if ($this->parentNode !== null) {
|
||||
$arrayRepresentation['parentNode'] = $this->parentNode->toArray(false);
|
||||
} else {
|
||||
$arrayRepresentation['parentNode'] = '';
|
||||
}
|
||||
if ($this->hasChildNodes() && $addChildNodes) {
|
||||
$arrayRepresentation['childNodes'] = $this->childNodes->toArray();
|
||||
} else {
|
||||
$arrayRepresentation['childNodes'] = '';
|
||||
}
|
||||
return $arrayRepresentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets data of the node by a given data array
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function dataFromArray($data)
|
||||
{
|
||||
$this->setId($data['id'] ?? $data['uid']);
|
||||
if (isset($data['parentNode']) && $data['parentNode'] !== '') {
|
||||
/** @var TreeNode $parentNode */
|
||||
$parentNode = GeneralUtility::makeInstance($data['parentNode']['serializeClassName'], $data['parentNode']);
|
||||
$this->setParentNode($parentNode);
|
||||
}
|
||||
if (isset($data['childNodes']) && $data['childNodes'] !== '') {
|
||||
/** @var TreeNodeCollection $childNodes */
|
||||
$childNodes = GeneralUtility::makeInstance($data['childNodes']['serializeClassName'], $data['childNodes']);
|
||||
$this->setChildNodes($childNodes);
|
||||
}
|
||||
// @todo: This is part of the category tree performance hack
|
||||
$this->additionalData = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns class state to be serialized.
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the current node with the given serialized information
|
||||
*
|
||||
* @throws Exception if the deserialized object type is not identical to the current one
|
||||
*/
|
||||
public function __unserialize(array $arrayRepresentation): void
|
||||
{
|
||||
if ($arrayRepresentation['serializeClassName'] !== static::class) {
|
||||
throw new Exception('Deserialized object type is not identical!', 1294586646);
|
||||
}
|
||||
$this->dataFromArray($arrayRepresentation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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\Backend\Tree;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Tree Node Collection
|
||||
*/
|
||||
class TreeNodeCollection extends \ArrayObject
|
||||
{
|
||||
/**
|
||||
* You can move an initial data array to initialize the instance and further objects.
|
||||
* This is useful for the deserialization.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
parent::__construct();
|
||||
if (!empty($data)) {
|
||||
$this->dataFromArray($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the internal nodes array
|
||||
*
|
||||
* @param int $flags Optional parameter, ignored. Added to be compatible with asort method signature in PHP 8.
|
||||
*/
|
||||
public function asort(int $flags = SORT_REGULAR): true
|
||||
{
|
||||
$this->uasort($this->nodeCompare(...));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a node with another one
|
||||
*
|
||||
* @see \TYPO3\CMS\Backend\Tree\TreeNode::compareTo
|
||||
* @internal
|
||||
*/
|
||||
public function nodeCompare(TreeNode $node, TreeNode $otherNode): int
|
||||
{
|
||||
return $node->compareTo($otherNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns class state to be serialized.
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the current node with the given serialized information
|
||||
*
|
||||
* @throws Exception if the deserialized object type is not identical to the current one
|
||||
*/
|
||||
public function __unserialize($data): void
|
||||
{
|
||||
if ($data['serializeClassName'] !== static::class) {
|
||||
throw new Exception('Deserialized object type is not identical!', 1294586647);
|
||||
}
|
||||
$this->dataFromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collection in an array representation for e.g. serialization
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$arrayRepresentation = [
|
||||
'serializeClassName' => static::class,
|
||||
];
|
||||
$iterator = $this->getIterator();
|
||||
while ($iterator->valid()) {
|
||||
$arrayRepresentation[] = $iterator->current()->toArray();
|
||||
$iterator->next();
|
||||
}
|
||||
return $arrayRepresentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data of the node collection by a given array
|
||||
*/
|
||||
public function dataFromArray(array $data): void
|
||||
{
|
||||
unset($data['serializeClassName']);
|
||||
foreach ($data as $index => $nodeArray) {
|
||||
$node = GeneralUtility::makeInstance($nodeArray['serializeClassName'], $nodeArray);
|
||||
$this->offsetSet($index, $node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?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\Backend\Tree;
|
||||
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
|
||||
/**
|
||||
* Representation Tree Node
|
||||
*/
|
||||
class TreeRepresentationNode extends TreeNode
|
||||
{
|
||||
/**
|
||||
* Node Label
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label = '';
|
||||
|
||||
/**
|
||||
* Node Type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = '';
|
||||
|
||||
/**
|
||||
* Node CSS Class
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class = '';
|
||||
|
||||
/**
|
||||
* Node Icon
|
||||
*
|
||||
* @var string|Icon
|
||||
*/
|
||||
protected $icon = '';
|
||||
|
||||
/**
|
||||
* Callback function that is called e.g after a click on the label
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $callbackAction = '';
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
*/
|
||||
public function setClass($class)
|
||||
{
|
||||
$this->class = $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getClass()
|
||||
{
|
||||
return $this->class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|Icon $icon
|
||||
*/
|
||||
public function setIcon($icon)
|
||||
{
|
||||
$this->icon = $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|Icon
|
||||
*/
|
||||
public function getIcon()
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $label
|
||||
*/
|
||||
public function setLabel($label)
|
||||
{
|
||||
$this->label = $label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLabel()
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callback action
|
||||
*
|
||||
* @param string $callbackAction
|
||||
*/
|
||||
public function setCallbackAction($callbackAction)
|
||||
{
|
||||
$this->callbackAction = $callbackAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the callback action
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCallbackAction()
|
||||
{
|
||||
return $this->callbackAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the node in an array representation that can be used for serialization
|
||||
*
|
||||
* @param bool $addChildNodes
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($addChildNodes = true)
|
||||
{
|
||||
$arrayRepresentation = parent::toArray();
|
||||
$arrayRepresentation = array_merge($arrayRepresentation, [
|
||||
'label' => $this->label,
|
||||
'type' => $this->type,
|
||||
'class' => $this->class,
|
||||
'icon' => $this->icon,
|
||||
'callbackAction' => $this->callbackAction,
|
||||
]);
|
||||
return $arrayRepresentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets data of the node by a given data array
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function dataFromArray($data)
|
||||
{
|
||||
parent::dataFromArray($data);
|
||||
$this->setLabel($data['label']);
|
||||
$this->setType($data['type']);
|
||||
$this->setClass($data['class']);
|
||||
$this->setIcon($data['icon']);
|
||||
$this->setCallbackAction($data['callbackAction']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
<?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\Tree\View;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
abstract class AbstractContentPagePositionMap
|
||||
{
|
||||
/**
|
||||
* Can be set to the language id to select content elements for.
|
||||
*/
|
||||
public int $cur_sys_language = 0;
|
||||
|
||||
protected BackendLayoutView $backendLayoutView;
|
||||
|
||||
public function __construct(BackendLayoutView $backendLayoutView)
|
||||
{
|
||||
$this->backendLayoutView = $backendLayoutView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a linked position icon
|
||||
*
|
||||
* @param array|null $row The record row. If this is an array the link will cause an insert after this
|
||||
* content element, otherwise the link will insert at the first position in the column.
|
||||
* @param int $colPos Column position value.
|
||||
* @param int $pid PID value.
|
||||
* @return string HTML
|
||||
*/
|
||||
abstract protected function insertPositionIcon(?array $row, int $colPos, int $pid): string;
|
||||
|
||||
/**
|
||||
* Create content element header (includes record type (CType) icon, content element title, etc.)
|
||||
*
|
||||
* @param array $row The element row
|
||||
* @return string HTML
|
||||
*/
|
||||
abstract protected function getRecordHeader(array $row): string;
|
||||
|
||||
/**
|
||||
* Creates HTML for inserting/moving content elements.
|
||||
*
|
||||
* @param int $pid page id onto which to insert content element.
|
||||
* @return string HTML
|
||||
*/
|
||||
public function printContentElementColumns(int $pid, array $pageInfo, ServerRequestInterface $request): string
|
||||
{
|
||||
$lines = [];
|
||||
$columnsConfiguration = $this->getColumnsConfiguration($pid);
|
||||
foreach ($columnsConfiguration as $columnConfiguration) {
|
||||
if ($columnConfiguration['isRestricted']) {
|
||||
// Do not fetch records of restricted columns
|
||||
continue;
|
||||
}
|
||||
$colPos = $columnConfiguration['colPos'];
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
|
||||
$queryBuilder
|
||||
->getRestrictions()
|
||||
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace))
|
||||
->removeByType(HiddenRestriction::class)
|
||||
->removeByType(StartTimeRestriction::class)
|
||||
->removeByType(EndTimeRestriction::class);
|
||||
$queryBuilder
|
||||
->select('*')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->eq('colPos', $queryBuilder->createNamedParameter($colPos, Connection::PARAM_INT))
|
||||
)
|
||||
->orderBy('sorting');
|
||||
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_language_uid',
|
||||
$queryBuilder->createNamedParameter($this->cur_sys_language, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
|
||||
$res = $queryBuilder->executeQuery();
|
||||
$lines[$colPos] = [
|
||||
$this->insertPositionIcon(null, $colPos, $pid),
|
||||
];
|
||||
|
||||
while ($row = $res->fetchAssociative()) {
|
||||
BackendUtility::workspaceOL('tt_content', $row);
|
||||
if (is_array($row)) {
|
||||
$lines[$colPos][] = $this->getRecordHeader($row);
|
||||
$lines[$colPos][] = $this->insertPositionIcon($row, $colPos, $pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->printRecordMap($lines, $columnsConfiguration, $pid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the table with the content columns
|
||||
*
|
||||
* @param array $lines Array with arrays of lines for each column
|
||||
* @param array $tcaColumnsConfiguration Column configuration array
|
||||
* @param int $pid The id of the page
|
||||
* @return string HTML
|
||||
*/
|
||||
protected function printRecordMap(array $lines, array $tcaColumnsConfiguration, int $pid): string
|
||||
{
|
||||
$lang = $this->getLanguageService();
|
||||
$hideRestrictedColumns = (bool)(BackendUtility::getPagesTSconfig($pid)['mod.']['web_layout.']['hideRestrictedCols'] ?? false);
|
||||
$backendLayout = $this->backendLayoutView->getSelectedBackendLayout($pid);
|
||||
|
||||
if (isset($backendLayout['__config']['backend_layout.'])) {
|
||||
// Build position map based on the fetched backend layout
|
||||
$colCount = (int)($backendLayout['__config']['backend_layout.']['colCount'] ?? 0);
|
||||
$rowCount = (int)($backendLayout['__config']['backend_layout.']['rowCount'] ?? 0);
|
||||
|
||||
// Cycle through rows
|
||||
$tableRows = [];
|
||||
for ($row = 1; $row <= $rowCount; $row++) {
|
||||
$rowConfig = $backendLayout['__config']['backend_layout.']['rows.'][$row . '.'] ?? null;
|
||||
if (!$rowConfig) {
|
||||
// Skip empty rows
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cycle through cells
|
||||
$tableCells = [];
|
||||
for ($col = 1; $col <= $colCount; $col++) {
|
||||
$columnConfig = $rowConfig['columns.'][$col . '.'] ?? null;
|
||||
if (!$columnConfig) {
|
||||
// Skip empty columns
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set table cell attributes
|
||||
$tableCellAttributes = [
|
||||
'class' => 'col-nowrap col-min',
|
||||
];
|
||||
if (isset($columnConfig['colspan'])) {
|
||||
$tableCellAttributes['colspan'] = $columnConfig['colspan'];
|
||||
}
|
||||
if (isset($columnConfig['rowspan'])) {
|
||||
$tableCellAttributes['rowspan'] = $columnConfig['rowspan'];
|
||||
}
|
||||
|
||||
$columnKey = null;
|
||||
$columnTitle = '';
|
||||
$isRestricted = false;
|
||||
$isUnassigned = true;
|
||||
if (isset($columnConfig['colPos'])) {
|
||||
// If colPos is defined, initialize column information (e.g. title and restricted state)
|
||||
$columnKey = (int)$columnConfig['colPos'];
|
||||
foreach ($tcaColumnsConfiguration as $tcaColumnConfiguration) {
|
||||
if ($tcaColumnConfiguration['colPos'] === $columnKey) {
|
||||
$columnTitle = '<strong>' . htmlspecialchars($lang->sL($tcaColumnConfiguration['title'])) . '</strong>';
|
||||
$isRestricted = $tcaColumnConfiguration['isRestricted'];
|
||||
$isUnassigned = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the cell content, based on the columns' state (e.g. restricted or unassigned)
|
||||
$cellContent = '';
|
||||
if ($isRestricted) {
|
||||
if ($hideRestrictedColumns) {
|
||||
// Hide in case this column is not accessible and hideRestrictedColumns is set
|
||||
$tableCellAttributes['class'] = 'hidden';
|
||||
} else {
|
||||
$cellContent = '
|
||||
<p class="column-title">
|
||||
' . $columnTitle . ' <em>(' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:noAccess')) . ')</em>
|
||||
</p>';
|
||||
$tableCellAttributes['class'] .= ' danger';
|
||||
}
|
||||
} elseif ($isUnassigned) {
|
||||
if ($hideRestrictedColumns) {
|
||||
// Hide in case this column is not assigned and hideRestrictedColumns is set
|
||||
$tableCellAttributes['class'] = 'hidden';
|
||||
} else {
|
||||
$cellContent = '
|
||||
<em>
|
||||
' . htmlspecialchars($lang->sL($columnConfig['name']) ?: '') . '
|
||||
' . ' (' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:notAssigned')) . ')' . '
|
||||
</em>';
|
||||
$tableCellAttributes['class'] .= ' warning';
|
||||
}
|
||||
} else {
|
||||
// If not restricted and not unassigned, wrap column title and render list (if available)
|
||||
$cellContent = '<p class="column-title">' . $columnTitle . '</p>';
|
||||
if (!empty($lines[$columnKey])) {
|
||||
$cellContent .= '
|
||||
<ul>
|
||||
' . implode(LF, array_map(static fn(string $line): string => '<li>' . $line . '</li>', $lines[$columnKey])) . '
|
||||
</ul>';
|
||||
}
|
||||
}
|
||||
|
||||
// Add the table cell
|
||||
$tableCells[] = '<td ' . GeneralUtility::implodeAttributes($tableCellAttributes) . '>' . $cellContent . '</td>';
|
||||
}
|
||||
|
||||
// Add the table row
|
||||
$tableRows[] = '<tr>' . implode(LF, $tableCells) . '</tr>';
|
||||
}
|
||||
|
||||
// Create the table content
|
||||
$tableContent
|
||||
= '<colgroup>' . str_repeat('<col span="1" style="width: calc(100% / ' . $colCount . ')">', $colCount) . '</colgroup>'
|
||||
. '<tbody>' . implode(LF, $tableRows) . '</tbody>';
|
||||
} else {
|
||||
// Build position map based on TCA colPos configuration
|
||||
$tableCells = [];
|
||||
foreach ($tcaColumnsConfiguration as $tcaColumnConfiguration) {
|
||||
if ($hideRestrictedColumns && $tcaColumnConfiguration['isRestricted']) {
|
||||
// Skip in case this column is not accessible and restricted columns should be hidden
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate the cell content, based on the columns' state (e.g. restricted or unassigned)
|
||||
$tableCellClasses = 'col-nowrap col-min';
|
||||
$columnTitle = '<strong>' . htmlspecialchars($tcaColumnConfiguration['title']) . '</strong>';
|
||||
if ($tcaColumnConfiguration['isRestricted']) {
|
||||
// If this colPos is restricted, add an information to the column title and color the cell
|
||||
$tableCellClasses .= ' danger';
|
||||
$cellContent = '
|
||||
<p class="column-title">
|
||||
' . $columnTitle . ' <em>(' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:noAccess')) . ')</em>
|
||||
</p>';
|
||||
} else {
|
||||
// If not restricted, wrap column title and render list (if available)
|
||||
$cellContent = '<p class="column-title">' . $columnTitle . '</p>';
|
||||
if (!empty($lines[$tcaColumnConfiguration['colPos']])) {
|
||||
$cellContent .= '
|
||||
<ul>
|
||||
' . implode(LF, array_map(static fn(string $line): string => '<li>' . $line . '</li>', $lines[$tcaColumnConfiguration['colPos']])) . '
|
||||
</ul>';
|
||||
}
|
||||
}
|
||||
|
||||
// Add the table cell
|
||||
$tableCells[] = '<td class="' . $tableCellClasses . '">' . $cellContent . '</td>';
|
||||
}
|
||||
|
||||
// Create the table content
|
||||
$tableContent = '<tbody><tr>' . implode(LF, $tableCells) . '</tr></tbody>';
|
||||
}
|
||||
|
||||
// Return the record map (table)
|
||||
return '
|
||||
<table class="page-position-grid">
|
||||
' . $tableContent . '
|
||||
</table>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch TCA colPos list from BackendLayoutView and prepare for map generation.
|
||||
* This also takes the "colPos_list" TSconfig into account.
|
||||
*/
|
||||
protected function getColumnsConfiguration(int $pageId): array
|
||||
{
|
||||
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageId);
|
||||
$items = [];
|
||||
// Prepare the columns configuration (using named keys, etc.)
|
||||
foreach ($backendLayout->getUsedColumns() as $colPos => $label) {
|
||||
$items[] = [
|
||||
'title' => $label,
|
||||
'colPos' => (int)$colPos,
|
||||
'isRestricted' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$sharedColPosList = trim(BackendUtility::getPagesTSconfig($pageId)['mod.']['SHARED.']['colPos_list'] ?? '');
|
||||
if ($sharedColPosList !== '') {
|
||||
$activeColPosArray = array_unique(GeneralUtility::intExplode(',', $sharedColPosList));
|
||||
if (!empty($items) && !empty($activeColPosArray)) {
|
||||
foreach ($items as &$item) {
|
||||
if (!in_array((int)$item['colPos'], $activeColPosArray, true)) {
|
||||
$item['isRestricted'] = true;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
<?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\Tree\View;
|
||||
|
||||
use Doctrine\DBAL\Result;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconProvider\AbstractSvgIconProvider;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Base class for creating a browsable array/page/folder tree in HTML
|
||||
*
|
||||
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
abstract class AbstractTreeView
|
||||
{
|
||||
/**
|
||||
* Database table to get the tree data from.
|
||||
* Leave blank if data comes from an array.
|
||||
*/
|
||||
protected string $table = 'pages';
|
||||
|
||||
/**
|
||||
* Defines the field of $table which is the parent id field (like pid for table pages).
|
||||
*/
|
||||
protected string $parentField = 'pid';
|
||||
|
||||
/**
|
||||
* WHERE clause used for selecting records for the tree. Is set by function init.
|
||||
*
|
||||
* @see init()
|
||||
*/
|
||||
protected string $clause = '';
|
||||
|
||||
/**
|
||||
* Field for ORDER BY. Is set by function init.
|
||||
*
|
||||
* @see init()
|
||||
*/
|
||||
public string $orderByFields = 'sorting';
|
||||
|
||||
/**
|
||||
* Default set of fields selected from the tree table.
|
||||
* Make SURE that these fields names listed herein are actually possible to select from $this->table (if that variable is set to a TCA table name)
|
||||
*
|
||||
* @see addField()
|
||||
*/
|
||||
protected array $fieldArray = [
|
||||
'uid',
|
||||
'pid',
|
||||
'title',
|
||||
'is_siteroot',
|
||||
'doktype',
|
||||
'nav_title',
|
||||
'mount_pid',
|
||||
'php_tree_stop',
|
||||
't3ver_state',
|
||||
'hidden',
|
||||
'starttime',
|
||||
'endtime',
|
||||
'fe_group',
|
||||
'module',
|
||||
'extendToSubpages',
|
||||
'nav_hide',
|
||||
't3ver_wsid',
|
||||
'crdate',
|
||||
'tstamp',
|
||||
'sorting',
|
||||
'deleted',
|
||||
'perms_userid',
|
||||
'perms_groupid',
|
||||
'perms_user',
|
||||
'perms_group',
|
||||
'perms_everybody',
|
||||
'editlock',
|
||||
'l18n_cfg',
|
||||
];
|
||||
|
||||
/**
|
||||
* If true, HTML code is also accumulated in ->tree array during rendering of the tree
|
||||
*/
|
||||
public bool $makeHTML = true;
|
||||
|
||||
// *********
|
||||
// Internal
|
||||
// *********
|
||||
// For record trees:
|
||||
// one-dim array of the uid's selected.
|
||||
protected array $ids = [];
|
||||
|
||||
// The hierarchy of element uids
|
||||
protected array $ids_hierarchy = [];
|
||||
|
||||
// The hierarchy of versioned element uids
|
||||
public array $orig_ids_hierarchy = [];
|
||||
|
||||
// Temporary, internal array
|
||||
public array $buffer_idH = [];
|
||||
|
||||
// For both types
|
||||
// Tree is accumulated in this variable
|
||||
public array $tree = [];
|
||||
|
||||
/**
|
||||
* @param string $clause Record WHERE clause
|
||||
* @param string $orderByFields Record ORDER BY field
|
||||
*/
|
||||
public function init($clause = '', $orderByFields = '')
|
||||
{
|
||||
if ($clause) {
|
||||
$this->clause = $clause;
|
||||
}
|
||||
if ($orderByFields) {
|
||||
$this->orderByFields = $orderByFields;
|
||||
}
|
||||
}
|
||||
|
||||
public function addField(string $field): void
|
||||
{
|
||||
$this->fieldArray[] = $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the tree, recs, ids, ids_hierarchy and orig_ids_hierarchy internal variables. Use it if you need it.
|
||||
*/
|
||||
protected function reset(): void
|
||||
{
|
||||
$this->tree = [];
|
||||
$this->ids = [];
|
||||
$this->ids_hierarchy = [];
|
||||
$this->orig_ids_hierarchy = [];
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
*
|
||||
* rendering parts
|
||||
*
|
||||
*******************************************/
|
||||
/**
|
||||
* Generate the plus/minus icon for the browsable tree.
|
||||
*
|
||||
* @param array $row Record for the entry
|
||||
* @param int $a The current entry number
|
||||
* @param int $c The total number of entries. If equal to $a, a "bottom" element is returned.
|
||||
* @param int $nextCount The number of sub-elements to the current element.
|
||||
* @param bool $isOpen The element was expanded to render subelements if this flag is set.
|
||||
* @return string Image tag with the plus/minus icon.
|
||||
* @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
|
||||
*/
|
||||
protected function PMicon($row, $a, $c, $nextCount, $isOpen)
|
||||
{
|
||||
if ($nextCount) {
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
|
||||
// Wrap the plus/minus icon in a link
|
||||
$anchor = $row['uid'] ? '#' . $row['uid'] : '';
|
||||
$name = $row['uid'] ? ' name="' . $row['uid'] . '"' : '';
|
||||
$aUrl = $anchor;
|
||||
if ($isOpen) {
|
||||
$class = 'treelist-control-open';
|
||||
$icon = $iconFactory->getIcon('actions-chevron-down', IconSize::SMALL);
|
||||
} else {
|
||||
$class = 'treelist-control-collapsed';
|
||||
$icon = $iconFactory->getIcon('actions-chevron-end', IconSize::SMALL);
|
||||
}
|
||||
return '<a class="treelist-control ' . $class . '" href="' . htmlspecialchars($aUrl) . '"' . $name . '>' . $icon->render(AbstractSvgIconProvider::MARKUP_IDENTIFIER_INLINE) . '</a>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
*
|
||||
* tree handling
|
||||
*
|
||||
*******************************************/
|
||||
/**
|
||||
* Returns TRUE/FALSE if the next level for $id should be expanded - based on
|
||||
* data in $this->stored[][] and ->expandAll flag.
|
||||
* Used in subclasses
|
||||
*
|
||||
* @param int $id Record id/key
|
||||
* @return bool
|
||||
* @internal
|
||||
* @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::expandNext()
|
||||
*/
|
||||
public function expandNext($id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/******************************
|
||||
*
|
||||
* Functions that might be overwritten by extended classes
|
||||
*
|
||||
********************************/
|
||||
|
||||
/**
|
||||
* Get the icon markup for the row
|
||||
*
|
||||
* @param array $row The row to get the icon for
|
||||
* @return string The icon markup, wrapped into a span tag, with the records title as title attribute
|
||||
*/
|
||||
protected function getIcon(array $row): string
|
||||
{
|
||||
$title = $this->getTitleAttrib($row);
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$icon = $row['is_siteroot'] ? $iconFactory->getIcon('apps-pagetree-folder-root', IconSize::SMALL) : $iconFactory->getIconForRecord($this->table, $row, IconSize::SMALL);
|
||||
return $icon->setTitle($title)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the image "title" attribute
|
||||
*
|
||||
* @param array $row The input row array (where the key "title" is used for the title)
|
||||
* @return string The attribute value (is htmlspecialchared() already)
|
||||
*/
|
||||
protected function getTitleAttrib($row)
|
||||
{
|
||||
return htmlspecialchars($row['title']);
|
||||
}
|
||||
|
||||
/********************************
|
||||
*
|
||||
* tree data building
|
||||
*
|
||||
********************************/
|
||||
/**
|
||||
* Fetches the data for the tree
|
||||
*
|
||||
* @param int $uid item id for which to select subitems (parent id)
|
||||
* @param int $depth Max depth (recursivity limit)
|
||||
* @param string $depthData HTML-code prefix for recursive calls.
|
||||
* @return int<0, max> The count of items on the level
|
||||
*/
|
||||
public function getTree(int $uid, int $depth = 999, string $depthData = ''): int
|
||||
{
|
||||
// Buffer for id hierarchy is reset:
|
||||
$this->buffer_idH = [];
|
||||
// Init vars
|
||||
$HTML = '';
|
||||
$a = 0;
|
||||
$res = $this->getDataInit($uid);
|
||||
$c = $res->rowCount();
|
||||
$crazyRecursionLimiter = 9999;
|
||||
$idH = [];
|
||||
// Traverse the records:
|
||||
while ($crazyRecursionLimiter > 0 && ($row = $this->getDataNext($res))) {
|
||||
if (!$this->getBackendUser()->isInWebMount($this->table === 'pages' ? $row : $row['pid'])) {
|
||||
// Current record is not within web mount => skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
$a++;
|
||||
$crazyRecursionLimiter--;
|
||||
$newID = $row['uid'];
|
||||
if ($newID == 0) {
|
||||
throw new \RuntimeException('Endless recursion detected: TYPO3 has detected an error in the database. Please fix it manually (e.g. using phpMyAdmin) and change the UID of ' . $this->table . ':0 to a new value. See https://forge.typo3.org/issues/16150 to get more information about a possible cause.', 1294586383);
|
||||
}
|
||||
// Reserve space.
|
||||
$this->tree[] = [];
|
||||
end($this->tree);
|
||||
// Get the key for this space
|
||||
$treeKey = key($this->tree);
|
||||
// Accumulate the id of the element in the internal arrays
|
||||
$this->ids[] = ($idH[$row['uid']]['uid'] = $row['uid']);
|
||||
$this->ids_hierarchy[$depth][] = $row['uid'];
|
||||
$this->orig_ids_hierarchy[$depth][] = (!empty($row['_ORIG_uid'])) ? $row['_ORIG_uid'] : $row['uid'];
|
||||
|
||||
// Make a recursive call to the next level
|
||||
$nextLevelDepthData = $depthData . '<span class="treeline-icon treeline-icon-' . ($a === $c ? 'clear' : 'line') . '"></span>';
|
||||
$hasSub = $this->expandNext($newID) && !($row['php_tree_stop'] ?? false);
|
||||
if ($depth > 1 && $hasSub) {
|
||||
$nextCount = $this->getTree($newID, $depth - 1, $nextLevelDepthData);
|
||||
if (!empty($this->buffer_idH)) {
|
||||
$idH[$row['uid']]['subrow'] = $this->buffer_idH;
|
||||
}
|
||||
// Set "did expand" flag
|
||||
$isOpen = true;
|
||||
} else {
|
||||
$nextCount = $this->getCount((int)$newID);
|
||||
// Clear "did expand" flag
|
||||
$isOpen = false;
|
||||
}
|
||||
// Set HTML-icons, if any:
|
||||
if ($this->makeHTML) {
|
||||
$HTML = $this->PMicon($row, $a, $c, $nextCount, $isOpen);
|
||||
}
|
||||
// Finally, add the row/HTML content to the ->tree array in the reserved key.
|
||||
$this->tree[$treeKey] = [
|
||||
'row' => $row,
|
||||
'HTML' => $HTML,
|
||||
'icon' => $this->getIcon($row),
|
||||
'invertedDepth' => $depth,
|
||||
'depthData' => $depthData,
|
||||
'hasSub' => $nextCount && $hasSub,
|
||||
'isFirst' => $a === 1,
|
||||
'isLast' => $a === $c,
|
||||
];
|
||||
}
|
||||
|
||||
$res->free();
|
||||
$this->buffer_idH = $idH;
|
||||
return $c;
|
||||
}
|
||||
|
||||
/********************************
|
||||
*
|
||||
* Data handling
|
||||
* Works with records and arrays
|
||||
*
|
||||
********************************/
|
||||
/**
|
||||
* Returns the number of records having the parent id, $uid
|
||||
*
|
||||
* @param int $uid Id to count subitems for
|
||||
*/
|
||||
protected function getCount(int $uid): int
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
|
||||
$count = $queryBuilder
|
||||
->count('uid')
|
||||
->from($this->table)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
$this->parentField,
|
||||
$queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)
|
||||
),
|
||||
QueryHelper::stripLogicalOperatorPrefix($this->clause)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
|
||||
return (int)$count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting the tree data: Selecting/Initializing data pointer to items for a certain parent id.
|
||||
* For tables: This will make a database query to select all children to "parent"
|
||||
*/
|
||||
protected function getDataInit(int $parentId): Result
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
|
||||
$queryBuilder
|
||||
->select(...$this->fieldArray)
|
||||
->from($this->table)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
$this->parentField,
|
||||
$queryBuilder->createNamedParameter($parentId, Connection::PARAM_INT)
|
||||
),
|
||||
QueryHelper::stripLogicalOperatorPrefix($this->clause)
|
||||
);
|
||||
|
||||
foreach (QueryHelper::parseOrderBy($this->orderByFields) as $orderPair) {
|
||||
[$fieldName, $order] = $orderPair;
|
||||
$queryBuilder->addOrderBy($fieldName, $order);
|
||||
}
|
||||
|
||||
return $queryBuilder->executeQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting the tree data: next entry
|
||||
*
|
||||
* @see getDataInit()
|
||||
*/
|
||||
protected function getDataNext(Result $res): array|false
|
||||
{
|
||||
while ($row = $res->fetchAssociative()) {
|
||||
BackendUtility::workspaceOL($this->table, $row, $this->getBackendUser()->workspace, true);
|
||||
if (is_array($row)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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\Tree\View;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Position map class for creating content elements
|
||||
*
|
||||
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class ContentCreationPagePositionMap extends AbstractContentPagePositionMap
|
||||
{
|
||||
/**
|
||||
* Default values defined for the item
|
||||
*/
|
||||
public array $defVals = [];
|
||||
|
||||
/**
|
||||
* Whether the item should directly be persisted (avoiding FormEngine)
|
||||
*/
|
||||
public bool $saveAndClose = false;
|
||||
|
||||
/**
|
||||
* The return url, forwarded to FormEngine (or SimpleDataHandler)
|
||||
*/
|
||||
public string $R_URI = '';
|
||||
|
||||
protected IconFactory $iconFactory;
|
||||
protected UriBuilder $uriBuilder;
|
||||
|
||||
public function __construct(IconFactory $iconFactory, UriBuilder $uriBuilder, BackendLayoutView $backendLayoutView)
|
||||
{
|
||||
$this->iconFactory = $iconFactory;
|
||||
$this->uriBuilder = $uriBuilder;
|
||||
parent::__construct($backendLayoutView);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function insertPositionIcon(?array $row, int $colPos, int $pid): string
|
||||
{
|
||||
if ($this->saveAndClose) {
|
||||
$target = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [
|
||||
'data' => [
|
||||
'tt_content' => [
|
||||
StringUtility::getUniqueId('NEW') => array_replace($this->defVals, [
|
||||
'colPos' => $colPos,
|
||||
'pid' => (is_array($row) ? -$row['uid'] : $pid),
|
||||
'sys_language_uid' => $this->cur_sys_language,
|
||||
]),
|
||||
],
|
||||
],
|
||||
'redirect' => $this->R_URI,
|
||||
]);
|
||||
} else {
|
||||
// @todo pass module context to this handler and pass to record_edit here
|
||||
$target = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
|
||||
'edit' => [
|
||||
'tt_content' => [
|
||||
(is_array($row) ? -$row['uid'] : $pid) => 'new',
|
||||
],
|
||||
],
|
||||
'returnUrl' => $this->R_URI,
|
||||
'defVals' => [
|
||||
'tt_content' => array_replace($this->defVals, [
|
||||
'colPos' => $colPos,
|
||||
'sys_language_uid' => $this->cur_sys_language,
|
||||
]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$buttonLabel = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:insertNewRecordHere'));
|
||||
return '
|
||||
<div class="page-position-action">
|
||||
<button type="button" class="btn btn-default btn-sm" data-target="' . htmlspecialchars($target) . '" title="' . $buttonLabel . '">
|
||||
' . $this->iconFactory->getIcon('actions-arrow-left-alt', IconSize::SMALL)->render() . ' ' . $buttonLabel . '
|
||||
</button>
|
||||
</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getRecordHeader(array $row): string
|
||||
{
|
||||
return '
|
||||
<div class="page-position-record">
|
||||
<span title="' . BackendUtility::getRecordIconAltText($row, 'tt_content') . '">
|
||||
' . $this->iconFactory->getIconForRecord('tt_content', $row, IconSize::SMALL)->render() . '
|
||||
' . BackendUtility::getRecordTitle('tt_content', $row, true) . '
|
||||
</span>
|
||||
</div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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\Tree\View;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Position map class for moving content elements
|
||||
*
|
||||
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class ContentMovingPagePositionMap extends AbstractContentPagePositionMap
|
||||
{
|
||||
/**
|
||||
* The move uid
|
||||
*/
|
||||
public int $moveUid = 0;
|
||||
|
||||
/**
|
||||
* The copy mode (either "move" or "copy")
|
||||
*/
|
||||
public string $copyMode = 'move';
|
||||
|
||||
protected IconFactory $iconFactory;
|
||||
|
||||
public function __construct(IconFactory $iconFactory, BackendLayoutView $backendLayoutView)
|
||||
{
|
||||
$this->iconFactory = $iconFactory;
|
||||
parent::__construct($backendLayoutView);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function insertPositionIcon(?array $row, int $colPos, int $pid): string
|
||||
{
|
||||
if (is_array($row)) {
|
||||
$attributes = [
|
||||
'data-action' => 'paste',
|
||||
'data-position' => '-' . $row['uid'],
|
||||
'data-colpos' => $colPos,
|
||||
];
|
||||
} else {
|
||||
$attributes = [
|
||||
'data-action' => 'paste',
|
||||
'data-position' => $pid,
|
||||
'data-colpos' => $colPos,
|
||||
];
|
||||
}
|
||||
$buttonLabelTransUnit = $this->copyMode === 'move' ? 'moveElementToHere' : 'copyElementToHere';
|
||||
$buttonLabel = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:' . $buttonLabelTransUnit));
|
||||
return '
|
||||
<div class="page-position-action">
|
||||
<button class="btn btn-default" title="' . $buttonLabel . '" ' . GeneralUtility::implodeAttributes($attributes, true) . '>
|
||||
' . $this->iconFactory->getIcon('actions-arrow-left-alt', IconSize::SMALL)->render() . ' <span class="t3js-button-label">' . $buttonLabel . '</span>
|
||||
</button>
|
||||
</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create record header (includes the record icon, record title etc.)
|
||||
*
|
||||
* @param array $row Record row.
|
||||
* @return string HTML
|
||||
*/
|
||||
protected function getRecordHeader(array $row): string
|
||||
{
|
||||
return '
|
||||
<div class="page-position-record">
|
||||
<span title="' . BackendUtility::getRecordIconAltText($row, 'tt_content') . '">
|
||||
' . $this->iconFactory->getIconForRecord('tt_content', $row, IconSize::SMALL)->render() . '
|
||||
' . ($this->moveUid === (int)$row['uid'] ? '<strong>' : '') . '
|
||||
' . BackendUtility::getRecordTitle('tt_content', $row, true) . '
|
||||
' . ($this->moveUid === (int)$row['uid'] ? '</strong>' : '') . '
|
||||
</span>
|
||||
</div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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\Tree\View;
|
||||
|
||||
interface LinkParameterProviderInterface
|
||||
{
|
||||
/**
|
||||
* Provides an array or GET parameters for URL generation
|
||||
*
|
||||
* @param array $values Array of values to include into the parameters or which might influence the parameters
|
||||
* @return string[] Array of parameters which have to be added to URLs
|
||||
*/
|
||||
public function getUrlParameters(array $values): array;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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\Tree\View;
|
||||
|
||||
/**
|
||||
* Generate a page-tree, non-browsable.
|
||||
*
|
||||
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class PageTreeView extends AbstractTreeView
|
||||
{
|
||||
protected ?int $currentPageId = null;
|
||||
|
||||
/**
|
||||
* Init function
|
||||
* REMEMBER to feed a $clause which will filter out non-readable pages!
|
||||
*
|
||||
* @param string $clause Part of where query which will filter out non-readable pages.
|
||||
* @param string $orderByFields Record ORDER BY field
|
||||
*/
|
||||
public function init($clause = '', $orderByFields = '')
|
||||
{
|
||||
$tag = \Local\Multilanguage\Service\DefaultLanguageTagService::getTag();
|
||||
parent::init(" AND deleted=0 AND (language_tag='" . $tag . "' OR language_tag='') " . $clause, $orderByFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE/FALSE if the next level for $id should be expanded - and all levels should, so we always return true.
|
||||
* Here the branch is expanded if the current id matches the global id for the listing/new
|
||||
*
|
||||
* @param int $id ID (uid) to test for
|
||||
* @return bool
|
||||
*/
|
||||
public function expandNext($id)
|
||||
{
|
||||
if ($this->currentPageId !== null) {
|
||||
return (int)$id === $this->currentPageId;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the plus/minus icon for the browsable tree.
|
||||
* In this case, there is no plus-minus icon displayed.
|
||||
*
|
||||
* @param array $row Record for the entry
|
||||
* @param int $a The current entry number
|
||||
* @param int $c The total number of entries. If equal to $a, a 'bottom' element is returned.
|
||||
* @param int $nextCount The number of sub-elements to the current element.
|
||||
* @param bool $isOpen The element was expanded to render subelements if this flag is set.
|
||||
* @return string Image tag with the plus/minus icon.
|
||||
* @see AbstractTreeView::PMicon()
|
||||
*/
|
||||
protected function PMicon($row, $a, $c, $nextCount, $isOpen)
|
||||
{
|
||||
return '<span class="treeline-icon treeline-icon-join' . ($a == $c ? 'bottom' : '') . '"></span>';
|
||||
}
|
||||
|
||||
public function setCurrentPageId(int $currentPageId): void
|
||||
{
|
||||
$this->currentPageId = $currentPageId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user