TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:14 +02:00
commit ff4622ba97
138 changed files with 9045 additions and 0 deletions
@@ -0,0 +1,284 @@
<?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\Dashboard\Widgets;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
/**
* This widget will show a list of pages where latest changes in pages and tt_content
* where made. The sys_history is used to get the latest changes.
*
* The list contains:
* - datetime of change
* - user (avatar, icon, name and realName)
* - page title and rootline
* - controls (show history, view webpage, edit page content, edit page properties)
*
* The following options are available during registration:
* - limit int number of pages to show in list
* - historyLimit int number of sys_history records to be fetched in order
* to find limit number of pages. Increase this value
* if number of pages in list is not achieved.
*/
readonly class LatestChangedPagesWidget implements WidgetRendererInterface
{
/**
* @var array{limit: int, historyLimit: int}
*/
private array $options;
public function __construct(
private BackendViewFactory $backendViewFactory,
private ConnectionPool $connectionPool,
private WidgetConfigurationInterface $configuration,
private SiteFinder $siteFinder,
array $options = [],
) {
$this->options = array_merge([
'limit' => 10,
'historyLimit' => 1000,
], $options);
}
public function getSettingsDefinitions(): array
{
return [
new SettingDefinition(
key: 'restrictToCurrentUser',
type: 'bool',
default: false,
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.settings.restrictToCurrentUser',
),
];
}
public function renderWidget(WidgetContext $context): WidgetResult
{
$restrictToCurrentUser = (bool)$context->settings->get('restrictToCurrentUser');
$sysHistoryEntries = $this->getSysHistoryEntries($this->options['historyLimit'], $restrictToCurrentUser);
$latestPages = $this->getLatestPagesFromSysHistory($sysHistoryEntries, $this->options['limit']);
$latestPages = $this->enrichPageInformation($latestPages);
$view = $this->backendViewFactory->create($context->request);
$view->assignMultiple([
'latestPages' => $latestPages,
'configuration' => $this->configuration,
'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
]);
return new WidgetResult(
content: $view->render('Widget/LatestChangedPagesWidget'),
refreshable: true,
label: $restrictToCurrentUser ? $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.title.restrictToCurrentUser') : null,
);
}
private function getSysHistoryEntries(int $limit, bool $restrictToCurrentUser): array
{
$queryBuilder = $this->getQueryBuilderSysHistory();
$workspaceId = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'id');
$queryBuilder
->select('tablename', 'recuid', 'tstamp', 'userid')
->from('sys_history')
->where(
$queryBuilder->expr()->in(
'tablename',
[
$queryBuilder->createNamedParameter('pages'),
$queryBuilder->createNamedParameter('tt_content'),
]
),
$queryBuilder->expr()->eq('workspace', $workspaceId),
)
->addOrderBy('tstamp', 'desc')
->setMaxResults($limit);
if ($restrictToCurrentUser) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq(
'userid',
$queryBuilder->createNamedParameter($this->getBackendUser()->user['uid'])
),
);
}
return $queryBuilder
->executeQuery()
->fetchAllAssociative();
}
private function getLatestPagesFromSysHistory(array $history, int $limit): array
{
$latestPages = [];
foreach ($history as $historyEntry) {
$pageId = $historyEntry['recuid'];
if ($historyEntry['tablename'] === 'tt_content') {
$pageId = $this->getPageOfContentElement($historyEntry['recuid']);
}
if (!$pageId || isset($latestPages[$pageId])) {
continue;
}
$pageRecord = BackendUtility::readPageAccess($pageId, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
if ($pageRecord === false || $pageRecord === []) {
// Backend user has no access to show page information. Dismiss this page.
continue;
}
$latestPages[$pageId]['history'] = $historyEntry;
$pageRecord['_uid'] = $pageRecord['language_tag'] > 0 ? $pageRecord['l10n_parent'] : $pageRecord['uid'];
$latestPages[$pageId]['pageRecord'] = $pageRecord;
try {
$latestPages[$pageId]['siteLanguage'] = $this->siteFinder->getSiteByPageId($pageRecord['_uid'])->getLanguageById($pageRecord['language_tag']);
} catch (SiteNotFoundException $exception) {
$latestPages[$pageId]['siteLanguage'] = null;
}
// Override tstamp of pageRecord with tstamp from history record if newer
$latestPages[$pageId]['pageRecord']['tstamp'] = max($historyEntry['tstamp'], $latestPages[$pageId]['pageRecord']['tstamp']);
if (count($latestPages) >= $limit) {
break;
}
}
return $latestPages;
}
private function enrichPageInformation(array $latestPages): array
{
$userNames = BackendUtility::getUserNames('username,realName,uid');
foreach ($latestPages as $pageId => &$page) {
$page['rootline'] = $this->getRootLine($pageId);
$uriPageId = $page['pageRecord']['language_tag'] > 0 ? $page['pageRecord']['l10n_parent'] : $page['pageRecord']['uid'];
$page['viewLink'] = (string)PreviewUriBuilder::create($page['pageRecord'])
->withRootLine(BackendUtility::BEgetRootLine($uriPageId))
->buildUri();
$page['userName'] = $userNames[$page['history']['userid']]['username'] ?? '';
$page['realName'] = $userNames[$page['history']['userid']]['realName'] ?? '';
}
return $latestPages;
}
private function getPageOfContentElement(int $uid): ?int
{
$queryBuilder = $this->getQueryBuilderForContentElements();
$contentRecord = $queryBuilder
->select('pid', 'sys_language_uid')
->from('tt_content')
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)))
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$contentRecord) {
// Content element has been deleted
return null;
}
$queryBuilder = $this->getQueryBuilderForPages();
if ((int)$contentRecord['language_tag'] > 0) {
return $queryBuilder
->select('uid')
->from('pages')
->where(
$queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($contentRecord['pid'], Connection::PARAM_INT)),
$queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter($contentRecord['language_tag'], Connection::PARAM_INT))
)
->setMaxResults(1)
->executeQuery()
->fetchAssociative()['uid'] ?? null;
}
return $queryBuilder
->select('uid')
->from('pages')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($contentRecord['pid'], Connection::PARAM_INT))
)
->setMaxResults(1)
->executeQuery()
->fetchAssociative()['uid'] ?? null;
}
private function getRootLine(int $pageId): string
{
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
return implode(' / ', array_slice(
array_map(
static fn(array $page): string => $page['title'],
array_reverse($rootLine)
),
0,
-1
));
}
private function getQueryBuilderSysHistory(): QueryBuilder
{
$queryBuilder = $this->connectionPool->getConnectionForTable('sys_history')->createQueryBuilder();
return $queryBuilder;
}
private function getQueryBuilderForContentElements(): QueryBuilder
{
$queryBuilder = $this->connectionPool->getConnectionForTable('tt_content')->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
return $queryBuilder;
}
private function getQueryBuilderForPages(): QueryBuilder
{
$queryBuilder = $this->connectionPool->getConnectionForTable('pages')->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
return $queryBuilder;
}
public function getOptions(): array
{
return $this->options;
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}