TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:30 +02:00
commit edc7fea85a
20 changed files with 2188 additions and 0 deletions
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Info\Controller\Event;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Module\ModuleInterface;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
/**
* Listeners to this Event will be able to modify the header and footer content of the info module
*/
final class ModifyInfoModuleContentEvent
{
private string $headerContent = '';
private string $footerContent = '';
public function __construct(
private readonly bool $access,
private readonly ServerRequestInterface $request,
private readonly ModuleInterface $currentModule,
private readonly ModuleTemplate $moduleTemplate,
) {}
/**
* Whether the current user has access to the main content of the info module.
* IMPORTANT: This is only for informational purposes. Listeners can therefore
* decide on their own if their content should be added to the module even if
* the user does not have access to the main module content.
*/
public function hasAccess(): bool
{
return $this->access;
}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getCurrentModule(): ModuleInterface
{
return $this->currentModule;
}
public function getModuleTemplate(): ModuleTemplate
{
return $this->moduleTemplate;
}
/**
* Set content for the header. Can also be used to e.g. reorder existing content.
* IMPORTANT: This overwrites existing content from previous listeners!
*/
public function setHeaderContent(string $content): void
{
$this->headerContent = $content;
}
/**
* Add additional content to the header
*/
public function addHeaderContent(string $content): void
{
$this->headerContent .= $content;
}
public function getHeaderContent(): string
{
return $this->headerContent;
}
/**
* Set content for the footer. Can also be used to e.g. reorder existing content.
* IMPORTANT: This overwrites existing content from previous listeners!
*/
public function setFooterContent(string $content): void
{
$this->footerContent = $content;
}
/**
* Add additional content to the footer
*/
public function addFooterContent(string $content): void
{
$this->footerContent .= $content;
}
public function getFooterContent(): string
{
return $this->footerContent;
}
}
@@ -0,0 +1,734 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Info\Controller;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Info\Controller\Event\ModifyInfoModuleContentEvent;
/**
* Status -> Pagetree Overview
*
* @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
*/
#[AsController]
readonly class PageInformationController
{
public function __construct(
protected IconFactory $iconFactory,
protected UriBuilder $uriBuilder,
protected ModuleTemplateFactory $moduleTemplateFactory,
protected EventDispatcherInterface $eventDispatcher,
protected TcaSchemaFactory $tcaSchemaFactory,
protected ComponentFactory $componentFactory,
protected BackendLayoutView $backendLayoutView,
protected ConnectionPool $connectionPool,
protected LocalizationRepository $localizationRepository,
) {}
public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$backendUser = $this->getBackendUser();
$languageService = $this->getLanguageService();
$module = $request->getAttribute('module');
$moduleData = $request->getAttribute('moduleData');
$currentSite = $request->getAttribute('site');
$pageId = (int)($request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0);
$pageinfo = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: [];
$hasAccess = false;
if (($pageId > 0 && $pageinfo !== []) || ($backendUser->isAdmin() && $pageId === 0)) {
$hasAccess = true;
}
if ($pageId === 0 && $backendUser->isAdmin()) {
$pageinfo = ['title' => '[root-level]', 'uid' => 0, 'pid' => 0];
}
$siteLanguages = [
$currentSite->getDefaultLanguage()->getLanguageId() => $currentSite->getDefaultLanguage(),
];
foreach ($currentSite->getAvailableLanguages($this->getBackendUser(), false, $pageId) as $language) {
$siteLanguages[$language->getLanguageId()] = $language;
}
$fieldConfiguration = $this->getFieldConfiguration($pageId);
$allowedModuleOptions = $this->getModuleOptions($siteLanguages, $fieldConfiguration);
if ($moduleData->cleanUp($allowedModuleOptions)) {
$backendUser->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray());
}
$selectedDepth = (int)($moduleData->get('depth') ?? 0);
$selectedGroup = (string)($moduleData->get('pages') ?? '0'); // field or table list to render
$selectedLanguage = (int)($moduleData->get('lang') ?? 0);
$mainContent = $this->renderMainTable($pageId, $selectedDepth, $selectedLanguage, $siteLanguages, $request, $fieldConfiguration[$selectedGroup]['fields'] ?? []);
$view = $this->moduleTemplateFactory->create($request);
$view->assign('hasAccess', $hasAccess);
if ($hasAccess) {
$view->setTitle($languageService->sL($module->getTitle()), $pageId !== 0 && isset($pageinfo['title']) ? $pageinfo['title'] : '');
$view->getDocHeaderComponent()->setPageBreadcrumb($pageinfo);
$view->makeDocHeaderModuleMenu(['id' => $pageId]);
$view->getDocHeaderComponent()->setShortcutContext($module->getIdentifier(), sprintf('%s [%d]', $languageService->sL($module->getTitle()), $pageId), ['id' => $pageId]);
$previewUriBuilder = PreviewUriBuilder::create($pageinfo);
if ($previewUriBuilder->isPreviewable()) {
// View page
$previewDataAttributes = $previewUriBuilder
->withRootLine(BackendUtility::BEgetRootLine($pageinfo['uid']))
->buildDispatcherDataAttributes();
$viewButton = $this->componentFactory->createLinkButton()
->setHref('#')
->setDataAttributes($previewDataAttributes ?? [])
->setDisabled(!$previewDataAttributes)
->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
->setIcon($this->iconFactory->getIcon('actions-view-page', IconSize::SMALL))
->setShowLabelText(true);
$view->addButtonToButtonBar($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
}
}
$event = $this->eventDispatcher->dispatch(new ModifyInfoModuleContentEvent($hasAccess, $request, $module, $view));
if ($hasAccess) {
$view->assignMultiple([
'pageUid' => $pageId,
'content' => $mainContent,
'depthDropdownOptions' => $allowedModuleOptions['depth'],
'depthDropdownCurrentValue' => $selectedDepth,
'pagesDropdownOptions' => $allowedModuleOptions['pages'],
'pagesDropdownCurrentValue' => $selectedGroup,
'langDropdownOptions' => $allowedModuleOptions['lang'],
'langDropdownCurrentValue' => $selectedLanguage,
'headerContent' => $event->getHeaderContent(),
'footerContent' => $event->getFooterContent(),
]);
}
return $view->renderResponse('PageInformation');
}
protected function getModuleOptions(array $siteLanguages, array $fieldConfiguration): array
{
$languageService = $this->getLanguageService();
$menu = [
'pages' => [],
'depth' => [
0 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'),
1 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'),
2 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'),
3 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'),
4 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'),
999 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi'),
],
'lang' => [],
];
foreach ($fieldConfiguration as $key => $item) {
$menu['pages'][$key] = $item['label'];
}
foreach ($siteLanguages as $language) {
$menu['lang'][$language->getLanguageId()] = $language->getTitle();
}
return $menu;
}
/**
* Generate configuration for field and table selection from TSConfig.
*/
protected function getFieldConfiguration(int $pageId): array
{
$languageService = $this->getLanguageService();
$fieldConfiguration = [];
$modTSconfig = BackendUtility::getPagesTSconfig($pageId)['mod.']['web_info.']['fieldDefinitions.'] ?? [];
$allowedTables = $this->getAllowedTableNames();
foreach ($modTSconfig as $key => $item) {
$fieldList = str_replace('###ALL_TABLES###', implode(',', $allowedTables), $item['fields']);
$fields = GeneralUtility::trimExplode(',', $fieldList, true);
$key = trim($key, '.');
$fieldConfiguration[$key] = [
'label' => $item['label'] ? $languageService->sL($item['label']) : $key,
'fields' => $fields,
];
}
return $fieldConfiguration;
}
/**
* A list of table names allowed to be listed when ###ALL_TABLES### is used in TSConfig.
* Some tables like 'pages' are blinded by default, all remaining ones are user access checked.
*/
protected function getAllowedTableNames(): array
{
$hideTables = ['pages', 'sys_filemounts', 'be_users', 'be_groups']; // Never show these tables
$allowedTables = [];
foreach ($this->tcaSchemaFactory->all() as $schemaName => $schema) {
if (in_array($schemaName, $hideTables, true)
|| $schema->hasCapability(TcaSchemaCapability::HideInUi)
|| !$this->getBackendUser()->check('tables_select', $schemaName)
) {
continue;
}
$allowedTables[] = 'table_' . $schemaName;
}
return $allowedTables;
}
/**
* Renders records from the pages table from page id
*
* @return string HTML for the listing
*/
protected function renderMainTable(int $id, int $depth, int $language, array $siteLanguages, ServerRequestInterface $request, array $fieldArray): string
{
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUser();
$out = '';
$pagesSchema = $this->tcaSchemaFactory->get('pages');
$languageFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
$translationOriginFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$row = $queryBuilder
->select('*')
->from('pages')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, Connection::PARAM_INT)),
$backendUser->getPagePermsClause(Permission::PAGE_SHOW)
)
->executeQuery()
->fetchAssociative();
BackendUtility::workspaceOL('pages', $row);
if ($language > 0) {
$localizedPageRecord = $this->localizationRepository->getPageTranslations($row['uid'], [$language], $this->getBackendUser()->workspace);
if ($localizedPageRecord !== []) {
$row = reset($localizedPageRecord)->toArray();
$row['uid'] = $row[$translationOriginFieldName];
}
}
if (!is_array($row)) {
return '';
}
$editUids = [];
// Getting children
$theRows = $this->getPageRecordsRecursive($row['uid'], $depth, $language);
// Get tree root page
$treeRootPage = $this->getTreeRootPage($row['uid'], $row[$languageFieldName]);
if ($backendUser->doesUserHaveAccess($treeRootPage, Permission::PAGE_EDIT) && $treeRootPage['uid'] > 0) {
$editUids[] = $treeRootPage['uid'];
}
$out .= $this->pages_drawItem($treeRootPage, $request, $siteLanguages, $fieldArray);
// Traverse all pages selected:
foreach ($theRows as $sRow) {
if ($backendUser->doesUserHaveAccess($sRow, Permission::PAGE_EDIT)) {
$editUids[] = $sRow['uid'];
}
$out .= $this->pages_drawItem($sRow, $request, $siteLanguages, $fieldArray);
}
// Header line is drawn
$headerCells = [];
$editIdList = implode(',', $editUids);
// Traverse fields (as set above) in order to create header values:
foreach ($fieldArray as $field) {
$editButton = '';
if (
$editIdList
&& $pagesSchema->hasField($field)
&& $backendUser->check('tables_modify', 'pages')
&& $backendUser->check('non_exclude_fields', 'pages:' . $field)
) {
$iTitle = sprintf(
$languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:editThisColumn'),
rtrim(trim($languageService->sL($pagesSchema->getField($field)->getLabel())), ':')
);
$urlParameters = [
'edit' => [
'pages' => [
$editIdList => 'edit',
],
],
'columnsOnly' => [
'pages' => [$field],
],
'module' => 'web_info_overview',
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
];
$url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
$editButton = '<a class="btn btn-default" href="' . htmlspecialchars($url)
. '" title="' . htmlspecialchars($iTitle) . '">'
. $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render() . '</a>';
}
switch ($field) {
case 'title':
$headerCells[$field] = $editButton . '&nbsp;<strong>'
. $languageService->sL($pagesSchema->getField($field)->getLabel())
. '</strong>';
break;
case 'uid':
$headerCells[$field] = '';
break;
case 'actual_backend_layout':
$headerCells[$field] = htmlspecialchars($languageService->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:actual_backend_layout'));
break;
default:
if (str_starts_with($field, 'table_')) {
$f2 = substr($field, 6);
if ($this->tcaSchemaFactory->has($f2)) {
$schema = $this->tcaSchemaFactory->get($f2);
$headerCells[$field] = '&nbsp;'
. '<span title="'
. htmlspecialchars($schema->getTitle($languageService->sL(...)) ?: $f2)
. '">'
. $this->iconFactory->getIconForRecord($f2, [], IconSize::SMALL)->render()
. '</span>';
}
} else {
if ($pagesSchema->hasField($field)) {
$headerCells[$field] = $editButton . '&nbsp;<strong>'
. htmlspecialchars($languageService->sL($pagesSchema->getField($field)->getLabel()))
. '</strong>';
} else {
// Invalid field configured in `mod.web_info.fieldDefinitions.*`,
// using field name as header label.
$headerCells[$field] = $editButton . '&nbsp;<strong>'
. htmlspecialchars($field)
. '</strong>';
}
}
}
}
return '
<div class="table-fit">
<table class="table table-striped table-hover" id="PageInformationControllerTable">
<thead>
' . $this->addElement($headerCells, $fieldArray) . '
</thead>
<tbody>
' . $out . '
</tbody>
</table>
</div>';
}
/**
* Get tree root page
*
* @param int $pid Starting page
* @param int $language Selected site language
*/
protected function getTreeRootPage(int $pid, int $language): array
{
$backendUser = $this->getBackendUser();
$pagesSchema = $this->tcaSchemaFactory->get('pages');
$languageFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
$translationOriginFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $backendUser->workspace));
if ($language > 0) {
return $queryBuilder
->select('*')
->from('pages')
->where(
$queryBuilder->expr()->eq($translationOriginFieldName, $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)),
$queryBuilder->expr()->eq($languageFieldName, $queryBuilder->createNamedParameter($language, Connection::PARAM_INT)),
$backendUser->getPagePermsClause(Permission::PAGE_SHOW)
)
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
}
return $queryBuilder
->select('*')
->from('pages')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)),
$backendUser->getPagePermsClause(Permission::PAGE_SHOW)
)
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
}
/**
* Adds pages-rows to an array, selecting recursively in the page tree.
*
* @param int $pid Starting page id to select from
* @param string $iconPrefix Prefix for icon code.
* @param int $depth Depth (decreasing)
* @param array $rows Array which will accumulate page rows
* @return array $rows with added rows.
*/
protected function getPageRecordsRecursive(int $pid, int $depth, int $language, string $iconPrefix = '', array $rows = []): array
{
$backendUser = $this->getBackendUser();
$pagesSchema = $this->tcaSchemaFactory->get('pages');
$languageFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
$depth--;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $backendUser->workspace));
$queryBuilder
->select('*')
->from('pages')
->where(
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)),
$queryBuilder->expr()->eq($languageFieldName, $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
$backendUser->getPagePermsClause(Permission::PAGE_SHOW)
);
if ($pagesSchema->hasCapability(TcaSchemaCapability::SortByField)) {
$queryBuilder->orderBy($pagesSchema->getCapability(TcaSchemaCapability::SortByField)->getFieldName());
}
if ($depth >= 0) {
$countQueryBuilder = clone $queryBuilder;
$countQueryBuilder->resetOrderBy()->count('uid');
$rowCount = $countQueryBuilder->executeQuery()->fetchOne();
$result = $queryBuilder->executeQuery();
$count = 0;
while ($row = $result->fetchAssociative()) {
BackendUtility::workspaceOL('pages', $row);
$uid = (int)$row['uid'];
if (is_array($row)) {
if ($language > 0) {
$localizedPageRecord = $this->localizationRepository->getPageTranslations($uid, [$language], $this->getBackendUser()->workspace);
if ($localizedPageRecord === []) {
continue;
}
$row = reset($localizedPageRecord)->toArray();
}
$count++;
$row['treeIcons'] = $iconPrefix
. '<span class="treeline-icon treeline-icon-join'
. ($rowCount === $count ? 'bottom' : '')
. '"></span>';
$rows[] = $row;
// Get the branch
$spaceOutIcons = '<span class="treeline-icon treeline-icon-'
. ($rowCount === $count ? 'clear' : 'line')
. '"></span>';
$rows = $this->getPageRecordsRecursive(
$uid,
$row['php_tree_stop'] ? 0 : $depth,
$language,
$iconPrefix . $spaceOutIcons,
$rows
);
}
}
}
return $rows;
}
/**
* Adds a list item for the pages-rendering
*/
protected function pages_drawItem(array $row, ServerRequestInterface $request, array $siteLanguages, array $fieldArray): string
{
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUser();
$pagesSchema = $this->tcaSchemaFactory->get('pages');
$languageFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
$backendLayouts = $this->getBackendLayouts($row, 'backend_layout');
$backendLayoutsNextLevel = $this->getBackendLayouts($row, 'backend_layout_next_level');
$userTsConfig = $this->getBackendUser()->getTSConfig();
$theIcon = $this->getIcon($row);
// Preparing and getting the data-array
$theData = [];
foreach ($fieldArray as $field) {
switch ($field) {
case 'title':
$showPageId = !empty($userTsConfig['options.']['pageTree.']['showPageIdWithTitle']);
$pTitle = htmlspecialchars(
(string)BackendUtility::getProcessedValue(
'pages',
$field,
$row[$field],
0,
false,
false,
0,
true,
0,
$row
)
);
$theData[$field] = '<div class="treeline-container">'
. ($row['treeIcons'] ?? '')
. $theIcon
. '<span class="treeline-label">'
. ($showPageId ? '[' . $row['uid'] . '] ' : '')
. $pTitle
. '</span>'
. '</div>';
break;
case $languageFieldName:
if (count($siteLanguages) === 1) {
$theData[$field] = '';
break;
}
$siteLanguage = $siteLanguages[$row[$languageFieldName]] ?? null;
if (!$siteLanguage) {
$theData[$field] = '';
break;
}
$theData[$field] = $this->iconFactory->getIcon($siteLanguage->getFlagIdentifier(), IconSize::SMALL)->setTitle($siteLanguage->getTitle())->render()
. ' ' . $siteLanguage->getTitle();
break;
case 'php_tree_stop':
// Intended fall through
case 'TSconfig':
$theData[$field] = $row[$field] ? '<strong>x</strong>' : '&nbsp;';
break;
case 'actual_backend_layout':
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage((int)$row['uid']);
$theData[$field] = htmlspecialchars($languageService->sL($backendLayout->getTitle()));
break;
case 'backend_layout':
$layoutValue = $backendLayouts[$row[$field]] ?? null;
$theData[$field] = $this->resolveBackendLayoutValue($layoutValue, $field, $row);
break;
case 'backend_layout_next_level':
$layoutValue = $backendLayoutsNextLevel[$row[$field]] ?? null;
$theData[$field] = $this->resolveBackendLayoutValue($layoutValue, $field, $row);
break;
case 'uid':
$uid = 0;
$editButton = '';
$viewButton = '';
if ($backendUser->doesUserHaveAccess($row, 2) && $row['uid'] > 0) {
$uid = (int)$row['uid'];
$urlParameters = [
'edit' => [
'pages' => [
$row['uid'] => 'edit',
],
],
'module' => 'web_info_overview',
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
];
$url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
$previewDataAttributes = PreviewUriBuilder::create($row)
->withRootLine(BackendUtility::BEgetRootLine($row['uid']))
->serializeDispatcherAttributes();
$viewButton
= '<button ' . ($previewDataAttributes ?? 'disabled="true"') . ' class="btn btn-default" title="'
. htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">'
. $this->iconFactory->getIcon('actions-view-page', IconSize::SMALL)->render()
. '</button>';
if ($backendUser->check('tables_modify', 'pages')) {
$editButton
= '<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="'
. htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:editPageProperties')) . '">'
. $this->iconFactory->getIcon('actions-page-open', IconSize::SMALL)->render()
. '</a>';
}
}
// Since the uid is overwritten with the edit button markup, we need to store
// the actual uid to be able to add it as data attribute to the table data cell.
// This also makes the distinction between record rows and the header line simpler.
$theData['_UID_'] = $uid;
$theData[$field] = '<div class="btn-group btn-group-sm" role="group">' . $viewButton . $editButton . '</div>';
break;
case 'shortcut':
case 'shortcut_mode':
if ((int)$row['doktype'] === PageRepository::DOKTYPE_SHORTCUT) {
$theData[$field] = htmlspecialchars((string)BackendUtility::getProcessedValue('pages', $field, $row[$field], 0, false, false, 0, true, 0, $row));
}
break;
default:
if (str_starts_with($field, 'table_')) {
$f2 = substr($field, 6);
if ($this->tcaSchemaFactory->has($f2)) {
$c = $this->numberOfRecords($f2, (int)$row['uid']);
$theData[$field] = ($c ?: '');
}
} else {
$theData[$field] = htmlspecialchars((string)BackendUtility::getProcessedValue('pages', $field, $row[$field], 0, false, false, 0, true, 0, $row));
}
}
}
return $this->addElement($theData, $fieldArray);
}
/**
* Creates the icon image tag for the page and wraps it in a link which will trigger the click menu.
*/
protected function getIcon(array $row): string
{
$backendUser = $this->getBackendUser();
$icon = '<span title="' . BackendUtility::getRecordIconAltText($row, 'pages') . '">' . $this->iconFactory->getIconForRecord('pages', $row, IconSize::SMALL)->render() . '</span>';
// The icon with link
if ($backendUser->checkRecordEditAccess('pages', $row)->isAllowed) {
$icon = BackendUtility::wrapClickMenuOnIcon($icon, 'pages', $row['uid']);
}
return $icon;
}
/**
* Counts and returns the number of records on the page with $pid
*/
protected function numberOfRecords(string $table, int $pid): int
{
if (!$this->tcaSchemaFactory->has($table)) {
return 0;
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
return (int)$queryBuilder->count('uid')
->from($table)
->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)))
->executeQuery()
->fetchOne();
}
/**
* Returns a table-row with the content from the fields in the input data array.
*
* @param array $data Record with field values, NOT htmlspecialchar'ed
* @return string HTML content for the table row
*/
protected function addElement(array $data, array $fieldArray): string
{
// Start up:
$attributes = '';
$rowTag = 'th';
if (isset($data['_UID_'])) {
$l10nParent = isset($data['_l10nparent_']) ? (int)$data['_l10nparent_'] : 0;
$attributes = ' data-uid="' . $data['_UID_'] . '" data-l10nparent="' . $l10nParent . '"';
$rowTag = 'td';
}
$out = '<tr' . $attributes . '>';
// Init rendering.
$colsp = '';
$lastKey = '';
$c = 0;
// __label is used as the label key to circumvent problems with uid used as label (see #67756)
// as it was introduced later on, check if it really exists before using it
if (array_key_exists('__label', $data)) {
$fieldArray[0] = '__label';
}
// Traverse field array which contains the data to present:
foreach ($fieldArray as $vKey) {
if (isset($data[$vKey])) {
$cssClass = '';
if ($lastKey === 'title') {
$cssClass = 'col-title col-responsive';
}
if ($lastKey) {
$out .= '<' . $rowTag . ' class="' . $cssClass . '"' . $colsp . '>' . $data[$lastKey] . '</' . $rowTag . '>';
}
$lastKey = $vKey;
$c = 1;
} else {
if (!$lastKey) {
$lastKey = $vKey;
}
$c++;
}
if ($c > 1) {
$colsp = ' colspan="' . $c . '"';
} else {
$colsp = '';
}
}
if ($lastKey) {
$cssClass = '';
if ($lastKey === 'title') {
$cssClass = 'col-title-flexible';
}
$out .= '<' . $rowTag . ' class="' . $cssClass . ' nowrap"' . $colsp . '>' . $data[$lastKey] . '</' . $rowTag . '>';
}
$out .= '</tr>';
return $out;
}
protected function getBackendLayouts(array $row, string $field): array
{
$languageService = $this->getLanguageService();
$configuration = ['row' => $row, 'table' => 'pages', 'field' => $field, 'items' => []];
// Below we call the itemsProcFunc to retrieve properly resolved backend layout items,
// including the translated labels and the correct field values (backend layout identifiers).
$this->backendLayoutView->addBackendLayoutItems($configuration);
$backendLayouts = [];
foreach ($configuration['items'] ?? [] as $backendLayout) {
if (($backendLayout['label'] ?? false) && ($backendLayout['value'] ?? false)) {
$backendLayouts[$backendLayout['value']] = $languageService->sL($backendLayout['label']) ?: $backendLayout['label'];
}
}
return $backendLayouts;
}
protected function resolveBackendLayoutValue(?string $layoutValue, string $field, array $row): string
{
$languageService = $this->getLanguageService();
if ($layoutValue !== null) {
// Directly return the resolved layout value from BackendLayoutView
return htmlspecialchars($layoutValue);
}
$layoutValue = htmlspecialchars((string)BackendUtility::getProcessedValue('pages', $field, $row[$field], 0, false, false, 0, true, 0, $row));
if ($layoutValue !== '') {
// If getProcessedValue() returns a non-empty string, the database field
// is filled with an invalid value (the backend layout does no longer exist).
return sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $layoutValue);
}
return '';
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,513 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Info\Controller;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Module\ModuleProvider;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Tree\View\PageTreeView;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\PageViewMode;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Imaging\IconState;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\LanguageAwareSchemaCapability;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Info\Controller\Event\ModifyInfoModuleContentEvent;
/**
* Status -> Localization overview
*
* @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
*/
#[AsController]
readonly class TranslationStatusController
{
public function __construct(
private IconFactory $iconFactory,
private UriBuilder $uriBuilder,
private ModuleProvider $moduleProvider,
private ModuleTemplateFactory $moduleTemplateFactory,
private EventDispatcherInterface $eventDispatcher,
private TcaSchemaFactory $tcaSchemaFactory,
private ComponentFactory $componentFactory,
private ConnectionPool $connectionPool,
) {}
public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$backendUser = $this->getBackendUser();
$languageService = $this->getLanguageService();
$module = $request->getAttribute('module');
$moduleData = $request->getAttribute('moduleData');
$currentSite = $request->getAttribute('site');
$pageId = (int)($request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0);
$pageinfo = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: [];
$hasAccess = false;
if (($pageId && $pageinfo !== []) || ($backendUser->isAdmin() && $pageId === 0)) {
$hasAccess = true;
}
if ($pageId === 0 && $backendUser->isAdmin()) {
$pageinfo = ['title' => '[root-level]', 'uid' => 0, 'pid' => 0];
}
$siteLanguages = $currentSite->getAvailableLanguages($backendUser, false, $pageId);
$allowedModuleOptions = $this->getModuleOptions($siteLanguages);
if ($moduleData->cleanUp($allowedModuleOptions)) {
$backendUser->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray());
}
$selectedDepth = (int)$moduleData->get('depth');
$selectedLanguage = (int)$moduleData->get('lang');
$mainContent = '';
if ($pageId > 0) {
$tree = $this->getTree($pageId, $selectedDepth);
$mainContent = $this->renderL10nTable($tree, $request, $siteLanguages, $selectedLanguage);
}
$view = $this->moduleTemplateFactory->create($request);
$view->assign('hasAccess', $hasAccess);
if ($hasAccess) {
$view->setTitle($languageService->sL($module->getTitle()), $pageId !== 0 && isset($pageinfo['title']) ? $pageinfo['title'] : '');
$view->getDocHeaderComponent()->setPageBreadcrumb($pageinfo);
$view->makeDocHeaderModuleMenu(['id' => $pageId]);
$view->getDocHeaderComponent()->setShortcutContext($module->getIdentifier(), sprintf('%s [%d]', $languageService->sL($module->getTitle()), $pageId), ['id' => $pageId]);
$previewUriBuilder = PreviewUriBuilder::create($pageinfo);
if ($previewUriBuilder->isPreviewable()) {
$previewDataAttributes = $previewUriBuilder
->withRootLine(BackendUtility::BEgetRootLine($pageinfo['uid']))
->buildDispatcherDataAttributes();
$viewButton = $this->componentFactory->createLinkButton()
->setHref('#')
->setDataAttributes($previewDataAttributes ?? [])
->setDisabled(!$previewDataAttributes)
->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
->setIcon($this->iconFactory->getIcon('actions-view-page', IconSize::SMALL))
->setShowLabelText(true);
$view->addButtonToButtonBar($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
}
}
$event = $this->eventDispatcher->dispatch(new ModifyInfoModuleContentEvent($hasAccess, $request, $module, $view));
if ($hasAccess) {
$view->assignMultiple([
'pageUid' => $pageId,
'depthDropdownOptions' => $allowedModuleOptions['depth'],
'depthDropdownCurrentValue' => $selectedDepth,
'langDropdownOptions' => $allowedModuleOptions['lang'],
'langDropdownCurrentValue' => $selectedLanguage,
'content' => $mainContent,
'headerContent' => $event->getHeaderContent(),
'footerContent' => $event->getFooterContent(),
]);
}
return $view->renderResponse('TranslationStatus');
}
private function getModuleOptions(array $siteLanguages): array
{
$languageService = $this->getLanguageService();
$menuArray = [
'depth' => [
0 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'),
1 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'),
2 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'),
3 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'),
4 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'),
999 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi'),
],
'lang' => [],
];
foreach ($siteLanguages as $language) {
if ($language->getLanguageId() === 0) {
$menuArray['lang'][0] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.allLanguages');
} else {
$menuArray['lang'][$language->getLanguageId()] = $language->getTitle();
}
}
return $menuArray;
}
private function getTree(int $pageId, int $selectedDepth): PageTreeView
{
$tree = GeneralUtility::makeInstance(PageTreeView::class);
$tree->init('AND ' . $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
$tree->tree[] = ['row' => BackendUtility::getRecordWSOL('pages', $pageId)];
// Create the tree from starting point
if ($selectedDepth) {
$tree->getTree($pageId, $selectedDepth);
}
return $tree;
}
/**
* Rendering the localization information table.
*
* @param PageTreeView $tree The Page tree data
* @return string HTML for the localization information table.
*/
private function renderL10nTable(PageTreeView $tree, ServerRequestInterface $request, array $siteLanguages, int $selectedLanguage): string
{
$lang = $this->getLanguageService();
$backendUser = $this->getBackendUser();
// Put together the TREE:
$output = '';
$langRecUids = [];
$userTsConfig = $backendUser->getTSConfig();
$showPageId = !empty($userTsConfig['options.']['pageTree.']['showPageIdWithTitle']);
$pageModule = 'web_layout';
$pageModuleAccess = $this->moduleProvider->accessGranted($pageModule, $backendUser);
foreach ($tree->tree as $data) {
$tCells = [];
$langRecUids[0][] = $data['row']['uid'];
$pageTitle = ($showPageId ? '[' . (int)$data['row']['uid'] . '] ' : '') . $data['row']['title'];
// Page icons / titles etc.
if ($pageModuleAccess) {
$pageModuleLink = (string)$this->uriBuilder->buildUriFromRoute($pageModule, ['id' => $data['row']['uid'], 'languages' => [0], 'viewMode' => PageViewMode::LayoutView->value]);
$pageModuleLink = '<a href="' . htmlspecialchars($pageModuleLink) . '" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editPage') . '">' . htmlspecialchars($pageTitle) . '</a>';
} else {
$pageModuleLink = htmlspecialchars($pageTitle);
}
$icon = '<span title="' . BackendUtility::getRecordIconAltText($data['row']) . '">'
. $this->iconFactory->getIconForRecord('pages', $data['row'], IconSize::SMALL)->setTitle(BackendUtility::getRecordIconAltText($data['row'], 'pages', false))->render()
. '</span>';
if ($backendUser->checkRecordEditAccess('pages', $data['row'])->isAllowed) {
$icon = BackendUtility::wrapClickMenuOnIcon($icon, 'pages', $data['row']['uid']);
}
$tCells[] = '<td class="col-title col-responsive">'
. '<div class="treeline-container">'
. (!empty($data['depthData']) ? $data['depthData'] : '')
. ($data['HTML'] ?? '')
. $icon
. '<span class="treeline-label">'
. $pageModuleLink
. ((string)$data['row']['nav_title'] !== '' ? ' <span>[Nav: <em>' . htmlspecialchars($data['row']['nav_title']) . '</em>]</span>' : '')
. '</span>'
. '</div>'
. '</td>';
$previewUriBuilder = PreviewUriBuilder::create($data['row']);
// DEFAULT language:
$pageTranslationVisibility = new PageTranslationVisibility((int)($data['row']['l18n_cfg'] ?? 0));
$status = $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() ? 'danger' : 'success';
// Create links:
$editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
'edit' => [
'pages' => [
$data['row']['uid'] => 'edit',
],
],
'module' => 'web_info_translations',
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
]);
$info = '<button ' . ($previewUriBuilder->serializeDispatcherAttributes() ?? 'disabled="true"')
. ' class="btn btn-default" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_viewPage') . '">'
. $this->iconFactory->getIcon('actions-view-page', IconSize::SMALL)->render() . '</button>';
if ($backendUser->check('tables_modify', 'pages')) {
$info .= '<a href="' . htmlspecialchars($editUrl)
. '" class="btn btn-default" title="' . $lang->sL(
'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editPageProperties'
) . '">' . $this->iconFactory->getIcon('actions-page-open', IconSize::SMALL)->render() . '</a>';
}
$info .= '&nbsp;';
$info .= $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() ? '<span title="' . htmlspecialchars($lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.l18n_cfg.I.1')) . '">D</span>' : '&nbsp;';
$info .= $pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists() ? '<span title="' . htmlspecialchars($lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.l18n_cfg.I.2')) . '">N</span>' : '&nbsp;';
// Put into cell:
$tCells[] = '<td class="' . $status . ' col-border-left col-nowrap"><div class="btn-group btn-group-sm">' . $info . '</div></td>';
$tCells[] = '<td class="' . $status . '" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_CEcount') . '" align="center">'
. ($this->getContentElementCount((int)$data['row']['uid'], 0) ?: '-')
. '</td>';
// Traverse system languages:
foreach ($siteLanguages as $siteLanguage) {
$languageId = $siteLanguage->getLanguageId();
if ($languageId === 0) {
continue;
}
if ($selectedLanguage === 0 || $selectedLanguage === $languageId) {
$row = $this->getLangStatus((int)$data['row']['uid'], $languageId);
if ($pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() || $pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) {
$status = 'danger';
} else {
$status = '';
}
if (is_array($row)) {
$langRecUids[$languageId][] = $row['uid'];
if (!$row['_HIDDEN']) {
$status = 'success';
}
if ($row['_COUNT'] > 1) {
$status = 'warning';
}
$info = ($showPageId ? ' [' . (int)$row['uid'] . '] ' : '')
. htmlspecialchars($row['title'])
. ((string)$row['nav_title'] !== '' ? ' [Nav: <em>' . htmlspecialchars($row['nav_title']) . '</em>]' : '')
. ($row['_COUNT'] > 1 ? '<div>' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_badThingThereAre') . '</div>' : '');
if ($pageModuleAccess) {
$pageModuleLink = (string)$this->uriBuilder->buildUriFromRoute($pageModule, ['id' => $data['row']['uid'], 'language' => [$languageId], 'viewMode' => PageViewMode::LanguageComparisonView->value]);
$pageModuleLink = '<a href="' . htmlspecialchars($pageModuleLink) . '" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editTranslatedPage') . '">' . $info . '</a>';
} else {
$pageModuleLink = $info;
}
$icon = '<span title="' . BackendUtility::getRecordIconAltText($row) . '">'
. $this->iconFactory->getIconForRecord('pages', $row, IconSize::SMALL)->setTitle(BackendUtility::getRecordIconAltText($row, 'pages', false))->render()
. '</span>';
$tCells[] = '<td class="col-responsive col-border-left ' . $status . '">'
. BackendUtility::wrapClickMenuOnIcon($icon, 'pages', (int)$row['uid'])
. $pageModuleLink
. '</td>';
// Edit whole record:
// Create links:
$editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
'edit' => [
'pages' => [
$row['uid'] => 'edit',
],
],
'module' => 'web_info_translations',
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
]);
// ViewPageLink
$info = '<button ' . ($previewUriBuilder
->withLanguage($languageId)
->serializeDispatcherAttributes() ?? 'disabled="true"')
. ' class="btn btn-default" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_viewTranslatedPage') . '">'
. $this->iconFactory->getIcon('actions-view', IconSize::SMALL)->render() . '</button>';
$info .= '<a href="' . htmlspecialchars($editUrl)
. '" class="btn btn-default" title="' . $lang->sL(
'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editTranslatedPageProperties'
) . '">' . $this->iconFactory->getIcon('actions-open', IconSize::SMALL)->render() . '</a>';
$tCells[] = '<td class="' . $status . '"><div class="btn-group btn-group-sm">' . $info . '</div></td>';
$tCells[] = '<td class="' . $status . '" title="' . $lang->sL(
'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_CEcount'
) . '" align="center">' . ($this->getContentElementCount((int)$data['row']['uid'], $languageId) ?: '-') . '</td>';
} else {
$idName = sprintf('new-overlay-%d-%d', $languageId, $data['row']['uid']);
$info = '<div class="form-check form-check-type-icon-toggle">'
. '<input type="checkbox" data-lang="' . $languageId . '" data-uid="' . (int)$data['row']['uid'] . '" name="newOL[' . $languageId . '][' . $data['row']['uid'] . ']" id="' . htmlspecialchars($idName) . '" class="form-check-input" value="1" />'
. '<label class="form-check-label" for="' . $idName . '">'
. '<span class="form-check-label-icon">'
. '<span class="form-check-label-icon-checked">' . $this->iconFactory->getIcon('actions-check', IconSize::SMALL)->render() . '</span>'
. '<span class="form-check-label-icon-unchecked">' . $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render() . '</span>'
. '</span>'
. '</label>'
. '</div>';
$tCells[] = '<td class="' . $status . ' col-border-left">&nbsp;</td>';
$tCells[] = '<td class="' . $status . '">&nbsp;</td>';
$tCells[] = '<td class="' . $status . '">' . $info . '</td>';
}
}
}
$output .= '<tr>' . implode('', $tCells) . '</tr>';
}
// Put together HEADER:
$headerCells = [];
$headerCells[] = '<th>' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_page') . '</th>';
if ($backendUser->check('tables_modify', 'pages')) {
$editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
'edit' => [
'pages' => [
implode(',', $langRecUids[0]) => 'edit',
],
],
'columnsOnly' => [
'pages' => ['title', 'nav_title', 'l18n_cfg', 'hidden'],
],
'module' => 'web_info_translations',
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
]);
$editIco = '<a href="' . htmlspecialchars($editUrl)
. '" class="btn btn-default btn-sm" title="' . $lang->sL(
'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editAllPageProperties'
) . '">' . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render() . '</a>';
} else {
$editIco = '';
}
if (isset($siteLanguages[0])) {
$defaultLanguageLabel = $siteLanguages[0]->getTitle();
} else {
$defaultLanguageLabel = $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_default');
}
$headerCells[] = '<th class="col-border-left" colspan="2">' . htmlspecialchars($defaultLanguageLabel) . '&nbsp;' . $editIco . '</th>';
foreach ($siteLanguages as $siteLanguage) {
$languageId = $siteLanguage->getLanguageId();
if ($languageId === 0) {
continue;
}
if ($selectedLanguage === 0 || $selectedLanguage === $languageId) {
// Title:
$headerCells[] = '<th class="col-border-left">' . htmlspecialchars($siteLanguage->getTitle()) . '</th>';
// Edit language overlay records:
if (is_array($langRecUids[$languageId] ?? null)) {
$editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
'edit' => [
'pages' => [
implode(',', $langRecUids[$languageId]) => 'edit',
],
],
'columnsOnly' => [
'pages' => ['title', 'nav_title', 'hidden'],
],
'module' => 'web_info_translations',
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
]);
$editButton = '<a href="' . htmlspecialchars($editUrl)
. '" class="btn btn-default" title="' . $lang->sL(
'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editAllTranslationedPageProperties'
) . '">' . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render() . '</a>';
} else {
$editButton = '';
}
// Create new overlay records:
$createLink = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [
'redirect' => $request->getAttribute('normalizedParams')->getRequestUri(),
]);
$newButton = '<a href="' . htmlspecialchars($createLink) . '" data-edit-url="' . htmlspecialchars($createLink) . '" class="btn btn-default btn-sm disabled t3js-language-new" data-lang="' . $languageId . '" title="' . $lang->sL(
'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_getlangsta_createNewTranslationHeaders'
) . '">' . $this->iconFactory->getIcon('actions-document-new', IconSize::SMALL, null, IconState::STATE_DISABLED)->render() . '</a>';
$headerCells[] = '<th>' . $editButton . '</th>';
$headerCells[] = '<th>' . $newButton . '</th>';
}
}
$output
= '<div class="table-fit">'
. '<table class="table table-striped table-hover" id="langTable">'
. '<thead>'
. '<tr>'
. implode('', $headerCells)
. '</tr>'
. '</thead>'
. '<tbody>'
. $output
. '</tbody>'
. '</table>'
. '</div>';
return $output;
}
/**
* Get an alternative language record for a specific page / language
*
* @param int $pageId Page ID to look up for.
* @param int $langId Language UID to select for.
* @return array|bool translated pages record
*/
private function getLangStatus(int $pageId, int $langId): bool|array
{
$schema = $this->tcaSchemaFactory->get('pages');
/** @var LanguageAwareSchemaCapability $languageCapability */
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder
->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace))
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$result = $queryBuilder
->select('*')
->from('pages')
->where(
$queryBuilder->expr()->eq(
$languageCapability->getTranslationOriginPointerField()->getName(),
$queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT)
)
)
->andWhere(
$queryBuilder->expr()->eq(
$languageCapability->getLanguageField()->getName(),
$queryBuilder->createNamedParameter($langId, Connection::PARAM_INT)
)
)
->executeQuery();
$row = $result->fetchAssociative();
BackendUtility::workspaceOL('pages', $row);
if (is_array($row)) {
$row['_COUNT'] = $queryBuilder->count('uid')->executeQuery()->fetchOne();
$row['_HIDDEN'] = $row['hidden'] || (int)$row['endtime'] > 0 && (int)$row['endtime'] < $GLOBALS['EXEC_TIME'] || $GLOBALS['EXEC_TIME'] < (int)$row['starttime'];
}
$result->free();
return $row;
}
/**
* Counting content elements for a single language on a page.
*
* @param int $pageId Page id to select for.
* @param int $sysLang Sys language uid
* @return int Number of content elements from the PID where the language is set to a certain value.
*/
private function getContentElementCount(int $pageId, int $sysLang): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
return (int)$queryBuilder
->count('uid')
->from('tt_content')
->where(
$queryBuilder->expr()->eq(
'pid',
$queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT)
)
)
->andWhere(
$queryBuilder->expr()->eq(
'sys_language_uid',
$queryBuilder->createNamedParameter($sysLang, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchOne();
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}