first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
commit 1218dd1517
12716 files changed
+989233

No files matched your search

@@ -0,0 +1,74 @@
<?php
namespace TYPO3\CMS\Backend\Tree;
/*
* 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!
*/
/**
* 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;
/**
* @param \TYPO3\CMS\Backend\Tree\AbstractTreeDataProvider $dataProvider
*/
public function setDataProvider(\TYPO3\CMS\Backend\Tree\AbstractTreeDataProvider $dataProvider)
{
$this->dataProvider = $dataProvider;
}
/**
* @return \TYPO3\CMS\Backend\Tree\AbstractTreeDataProvider
*/
public function getDataProvider()
{
return $this->dataProvider;
}
/**
* @param \TYPO3\CMS\Backend\Tree\Renderer\AbstractTreeRenderer $nodeRenderer
*/
public function setNodeRenderer(\TYPO3\CMS\Backend\Tree\Renderer\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,43 @@
<?php
namespace TYPO3\CMS\Backend\Tree;
/*
* 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!
*/
/**
* Abstract Tree Data Provider
*/
abstract class AbstractTreeDataProvider
{
/**
* Root Node
*
* @var \TYPO3\CMS\Backend\Tree\TreeNode
*/
protected $rootNode;
/**
* Returns the root node
*
* @return \TYPO3\CMS\Backend\Tree\TreeNode
*/
abstract public function getRoot();
/**
* Fetches the subnodes of the given node
*
* @param \TYPO3\CMS\Backend\Tree\TreeNode $node
* @return \TYPO3\CMS\Backend\Tree\TreeNodeCollection
*/
abstract public function getNodes(\TYPO3\CMS\Backend\Tree\TreeNode $node);
}
@@ -0,0 +1,45 @@
<?php
namespace TYPO3\CMS\Backend\Tree;
/*
* 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!
*/
/**
* 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,48 @@
<?php
namespace TYPO3\CMS\Backend\Tree\Renderer;
/*
* 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!
*/
/**
* Abstract Renderer
*/
abstract class AbstractTreeRenderer
{
/**
* Renders a node recursive or just a single instance
*
* @param \TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node
* @param bool $recursive
* @return mixed
*/
abstract public function renderNode(\TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node, $recursive = true);
/**
* Renders a node collection recursive or just a single instance
*
* @param \TYPO3\CMS\Backend\Tree\TreeNodeCollection $collection
* @param bool $recursive
* @return mixed
*/
abstract public function renderNodeCollection(\TYPO3\CMS\Backend\Tree\TreeNodeCollection $collection, $recursive = true);
/**
* Renders an tree recursive or just a single instance
*
* @param \TYPO3\CMS\Backend\Tree\AbstractTree $tree
* @param bool $recursive
* @return mixed
*/
abstract public function renderTree(\TYPO3\CMS\Backend\Tree\AbstractTree $tree, $recursive = true);
}
@@ -0,0 +1,80 @@
<?php
namespace TYPO3\CMS\Backend\Tree\Renderer;
/*
* 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!
*/
/**
* Renderer for unordered lists
*/
class UnorderedListTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\AbstractTreeRenderer
{
/**
* recursion level
*
* @var int
*/
protected $recursionLevel = 0;
/**
* Renders a node recursive or just a single instance
*
* @param \TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node
* @param bool $recursive
* @return string
*/
public function renderNode(\TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node, $recursive = true)
{
$code = '<li><span class="' . htmlspecialchars($node->getIcon()) . '">&nbsp;</span>' . htmlspecialchars($node->getLabel());
if ($recursive && $node->getChildNodes() !== null) {
$this->recursionLevel++;
$code .= $this->renderNodeCollection($node->getChildNodes());
$this->recursionLevel--;
}
$code .= '</li>';
return $code;
}
/**
* Renders a node collection recursive or just a single instance
*
* @param \TYPO3\CMS\Backend\Tree\AbstractTree $tree
* @param bool $recursive
* @return string
*/
public function renderTree(\TYPO3\CMS\Backend\Tree\AbstractTree $tree, $recursive = true)
{
$this->recursionLevel = 0;
$code = '<ul class="level' . $this->recursionLevel . '" style="margin-left:10px">';
$code .= $this->renderNode($tree->getRoot(), $recursive);
$code .= '</ul>';
return $code;
}
/**
* Renders an tree recursive or just a single instance
*
* @param \TYPO3\CMS\Backend\Tree\TreeNodeCollection $collection
* @param bool $recursive
* @return string
*/
public function renderNodeCollection(\TYPO3\CMS\Backend\Tree\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,737 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Tree\Repository;
/*
* 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!
*/
use Doctrine\DBAL\Connection;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
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\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
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 $fields = [
'uid',
'pid',
'sorting',
'starttime',
'endtime',
'hidden',
'fe_group',
'title',
'nav_title',
'nav_hide',
'alias',
'php_tree_stop',
'doktype',
'is_siteroot',
'module',
'extendToSubpages',
'content_from_pid',
't3ver_oid',
't3ver_id',
't3ver_wsid',
't3ver_label',
't3ver_state',
't3ver_stage',
't3ver_tstamp',
't3ver_move_id',
'perms_userid',
'perms_user',
'perms_groupid',
'perms_group',
'perms_everybody',
'mount_pid',
'shortcut',
'shortcut_mode',
'mount_pid_ol',
'url',
'sys_language_uid',
'l10n_parent',
];
/**
* The workspace ID to operate on
*
* @var int
*/
protected $currentWorkspace = 0;
/**
* Full page tree when selected without permissions applied.
*
* @var array
*/
protected $fullPageTree = [];
/**
* @var array
*/
protected $additionalQueryRestrictions = [];
/**
* @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->currentWorkspace = $workspaceId;
if (!empty($additionalFieldsToQuery)) {
$this->fields = array_merge($this->fields, $additionalFieldsToQuery);
}
if (!empty($additionalQueryRestrictions)) {
$this->additionalQueryRestrictions = $additionalQueryRestrictions;
}
}
/**
* 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 $callback a callback to be used to check for permissions and filter out pages not to be included.
* @param array $dbMounts
* @param bool $resolveUserPermissions
* @return array
*/
public function getTree(
int $entryPoint,
callable $callback = null,
array $dbMounts = [],
$resolveUserPermissions = false
): array {
$this->fetchAllPages($dbMounts, $resolveUserPermissions);
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
*
* @param array $tree
* @param callable $callback
*/
protected function applyCallbackToChildren(array &$tree, callable $callback)
{
if (!isset($tree['_children'])) {
return;
}
foreach ($tree['_children'] as $k => &$childPage) {
if (!call_user_func_array($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
* @return array An array with page records and their children
*/
public function getTreeLevels(array $pageTree, int $depth): array
{
$parentPageIds = [$pageTree['uid']];
$groupedAndSortedPagesByPid = [];
for ($i = 0; $i < $depth; $i++) {
if (empty($parentPageIds)) {
break;
}
$pageRecords = $this->getChildPageRecords($parentPageIds);
$groupedAndSortedPagesByPid = $this->groupAndSortPages($pageRecords, $groupedAndSortedPagesByPid);
$parentPageIds = array_column($pageRecords, 'uid');
}
$this->addChildrenToPage($pageTree, $groupedAndSortedPagesByPid);
return $pageTree;
}
/**
* Retrieve the page records based on the given parent page ids
*
* @param array $parentPageIds
* @return array
*/
protected function getChildPageRecords(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);
}
}
$pageRecords = $queryBuilder
->select(...$this->fields)
->from('pages')
->where(
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter($parentPageIds, Connection::PARAM_INT_ARRAY))
)
->andWhere(
QueryHelper::stripLogicalOperatorPrefix($GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW))
)
->execute()
->fetchAll();
// This is necessary to resolve all IDs in a workspace
if ($this->currentWorkspace !== 0 && !empty($pageRecords)) {
$livePagePids = [];
$livePageIds = [];
$movePlaceholderData = [];
foreach ($pageRecords as $pageRecord) {
$livePageIds[] = (int)$pageRecord['uid'];
$livePagePids[(int)$pageRecord['uid']] = (int)$pageRecord['pid'];
if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_PLACEHOLDER) {
$movePlaceholderData[$pageRecord['t3ver_move_id']] = [
'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();
$pageRecords = $queryBuilder
->select(...$this->fields)
->from('pages')
->where(
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($recordIds, Connection::PARAM_INT_ARRAY))
)
->execute()
->fetchAll();
foreach ($pageRecords as &$pageRecord) {
if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_PLACEHOLDER) {
$liveRecord = BackendUtility::getRecord('pages', $pageRecord['t3ver_move_id']);
$pageRecord['uid'] = (int)$liveRecord['uid'];
$pageRecord['t3ver_oid'] = (int)$pageRecord['t3ver_move_id'];
$pageRecord['title'] = $liveRecord['title'];
} elseif ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_POINTER && !empty($movePlaceholderData[$pageRecord['t3ver_oid']])) {
$pageRecord['uid'] = $pageRecord['t3ver_oid'];
$pageRecord['sorting'] = (int)$movePlaceholderData[$pageRecord['t3ver_oid']]['sorting'];
$pageRecord['t3ver_state'] = VersionState::MOVE_PLACEHOLDER;
$pageRecord['pid'] = (int)$movePlaceholderData[$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'];
}
}
} 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. That's why it's internal.
*
* @param array $dbMounts
* @param bool $resolveUserPermissions
* @return array the full page tree of the whole installation
*/
protected function fetchAllPages(array $dbMounts, bool $resolveUserPermissions = false): 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);
}
}
$query = $queryBuilder
->select(...$this->fields)
->from('pages')
->where(
// Only show records in default language
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
);
if ($resolveUserPermissions) {
$query->andWhere(
QueryHelper::stripLogicalOperatorPrefix($GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW))
);
}
$pageRecords = $query->execute()->fetchAll();
$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 = [];
$movePlaceholderData = [];
// 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 ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_PLACEHOLDER) {
$movePlaceholderData[$pageRecord['t3ver_move_id']] = [
'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();
$pageRecords = $queryBuilder
->select(...$this->fields)
->from('pages')
->where(
$queryBuilder->expr()->in('uid', $recordIds)
)
->execute()
->fetchAll();
}
// 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 ($parentPageId === -1) {
// When a move pointer is found, the pid+sorting of the MOVE_PLACEHOLDER should be used (this is the
// workspace record holding this information), also the t3ver_state is set to the MOVE_PLACEHOLDER
// because the record is then added
if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_POINTER && !empty($movePlaceholderData[$pageRecord['t3ver_oid']])) {
$parentPageId = (int)$movePlaceholderData[$pageRecord['t3ver_oid']]['pid'];
$pageRecord['sorting'] = (int)$movePlaceholderData[$pageRecord['t3ver_oid']]['sorting'];
$pageRecord['t3ver_state'] = VersionState::MOVE_PLACEHOLDER;
} 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;
}
$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 $page
* @param array[] $groupedAndSortedPagesByPid
*/
protected function addChildrenToPage(array &$page, array &$groupedAndSortedPagesByPid)
{
$page['_children'] = $groupedAndSortedPagesByPid[(int)$page['uid']] ?? [];
ksort($page['_children']);
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
*
* @param string $searchFilter
* @param array $allowedMountPointPageIds
* @param string $additionalWhereClause
* @return array
*/
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 ws_id=0
$workspaceIdExpression = $expressionBuilder->eq('t3ver_wsid', 0);
} else {
// Include live records PLUS records from the given workspace
$workspaceIdExpression = $expressionBuilder->in(
't3ver_wsid',
[0, $this->currentWorkspace]
);
}
$queryBuilder = $queryBuilder
->select(...$this->fields)
->from('pages')
->where(
// Only show records in default language
$expressionBuilder->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$workspaceIdExpression,
QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
);
$searchParts = $expressionBuilder->orX();
if (is_numeric($searchFilter) && $searchFilter > 0) {
$searchParts->add(
$expressionBuilder->eq('uid', $queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_INT))
);
}
$searchFilter = '%' . $queryBuilder->escapeLikeWildcards($searchFilter) . '%';
$searchWhereAlias = $expressionBuilder->orX(
$expressionBuilder->like(
'nav_title',
$queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_STR)
),
$expressionBuilder->like(
'title',
$queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_STR)
)
);
$searchParts->add($searchWhereAlias);
$queryBuilder->andWhere($searchParts);
$pageRecords = $queryBuilder
->execute()
->fetchAll();
$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_state'] === VersionState::MOVE_POINTER) {
$movePlaceholder = BackendUtility::getMovePlaceholder('pages', $pageRecord['t3ver_oid'], '*', $this->currentWorkspace);
$movePlaceholderData[$pageRecord['t3ver_oid']] = [
'pid' => (int)$movePlaceholder['pid'],
'sorting' => (int)$movePlaceholder['sorting']
];
}
if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_PLACEHOLDER) {
$movePlaceholderData[$pageRecord['t3ver_move_id']] = [
'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
->select(...$this->fields)
->from('pages')
->where(
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($recordIds, Connection::PARAM_INT_ARRAY))
);
$queryBuilder->andWhere($searchParts);
$pageRecords = $queryBuilder
->execute()
->fetchAll();
}
}
$pages = [];
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 ($parentPageId === -1) {
// This probably should also remove the live version
if ((int)$pageRecord['t3ver_state'] === VersionState::DELETE_PLACEHOLDER) {
continue;
}
// When a move pointer is found, the pid+sorting of the MOVE_PLACEHOLDER should be used (this is the
// workspace record holding this information), also the t3ver_state is set to the MOVE_PLACEHOLDER
// because the record is then added
if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_POINTER && !empty($movePlaceholderData[$pageRecord['t3ver_oid']])) {
$parentPageId = (int)$movePlaceholderData[$pageRecord['t3ver_oid']]['pid'];
$pageRecord['sorting'] = (int)$movePlaceholderData[$pageRecord['t3ver_oid']]['sorting'];
$pageRecord['t3ver_state'] = VersionState::MOVE_PLACEHOLDER;
} else {
// Just a record in a workspace (not moved etc)
if (isset($livePagePids[$pageRecord['t3ver_oid']])) {
$parentPageId = (int)$livePagePids[$pageRecord['t3ver_oid']];
} else {
$liveRecord = BackendUtility::getRecord('pages', $pageRecord['t3ver_oid'], 'uid,pid');
$parentPageId = (int)$liveRecord['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;
}
$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
*
* @param array $pages
* @param array $mountPoints
* @return array
*/
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::getRecord('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
*
* @param array $pages
* @param array $groupedAndSortedPagesByPid
* @return array
*/
protected function groupAndSortPages(array $pages, $groupedAndSortedPagesByPid = []): array
{
foreach ($pages as $key => $pageRecord) {
$parentPageId = (int)$pageRecord['pid'];
$sorting = (int)$pageRecord['sorting'];
while (isset($groupedAndSortedPagesByPid[$parentPageId][$sorting])) {
$sorting++;
}
$groupedAndSortedPagesByPid[$parentPageId][$sorting] = $pageRecord;
}
return $groupedAndSortedPagesByPid;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,99 @@
<?php
namespace TYPO3\CMS\Backend\Tree;
/*
* 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!
*/
/**
* 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 \TYPO3\CMS\Backend\Tree\TreeNodeCollection
{
/**
* Checks if a specific node is inside the collection
*
* @param \TYPO3\CMS\Backend\Tree\TreeNode $node
* @return bool
*/
public function contains(\TYPO3\CMS\Backend\Tree\TreeNode $node)
{
return $this->offsetOf($node) !== -1;
}
/**
* Returns the offset key of given node
*
* @param \TYPO3\CMS\Backend\Tree\TreeNode $node
* @return int
*/
protected function offsetOf(\TYPO3\CMS\Backend\Tree\TreeNode $node)
{
return $this->binarySearch($node, 0, $this->count() - 1);
}
/**
* Binary search that returns the offset of a given node
*
* @param \TYPO3\CMS\Backend\Tree\TreeNode $node
* @param int $start
* @param int $end
* @return int
*/
protected function binarySearch(\TYPO3\CMS\Backend\Tree\TreeNode $node, $start, $end)
{
if (!$start && $end - $start >= 2 || $end - $start > 2) {
$divider = 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 \TYPO3\CMS\Backend\Tree\TreeNode $node
*/
public function append($node)
{
parent::append($node);
$this->asort();
$this->normalize();
}
}
@@ -0,0 +1,239 @@
<?php
namespace TYPO3\CMS\Backend\Tree;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Tree Node
*/
class TreeNode implements \TYPO3\CMS\Backend\Tree\ComparableNodeInterface, \Serializable
{
/**
* Node Identifier
*
* @var string
*/
protected $id = '';
/**
* Parent Node
*
* @var \TYPO3\CMS\Backend\Tree\TreeNode
*/
protected $parentNode;
/**
* Child Nodes
*
* @var \TYPO3\CMS\Backend\Tree\TreeNodeCollection
*/
protected $childNodes;
/**
* 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 (!empty($data)) {
$this->dataFromArray($data);
}
}
/**
* Sets the child nodes collection
*
* @param \TYPO3\CMS\Backend\Tree\TreeNodeCollection $childNodes
*/
public function setChildNodes(\TYPO3\CMS\Backend\Tree\TreeNodeCollection $childNodes)
{
$this->childNodes = $childNodes;
}
/**
* Removes child nodes collection
*/
public function removeChildNodes()
{
if ($this->childNodes !== null) {
unset($this->childNodes);
$this->childNodes = null;
}
}
/**
* Returns child nodes collection
*
* @return \TYPO3\CMS\Backend\Tree\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 $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* Returns the identifier
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Sets the parent node
*
* @param \TYPO3\CMS\Backend\Tree\TreeNode|null $parentNode
*/
public function setParentNode(\TYPO3\CMS\Backend\Tree\TreeNode $parentNode = null)
{
$this->parentNode = $parentNode;
}
/**
* Returns the parent node
*
* @return \TYPO3\CMS\Backend\Tree\TreeNode
*/
public function getParentNode()
{
return $this->parentNode;
}
/**
* Compares a node if it's identical to another node by the id property.
*
* @param \TYPO3\CMS\Backend\Tree\TreeNode $other
* @return bool
*/
public function equals(\TYPO3\CMS\Backend\Tree\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 \TYPO3\CMS\Backend\Tree\TreeNode $other
* @return int See description above
*/
public function compareTo($other)
{
if ($this->equals($other)) {
return 0;
}
return $this->id > $other->getId() ? 1 : -1;
}
/**
* 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']);
if (isset($data['parentNode']) && $data['parentNode'] !== '') {
$this->setParentNode(GeneralUtility::makeInstance($data['parentNode']['serializeClassName'], $data['parentNode']));
}
if (isset($data['childNodes']) && $data['childNodes'] !== '') {
$this->setChildNodes(GeneralUtility::makeInstance($data['childNodes']['serializeClassName'], $data['childNodes']));
}
}
/**
* Returns the serialized instance
*
* @return string
*/
public function serialize()
{
return serialize($this->toArray());
}
/**
* Fills the current node with the given serialized informations
*
* @throws \TYPO3\CMS\Core\Exception if the deserialized object type is not identical to the current one
* @param string $serializedString
*/
public function unserialize($serializedString)
{
$arrayRepresentation = unserialize($serializedString);
if ($arrayRepresentation['serializeClassName'] !== static::class) {
throw new \TYPO3\CMS\Core\Exception('Deserialized object type is not identical!', 1294586646);
}
$this->dataFromArray($arrayRepresentation);
}
}
@@ -0,0 +1,116 @@
<?php
namespace TYPO3\CMS\Backend\Tree;
/*
* 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!
*/
/**
* Tree Node Collection
*/
class TreeNodeCollection extends \ArrayObject
{
/**
* 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 = [])
{
parent::__construct();
if (!empty($data)) {
$this->dataFromArray($data);
}
}
/**
* Sorts the internal nodes array
*/
public function asort()
{
$this->uasort([$this, 'nodeCompare']);
}
/**
* Compares a node with another one
*
* @see \TYPO3\CMS\Backend\Tree\TreeNode::compareTo
* @internal
* @param TreeNode $node
* @param TreeNode $otherNode
* @return int
*/
public function nodeCompare(\TYPO3\CMS\Backend\Tree\TreeNode $node, \TYPO3\CMS\Backend\Tree\TreeNode $otherNode)
{
return $node->compareTo($otherNode);
}
/**
* Returns the serialized instance
*
* @return string
*/
public function serialize()
{
return serialize($this->toArray());
}
/**
* Initializes the current object with a serialized instance
*
* @param string $serializedString
* @throws \TYPO3\CMS\Core\Exception if the deserialized is not identical to the current class
*/
public function unserialize($serializedString)
{
$arrayRepresentation = unserialize($serializedString);
if ($arrayRepresentation['serializeClassName'] !== static::class) {
throw new \TYPO3\CMS\Core\Exception('Deserialized object type is not identical!', 1294586647);
}
$this->dataFromArray($arrayRepresentation);
}
/**
* Returns the collection in an array representation for e.g. serialization
*
* @return array
*/
public function toArray()
{
$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
*
* @param array $data
*/
public function dataFromArray($data)
{
unset($data['serializeClassName']);
foreach ($data as $index => $nodeArray) {
$node = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($nodeArray['serializeClassName'], $nodeArray);
$this->offsetSet($index, $node);
}
}
}
@@ -0,0 +1,175 @@
<?php
namespace TYPO3\CMS\Backend\Tree;
/*
* 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!
*/
use TYPO3\CMS\Core\Imaging\Icon;
/**
* Representation Tree Node
*/
class TreeRepresentationNode extends \TYPO3\CMS\Backend\Tree\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']);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,177 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Site\PseudoSiteFinder;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Generate a page-tree, browsable.
*/
class BrowseTreeView extends AbstractTreeView
{
/**
* @var array
*/
public $fieldArray = [
'uid',
'pid',
'title',
'doktype',
'nav_title',
'mount_pid',
'php_tree_stop',
't3ver_id',
't3ver_state',
'hidden',
'starttime',
'endtime',
'fe_group',
'module',
'extendToSubpages',
'nav_hide',
't3ver_wsid',
't3ver_move_id',
'is_siteroot'
];
/**
* override to use this treeName
* @var string
*/
public $treeName = 'browsePages';
/**
* override to use this table
* @var string
*/
public $table = 'pages';
/**
* override to use this domIdPrefix
* @var string
*/
public $domIdPrefix = 'pages';
/**
* @var bool
*/
public $ext_showNavTitle = false;
/**
* Initialize, setting what is necessary for browsing pages.
* Using the current user.
*
* @param string $clause Additional clause for selecting pages.
* @param string $orderByFields record ORDER BY field
*/
public function init($clause = '', $orderByFields = '')
{
$backendUser = $this->getBackendUser();
// This will hide records from display - it has nothing to do with user rights!!
$clauseExcludePidList = '';
$pidList = $backendUser->getTSConfig()['options.']['hideRecords.']['pages'] ?? '';
if (!empty($pidList)) {
if ($pidList = implode(',', GeneralUtility::intExplode(',', $pidList))) {
$clauseExcludePidList = ' AND pages.uid NOT IN (' . $pidList . ')';
}
}
// This is very important for making trees of pages: Filtering out deleted pages, pages with no access to and sorting them correctly:
parent::init(' AND deleted=0 AND sys_language_uid=0 AND ' . $backendUser->getPagePermsClause(Permission::PAGE_SHOW) . ' ' . $clause . $clauseExcludePidList, 'sorting');
$this->title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
$this->MOUNTS = $backendUser->returnWebmounts();
if ($pidList) {
// Remove mountpoint if explicitly set in options.hideRecords.pages (see above)
$hideList = explode(',', $pidList);
$this->MOUNTS = array_diff($this->MOUNTS, $hideList);
}
}
/**
* Creates title attribute content for pages.
* Uses API function in \TYPO3\CMS\Backend\Utility\BackendUtility which will retrieve lots of useful information for pages.
*
* @param array $row The table row.
* @return string
*/
public function getTitleAttrib($row)
{
return BackendUtility::titleAttribForPages($row, '1=1 ' . $this->clause, 0);
}
/**
* Wrapping the image tag, $icon, for the row, $row (except for mount points)
*
* @param string $icon The image tag for the icon
* @param array $row The row for the current element
* @return string The processed icon input value.
* @internal
*/
public function wrapIcon($icon, $row)
{
// Wrap icon in click-menu link.
$theIcon = '';
if (!$this->ext_IconMode) {
$theIcon = BackendUtility::wrapClickMenuOnIcon($icon, $this->treeName, $this->getId($row), 0);
} elseif ($this->ext_IconMode === 'titlelink') {
$aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,'
. GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($row)) . ',' . $this->bank . ');';
$theIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
}
return $theIcon;
}
/**
* Returns the title for the input record. If blank, a "no title" label (localized) will be returned.
* Do NOT htmlspecialchar the string from this function - has already been done.
*
* @param array $row The input row array (where the key "title" is used for the title)
* @param int $titleLen Title length (30)
* @return string The title.
*/
public function getTitleStr($row, $titleLen = 30)
{
if ($this->ext_showNavTitle && isset($row['nav_title']) && trim($row['nav_title']) !== '') {
$title = parent::getTitleStr(['title' => $row['nav_title']], $titleLen);
} else {
$title = parent::getTitleStr($row, $titleLen);
}
if (!empty($row['is_siteroot'])
&& $this->getBackendUser()->getTSConfig()['options.']['pageTree.']['showDomainNameWithTitle'] ?? false
) {
$pageId = (int)$row['uid'];
$siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
try {
$site = $siteFinder->getSiteByRootPageId($pageId);
$title .= ' [' . (string)$site->getBase() . ']';
} catch (SiteNotFoundException $e) {
// No site found, let's see if it is a legacy-pseudo-site
$pseudoSiteFinder = GeneralUtility::makeInstance(PseudoSiteFinder::class);
try {
$site = $pseudoSiteFinder->getSiteByRootPageId($pageId);
$title .= ' [' . trim((string)$site->getBase(), '/') . ']';
} catch (SiteNotFoundException $e) {
// No pseudo-site found either
}
}
}
return $title;
}
}
@@ -0,0 +1,65 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Local position map class when creating new Content Elements
*
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class ContentCreationPagePositionMap extends PagePositionMap
{
/**
* @var bool
*/
public $dontPrintPageInsertIcons = 1;
/**
* Wrapping the title of the record - here we just return it.
*
* @param string $str The title value.
* @param array $row The record row.
* @return string Wrapped title string.
*/
public function wrapRecordTitle($str, $row)
{
return $str;
}
/**
* Create on-click event value.
*
* @param array $row The record.
* @param string $vv Column position value.
* @param int $moveUid Move uid
* @param int $pid PID value.
* @param int $sys_lang System language
* @return string
*/
public function onClickInsertRecord($row, $vv, $moveUid, $pid, $sys_lang = 0)
{
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$location = (string)$uriBuilder->buildUriFromRoute('record_edit', [
'edit[tt_content][' . (is_array($row) ? -$row['uid'] : $pid) . ']' => 'new',
'defVals[tt_content][colPos]' => $vv,
'defVals[tt_content][sys_language_uid]' => $sys_lang,
'returnUrl' => GeneralUtility::_GP('returnUrl')
]);
return $this->clientContext . '.location.href=' . GeneralUtility::quoteJSvalue($location) . '+document.editForm.defValues.value; return false;';
}
}
@@ -0,0 +1,116 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Position map class for content elements within the page module
*
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class ContentLayoutPagePositionMap extends PagePositionMap
{
/**
* @var bool
*/
public $dontPrintPageInsertIcons = 1;
/**
* @var string
*/
public $l_insertNewRecordHere = 'newContentElement';
/**
* Wrapping the title of the record.
*
* @param string $str The title value.
* @param array $row The record row.
* @return string Wrapped title string.
*/
public function wrapRecordTitle($str, $row)
{
$aOnClick = 'jumpToUrl(' . GeneralUtility::quoteJSvalue($this->linkToCurrentModule(['edit_record' => 'tt_content:' . $row['uid']])) . ');return false;';
return '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $str . '</a>';
}
/**
* Wrapping the column header
*
* @param string $str Header value
* @param string $vv Column info.
* @return string
* @see printRecordMap()
*/
public function wrapColumnHeader($str, $vv)
{
$aOnClick = 'jumpToUrl(' . GeneralUtility::quoteJSvalue($this->linkToCurrentModule(['edit_record' => '_EDIT_COL:' . $vv])) . ');return false;';
return '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $str . '</a>';
}
/**
* Create on-click event value.
*
* @param array $row The record.
* @param string $vv Column position value.
* @param int $moveUid Move uid
* @param int $pid PID value.
* @param int $sys_lang System language
* @return string
*/
public function onClickInsertRecord($row, $vv, $moveUid, $pid, $sys_lang = 0)
{
if (is_array($row)) {
$linkInformation = 'tt_content:new/-' . $row['uid'] . '/' . $row['colPos'];
} else {
$linkInformation = 'tt_content:new/' . $pid . '/' . $vv;
}
$location = $this->linkToCurrentModule(['edit_record' => $linkInformation]);
return 'jumpToUrl(' . GeneralUtility::quoteJSvalue($location) . ');return false;';
}
/**
* Wrapping the record header (from getRecordHeader())
*
* @param string $str HTML content
* @param array $row Record array.
* @return string HTML content
*/
public function wrapRecordHeader($str, $row)
{
if ($row['uid'] == $this->moveUid) {
/** @var IconFactory $iconFactory */
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
return $iconFactory->getIcon('status-status-current', Icon::SIZE_SMALL)->render() . $str;
}
return $str;
}
/**
* Returns URL to the current script.
* In particular the "popView" and "new_unique_uid" Get vars are unset.
*
* @param array $params Parameters array, merged with global GET vars.
* @return string URL
*/
protected function linkToCurrentModule($params)
{
unset($params['popView']);
unset($params['new_unique_uid']);
return GeneralUtility::linkThisScript($params);
}
}
@@ -0,0 +1,63 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
/**
* 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.
*/
class ContentMovingPagePositionMap extends PagePositionMap
{
/**
* @var int
*/
public $dontPrintPageInsertIcons = 1;
/**
* Page tree implementation class name
*
* @var string
*/
protected $pageTreeClassName = PageTreeView::class;
/**
* Wrapping page title.
*
* @param string $str Page title.
* @param array $rec Page record (?)
* @return string Wrapped title.
*/
public function linkPageTitle($str, $rec)
{
$url = \TYPO3\CMS\Core\Utility\GeneralUtility::linkThisScript(['uid' => (int)$rec['uid'], 'moveUid' => $this->moveUid]);
return '<a href="' . htmlspecialchars($url) . '">' . $str . '</a>';
}
/**
* Wrapping the title of the record.
*
* @param string $str The title value.
* @param array $row The record row.
* @return string Wrapped title string.
*/
public function wrapRecordTitle($str, $row)
{
if ($this->moveUid == $row['uid']) {
$str = '<strong>' . $str . '</strong>';
}
return parent::wrapRecordTitle($str, $row);
}
}
@@ -0,0 +1,160 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
/**
* Base extension class which generates the folder tree.
* Used directly by the RTE.
* also used for the linkpicker on files
*
* Browsable folder tree, used in Element Browser and RTE (for which it will be extended)
* previously located inside typo3/class.browse_links.php
*/
class ElementBrowserFolderTreeView extends FolderTreeView
{
/**
* @var string
*/
public $ext_IconMode = 'titlelink';
/**
* @var LinkParameterProviderInterface
*/
protected $linkParameterProvider;
/**
* @param LinkParameterProviderInterface $linkParameterProvider
*/
public function setLinkParameterProvider(LinkParameterProviderInterface $linkParameterProvider)
{
$this->linkParameterProvider = $linkParameterProvider;
$this->thisScript = $linkParameterProvider->getScriptUrl();
}
/**
* Wrapping the folder icon
*
* @param string $icon The image tag for the icon
* @param Folder $folderObject The row for the current element
*
* @return string The processed icon input value.
* @internal
*/
public function wrapIcon($icon, $folderObject)
{
// Add title attribute to input icon tag
$theFolderIcon = '';
// Wrap icon in link (in ElementBrowser only the "titlelink" is used).
if ($this->ext_IconMode === 'titlelink') {
$parameters = HttpUtility::buildQueryString(
$this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()])
);
$aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . $parameters) . ');';
$theFolderIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
}
return $theFolderIcon;
}
/**
* Wrapping the title in a link, if applicable.
*
* @param string $title Title, ready for output.
* @param Folder $folderObject The record
* @param int $bank Bank pointer (which mount point number)
* @return string Wrapping title string.
*/
public function wrapTitle($title, $folderObject, $bank = 0)
{
$parameters = HttpUtility::buildQueryString(
$this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()])
);
return '<a href="#" onclick="return jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($this->getThisScript() . $parameters)) . ');">' . $title . '</a>';
}
/**
* Returns TRUE if the input "record" contains a folder which can be linked.
*
* @param Folder $folderObject Object with information about the folder element. Contains keys like title, uid, path, _title
* @return bool TRUE
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function ext_isLinkable(Folder $folderObject)
{
trigger_error('This method is obsolete and will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
return true;
}
/**
* @param string $cmd
* @param bool $isOpen
* @return string
*/
protected function renderPMIconAndLink($cmd, $isOpen)
{
if (static::class === __CLASS__) {
return $this->PMiconATagWrap('', $cmd, !$isOpen);
}
return parent::renderPMIconAndLink($cmd, $isOpen);
}
/**
* Wrap the plus/minus icon in a link
*
* @param string $icon HTML string to wrap, probably an image tag.
* @param string $cmd Command for 'PM' get var
* @param bool|string $bMark If set, the link will have an anchor point (=$bMark) and a name attribute (=$bMark)
* @param bool $isOpen check if the item has children
* @return string Link-wrapped input string
* @internal
*/
public function PM_ATagWrap($icon, $cmd, $bMark = '', $isOpen = false)
{
$anchor = $bMark ? '#' . $bMark : '';
$name = $bMark ? ' name=' . $bMark : '';
$urlParameters = $this->linkParameterProvider->getUrlParameters([]);
$urlParameters['PM'] = $cmd;
$aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . HttpUtility::buildQueryString($urlParameters)) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
return '<a href="#"' . htmlspecialchars($name) . ' onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
}
/**
* Wrap the plus/minus icon in a link
*
* @param string $icon HTML string to wrap, probably an image tag.
* @param string $cmd Command for 'PM' get var
* @param bool $isExpand Whether to be expanded
* @return string Link-wrapped input string
* @internal
*/
public function PMiconATagWrap($icon, $cmd, $isExpand = true)
{
if (empty($this->scope)) {
$this->scope = [
'class' => static::class,
'script' => $this->thisScript,
'browser' => $this->linkParameterProvider->getUrlParameters([]),
];
}
return parent::PMiconATagWrap($icon, $cmd, $isExpand);
}
}
@@ -0,0 +1,199 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Frontend\Page\PageRepository;
use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
/**
* Class which generates the selectable page tree
*
* Browsable tree, used in PagePositionMaps (move elements), the Link Wizard and the Database Browser (for which it will be extended)
*/
class ElementBrowserPageTreeView extends BrowseTreeView
{
/**
* whether the page ID should be shown next to the title, activate through
* userTSconfig (options.pageTree.showPageIdWithTitle)
*
* @var bool
*/
public $ext_showPageId = false;
/**
* @var bool
*/
public $ext_pArrPages = true;
/**
* @var LinkParameterProviderInterface
*/
protected $linkParameterProvider;
/**
* Constructor. Just calling init()
*/
public function __construct()
{
parent::__construct();
$this->init();
$this->clause = ' AND doktype <> ' . PageRepository::DOKTYPE_RECYCLER . $this->clause;
}
/**
* @param LinkParameterProviderInterface $linkParameterProvider
*/
public function setLinkParameterProvider(LinkParameterProviderInterface $linkParameterProvider)
{
$this->linkParameterProvider = $linkParameterProvider;
$this->thisScript = $linkParameterProvider->getScriptUrl();
}
/**
* Wrapping the title in a link, if applicable.
*
* @param string $title Title, (must be ready for output, that means it must be htmlspecialchars()'ed).
* @param array $v The record
* @param bool $ext_pArrPages (ignored)
* @return string Wrapping title string.
*/
public function wrapTitle($title, $v, $ext_pArrPages = false)
{
if ($this->ext_isLinkable($v['doktype'], $v['uid'])) {
$url = GeneralUtility::makeInstance(LinkService::class)->asString(['type' => LinkService::TYPE_PAGE, 'pageuid' => (int)$v['uid']]);
return '<span class="list-tree-title"><a href="' . htmlspecialchars($url) . '" class="t3js-pageLink">' . $title . '</a></span>';
}
return '<span class="list-tree-title text-muted">' . $title . '</span>';
}
/**
* Create the page navigation tree in HTML
*
* @param array|string $treeArr Tree array
* @return string HTML output.
*/
public function printTree($treeArr = '')
{
$titleLen = (int)$GLOBALS['BE_USER']->uc['titleLen'];
if (!is_array($treeArr)) {
$treeArr = $this->tree;
}
$out = '';
// We need to count the opened <ul>'s every time we dig into another level,
// so we know how many we have to close when all children are done rendering
$closeDepth = [];
foreach ($treeArr as $treeItem) {
if ($treeItem['isMountPointPath']) {
$out .= '<li class="list-tree-path">' . $treeItem['title'] . '</li>';
continue;
}
$classAttr = $treeItem['row']['_CSSCLASS'];
if ($treeItem['isFirst']) {
$out .= '<ul class="list-tree">';
}
// Add CSS classes to the list item
if ($treeItem['hasSub']) {
$classAttr .= ' list-tree-control-open';
}
$selected = '';
if ($this->linkParameterProvider->isCurrentlySelectedItem(['pid' => (int)$treeItem['row']['uid']])) {
$selected = ' bg-success';
$classAttr .= ' active';
}
$urlParameters = $this->linkParameterProvider->getUrlParameters(['pid' => (int)$treeItem['row']['uid']]);
$aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . HttpUtility::buildQueryString($urlParameters)) . ');';
$cEbullet = $this->ext_isLinkable($treeItem['row']['doktype'], $treeItem['row']['uid'])
? '<a href="#" class="list-tree-show" onclick="' . htmlspecialchars($aOnClick) . '"><i class="fa fa-caret-square-o-right"></i></a>'
: '';
$out .= '
<li' . ($classAttr ? ' class="' . trim($classAttr) . '"' : '') . '>
<span class="list-tree-group' . $selected . '">
' . $cEbullet . $treeItem['HTML'] . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLen), $treeItem['row'], $this->ext_pArrPages) . '
</span>
';
if (!$treeItem['hasSub']) {
$out .= '</li>';
}
// We have to remember if this is the last one
// on level X so the last child on level X+1 closes the <ul>-tag
if ($treeItem['isLast']) {
$closeDepth[$treeItem['invertedDepth']] = 1;
}
// If this is the last one and does not have subitems, we need to close
// the tree as long as the upper levels have last items too
if ($treeItem['isLast'] && !$treeItem['hasSub']) {
for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
$closeDepth[$i] = 0;
$out .= '</ul></li>';
}
}
}
return '<ul class="list-tree list-tree-root">' . $out . '</ul>';
}
/**
* Returns TRUE if a doktype can be linked.
*
* @param int $doktype Doktype value to test
* @param int $uid uid to test.
* @return bool
*/
public function ext_isLinkable($doktype, $uid)
{
return $uid && $doktype < PageRepository::DOKTYPE_SPACER;
}
/**
* Wrap the plus/minus icon in a link
*
* @param string $icon HTML string to wrap, probably an image tag.
* @param string $cmd Command for 'PM' get var
* @param string $bMark If set, the link will have an anchor point (=$bMark) and a name attribute (=$bMark)
* @param bool $isOpen
* @return string Link-wrapped input string
*/
public function PM_ATagWrap($icon, $cmd, $bMark = '', $isOpen = false)
{
$anchor = $bMark ? '#' . $bMark : '';
$name = $bMark ? ' name=' . $bMark : '';
$urlParameters = $this->linkParameterProvider->getUrlParameters([]);
$urlParameters['PM'] = $cmd;
$aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . HttpUtility::buildQueryString($urlParameters)) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
return '<a class="list-tree-control ' . ($isOpen ? 'list-tree-control-open' : 'list-tree-control-closed')
. '" href="#"' . htmlspecialchars($name) . ' onclick="' . htmlspecialchars($aOnClick) . '"><i class="fa"></i></a>';
}
/**
* Wrapping the image tag, $icon, for the row, $row
*
* @param string $icon The image tag for the icon
* @param array $row The row for the current element
* @return string The processed icon input value.
*/
public function wrapIcon($icon, $row)
{
if ($this->ext_showPageId) {
$icon .= '[' . $row['uid'] . ']&nbsp;';
}
return $icon;
}
}
@@ -0,0 +1,717 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
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\Utility\GeneralUtility;
/**
* Generate a folder tree,
* specially made for browsing folders in the File module
*
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class FolderTreeView extends AbstractTreeView
{
/**
* The users' file Storages
*
* @var ResourceStorage[]
*/
protected $storages;
/**
* @var array
*/
protected $storageHashNumbers;
/**
* Indicates, whether the AJAX call was successful,
* i.e. the requested page has been found
*
* @var bool
*/
protected $ajaxStatus = false;
/**
* @var array
*/
protected $scope;
/**
* @var IconFactory
*/
protected $iconFactory;
/**
* override to not use a title attribute
* @var string
*/
public $titleAttrib = '';
/**
* override to use this treeName
* does not need to be set in __construct()
* @var string
*/
public $treeName = 'folder';
/**
* override to use this domIdPrefix
* @var string
*/
public $domIdPrefix = 'folder';
/**
* Constructor function of the class
*/
public function __construct()
{
parent::__construct();
$this->init();
$this->storages = $this->BE_USER->getFileStorages();
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
}
/**
* Generate the plus/minus icon for the browsable tree.
*
* @param Folder $folderObject Entry folder object
* @param int $subFolderCounter The current entry number
* @param int $totalSubFolders 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 $isExpanded The element was expanded to render subelements if this flag is set.
*
* @return string Image tag with the plus/minus icon.
* @internal
* @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
*/
public function PMicon($folderObject, $subFolderCounter, $totalSubFolders, $nextCount, $isExpanded)
{
$icon = '';
if ($nextCount) {
$cmd = $this->generateExpandCollapseParameter($this->bank, !$isExpanded, $folderObject);
$icon = $this->PMiconATagWrap($icon, $cmd, !$isExpanded);
}
return $icon;
}
/**
* Wrap the plus/minus icon in a link
*
* @param string $icon HTML string to wrap, probably an image tag.
* @param string $cmd Command for 'PM' get var
* @param bool $isExpand Whether to be expanded
* @return string Link-wrapped input string
* @internal
*/
public function PMiconATagWrap($icon, $cmd, $isExpand = true)
{
if (empty($this->scope)) {
$this->scope = [
'class' => static::class,
'script' => $this->thisScript,
];
}
if ($this->thisScript) {
// Activates dynamic AJAX based tree
$scopeData = json_encode($this->scope);
$scopeHash = GeneralUtility::hmac($scopeData);
$js = htmlspecialchars('Tree.load(' . GeneralUtility::quoteJSvalue($cmd) . ', ' . (int)$isExpand . ', this, ' . GeneralUtility::quoteJSvalue($scopeData) . ', ' . GeneralUtility::quoteJSvalue($scopeHash) . ');');
return '<a class="list-tree-control' . (!$isExpand ? ' list-tree-control-open' : ' list-tree-control-closed') . '" onclick="' . $js . '"><i class="fa"></i></a>';
}
return $icon;
}
/**
* @param string $cmd
* @param bool $isOpen
* @return string
*/
protected function renderPMIconAndLink($cmd, $isOpen)
{
$link = $this->thisScript ? ' href="' . htmlspecialchars($this->getThisScript() . 'PM=' . $cmd) . '"' : '';
return '<a class="list-tree-control list-tree-control-' . ($isOpen ? 'open' : 'closed') . '"' . $link . '><i class="fa"></i></a>';
}
/**
* Wrapping the folder icon
*
* @param string $icon The image tag for the icon
* @param Folder $folderObject The row for the current element
*
* @return string The processed icon input value.
* @internal
*/
public function wrapIcon($icon, $folderObject)
{
// Add title attribute to input icon tag
$theFolderIcon = '';
// Wrap icon in click-menu link.
if (!$this->ext_IconMode) {
// Check storage access to wrap with click menu
if (!$folderObject instanceof InaccessibleFolder) {
$tableName = $this->getTableNameForClickMenu($folderObject);
$theFolderIcon = BackendUtility::wrapClickMenuOnIcon($icon, $tableName, $folderObject->getCombinedIdentifier(), 'tree');
}
} elseif ($this->ext_IconMode === 'titlelink') {
$aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($folderObject)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($folderObject)) . ',' . $this->bank . ');';
$theFolderIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
}
return $theFolderIcon;
}
/**
* Wrapping $title in a-tags.
*
* @param string $title Title string
* @param Folder $folderObject the folder record
* @param int $bank Bank pointer (which mount point number)
*
* @return string
* @internal
*/
public function wrapTitle($title, $folderObject, $bank = 0)
{
// Check storage access to wrap with click menu
if ($folderObject instanceof InaccessibleFolder) {
return $title;
}
$aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($folderObject)) . ', this, ' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($folderObject)) . ', ' . $bank . ');';
$tableName = $this->getTableNameForClickMenu($folderObject);
$clickMenuParts = BackendUtility::wrapClickMenuOnIcon('', $tableName, $folderObject->getCombinedIdentifier(), 'tree', '', '', true);
return '<a href="#" title="' . htmlspecialchars(strip_tags($title)) . '" onclick="' . htmlspecialchars($aOnClick) . '" ' . GeneralUtility::implodeAttributes($clickMenuParts) . '>' . $title . '</a>';
}
/**
* Returns the id from the record - for folders, this is an md5 hash.
*
* @param Folder $folderObject The folder object
*
* @return int The "uid" field value.
*/
public function getId($folderObject)
{
return GeneralUtility::md5int($folderObject->getCombinedIdentifier());
}
/**
* Returns jump-url parameter value.
*
* @param Folder $folderObject The folder object
*
* @return string The jump-url parameter.
*/
public function getJumpToParam($folderObject)
{
return rawurlencode($folderObject->getCombinedIdentifier());
}
/**
* Returns the title for the input record. If blank, a "no title" label (localized) will be returned.
* '_title' is used for setting an alternative title for folders.
*
* @param array $row The input row array (where the key "_title" is used for the title)
* @param int $titleLen Title length (30)
* @return string The title
*/
public function getTitleStr($row, $titleLen = 30)
{
return $row['_title'] ?? parent::getTitleStr($row, $titleLen);
}
/**
* Returns the value for the image "title" attribute
*
* @param Folder $folderObject The folder to be used
*
* @return string The attribute value (is htmlspecialchared() already)
*/
public function getTitleAttrib($folderObject)
{
return htmlspecialchars($folderObject->getName());
}
/**
* Will create and return the HTML code for a browsable tree of folders.
* Is based on the mounts found in the internal array ->MOUNTS (set in the constructor)
*
* @return string HTML code for the browsable tree
*/
public function getBrowsableTree()
{
// Get stored tree structure AND updating it if needed according to incoming PM GET var.
$this->initializePositionSaving();
// Init done:
$treeItems = [];
// Traverse mounts:
foreach ($this->storages as $storageObject) {
$this->getBrowseableTreeForStorage($storageObject);
// Add tree:
$treeItems = array_merge($treeItems, $this->tree);
}
return $this->printTree($treeItems);
}
/**
* Get a tree for one storage
*
* @param ResourceStorage $storageObject
*/
public function getBrowseableTreeForStorage(ResourceStorage $storageObject)
{
// If there are filemounts, show each, otherwise just the rootlevel folder
$fileMounts = $storageObject->getFileMounts();
$rootLevelFolders = [];
if (!empty($fileMounts)) {
foreach ($fileMounts as $fileMountInfo) {
$rootLevelFolders[] = [
'folder' => $fileMountInfo['folder'],
'name' => $fileMountInfo['title']
];
}
} elseif ($this->BE_USER->isAdmin()) {
$rootLevelFolders[] = [
'folder' => $storageObject->getRootLevelFolder(),
'name' => $storageObject->getName()
];
}
// Clean the tree
$this->reset();
// Go through all "root level folders" of this tree (can be the rootlevel folder or any file mount points)
foreach ($rootLevelFolders as $rootLevelFolderInfo) {
/** @var Folder $rootLevelFolder */
$rootLevelFolder = $rootLevelFolderInfo['folder'];
$rootLevelFolderName = $rootLevelFolderInfo['name'];
$folderHashSpecUID = GeneralUtility::md5int($rootLevelFolder->getCombinedIdentifier());
$this->specUIDmap[$folderHashSpecUID] = $rootLevelFolder->getCombinedIdentifier();
// Hash key
$storageHashNumber = $this->getShortHashNumberForStorage($storageObject, $rootLevelFolder);
// Set first:
$this->bank = $storageHashNumber;
$isOpen = $this->stored[$storageHashNumber][$folderHashSpecUID] || $this->expandFirst;
// Set PM icon:
$cmd = $this->generateExpandCollapseParameter($this->bank, !$isOpen, $rootLevelFolder);
// Only show and link icon if storage is browseable
if (!$storageObject->isBrowsable() || $this->getNumberOfSubfolders($rootLevelFolder) === 0) {
$firstHtml = '';
} else {
$firstHtml = $this->renderPMIconAndLink($cmd, $isOpen);
}
// Mark a storage which is not online, as offline
// maybe someday there will be a special icon for this
if ($storageObject->isOnline() === false) {
$rootLevelFolderName .= ' (' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_file.xlf:sys_file_storage.isOffline') . ')';
}
// Preparing rootRec for the mount
$icon = $this->iconFactory->getIconForResource($rootLevelFolder, Icon::SIZE_SMALL, null, ['mount-root' => true]);
$firstHtml .= $this->wrapIcon($icon, $rootLevelFolder);
$row = [
'uid' => $folderHashSpecUID,
'title' => $rootLevelFolderName,
'path' => $rootLevelFolder->getCombinedIdentifier(),
'folder' => $rootLevelFolder
];
// Add the storage root to ->tree
$this->tree[] = [
'HTML' => $firstHtml,
'row' => $row,
'bank' => $this->bank,
// hasSub is TRUE when the root of the storage is expanded
'hasSub' => $isOpen && $storageObject->isBrowsable(),
'invertedDepth' => 1000,
];
// If the mount is expanded, go down:
if ($isOpen && $storageObject->isBrowsable()) {
// Set depth:
$this->getFolderTree($rootLevelFolder, 999);
}
}
}
/**
* Fetches the data for the tree
*
* @param Folder $folderObject the folderobject
* @param int $depth Max depth (recursivity limit)
* @param string $type HTML-code prefix for recursive calls.
*
* @return int The count of items on the level
* @see getBrowsableTree()
*/
public function getFolderTree(Folder $folderObject, $depth = 999, $type = '')
{
$depth = (int)$depth;
// This generates the directory tree
/* array of \TYPO3\CMS\Core\Resource\Folder */
if ($folderObject instanceof InaccessibleFolder) {
$subFolders = [];
} else {
$subFolders = $folderObject->getSubfolders();
$subFolders = \TYPO3\CMS\Core\Resource\Utility\ListUtility::resolveSpecialFolderNames($subFolders);
uksort($subFolders, 'strnatcasecmp');
}
$totalSubFolders = count($subFolders);
$HTML = '';
$subFolderCounter = 0;
$treeKey = '';
/** @var Folder $subFolder */
foreach ($subFolders as $subFolderName => $subFolder) {
$subFolderCounter++;
// Reserve space.
$this->tree[] = [];
// Get the key for this space
end($this->tree);
$isLocked = $subFolder instanceof InaccessibleFolder;
$treeKey = key($this->tree);
$specUID = GeneralUtility::md5int($subFolder->getCombinedIdentifier());
$this->specUIDmap[$specUID] = $subFolder->getCombinedIdentifier();
$row = [
'uid' => $specUID,
'path' => $subFolder->getCombinedIdentifier(),
'title' => $subFolderName,
'folder' => $subFolder
];
// Make a recursive call to the next level
if (!$isLocked && $depth > 1 && $this->expandNext($specUID)) {
$nextCount = $this->getFolderTree($subFolder, $depth - 1, $type);
// Set "did expand" flag
$isOpen = 1;
} else {
$nextCount = $isLocked ? 0 : $this->getNumberOfSubfolders($subFolder);
// Clear "did expand" flag
$isOpen = 0;
}
// Set HTML-icons, if any:
if ($this->makeHTML) {
$HTML = $this->PMicon($subFolder, $subFolderCounter, $totalSubFolders, $nextCount, $isOpen);
$type = '';
$role = $subFolder->getRole();
if ($role !== FolderInterface::ROLE_DEFAULT) {
$row['_title'] = '<strong>' . $subFolderName . '</strong>';
}
$icon = '<span title="' . htmlspecialchars($subFolderName) . '">'
. $this->iconFactory->getIconForResource($subFolder, Icon::SIZE_SMALL, null, ['folder-open' => (bool)$isOpen])
. '</span>';
$HTML .= $this->wrapIcon($icon, $subFolder);
}
// Finally, add the row/HTML content to the ->tree array in the reserved key.
$this->tree[$treeKey] = [
'row' => $row,
'HTML' => $HTML,
'hasSub' => $nextCount && $this->expandNext($specUID),
'isFirst' => $subFolderCounter == 1,
'isLast' => false,
'invertedDepth' => $depth,
'bank' => $this->bank
];
}
if ($subFolderCounter > 0) {
$this->tree[$treeKey]['isLast'] = true;
}
return $totalSubFolders;
}
/**
* Compiles the HTML code for displaying the structure found inside the ->tree array
*
* @param array|string $treeItems "tree-array" - if blank string, the internal ->tree array is used.
* @return string The HTML code for the tree
*/
public function printTree($treeItems = '')
{
$doExpand = false;
$doCollapse = false;
$ajaxOutput = '';
$titleLength = (int)$this->BE_USER->uc['titleLen'];
if (!is_array($treeItems)) {
$treeItems = $this->tree;
}
if (empty($treeItems)) {
$message = GeneralUtility::makeInstance(
FlashMessage::class,
$this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:foldertreeview.noFolders.message'),
$this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:foldertreeview.noFolders.title'),
FlashMessage::INFO
);
/** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
/** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($message);
return $defaultFlashMessageQueue->renderFlashMessages();
}
$expandedFolderHash = '';
$invertedDepthOfAjaxRequestedItem = 0;
$out = '<ul class="list-tree list-tree-root">';
// Evaluate AJAX request
if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX) {
list(, $expandCollapseCommand, $expandedFolderHash, ) = $this->evaluateExpandCollapseParameter();
if ($expandCollapseCommand == 1) {
$doExpand = true;
} else {
$doCollapse = true;
}
}
// We need to count the opened <ul>'s every time we dig into another level,
// so we know how many we have to close when all children are done rendering
$closeDepth = [];
foreach ($treeItems as $treeItem) {
/** @var Folder $folderObject */
$folderObject = $treeItem['row']['folder'];
$classAttr = $treeItem['row']['_CSSCLASS'] ?? '';
$folderIdentifier = $folderObject->getCombinedIdentifier();
// this is set if the AJAX request has just opened this folder (via the PM command)
$isExpandedFolderIdentifier = $expandedFolderHash == GeneralUtility::md5int($folderIdentifier);
$idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($folderObject) . '_' . $treeItem['bank']);
$itemHTML = '';
// If this item is the start of a new level,
// then a new level <ul> is needed, but not in ajax mode
if (!empty($treeItem['isFirst']) && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
$itemHTML = '<ul class="list-tree">';
}
// Add CSS classes to the list item
if (!empty($treeItem['hasSub'])) {
$classAttr .= ' list-tree-control-open';
}
$itemHTML .= '
<li id="' . $idAttr . '" ' . ($classAttr ? ' class="' . trim($classAttr) . '"' : '') . '><span class="list-tree-group">' . $treeItem['HTML'] . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLength), $folderObject, $treeItem['bank']) . '</span>';
if (empty($treeItem['hasSub'])) {
$itemHTML .= '</li>';
}
// We have to remember if this is the last one
// on level X so the last child on level X+1 closes the <ul>-tag
if (!empty($treeItem['isLast']) && !($doExpand && $isExpandedFolderIdentifier)) {
$closeDepth[$treeItem['invertedDepth']] = 1;
}
// If this is the last one and does not have subitems, we need to close
// the tree as long as the upper levels have last items too
if (!empty($treeItem['isLast']) && empty($treeItem['hasSub']) && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
for ($i = $treeItem['invertedDepth']; !empty($closeDepth[$i]); $i++) {
$closeDepth[$i] = 0;
$itemHTML .= '</ul></li>';
}
}
// Ajax request: collapse
if ($doCollapse && $isExpandedFolderIdentifier) {
$this->ajaxStatus = true;
return $itemHTML;
}
// Ajax request: expand
if ($doExpand && $isExpandedFolderIdentifier) {
$ajaxOutput .= $itemHTML;
$invertedDepthOfAjaxRequestedItem = $treeItem['invertedDepth'];
} elseif ($invertedDepthOfAjaxRequestedItem) {
if ($treeItem['invertedDepth'] && ($treeItem['invertedDepth'] < $invertedDepthOfAjaxRequestedItem)) {
$ajaxOutput .= $itemHTML;
} else {
$this->ajaxStatus = true;
return $ajaxOutput;
}
}
$out .= $itemHTML;
}
// If this is an AJAX request, output directly
if ($ajaxOutput) {
$this->ajaxStatus = true;
return $ajaxOutput;
}
// Finally close the first ul
$out .= '</ul>';
return $out;
}
/**
* Returns table name for click menu
*
* @param Folder $folderObject
* @return string
*/
protected function getTableNameForClickMenu(Folder $folderObject)
{
if (strpos($folderObject->getRole(), FolderInterface::ROLE_MOUNT) !== false) {
$tableName = 'sys_filemounts';
} elseif ($folderObject->getIdentifier() === $folderObject->getStorage()->getRootLevelFolder()->getIdentifier()) {
$tableName = 'sys_file_storage';
} else {
$tableName = 'sys_file';
}
return $tableName;
}
/**
* Counts the number of directories in a file path.
*
* @param Folder $folderObject File path.
*
* @return int
*/
public function getNumberOfSubfolders(Folder $folderObject)
{
$subFolders = $folderObject->getSubfolders();
return count($subFolders);
}
/**
* Get stored tree structure AND updating it if needed according to incoming PM GET var.
*
* @internal
*/
public function initializePositionSaving()
{
// Get stored tree structure:
$this->stored = unserialize($this->BE_USER->uc['browseTrees'][$this->treeName], ['allowed_classes' => false]);
$this->getShortHashNumberForStorage();
// PM action:
// (If an plus/minus icon has been clicked,
// the PM GET var is sent and we must update the stored positions in the tree):
// 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
list($storageHashNumber, $doExpand, $numericFolderHash, $treeName) = $this->evaluateExpandCollapseParameter();
if ($treeName && $treeName == $this->treeName) {
if (in_array($storageHashNumber, $this->storageHashNumbers)) {
if ($doExpand == 1) {
// Set
$this->stored[$storageHashNumber][$numericFolderHash] = 1;
} else {
// Clear
unset($this->stored[$storageHashNumber][$numericFolderHash]);
}
$this->savePosition();
}
}
}
/**
* Helper method to map md5-hash to shorter number
*
* @param ResourceStorage $storageObject
* @param Folder $startingPointFolder
*
* @return int
*/
protected function getShortHashNumberForStorage(ResourceStorage $storageObject = null, Folder $startingPointFolder = null)
{
if (!$this->storageHashNumbers) {
$this->storageHashNumbers = [];
foreach ($this->storages as $storageUid => $storage) {
$fileMounts = $storage->getFileMounts();
if (!empty($fileMounts)) {
foreach ($fileMounts as $fileMount) {
$nkey = hexdec(substr(GeneralUtility::md5int($fileMount['folder']->getCombinedIdentifier()), 0, 4));
$this->storageHashNumbers[$storageUid . $fileMount['folder']->getCombinedIdentifier()] = $nkey;
}
} else {
$folder = $storage->getRootLevelFolder();
$nkey = hexdec(substr(GeneralUtility::md5int($folder->getCombinedIdentifier()), 0, 4));
$this->storageHashNumbers[$storageUid . $folder->getCombinedIdentifier()] = $nkey;
}
}
}
if ($storageObject) {
if ($startingPointFolder) {
return $this->storageHashNumbers[$storageObject->getUid() . $startingPointFolder->getCombinedIdentifier()];
}
return $this->storageHashNumbers[$storageObject->getUid()];
}
return null;
}
/**
* Gets the values from the Expand/Collapse Parameter (&PM)
* previously known as "PM" (plus/minus)
* PM action:
* (If an plus/minus icon has been clicked,
* the PM GET var is sent and we must update the stored positions in the tree):
* 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
*
* @param string $PM The "plus/minus" command
* @return array
*/
protected function evaluateExpandCollapseParameter($PM = null)
{
if ($PM === null) {
$PM = GeneralUtility::_GP('PM');
// IE takes anchor as parameter
if (($PMpos = strpos($PM, '#')) !== false) {
$PM = substr($PM, 0, $PMpos);
}
}
// Take the first three parameters
list($mountKey, $doExpand, $folderIdentifier) = array_pad(explode('_', $PM, 3), 3, null);
// In case the folder identifier contains "_", we just need to get the fourth/last parameter
list($folderIdentifier, $treeName) = array_pad(GeneralUtility::revExplode('_', $folderIdentifier, 2), 2, null);
return [
$mountKey,
$doExpand,
$folderIdentifier,
$treeName
];
}
/**
* Generates the "PM" string to sent to expand/collapse items
*
* @param string $mountKey The mount key / storage UID
* @param bool $doExpand Whether to expand/collapse
* @param Folder $folderObject The folder object
* @param string $treeName The name of the tree
*
* @return string
*/
protected function generateExpandCollapseParameter($mountKey = null, $doExpand = false, Folder $folderObject = null, $treeName = null)
{
$parts = [
$mountKey ?? $this->bank,
$doExpand == 1 ? 1 : 0,
$folderObject !== null ? GeneralUtility::md5int($folderObject->getCombinedIdentifier()) : '',
$treeName ?? $this->treeName
];
return implode('_', $parts);
}
/**
* Gets the AJAX status.
*
* @return bool
*/
public function getAjaxStatus()
{
return $this->ajaxStatus;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,36 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
/**
* Extension for the tree class that generates the tree of pages in the page-wizard mode
*
* @see \TYPO3\CMS\Backend\Controller\NewRecordController
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class NewRecordPageTreeView extends PageTreeView
{
/**
* Determines whether to expand a branch or not.
* Here the branch is expanded if the current id matches the global id for the listing/new
*
* @param int $id The ID (page id) of the element
* @return bool Returns TRUE if the IDs matches
*/
public function expandNext($id)
{
return $id == $GLOBALS['SOBE']->id;
}
}
@@ -0,0 +1,79 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Position map class for moving pages,
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class PageMovingPagePositionMap extends PagePositionMap
{
/**
* @var string
*/
public $l_insertNewPageHere = 'movePageToHere';
/**
* Page tree implementation class name
*
* @var string
*/
protected $pageTreeClassName = PageTreeView::class;
/**
* Creates the onclick event for the insert-icons.
*
* @param int $pid The pid.
* @param int $newPagePID New page id.
* @return string Onclick attribute content
*/
public function onClickEvent($pid, $newPagePID)
{
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
return 'window.location.href=' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('tce_db', [
'cmd[pages][' . $this->moveUid . '][' . $this->moveOrCopy . ']' => $pid,
'redirect' => $this->R_URI,
])) . ';return false;';
}
/**
* Wrapping page title.
*
* @param string $str Page title.
* @param array $rec Page record (?)
* @return string Wrapped title.
*/
public function linkPageTitle($str, $rec)
{
$url = GeneralUtility::linkThisScript(['uid' => (int)$rec['uid'], 'moveUid' => $this->moveUid]);
return '<a href="' . htmlspecialchars($url) . '">' . $str . '</a>';
}
/**
* Wrap $t_code in bold IF the $dat uid matches $id
*
* @param string $t_code Title string
* @param array $dat Information array with record array inside.
* @param int $id The current id.
* @return string The title string.
*/
public function boldTitle($t_code, $dat, $id)
{
return parent::boldTitle($t_code, $dat, $this->moveUid);
}
}
@@ -0,0 +1,663 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
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\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Position map class - generating a page tree / content element list which links for inserting (copy/move) of records.
* Used for pages / tt_content element wizards of various kinds.
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class PagePositionMap
{
use PublicPropertyDeprecationTrait;
/**
* Properties which have been moved to protected status from public
*
* @var array
*/
protected $deprecatedPublicProperties = [
'getModConfigCache' => 'Using $getModConfigCache of class PagePositionMap is discouraged. This property will be removed in TYPO3 v10.0.',
'modConfigStr' => 'Using $$modConfigStr of class PagePositionMap is discouraged. This property will be removed in TYPO3 v10.0.',
];
// EXTERNAL, static:
/**
* @var string
*/
public $moveOrCopy = 'move';
/**
* @var int
*/
public $dontPrintPageInsertIcons = 0;
// How deep the position page tree will go.
/**
* @var int
*/
public $depth = 2;
// Can be set to the sys_language uid to select content elements for.
/**
* @var string
*/
public $cur_sys_language;
// INTERNAL, dynamic:
// Request uri
/**
* @var string
*/
public $R_URI = '';
// Element id.
/**
* @var string
*/
public $elUid = '';
// tt_content element uid to move.
/**
* @var string
*/
public $moveUid = '';
// Caching arrays:
/**
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
protected $getModConfigCache = [];
/**
* @var array
*/
public $checkNewPageCache = [];
// Label keys:
/**
* @var string
*/
public $l_insertNewPageHere = 'insertNewPageHere';
/**
* @var string
*/
public $l_insertNewRecordHere = 'insertNewRecordHere';
/**
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
protected $modConfigStr = 'mod.web_list.newPageWiz';
/**
* Page tree implementation class name
*
* @var string
*/
protected $pageTreeClassName = ElementBrowserPageTreeView::class;
/**
* @var IconFactory
*/
protected $iconFactory;
/**
* @var string
*/
protected $clientContext;
/**
* Constructor allowing to set pageTreeImplementation
*
* @param string $pageTreeClassName
* @param string $clientContext JavaScript context of view client (either 'window' or 'list_frame')
*/
public function __construct(string $pageTreeClassName = null, string $clientContext = 'window')
{
if ($pageTreeClassName !== null) {
$this->pageTreeClassName = $pageTreeClassName;
}
$this->clientContext = $clientContext;
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
}
/*************************************
*
* Page position map:
*
**************************************/
/**
* Creates a "position tree" based on the page tree.
*
* @param int $id Current page id
* @param array $pageinfo Current page record.
* @param string $perms_clause Page selection permission clause.
* @param string $R_URI Current REQUEST_URI
* @return string HTML code for the tree.
*/
public function positionTree($id, $pageinfo, $perms_clause, $R_URI)
{
// Make page tree object:
/** @var \TYPO3\CMS\Backend\Tree\View\PageTreeView $pageTree */
$pageTree = GeneralUtility::makeInstance($this->pageTreeClassName);
$pageTree->init(' AND ' . $perms_clause);
$pageTree->addField('pid');
// Initialize variables:
$this->R_URI = $R_URI;
$this->elUid = $id;
// Create page tree, in $this->depth levels.
$pageTree->getTree($pageinfo['pid'], $this->depth);
// Initialize variables:
$saveLatestUid = [];
$latestInvDepth = $this->depth;
// Traverse the tree:
$lines = [];
foreach ($pageTree->tree as $cc => $dat) {
if ($latestInvDepth > $dat['invertedDepth']) {
$margin = 'style="margin-left: ' . ($dat['invertedDepth'] * 16 + 9) . 'px;"';
$lines[] = '<ul class="list-tree" ' . $margin . '>';
}
// Make link + parameters.
$latestInvDepth = $dat['invertedDepth'];
$saveLatestUid[$latestInvDepth] = $dat;
if (isset($pageTree->tree[$cc - 1])) {
$prev_dat = $pageTree->tree[$cc - 1];
// If current page, subpage?
if ($prev_dat['row']['uid'] == $id) {
// 1) It must be allowed to create a new page and 2) If there are subpages there is no need to render a subpage icon here - it'll be done over the subpages...
if (!$this->dontPrintPageInsertIcons && $this->checkNewPageInPid($id) && !($prev_dat['invertedDepth'] > $pageTree->tree[$cc]['invertedDepth'])) {
end($lines);
$margin = 'style="margin-left: ' . (($dat['invertedDepth'] - 1) * 16 + 9) . 'px;"';
$lines[] = '<ul class="list-tree" ' . $margin . '><li><span class="text-nowrap"><a href="#" onclick="' . htmlspecialchars($this->onClickEvent($id, $id)) . '"><i class="t3-icon fa fa-long-arrow-left" title="' . $this->insertlabel() . '"></i></a></span></li></ul>';
}
}
// If going down
if ($prev_dat['invertedDepth'] > $pageTree->tree[$cc]['invertedDepth']) {
$prevPid = $pageTree->tree[$cc]['row']['pid'];
} elseif ($prev_dat['invertedDepth'] < $pageTree->tree[$cc]['invertedDepth']) {
// If going up
// First of all the previous level should have an icon:
if (!$this->dontPrintPageInsertIcons && $this->checkNewPageInPid($prev_dat['row']['pid'])) {
$prevPid = -$prev_dat['row']['uid'];
end($lines);
$lines[] = '<li><span class="text-nowrap"><a href="#" onclick="' . htmlspecialchars($this->onClickEvent($prevPid, $prev_dat['row']['pid'])) . '"><i class="t3-icon fa fa-long-arrow-left" title="' . $this->insertlabel() . '"></i></a></span></li>';
}
// Then set the current prevPid
$prevPid = -$prev_dat['row']['pid'];
if ($prevPid !== $dat['row']['pid']) {
$lines[] = '</ul>';
}
} else {
// In on the same level
$prevPid = -$prev_dat['row']['uid'];
}
} else {
// First in the tree
$prevPid = $dat['row']['pid'];
}
// print arrow on the same level
if (!$this->dontPrintPageInsertIcons && $this->checkNewPageInPid($dat['row']['pid'])) {
$lines[] = '<span class="text-nowrap"><a href="#" onclick="' . htmlspecialchars($this->onClickEvent($prevPid, $dat['row']['pid'])) . '"><i class="t3-icon fa fa-long-arrow-left" title="' . $this->insertlabel() . '"></i></a></span>';
}
// The line with the icon and title:
$toolTip = BackendUtility::getRecordToolTip($dat['row'], 'pages');
$icon = '<span ' . $toolTip . '>' . $this->iconFactory->getIconForRecord('pages', $dat['row'], Icon::SIZE_SMALL)->render() . '</span>';
$lines[] = '<span class="text-nowrap">' . $icon . $this->linkPageTitle($this->boldTitle(htmlspecialchars(GeneralUtility::fixed_lgd_cs($dat['row']['title'], $this->getBackendUser()->uc['titleLen'])), $dat, $id), $dat['row']) . '</span>';
}
// If the current page was the last in the tree:
$prev_dat = end($pageTree->tree);
if ($prev_dat['row']['uid'] == $id) {
if (!$this->dontPrintPageInsertIcons && $this->checkNewPageInPid($id)) {
$lines[] = '<ul class="list-tree" style="margin-left: 25px"><li><span class="text-nowrap"><a href="#" onclick="' . htmlspecialchars($this->onClickEvent($id, $id)) . '"><i class="t3-icon fa fa-long-arrow-left" title="' . $this->insertlabel() . '"></i></a></span></li></ul>';
}
}
for ($a = $latestInvDepth; $a <= $this->depth; $a++) {
$dat = $saveLatestUid[$a];
$prevPid = -$dat['row']['uid'];
if (!$this->dontPrintPageInsertIcons && $this->checkNewPageInPid($dat['row']['pid'])) {
if ($latestInvDepth < $dat['invertedDepth']) {
$lines[] = '</ul>';
}
$lines[] = '<span class="text-nowrap"><a href="#" onclick="' . htmlspecialchars($this->onClickEvent($prevPid, $dat['row']['pid'])) . '"><i class="t3-icon fa fa-long-arrow-left" title="' . $this->insertlabel() . '"></i></a></span>';
}
}
$code = '<ul class="list-tree">';
foreach ($lines as $line) {
if ((strpos($line, '<ul') === 0) || (strpos($line, '</ul') === 0)) {
$code .= $line;
} else {
$code .= '<li>' . $line . '</li>';
}
}
$code .= '</ul>';
return $code;
}
/**
* Wrap $t_code in bold IF the $dat uid matches $id
*
* @param string $t_code Title string
* @param array $dat Information array with record array inside.
* @param int $id The current id.
* @return string The title string.
*/
public function boldTitle($t_code, $dat, $id)
{
if ($dat['row']['uid'] == $id) {
$t_code = '<strong>' . $t_code . '</strong>';
}
return $t_code;
}
/**
* Creates the onclick event for the insert-icons.
*
* TSconfig mod.newPageWizard.override may contain an alternative module / route which can be
* used instead of the normal create new page wizard.
*
* @param int $pid The pid.
* @param int $newPagePID New page id.
* @return string Onclick attribute content
*/
public function onClickEvent($pid, $newPagePID)
{
$TSconfig = BackendUtility::getPagesTSconfig($newPagePID)['mod.']['newPageWizard.'] ?? [];
if (isset($TSconfig['override']) && !empty($TSconfig['override'])) {
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$url = $uriBuilder->buildUriFromRoute(
$TSconfig['override'],
[
'positionPid' => $pid,
'newPageId' => $newPagePID,
'cmd' => 'crPage',
'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
]
);
return $this->clientContext . '.location.href=' . GeneralUtility::quoteJSvalue((string)$url) . ';';
}
$params = '&edit[pages][' . $pid . ']=new&returnNewPageId=1';
return BackendUtility::editOnClick($params, '', $this->R_URI);
}
/**
* Get label, htmlspecialchars()'ed
*
* @return string The localized label for "insert new page here
*/
public function insertlabel()
{
return htmlspecialchars($this->getLanguageService()->getLL($this->l_insertNewPageHere));
}
/**
* Wrapping page title.
*
* @param string $str Page title.
* @param array $rec Page record (?)
* @return string Wrapped title.
*/
public function linkPageTitle($str, $rec)
{
return $str;
}
/**
* Checks if the user has permission to created pages inside of the $pid page.
* Uses caching so only one regular lookup is made - hence you can call the function multiple times without worrying about performance.
*
* @param int $pid Page id for which to test.
* @return bool
*/
public function checkNewPageInPid($pid)
{
if (!isset($this->checkNewPageCache[$pid])) {
$pidInfo = BackendUtility::getRecord('pages', $pid);
$this->checkNewPageCache[$pid] = $this->getBackendUser()->isAdmin() || $this->getBackendUser()->doesUserHaveAccess($pidInfo, 8);
}
return $this->checkNewPageCache[$pid];
}
/**
* Returns module configuration for a pid.
*
* @param int $pid Page id for which to get the module configuration.
* @return array The properties of the module configuration for the page id.
* @see onClickEvent()
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function getModConfig($pid)
{
trigger_error('PagePositionMap->getModConfig() will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
if (!isset($this->getModConfigCache[$pid])) {
// Acquiring TSconfig for this PID:
$this->getModConfigCache[$pid]['properties'] = BackendUtility::getPagesTSconfig($pid)['mod.']['web_list.']['newPageWiz.'] ?? [];
}
return $this->getModConfigCache[$pid]['properties'];
}
/*************************************
*
* Content element positioning:
*
**************************************/
/**
* Creates HTML for inserting/moving content elements.
*
* @param int $pid page id onto which to insert content element.
* @param int $moveUid Move-uid (tt_content element uid?)
* @param string $colPosList List of columns to show
* @param bool $showHidden If not set, then hidden/starttime/endtime records are filtered out.
* @param string $R_URI Request URI
* @return string HTML
*/
public function printContentElementColumns($pid, $moveUid, $colPosList, $showHidden, $R_URI)
{
$this->R_URI = $R_URI;
$this->moveUid = $moveUid;
$colPosArray = GeneralUtility::trimExplode(',', $colPosList, true);
$lines = [];
foreach ($colPosArray as $kk => $vv) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
if ($showHidden) {
$queryBuilder->getRestrictions()
->removeByType(HiddenRestriction::class)
->removeByType(StartTimeRestriction::class)
->removeByType(EndTimeRestriction::class);
}
$queryBuilder
->select('*')
->from('tt_content')
->where(
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('colPos', $queryBuilder->createNamedParameter($vv, \PDO::PARAM_INT))
)
->orderBy('sorting');
if ((string)$this->cur_sys_language !== '') {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq(
'sys_language_uid',
$queryBuilder->createNamedParameter($this->cur_sys_language, \PDO::PARAM_INT)
)
);
}
$res = $queryBuilder->execute();
$lines[$vv] = [];
$lines[$vv][] = $this->insertPositionIcon('', $vv, $kk, $moveUid, $pid);
while ($row = $res->fetch()) {
BackendUtility::workspaceOL('tt_content', $row);
if (is_array($row)) {
$lines[$vv][] = $this->wrapRecordHeader($this->getRecordHeader($row), $row);
$lines[$vv][] = $this->insertPositionIcon($row, $vv, $kk, $moveUid, $pid);
}
}
}
return $this->printRecordMap($lines, $colPosArray, $pid);
}
/**
* Creates the table with the content columns
*
* @param array $lines Array with arrays of lines for each column
* @param array $colPosArray Column position array
* @param int $pid The id of the page
* @return string HTML
*/
public function printRecordMap($lines, $colPosArray, $pid = 0)
{
$count = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange(count($colPosArray), 1);
$backendLayout = GeneralUtility::callUserFunction(\TYPO3\CMS\Backend\View\BackendLayoutView::class . '->getSelectedBackendLayout', $pid, $this);
if (isset($backendLayout['__config']['backend_layout.'])) {
$this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
$table = '<div class="table-fit"><table class="table table-condensed table-bordered table-vertical-top">';
$colCount = (int)$backendLayout['__config']['backend_layout.']['colCount'];
$rowCount = (int)$backendLayout['__config']['backend_layout.']['rowCount'];
$table .= '<colgroup>';
for ($i = 0; $i < $colCount; $i++) {
$table .= '<col style="width:' . 100 / $colCount . '%"></col>';
}
$table .= '</colgroup>';
$table .= '<tbody>';
$tcaItems = GeneralUtility::callUserFunction(\TYPO3\CMS\Backend\View\BackendLayoutView::class . '->getColPosListItemsParsed', $pid, $this);
// Cycle through rows
for ($row = 1; $row <= $rowCount; $row++) {
$rowConfig = $backendLayout['__config']['backend_layout.']['rows.'][$row . '.'];
if (!isset($rowConfig)) {
continue;
}
$table .= '<tr>';
for ($col = 1; $col <= $colCount; $col++) {
$columnConfig = $rowConfig['columns.'][$col . '.'];
if (!isset($columnConfig)) {
continue;
}
// Which tt_content colPos should be displayed inside this cell
$columnKey = (int)$columnConfig['colPos'];
$head = '';
foreach ($tcaItems as $item) {
if ($item[1] == $columnKey) {
$head = htmlspecialchars($this->getLanguageService()->sL($item[0]));
}
}
// Render the grid cell
$table .= '<td'
. (isset($columnConfig['colspan']) ? ' colspan="' . $columnConfig['colspan'] . '"' : '')
. (isset($columnConfig['rowspan']) ? ' rowspan="' . $columnConfig['rowspan'] . '"' : '')
. ' class="col-nowrap col-min'
. (!isset($columnConfig['colPos']) ? ' warning' : '')
. (isset($columnConfig['colPos']) && !$head ? ' danger' : '') . '">';
// Render header
$table .= '<p>';
if (isset($columnConfig['colPos']) && $head) {
$table .= '<strong>' . $this->wrapColumnHeader($head, '') . '</strong>';
} elseif ($columnConfig['colPos']) {
$table .= '<em>' . $this->wrapColumnHeader($this->getLanguageService()->getLL('noAccess'), '') . '</em>';
} else {
$table .= '<em>' . $this->wrapColumnHeader(($this->getLanguageService()->sL($columnConfig['name']) ?: '') . ' (' . $this->getLanguageService()->getLL('notAssigned') . ')', '') . '</em>';
}
$table .= '</p>';
// Render lines
if (isset($columnConfig['colPos']) && $head && !empty($lines[$columnKey])) {
$table .= '<ul class="list-unstyled">';
foreach ($lines[$columnKey] as $line) {
$table .= '<li>' . $line . '</li>';
}
$table .= '</ul>';
}
$table .= '</td>';
}
$table .= '</tr>';
}
$table .= '</tbody>';
$table .= '</table></div>';
} else {
// Traverse the columns here:
$row = '';
foreach ($colPosArray as $kk => $vv) {
$row .= '<td class="col-nowrap col-min" width="' . round(100 / $count) . '%">';
$row .= '<p><strong>' . $this->wrapColumnHeader(htmlspecialchars($this->getLanguageService()->sL(BackendUtility::getLabelFromItemlist('tt_content', 'colPos', $vv))), $vv) . '</strong></p>';
if (!empty($lines[$vv])) {
$row .= '<ul class="list-unstyled">';
foreach ($lines[$vv] as $line) {
$row .= '<li>' . $line . '</li>';
}
$row .= '</ul>';
}
$row .= '</td>';
}
$table = '
<!--
Map of records in columns:
-->
<div class="table-fit">
<table class="table table-condensed table-bordered table-vertical-top">
<tr>' . $row . '</tr>
</table>
</div>
';
}
return $table;
}
/**
* Wrapping the column header
*
* @param string $str Header value
* @param string $vv Column info.
* @return string
* @see printRecordMap()
*/
public function wrapColumnHeader($str, $vv)
{
return $str;
}
/**
* Creates a linked position icon.
*
* @param mixed $row Element 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 string $vv Column position value.
* @param int $kk Column key.
* @param int $moveUid Move uid
* @param int $pid PID value.
* @return string
*/
public function insertPositionIcon($row, $vv, $kk, $moveUid, $pid)
{
if (is_array($row) && !empty($row['uid'])) {
// Use record uid for the hash when inserting after this content element
$uid = $row['uid'];
} else {
// No uid means insert at first position in the column
$uid = '';
}
$cc = hexdec(substr(md5($uid . '-' . $vv . '-' . $kk), 0, 4));
return '<a href="#" onclick="' . htmlspecialchars($this->onClickInsertRecord($row, $vv, $moveUid, $pid, $this->cur_sys_language)) . '" data-dismiss="modal">' . '<i class="t3-icon fa fa-long-arrow-left" name="mImgEnd' . $cc . '" title="' . htmlspecialchars($this->getLanguageService()->getLL($this->l_insertNewRecordHere)) . '"></i></a>';
}
/**
* Create on-click event value.
*
* @param mixed $row The record. If this is not an array with the record data the insert will be for the first position
* in the column
* @param string $vv Column position value.
* @param int $moveUid Move uid
* @param int $pid PID value.
* @param int $sys_lang System language (not used currently)
* @return string
*/
public function onClickInsertRecord($row, $vv, $moveUid, $pid, $sys_lang = 0)
{
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
if (is_array($row)) {
$location = $uriBuilder->buildUriFromRoute('tce_db', [
'cmd[tt_content][' . $moveUid . '][' . $this->moveOrCopy . ']' => '-' . $row['uid'],
'redirect' => $this->R_URI,
]);
} else {
$location = $uriBuilder->buildUriFromRoute('tce_db', [
'cmd[tt_content][' . $moveUid . '][' . $this->moveOrCopy . ']' => $pid,
'data[tt_content][' . $moveUid . '][colPos]' => $vv,
'redirect' => $this->R_URI,
]);
}
// returns to prev. page
return $this->clientContext . '.location.href=' . GeneralUtility::quoteJSvalue((string)$location) . ';return false;';
}
/**
* Wrapping the record header (from getRecordHeader())
*
* @param string $str HTML content
* @param string $row Record array.
* @return string HTML content
*/
public function wrapRecordHeader($str, $row)
{
return $str;
}
/**
* Create record header (includes the record icon, record title etc.)
*
* @param array $row Record row.
* @return string HTML
*/
public function getRecordHeader($row)
{
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$toolTip = BackendUtility::getRecordToolTip($row, 'tt_content');
$line = '<span ' . $toolTip . ' title="' . BackendUtility::getRecordIconAltText($row, 'tt_content') . '">' . $iconFactory->getIconForRecord('tt_content', $row, Icon::SIZE_SMALL)->render() . '</span>';
$line .= BackendUtility::getRecordTitle('tt_content', $row, true);
return $this->wrapRecordTitle($line, $row);
}
/**
* Wrapping the title of the record.
*
* @param string $str The title value.
* @param array $row The record row.
* @return string Wrapped title string.
*/
public function wrapRecordTitle($str, $row)
{
return '<a href="' . htmlspecialchars(GeneralUtility::linkThisScript(['uid' => (int)$row['uid'], 'moveUid' => ''])) . '">' . $str . '</a>';
}
/**
* Returns the BackendUser
*
* @return BackendUserAuthentication
*/
protected function getBackendUser()
{
return $GLOBALS['BE_USER'];
}
/**
* Returns the LanguageService
*
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,146 @@
<?php
namespace TYPO3\CMS\Backend\Tree\View;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Generate a page-tree, non-browsable.
*/
class PageTreeView extends AbstractTreeView
{
/**
* @var array
*/
public $fieldArray = [
'uid',
'pid',
'is_siteroot',
'title',
'doktype',
'nav_title',
'mount_pid',
'php_tree_stop',
't3ver_id',
't3ver_state',
'hidden',
'starttime',
'endtime',
'fe_group',
'module',
'extendToSubpages',
'nav_hide'
];
/**
* override to use this treeName
* @var string
*/
public $treeName = 'pages';
/**
* override to use this table
* @var string
*/
public $table = 'pages';
/**
* @var bool
*/
public $ext_showNavTitle = false;
/**
* 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 = '')
{
parent::init(' AND deleted=0 AND sys_language_uid=0 ' . $clause, 'sorting');
}
/**
* Returns TRUE/FALSE if the next level for $id should be expanded - and all levels should, so we always return 1.
*
* @param int $id ID (uid) to test for (see extending classes where this is checked against session data)
* @return bool
*/
public function expandNext($id)
{
return 1;
}
/**
* 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 $isExpand The element was expanded to render subelements if this flag is set.
* @return string Image tag with the plus/minus icon.
* @internal
* @see AbstractTreeView::PMicon()
*/
public function PMicon($row, $a, $c, $nextCount, $isExpand)
{
return '<span class="treeline-icon treeline-icon-join' . ($a == $c ? 'bottom' : '') . '"></span>';
}
/**
* Get stored tree structure AND updating it if needed according to incoming PM GET var.
* - Here we just set it to nothing since we want to just render the tree, nothing more.
*
* @internal
*/
public function initializePositionSaving()
{
$this->stored = [];
}
/**
* Returns the title for the input record. If blank, a "no title" label (localized) will be returned.
* Do NOT htmlspecialchar the string from this function - has already been done.
*
* @param array $row The input row array (where the key "title" is used for the title)
* @param int $titleLen Title length (30)
* @return string The title.
*/
public function getTitleStr($row, $titleLen = 30)
{
$lang = $this->getLanguageService();
if ($this->ext_showNavTitle && isset($row['nav_title']) && trim($row['nav_title']) !== '') {
$title = '<span title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:title')) . ' '
. htmlspecialchars(trim($row['title'])) . '">'
. htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['nav_title'], $titleLen))
. '</span>';
} else {
$title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['title'], $titleLen));
if (isset($row['nav_title']) && trim($row['nav_title']) !== '') {
$title = '<span title="'
. htmlspecialchars($lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.nav_title'))
. ' ' . htmlspecialchars(trim($row['nav_title'])) . '">' . $title
. '</span>';
}
$title = trim($row['title']) === ''
? '<em>[' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>'
: $title;
}
return $title;
}
}