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,30 @@
<?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;
/**
* In case a widget should provide additional CSS files, the widget must implement this interface.
*/
interface AdditionalCssInterface
{
/**
* This method returns an array with paths to required CSS files.
* e.g. ['EXT:myext/Resources/Public/Css/my_widget.css']
*/
public function getCssFiles(): array;
}
@@ -0,0 +1,30 @@
<?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;
/**
* In case a widget should provide additional JavaScript files, the widget must implement this interface.
*/
interface AdditionalJavaScriptInterface
{
/**
* This method returns an array with paths to required JS files.
* e.g. ['EXT:myext/Resources/Public/JavaScript/my_widget.js']
*/
public function getJsFiles(): array;
}
@@ -0,0 +1,23 @@
<?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;
/**
* This interface should be used to describe a widget as restricted => it will not be able to assign to a user group
*/
interface AdminOnlyWidgetInterface {}
+110
View File
@@ -0,0 +1,110 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
/**
* Concrete Bar Chart widget implementation
*
* Shows a widget with a bar chart. The data for this chart will be provided by the data provider you will set.
* You can add a button to the widget by defining a button provider.
*
* There are no options available for this widget
*
* @see ChartDataProviderInterface
* @see ButtonProviderInterface
*/
class BarChartWidget implements WidgetInterface, RequestAwareWidgetInterface, EventDataInterface, AdditionalCssInterface, JavaScriptInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly ChartDataProviderInterface $dataProvider,
private readonly BackendViewFactory $backendViewFactory,
private readonly ?ButtonProviderInterface $buttonProvider = null,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'button' => $this->buttonProvider,
'options' => $this->options,
'configuration' => $this->configuration,
]);
return $view->render('Widget/ChartWidget');
}
public function getEventData(): array
{
return [
'graphConfig' => [
'type' => 'bar',
'options' => [
'maintainAspectRatio' => false,
'plugins' => [
'legend' => [
'display' => false,
],
],
'scales' => [
'y' => [
'ticks' => [
'beginAtZero' => true,
],
],
'x' => [
'ticks' => [
'maxTicksLimit' => 15,
],
],
],
],
'data' => $this->dataProvider->getChartData(),
],
];
}
public function getCssFiles(): array
{
return [];
}
public function getJavaScriptModuleInstructions(): array
{
return [
JavaScriptModuleInstruction::create('@typo3/dashboard/contrib/chartjs.js'),
JavaScriptModuleInstruction::create('@typo3/dashboard/chart-initializer.js'),
];
}
public function getOptions(): array
{
return $this->options;
}
}
+133
View File
@@ -0,0 +1,133 @@
<?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\Backend\Bookmark\BookmarkService;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsInterface;
/**
* Widget to display user bookmarks on the dashboard.
*
* The widget renders a custom element that reads bookmark data
* from the central BookmarkStore in the top frame.
*/
final readonly class BookmarksWidget implements WidgetRendererInterface, JavaScriptInterface
{
public function __construct(
private WidgetConfigurationInterface $configuration,
private BackendViewFactory $backendViewFactory,
private BookmarkService $bookmarkService,
/** @var array{limit?: int, group?: string} */
private array $options = [],
) {}
/**
* @return SettingDefinition[]
*/
public function getSettingsDefinitions(): array
{
return [
new SettingDefinition(
key: 'group',
type: 'string',
default: $this->options['group'] ?? '',
label: 'dashboard.widget_bookmarks:widget.bookmarks.setting.groups.label',
description: 'dashboard.widget_bookmarks:widget.bookmarks.setting.groups.description',
readonly: array_key_exists('group', $this->options),
enum: $this->getAvailableGroups(),
),
new SettingDefinition(
key: 'limit',
type: 'int',
default: (int)($this->options['limit'] ?? 0),
label: 'dashboard.widget_bookmarks:widget.bookmarks.setting.limit.label',
description: 'dashboard.widget_bookmarks:widget.bookmarks.setting.limit.description',
readonly: array_key_exists('limit', $this->options),
),
];
}
public function renderWidget(WidgetContext $context): WidgetResult
{
$view = $this->backendViewFactory->create($context->request);
$view->assignMultiple([
'options' => $this->options,
'settings' => $context->settings,
'configuration' => $this->configuration,
]);
return new WidgetResult(
content: $view->render('Widget/BookmarksWidget'),
label: $this->resolveLabel($context->settings),
refreshable: true,
);
}
public function getJavaScriptModuleInstructions(): array
{
return [
JavaScriptModuleInstruction::create('@typo3/dashboard/widget/bookmarks-widget-element.js'),
];
}
private function getAvailableGroups(): array
{
$groups = [
'' => 'dashboard.widget_bookmarks:widget.bookmarks.setting.showAll',
];
// Only show groups that contain bookmarks
$groupsWithBookmarks = [];
foreach ($this->bookmarkService->getBookmarks() as $bookmark) {
$groupsWithBookmarks[$bookmark->groupId] = true;
}
foreach ($this->bookmarkService->getGroups() as $group) {
if (isset($groupsWithBookmarks[$group->id])) {
$groups[(string)$group->id] = $group->label;
}
}
return $groups;
}
private function resolveLabel(SettingsInterface $settings): ?string
{
$group = $settings->get('group');
if ($group !== '') {
$groupId = is_numeric($group) ? (int)$group : $group;
foreach ($this->bookmarkService->getGroups() as $bookmarkGroup) {
if ($bookmarkGroup->id === $groupId) {
$widgetTitle = $this->getLanguageService()->sL($this->configuration->getTitle());
return $widgetTitle . ': ' . $bookmarkGroup->label;
}
}
}
// Fall back to default widget title
return null;
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,41 @@
<?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;
/**
* In case a widget should have a button in the footer of the widget, this button must implement this interface.
*/
interface ButtonProviderInterface
{
/**
* This method should return the title that will be shown as the text on the button. As the title will be
* translated within the template, you can also return a localization string like
* 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:button'
*/
public function getTitle(): string;
/**
* Return the link
*/
public function getLink(): string;
/**
* Specify the target of the link like '_blank'
*/
public function getTarget(): string;
}
@@ -0,0 +1,34 @@
<?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;
/**
* Defines API for provider, used for chart widgets.
*/
interface ChartDataProviderInterface
{
/**
* This method should provide the data for the graph.
* The data and options you have depend on the type of chart.
* More information can be found in the documentation of the specific type.
*
* @link https://www.chartjs.org/docs/latest/charts/bar.html#data-structure
* @link https://www.chartjs.org/docs/latest/charts/doughnut.html#data-structure
*/
public function getChartData(): array;
}
+73
View File
@@ -0,0 +1,73 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
/**
* Concrete CTA button implementation
*
* Shows a widget with a CTA button to easily go to a specific page or do a specific action. You can add a button to the
* widget by defining a button provider.
*
* The following options are available during registration:
* - text string Adds a text to the widget to give some more background information about
* what a user can expect when clicking the button. You can either enter a
* normal string or a translation string (eg. LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.text)
* @see ButtonProviderInterface
*/
class CtaWidget implements WidgetInterface, RequestAwareWidgetInterface
{
/**
* @var array{text: string}
*/
private readonly array $options;
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly BackendViewFactory $backendViewFactory,
private readonly ?ButtonProviderInterface $buttonProvider = null,
array $options = [],
) {
$this->options = array_merge(['text' => ''], $options);
}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'text' => $this->options['text'],
'options' => $this->options,
'button' => $this->buttonProvider,
'configuration' => $this->configuration,
]);
return $view->render('Widget/CtaWidget');
}
public function getOptions(): array
{
return $this->options;
}
}
+100
View File
@@ -0,0 +1,100 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
/**
* Concrete Doughnut Chart widget implementation
*
* Shows a widget with a doughnut chart. The data for this chart will be provided by the data provider you will set.
* You can add a button to the widget by defining a button provider.
*
* There are no options available for this widget
*
* @see ChartDataProviderInterface
* @see ButtonProviderInterface
*/
class DoughnutChartWidget implements WidgetInterface, RequestAwareWidgetInterface, EventDataInterface, AdditionalCssInterface, JavaScriptInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly ChartDataProviderInterface $dataProvider,
private readonly BackendViewFactory $backendViewFactory,
private readonly ?ButtonProviderInterface $buttonProvider = null,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'button' => $this->buttonProvider,
'options' => $this->options,
'configuration' => $this->configuration,
]);
return $view->render('Widget/ChartWidget');
}
public function getEventData(): array
{
return [
'graphConfig' => [
'type' => 'doughnut',
'options' => [
'maintainAspectRatio' => false,
'plugins' => [
'legend' => [
'display' => true,
'position' => 'bottom',
],
],
'cutoutPercentage' => 60,
],
'data' => $this->dataProvider->getChartData(),
],
];
}
public function getCssFiles(): array
{
return [];
}
public function getJavaScriptModuleInstructions(): array
{
return [
JavaScriptModuleInstruction::create('@typo3/dashboard/contrib/chartjs.js'),
JavaScriptModuleInstruction::create('@typo3/dashboard/chart-initializer.js'),
];
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* In case HTML element shall contain additional attributes
*/
interface ElementAttributesInterface
{
/**
* @return array<string, string|null>
*/
public function getElementAttributes(): array;
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* In case a widget should provide additional data as JSON payload, the widget must implement this interface.
*/
interface EventDataInterface
{
/**
* This method returns data which should be sent to the widget as JSON encoded value.
*/
public function getEventData(): array;
}
+31
View File
@@ -0,0 +1,31 @@
<?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\Core\Page\JavaScriptModuleInstruction;
/**
* Provides potential JavaScript declarations to be loaded/initialized for a particular widget.
*/
interface JavaScriptInterface
{
/**
* @return list<JavaScriptModuleInstruction>
*/
public function getJavaScriptModuleInstructions(): array;
}
+83
View File
@@ -0,0 +1,83 @@
<?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\View\BackendViewFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Dashboard\Widgets\Provider\LatestBeLoginsDataProvider;
/**
* This widget will show a list of recent backend user logins.
*
* The list contains:
* - backend user avatar and name
* - login time
*
* The following options are available during registration:
* - limit `int` number of logins to display
*/
readonly class LatestBeLoginsWidget implements WidgetRendererInterface, AdminOnlyWidgetInterface
{
public function __construct(
private BackendViewFactory $backendViewFactory,
private LatestBeLoginsDataProvider $dataProvider,
private ButtonProviderInterface $buttonProvider,
private WidgetConfigurationInterface $configuration,
) {}
public function getSettingsDefinitions(): array
{
return [
new SettingDefinition(
key: 'limit',
type: 'int',
default: 10,
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestBeLogins.settings.limit',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestBeLogins.settings.limit.description',
options: [
'min' => 1,
],
),
];
}
public function renderWidget(WidgetContext $context): WidgetResult
{
$limit = $context->settings->get('limit');
$items = $this->dataProvider->getItems($limit);
$view = $this->backendViewFactory->create($context->request);
$view->assignMultiple([
'items' => $items,
'button' => $this->buttonProvider,
'configuration' => $this->configuration,
'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
]);
return new WidgetResult(
content: $view->render('Widget/LatestBeLoginsWidget'),
refreshable: true,
);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -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'];
}
}
@@ -0,0 +1,30 @@
<?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;
/**
* The data provider of a ListWidget, should implement this interface
*/
interface ListDataProviderInterface
{
/**
* Return the items to be shown. This should be an array like ['item 1', 'item 2', 'item 3']. This is a
* real simple list of items.
*/
public function getItems(): array;
}
+72
View File
@@ -0,0 +1,72 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
/**
* Concrete List Widget implementation
*
* The widget will show a simple list with items provided by a data provider. You can add a button to the widget by
* defining a button provider.
*
* There are no options available for this widget
*
* @see ListDataProviderInterface
* @see ButtonProviderInterface
*/
class ListWidget implements WidgetInterface, RequestAwareWidgetInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly ListDataProviderInterface $dataProvider,
private readonly BackendViewFactory $backendViewFactory,
private readonly ?ButtonProviderInterface $buttonProvider = null,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'items' => $this->getItems(),
'options' => $this->options,
'button' => $this->buttonProvider,
'configuration' => $this->configuration,
]);
return $view->render('Widget/ListWidget');
}
protected function getItems(): array
{
return $this->dataProvider->getItems();
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* The dataprovider of a NumberWithIcon widget should implement this interface
*/
interface NumberWithIconDataProviderInterface
{
/**
* Return the number that should be shown in the widget
*/
public function getNumber(): int;
}
+75
View File
@@ -0,0 +1,75 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
/**
* Concrete Number with Icon implementation
*
* The widget will show widget with an icon, a number, a title and a subtitle. The number is provided by a data
* provider.
*
* The following options are available during registration:
* - icon string The icon-identifier of the icon that should be shown in the widget. You should
* register your icon with the Icon API
* - title string The main title that will be shown in the widget as an explanation of the shown number.
* You can either enter a normal string or a translation string
* (eg. LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title)
* - subtitle string The subtitle that will give some additional information about the number and title.
* You can either enter a normal string or a translation string
* (eg. LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle)
*
* @see NumberWithIconDataProviderInterface
*/
class NumberWithIconWidget implements WidgetInterface, RequestAwareWidgetInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly NumberWithIconDataProviderInterface $dataProvider,
private readonly BackendViewFactory $backendViewFactory,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'icon' => $this->options['icon'] ?? '',
'title' => $this->options['title'] ?? '',
'subtitle' => $this->options['subtitle'] ?? '',
'number' => $this->dataProvider->getNumber(),
'options' => $this->options,
'configuration' => $this->configuration,
]);
return $view->render('Widget/NumberWithIconWidget');
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,47 @@
<?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\Provider;
use TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface;
/**
* Provides a button for the footer of a widget
*/
readonly class ButtonProvider implements ButtonProviderInterface
{
public function __construct(
private string $title,
private string $link,
private string $target = ''
) {}
public function getTitle(): string
{
return $this->title;
}
public function getLink(): string
{
return $this->link;
}
public function getTarget(): string
{
return $this->target;
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets\Provider;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
/**
* Data provider for "Latest backend logins" widget.
* Fetches successful backend user login entries from sys_log.
*/
readonly class LatestBeLoginsDataProvider
{
public function __construct(
private ConnectionPool $connectionPool
) {}
public function getItems(int $limit = 10): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_log');
$logEntries = $queryBuilder
->select('uid', 'tstamp', 'userid', 'details')
->from('sys_log')
->where(
$queryBuilder->expr()->eq(
'type',
$queryBuilder->createNamedParameter(SystemLogType::LOGIN, Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
'action',
$queryBuilder->createNamedParameter(SystemLogLoginAction::LOGIN, Connection::PARAM_INT)
)
)
->orderBy('tstamp', 'DESC')
->setMaxResults($limit)
->executeQuery()
->fetchAllAssociative();
// Enrich with user information
$items = [];
$userNames = BackendUtility::getUserNames('username,realName,uid');
foreach ($logEntries as $entry) {
$userId = (int)$entry['userid'];
$userInfo = $userNames[$userId] ?? null;
if ($userInfo !== null) {
$items[] = [
'timestamp' => (int)$entry['tstamp'],
'userId' => $userId,
'username' => $userInfo['username'] ?? '',
'realName' => $userInfo['realName'] ?? '',
'details' => $entry['details'],
];
}
}
return $items;
}
}
@@ -0,0 +1,58 @@
<?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\Provider;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\Widgets\NumberWithIconDataProviderInterface;
class NumberOfFailedLoginsDataProvider implements NumberWithIconDataProviderInterface
{
public function getNumber(int $secondsBack = 86400): int
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
return (int)$queryBuilder->count('uid')
->from('sys_log')
->where(
$queryBuilder->expr()->eq(
'type',
$queryBuilder->createNamedParameter(SystemLogType::LOGIN, Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
'action',
$queryBuilder->createNamedParameter(SystemLogLoginAction::ATTEMPT, Connection::PARAM_INT)
),
$queryBuilder->expr()->neq(
'error',
$queryBuilder->createNamedParameter(SystemLogErrorClassification::MESSAGE, Connection::PARAM_INT)
),
$queryBuilder->expr()->gt(
'tstamp',
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'] - $secondsBack, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchOne();
}
}
@@ -0,0 +1,63 @@
<?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\Provider;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface;
use TYPO3\CMS\Dashboard\Widgets\ElementAttributesInterface;
/**
* Provide link for sys log button.
* Check whether belog is enabled and add link to module.
* No link is returned if not enabled.
*/
readonly class SysLogButtonProvider implements ButtonProviderInterface, ElementAttributesInterface
{
public function __construct(
private string $title,
private string $target = '',
private string $channel = '',
) {}
public function getTitle(): string
{
return $this->title;
}
public function getLink(): string
{
return '';
}
public function getTarget(): string
{
return $this->target;
}
public function getElementAttributes(): array
{
if (!ExtensionManagementUtility::isLoaded('belog')) {
return [];
}
return [
'data-dispatch-action' => 'TYPO3.ModuleMenu.showModule',
'data-dispatch-args-list' => 'system_log,&'
. http_build_query(['constraint' => ['channel' => $this->channel ?: 'php']]),
];
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets\Provider;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\WidgetApi;
use TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface;
/**
* Provides chart data for sys log errors.
*/
class SysLogErrorsDataProvider implements ChartDataProviderInterface
{
protected array $labels = [];
protected array $data = [];
/**
* @param int $days Number of days to gather information for.
*/
public function __construct(protected readonly int $days = 31) {}
public function getChartData(): array
{
$this->calculateDataForLastDays();
return [
'labels' => $this->labels,
'datasets' => [
[
'label' => $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.chart.dataSet.0'),
'backgroundColor' => WidgetApi::getDefaultChartColors()[0],
'border' => 0,
'data' => $this->data,
],
],
];
}
protected function getNumberOfErrorsInPeriod(int $start, int $end): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
return (int)$queryBuilder
->count('*')
->from('sys_log')
->where(
$queryBuilder->expr()->eq(
'type',
$queryBuilder->createNamedParameter(SystemLogType::ERROR, Connection::PARAM_INT)
),
$queryBuilder->expr()->gte(
'tstamp',
$queryBuilder->createNamedParameter($start, Connection::PARAM_INT)
),
$queryBuilder->expr()->lte(
'tstamp',
$queryBuilder->createNamedParameter($end, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchOne();
}
protected function calculateDataForLastDays(): void
{
$format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?: 'Y-m-d';
for ($daysBefore = $this->days; $daysBefore >= 0; $daysBefore--) {
$this->labels[] = date($format, (int)strtotime('-' . $daysBefore . ' day'));
$startPeriod = (int)strtotime('-' . $daysBefore . ' day 0:00:00');
$endPeriod = (int)strtotime('-' . $daysBefore . ' day 23:59:59');
$this->data[] = $this->getNumberOfErrorsInPeriod($startPeriod, $endPeriod);
}
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,66 @@
<?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\Provider;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\WidgetApi;
use TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface;
readonly class TypeOfUsersChartDataProvider implements ChartDataProviderInterface
{
public function __construct(private LanguageServiceFactory $languageServiceFactory) {}
public function getChartData(): array
{
$languageService = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
$adminUsers = $this->getNumberOfUsers(true);
$normalUsers = $this->getNumberOfUsers(false);
return [
'labels' => [
$languageService->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.normalUsers'),
$languageService->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.adminUsers'),
],
'datasets' => [
[
'backgroundColor' => WidgetApi::getDefaultChartColors(),
'data' => [$normalUsers, $adminUsers],
],
],
];
}
protected function getNumberOfUsers(bool $admin = false): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
return (int)$queryBuilder
->count('*')
->from('be_users')
->where(
$queryBuilder->expr()->eq(
'admin',
$queryBuilder->createNamedParameter($admin ? 1 : 0, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchOne();
}
}
@@ -0,0 +1,31 @@
<?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 Psr\Http\Message\ServerRequestInterface;
/**
* Interface for widgets that need the ServerRequestInterface Request.
* The setter is called immediately after class instantiation.
* Useful for Widgets that depend on a request, for instance when dealing
* with views based on BackendViewFactory.
*/
interface RequestAwareWidgetInterface
{
public function setRequest(ServerRequestInterface $request): void;
}
+195
View File
@@ -0,0 +1,195 @@
<?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 Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\Exception\InvalidRssFeedException;
/**
* Concrete RSS widget implementation
*
* The widget will show a certain number of items of the given RSS feed. The feed will be set by the feedUrl option. You
* can add a button to the widget by defining a button provider.
*
* The following options are available during registration:
* - feedUrl string Defines the URL or file providing the RSS Feed.
* This is read by the widget in order to fetch entries to show.
* - limit int default: 5 Defines how many RSS items should be shown.
* - lifetime int default: 43200 Defines how long to wait, in seconds, until fetching RSS Feed again
*
* @see ButtonProviderInterface
*/
readonly class RssWidget implements WidgetRendererInterface
{
public function __construct(
private WidgetConfigurationInterface $configuration,
#[Autowire(service: 'cache.dashboard.rss')]
private FrontendInterface $cache,
private BackendViewFactory $backendViewFactory,
private ?ButtonProviderInterface $buttonProvider = null,
/** @var array{limit?: int, lifeTime?: int, feedUrl?: string} */
private array $options = [],
) {}
/**
* @return SettingDefinition[]
*/
public function getSettingsDefinitions(): array
{
return [
new SettingDefinition(
key: 'label',
type: 'string',
default: '',
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.label.label',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.label.description',
readonly: array_key_exists('feedUrl', $this->options),
),
new SettingDefinition(
key: 'feedUrl',
type: 'url',
default: (string)($this->options['feedUrl'] ?? ''),
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.fieldUrl.label',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.fieldUrl.description',
readonly: array_key_exists('feedUrl', $this->options),
options: [
'pattern' => 'https?://.+',
],
),
new SettingDefinition(
key: 'limit',
type: 'int',
default: (int)($this->options['limit'] ?? 5),
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.limit.label',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.limit.description',
readonly: array_key_exists('limit', $this->options),
),
new SettingDefinition(
key: 'lifeTime',
type: 'int',
default: (int)($this->options['lifeTime'] ?? 43200),
label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.lifeTime.label',
description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.lifeTime.description',
readonly: true,
),
];
}
public function renderWidget(WidgetContext $context): WidgetResult
{
$view = $this->backendViewFactory->create($context->request);
$feedUrl = $context->settings->get('feedUrl');
$items = [];
if ($feedUrl) {
try {
$items = $this->getFeedItems($context->settings);
} catch (InvalidRssFeedException) {
$view->assign('invalidFeed', true);
}
}
$view->assignMultiple([
'feedUrl' => $feedUrl,
'items' => $items,
'settings' => $context->settings,
'options' => $this->options,
'button' => $this->buttonProvider,
'configuration' => $this->configuration,
]);
return new WidgetResult(
label: $context->settings->get('label') !== '' ? $context->settings->get('label') : null,
content: $view->render('Widget/RssWidget'),
refreshable: true,
);
}
protected function getFeedItems(SettingsInterface $settings): array
{
$cacheHash = md5($settings->get('feedUrl') . '-' . $settings->get('limit'));
if ($items = $this->cache->get($cacheHash)) {
return $items;
}
$feedContent = GeneralUtility::getUrl($settings->get('feedUrl'));
if ($feedContent === false) {
throw new InvalidRssFeedException('RSS URL could not be fetched', 1573385431);
}
try {
$feedXml = simplexml_load_string($feedContent);
} catch (\Exception $e) {
throw new InvalidRssFeedException('Received RSS feed could not be parsed.', 1573385432, $e);
}
$items = match ($this->determineFeedType($feedXml)) {
'atom' => $this->parseAtomFeed($feedXml),
'rss' => $this->parseRssFeed($feedXml),
default => []
};
usort($items, static function ($item1, $item2) {
return new \DateTime($item2['pubDate']) <=> new \DateTime($item1['pubDate']);
});
$items = array_slice($items, 0, (int)$settings->get('limit'));
$this->cache->set($cacheHash, $items, ['dashboard_rss'], (int)$settings->get('lifeTime'));
return $items;
}
protected function determineFeedType(\SimpleXMLElement $feedXml): string
{
return $feedXml->getName() === 'feed' ? 'atom' : 'rss';
}
protected function parseRssFeed(\SimpleXMLElement $rssFeed): array
{
$items = [];
foreach ($rssFeed->channel->item as $item) {
$items[] = [
'title' => trim((string)$item->title),
'link' => trim((string)$item->link),
'pubDate' => trim((string)$item->pubDate),
'description' => trim(strip_tags((string)$item->description)),
];
}
return $items;
}
protected function parseAtomFeed(\SimpleXMLElement $atomFeed): array
{
$items = [];
foreach ($atomFeed->entry as $entry) {
$items[] = [
'title' => trim((string)$entry->title),
'link' => trim((string)($entry->link['href'] ?? '')),
'pubDate' => trim((string)($entry->published ?? $entry->updated ?? '')),
'description' => trim(strip_tags((string)($entry->summary ?? $entry->content ?? ''))),
'author' => [
'name' => trim((string)($entry->author->name ?? '')),
'email' => trim((string)($entry->author->email ?? '')),
'url' => trim((string)($entry->author->url ?? '')),
],
];
}
return $items;
}
}
@@ -0,0 +1,68 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Information\Typo3Version;
/**
* Concrete TYPO3 information widget
*
* This widget will give some general information about TYPO3 version and the version installed.
*
* There are no options available for this widget
*/
class T3GeneralInformationWidget implements WidgetInterface, RequestAwareWidgetInterface
{
private ServerRequestInterface $request;
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly BackendViewFactory $backendViewFactory,
private readonly array $options = [],
) {}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function renderWidgetContent(): string
{
$typo3Information = new Typo3Information();
$typo3Version = new Typo3Version();
$view = $this->backendViewFactory->create($this->request);
$view->assignMultiple([
'title' => 'TYPO3 CMS ' . $typo3Version->getVersion(),
'copyrightYear' => $typo3Information->getCopyrightYear(),
'currentVersion' => $typo3Version->getVersion(),
'donationUrl' => $typo3Information::URL_DONATE,
'copyRightNotice' => $typo3Information->getCopyrightNotice(),
'options' => $this->options,
'configuration' => $this->configuration,
]);
return $view->render('Widget/T3GeneralInformationWidget');
}
public function getOptions(): array
{
return $this->options;
}
}
+89
View File
@@ -0,0 +1,89 @@
<?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;
readonly class WidgetConfiguration implements WidgetConfigurationInterface
{
/**
* @throws \InvalidArgumentException If non valid parameters were provided.
*/
public function __construct(
private string $identifier,
private string $serviceName,
private array $groupNames,
private string $title,
private string $description,
private string $iconIdentifier,
private string $height,
private string $width,
private bool $adminOnly = false,
) {
$allowedSizes = ['small', 'medium', 'large'];
if (!in_array($height, $allowedSizes, true)) {
throw new \InvalidArgumentException('Height of widgets has to be small, medium or large', 1584778196);
}
if (!in_array($width, $allowedSizes, true)) {
throw new \InvalidArgumentException('Width of widgets has to be small, medium or large', 1585249769);
}
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getServiceName(): string
{
return $this->serviceName;
}
public function getGroupNames(): array
{
return $this->groupNames;
}
public function getTitle(): string
{
return $this->title;
}
public function getDescription(): string
{
return $this->description;
}
public function getIconIdentifier(): string
{
return $this->iconIdentifier;
}
public function getHeight(): string
{
return $this->height;
}
public function getWidth(): string
{
return $this->width;
}
public function isAdminOnly(): bool
{
return $this->adminOnly;
}
}
@@ -0,0 +1,66 @@
<?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;
/**
* Defines API of configuration for a widget.
* This is separated by concrete implementation of a widget (WidgetInterface).
* The configuration is used to generate UX, and other stuff.
*/
interface WidgetConfigurationInterface
{
/**
* Returns the unique identifier of a widget
*/
public function getIdentifier(): string;
/**
* Returns the service name providing the widget implementation
*/
public function getServiceName(): string;
/**
* Returns array of group names associated to this widget
*/
public function getGroupNames(): array;
/**
* Returns the title of a widget, this is used for the widget selector
*/
public function getTitle(): string;
/**
* Returns the description of a widget, this is used for the widget selector
*/
public function getDescription(): string;
/**
* Returns the icon identifier of a widget, this is used for the widget selector
*/
public function getIconIdentifier(): string;
/**
* Returns the height of a widget (small, medium, large)
*/
public function getHeight(): string;
/**
* Returns the width of a widget (small, medium, large)
*/
public function getWidth(): string;
}
+44
View File
@@ -0,0 +1,44 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Settings\SettingsInterface;
/**
* Widget context containing all necessary data for widget rendering.
*
* This readonly value object encapsulates all the context information needed
* by widgets during rendering, including:
* - Widget instance identifier and raw configuration data
* - Widget configuration and settings
* - Current HTTP request context
*
* The widget context is passed to widgets implementing WidgetRendererInterface
* and provides a clean interface for accessing widget-specific data and settings.
*/
final readonly class WidgetContext
{
public function __construct(
public string $identifier,
public array $rawData,
public WidgetConfigurationInterface $configuration,
public SettingsInterface $settings,
public ServerRequestInterface $request,
) {}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Dashboard\Widgets;
/**
* The WidgetInterface is the base interface for all kind of widgets.
* All widgets must implement this interface.
* It contains the methods which are required for all widgets.
*/
interface WidgetInterface
{
/**
* This method returns the content of a widget. The returned markup will be delivered
* by an AJAX call and will not be escaped.
* Be aware of XSS and ensure that the content is well encoded.
*/
public function renderWidgetContent(): string;
/**
* This method returns the options of the widget as set in the registration.
*/
public function getOptions(): array;
}
@@ -0,0 +1,40 @@
<?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\Core\Settings\SettingDefinition;
/**
* The WidgetRendererInterface is the (new) base interface for all kind of widgets.
* All widgets should implement this interface. (but can also implement WidgetInterface for the time being)
* It contains the methods which are required for all widgets.
*/
interface WidgetRendererInterface
{
/**
* This method returns the content of a widget. The returned markup will be delivered
* by an AJAX call and will not be escaped.
* Be aware of XSS and ensure that the content is well encoded.
*/
public function renderWidget(WidgetContext $context): WidgetResult;
/**
* @return SettingDefinition[]
*/
public function getSettingsDefinitions(): array;
}
+39
View File
@@ -0,0 +1,39 @@
<?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;
/**
* Widget rendering result containing content and metadata.
*
* This readonly value object represents the result of widget rendering,
* containing the rendered HTML content and additional metadata about
* the widget's capabilities and state.
*
* The result includes:
* - HTML content to be displayed in the dashboard
* - Optional custom label overriding the widget's default title
* - Refreshable flag indicating whether the widget supports refresh operations
*/
final readonly class WidgetResult
{
public function __construct(
public string $content,
public ?string $label = null,
public bool $refreshable = false,
) {}
}