307 lines
14 KiB
PHP
307 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Backend\View\BackendLayout;
|
|
|
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
|
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
|
use TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent;
|
|
use TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent;
|
|
use TYPO3\CMS\Backend\View\PageLayoutContext;
|
|
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
|
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
|
use TYPO3\CMS\Core\Database\Connection;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
|
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
|
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
|
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
|
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Versioning\VersionState;
|
|
|
|
/**
|
|
* Class responsible for fetching the content data related to a BackendLayout
|
|
*
|
|
* - Reads content records
|
|
* - Performs workspace overlay on records
|
|
* - Capable of returning all records in active language as flat array
|
|
* - Capable of returning records for a given column in a given (optional) language
|
|
* - Capable of returning translation data (brief info about translation consistency)
|
|
*
|
|
* @internal
|
|
*/
|
|
#[Autoconfigure(public: true)]
|
|
readonly class ContentFetcher
|
|
{
|
|
public function __construct(
|
|
private EventDispatcherInterface $eventDispatcher,
|
|
#[Autowire(service: 'cache.runtime')]
|
|
private FrontendInterface $runtimeCache,
|
|
private ConnectionPool $connectionPool,
|
|
private TcaSchemaFactory $tcaSchemaFactory,
|
|
private FlashMessageService $flashMessageService,
|
|
private BackendLayoutView $backendLayoutView,
|
|
) {}
|
|
|
|
/**
|
|
* Gets content records per column.
|
|
* This is required for correct workspace overlays.
|
|
*
|
|
* @return array Associative array for each column (colPos) or for all columns if $columnNumber is null
|
|
*/
|
|
public function getContentRecordsPerColumn(PageLayoutContext $pageLayoutContext, ?int $columnNumber = null, ?int $languageId = null): array
|
|
{
|
|
$languageId = $languageId ?? $pageLayoutContext->getSiteLanguage()->getLanguageId();
|
|
$cachedFetchedContentRecords = $this->runtimeCache->get('ContentFetcher_fetchedContentRecords') ?: [];
|
|
if (empty($cachedFetchedContentRecords)) {
|
|
$fetchedContentRecords = [];
|
|
$isLanguageComparisonMode = $pageLayoutContext->getDrawingConfiguration()->isLanguageComparisonMode();
|
|
$queryBuilder = $this->getQueryBuilder($pageLayoutContext);
|
|
$result = $queryBuilder->executeQuery();
|
|
$records = $this->getResult($result);
|
|
foreach ($records as $record) {
|
|
$recordLanguage = $record['language_tag'] ?? '';
|
|
$recordColumnNumber = (int)$record['colPos'];
|
|
if ($recordLanguage === -1) {
|
|
// Record is set to "all languages", place it according to view mode.
|
|
if ($isLanguageComparisonMode) {
|
|
// Force the record to only be shown in default language in "Languages" view mode.
|
|
$recordLanguage = 0;
|
|
} else {
|
|
// Force the record to be shown in the currently active language in "Columns" view mode.
|
|
$recordLanguage = $languageId;
|
|
}
|
|
}
|
|
$fetchedContentRecords[$recordLanguage][$recordColumnNumber][] = $record;
|
|
}
|
|
$this->runtimeCache->set('ContentFetcher_fetchedContentRecords', $fetchedContentRecords);
|
|
} else {
|
|
$fetchedContentRecords = $cachedFetchedContentRecords;
|
|
}
|
|
|
|
$contentByLanguage = $fetchedContentRecords[$languageId] ?? [];
|
|
|
|
if ($columnNumber === null) {
|
|
return $contentByLanguage;
|
|
}
|
|
|
|
return $contentByLanguage[$columnNumber] ?? [];
|
|
}
|
|
|
|
public function getFlatContentRecords(PageLayoutContext $pageLayoutContext, int $languageId): iterable
|
|
{
|
|
$contentRecords = $this->getContentRecordsPerColumn($pageLayoutContext, null, $languageId);
|
|
return empty($contentRecords) ? [] : array_merge(...$contentRecords);
|
|
}
|
|
|
|
/**
|
|
* Allows to decide via an Event whether a custom type has children which were rendered or should not be rendered.
|
|
*/
|
|
public function getUnusedRecords(PageLayoutContext $pageLayoutContext): iterable
|
|
{
|
|
$unrendered = [];
|
|
$recordIdentityMap = $pageLayoutContext->getRecordIdentityMap();
|
|
$languageId = $pageLayoutContext->getDrawingConfiguration()->getPrimaryLanguageId();
|
|
// @todo consider to invoke the identity-map much earlier (to avoid fetching database records again)
|
|
foreach ($this->getContentRecordsPerColumn($pageLayoutContext, null, $languageId) as $contentRecordsInColumn) {
|
|
foreach ($contentRecordsInColumn as $contentRecord) {
|
|
$used = $recordIdentityMap->hasIdentifier('tt_content', (int)$contentRecord['uid']);
|
|
// A hook mentioned that this record is used somewhere, so this is in fact "rendered" already
|
|
$event = new IsContentUsedOnPageLayoutEvent($contentRecord, $used, $pageLayoutContext);
|
|
$event = $this->eventDispatcher->dispatch($event);
|
|
if (!$event->isRecordUsed()) {
|
|
$unrendered[] = $contentRecord;
|
|
}
|
|
}
|
|
}
|
|
return $unrendered;
|
|
}
|
|
|
|
public function getTranslationData(PageLayoutContext $pageLayoutContext, iterable $contentElements, int $language): array
|
|
{
|
|
if ($language === 0) {
|
|
return [];
|
|
}
|
|
|
|
$languageTranslationInfo = $this->runtimeCache->get('ContentFetcher_TranslationInfo_' . $language) ?: [];
|
|
if (empty($languageTranslationInfo)) {
|
|
$contentRecordsInDefaultLanguage = $this->getContentRecordsPerColumn($pageLayoutContext, null, 0);
|
|
if (!empty($contentRecordsInDefaultLanguage)) {
|
|
$contentRecordsInDefaultLanguage = array_merge(...$contentRecordsInDefaultLanguage);
|
|
}
|
|
$untranslatedRecordUids = array_flip(
|
|
array_column(
|
|
// Eliminate records with "-1" as sys_language_uid since they can not be translated
|
|
array_filter($contentRecordsInDefaultLanguage, static function (array $record): bool {
|
|
return true;
|
|
}),
|
|
'uid'
|
|
)
|
|
);
|
|
|
|
foreach ($contentElements as $contentElement) {
|
|
if (($contentElement['language_tag'] ?? '') === '') {
|
|
continue;
|
|
}
|
|
if ((int)$contentElement['l18n_parent'] === 0) {
|
|
$languageTranslationInfo['hasStandAloneContent'] = true;
|
|
$languageTranslationInfo['mode'] = 'free';
|
|
}
|
|
if ((int)$contentElement['l18n_parent'] > 0) {
|
|
$languageTranslationInfo['hasTranslations'] = true;
|
|
$languageTranslationInfo['mode'] = 'connected';
|
|
}
|
|
if ((int)$contentElement['l10n_source'] > 0) {
|
|
unset($untranslatedRecordUids[(int)$contentElement['l10n_source']]);
|
|
}
|
|
}
|
|
if (!isset($languageTranslationInfo['hasTranslations'])) {
|
|
$languageTranslationInfo['hasTranslations'] = false;
|
|
}
|
|
|
|
foreach ($untranslatedRecordUids as $uid => $index) {
|
|
$contentElementInDefaultLanguage = $contentRecordsInDefaultLanguage[$index];
|
|
if (!$this->backendLayoutView->isCTypeAllowedInColPosByPage($contentElementInDefaultLanguage['CType'], $contentElementInDefaultLanguage['colPos'], $contentElementInDefaultLanguage['pid'])) {
|
|
unset($untranslatedRecordUids[$uid]);
|
|
}
|
|
}
|
|
|
|
$untranslatedRecordUidsWithoutWorkspaceDeletedRecords = $this->removeWorkspaceDeletedPlaceholdersUidsFromUntranslatedRecordUids($pageLayoutContext, array_keys($untranslatedRecordUids), $language);
|
|
$languageTranslationInfo['untranslatedRecordUids'] = $untranslatedRecordUidsWithoutWorkspaceDeletedRecords;
|
|
if (array_keys($untranslatedRecordUids) !== $untranslatedRecordUidsWithoutWorkspaceDeletedRecords) {
|
|
$languageTranslationInfo['hasElementsWithWorkspaceDeletePlaceholders'] = true;
|
|
}
|
|
|
|
// Check for inconsistent translations, force "mixed" mode and dispatch a FlashMessage to user if such a case is encountered.
|
|
if (isset($languageTranslationInfo['hasStandAloneContent'])
|
|
&& $languageTranslationInfo['hasTranslations']
|
|
) {
|
|
$languageTranslationInfo['mode'] = 'mixed';
|
|
|
|
// We do not want to show the staleTranslationWarning if allowInconsistentLanguageHandling is enabled
|
|
if (!$pageLayoutContext->getDrawingConfiguration()->getAllowInconsistentLanguageHandling()) {
|
|
$siteLanguage = $pageLayoutContext->getSiteLanguage($language);
|
|
$languageService = $this->getLanguageService();
|
|
$message = FlashMessage::createFromArray([
|
|
'message' => $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:staleTranslationWarning'),
|
|
'title' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:staleTranslationWarningTitle'), $siteLanguage->getTitle()),
|
|
'severity' => ContextualFeedbackSeverity::WARNING->value,
|
|
]);
|
|
$queue = $this->flashMessageService->getMessageQueueByIdentifier();
|
|
$queue->addMessage($message);
|
|
}
|
|
}
|
|
|
|
$this->runtimeCache->set('ContentFetcher_TranslationInfo_' . $language, $languageTranslationInfo);
|
|
}
|
|
return $languageTranslationInfo;
|
|
}
|
|
|
|
protected function removeWorkspaceDeletedPlaceholdersUidsFromUntranslatedRecordUids(PageLayoutContext $pageLayoutContext, array $untranslatedRecordUids, int $language): array
|
|
{
|
|
if ($this->getBackendUser()->workspace <= 0) {
|
|
// Early return if we're not in a workspace to suppress some queries.
|
|
return $untranslatedRecordUids;
|
|
}
|
|
$queryBuilder = $this->getQueryBuilder($pageLayoutContext);
|
|
$queryBuilder->andWhere(
|
|
$queryBuilder->expr()->and(
|
|
$queryBuilder->expr()->in(
|
|
'l18n_parent',
|
|
$queryBuilder->createNamedParameter($untranslatedRecordUids, Connection::PARAM_INT_ARRAY)
|
|
),
|
|
$queryBuilder->expr()->eq(
|
|
'sys_language_uid',
|
|
$queryBuilder->createNamedParameter($language, Connection::PARAM_INT)
|
|
)
|
|
)
|
|
);
|
|
$result = $queryBuilder->executeQuery();
|
|
$uidsToRemoveFromUntranslatedRecordUids = [];
|
|
while ($row = $result->fetchAssociative()) {
|
|
BackendUtility::workspaceOL('tt_content', $row, -99, true);
|
|
if ($row && VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) {
|
|
$uidsToRemoveFromUntranslatedRecordUids[] = $row['l18n_parent'];
|
|
}
|
|
}
|
|
return array_diff($untranslatedRecordUids, $uidsToRemoveFromUntranslatedRecordUids);
|
|
}
|
|
|
|
protected function getQueryBuilder(PageLayoutContext $pageLayoutContext): QueryBuilder
|
|
{
|
|
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
|
|
$queryBuilder->getRestrictions()
|
|
->removeAll()
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
|
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
|
|
$queryBuilder
|
|
->select('*')
|
|
->from('tt_content');
|
|
|
|
$queryBuilder->andWhere(
|
|
$queryBuilder->expr()->eq(
|
|
'tt_content.pid',
|
|
$queryBuilder->createNamedParameter($pageLayoutContext->getPageId(), Connection::PARAM_INT)
|
|
)
|
|
);
|
|
|
|
$schema = $this->tcaSchemaFactory->get('tt_content');
|
|
$sortBy = $schema->hasCapability(TcaSchemaCapability::SortByField) ? (string)$schema->getCapability(TcaSchemaCapability::SortByField) : '';
|
|
if ($sortBy === '' && $schema->hasCapability(TcaSchemaCapability::DefaultSorting)) {
|
|
$sortBy = (string)$schema->getCapability(TcaSchemaCapability::DefaultSorting)->getValue();
|
|
}
|
|
foreach (QueryHelper::parseOrderBy($sortBy) as $orderBy) {
|
|
$queryBuilder->addOrderBy($orderBy[0], $orderBy[1]);
|
|
}
|
|
|
|
$event = new ModifyDatabaseQueryForContentEvent($queryBuilder, 'tt_content', $pageLayoutContext->getPageId());
|
|
$event = $this->eventDispatcher->dispatch($event);
|
|
return $event->getQueryBuilder();
|
|
}
|
|
|
|
protected function getResult($result): array
|
|
{
|
|
$output = [];
|
|
while ($row = $result->fetchAssociative()) {
|
|
BackendUtility::workspaceOL('tt_content', $row, -99, true);
|
|
if ($row && VersionState::tryFrom($row['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) {
|
|
$output[] = $row;
|
|
}
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
protected function getLanguageService(): LanguageService
|
|
{
|
|
return $GLOBALS['LANG'];
|
|
}
|
|
|
|
public function getBackendUser(): BackendUserAuthentication
|
|
{
|
|
return $GLOBALS['BE_USER'];
|
|
}
|
|
}
|