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,458 @@
<?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\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Dto\Settings\EditableSetting;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\Category;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsDiff;
use TYPO3\CMS\Core\Settings\SettingsTypeRegistry;
use TYPO3\CMS\Dashboard\DashboardPreset;
use TYPO3\CMS\Dashboard\DashboardPresetRegistry;
use TYPO3\CMS\Dashboard\Factory\WidgetSettingsFactory;
use TYPO3\CMS\Dashboard\Repository\DashboardRepository;
use TYPO3\CMS\Dashboard\WidgetGroupInitializationService;
use TYPO3\CMS\Dashboard\WidgetRegistry;
/**
* @internal
*/
#[AsController]
readonly class DashboardAjaxController
{
public function __construct(
protected DashboardRepository $dashboardRepository,
protected DashboardPresetRegistry $dashboardPresetRegistry,
protected WidgetRegistry $widgetRegistry,
protected WidgetGroupInitializationService $widgetGroupInitializationService,
protected WidgetSettingsFactory $widgetSettingsFactory,
protected SettingsTypeRegistry $settingsTypeRegistry,
protected UriBuilder $uriBuilder,
) {}
public function getDashboards(ServerRequestInterface $request): ResponseInterface
{
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboards = [];
foreach ($availableDashboards as $dashboard) {
$dashboard->initializeWidgets($request);
$dashboards[] = $dashboard->getTransferData();
}
return new JsonResponse($dashboards);
}
public function addDashboard(ServerRequestInterface $request): ResponseInterface
{
$presetIdentifier = (string)($request->getParsedBody()['preset'] ?? '');
$dashboardPreset = $this->dashboardPresetRegistry->getDashboardPresets()[$presetIdentifier] ?? null;
if (!$dashboardPreset instanceof DashboardPreset) {
return new JsonResponse([
'status' => 'error',
'message' => 'Invalid dashboard preset!',
]);
}
$dashboardEntity = $this->dashboardRepository->create(
$dashboardPreset,
(int)$this->getBackendUser()->user['uid'],
(string)($request->getParsedBody()['title'] ?? '')
);
$dashboardEntity->initializeWidgets($request);
return new JsonResponse([
'status' => 'ok',
'dashboard' => $dashboardEntity->getTransferData(),
]);
}
public function editDashboard(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['identifier'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboardEntity, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$this->dashboardRepository->updateDashboardSettings(
$dashboardIdentifier,
[
'title' => (string)($request->getParsedBody()['title'] ?? ''),
]
);
// Fetch updated Dashboard
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
$dashboardEntity->initializeWidgets($request);
return new JsonResponse([
'status' => 'ok',
'dashboard' => $dashboardEntity->getTransferData(),
]);
}
public function updateDashboard(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['identifier'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboardEntity, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$widgets = $request->getParsedBody()['widgets'] ?? [];
$data = [];
foreach ($widgets as $widget) {
$data[$widget['identifier']] = [
'identifier' => $widget['type'],
];
}
// positions
$widgetPositions = $request->getParsedBody()['widgetPositions'] ?? [];
foreach ($widgetPositions as $columnCount => $widgets) {
foreach ($widgets as $widget) {
if (!isset($widget['identifier']) || !isset($widget['height']) || !isset($widget['width']) || !isset($widget['x']) || !isset($widget['y'])) {
return new JsonResponse([
'status' => 'error',
'message' => 'Invalid widget positions!',
]);
}
$identifier = $widget['identifier'] ?? '';
unset($widget['identifier']);
$data[$identifier]['positions'][$columnCount] = array_map('intval', $widget);
}
}
// settings
$dashboardEntity->initializeWidgets($request);
foreach ($widgets as $widget) {
$dashboardWidget = $dashboardEntity->getWidget($widget['identifier']);
if ($dashboardWidget) {
$data[$widget['identifier']]['settings'] = $dashboardWidget->getRawConfig()['settings'] ?? [];
}
}
$this->dashboardRepository->updateWidgetConfig($dashboardEntity, $data);
// Fetch updated Dashboard
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
$dashboardEntity->initializeWidgets($request);
return new JsonResponse([
'status' => 'ok',
'dashboard' => $dashboardEntity->getTransferData(),
]);
}
public function deleteDashboard(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['identifier'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboard, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$this->dashboardRepository->delete($dashboard);
return new JsonResponse([
'status' => 'ok',
]);
}
public function getPresets(ServerRequestInterface $request): ResponseInterface
{
$presets = $this->dashboardPresetRegistry->getDashboardPresets();
return new JsonResponse($presets);
}
public function getCategories(ServerRequestInterface $request): ResponseInterface
{
$widgetGroups = $this->widgetGroupInitializationService->buildWidgetGroupsConfiguration();
return new JsonResponse($widgetGroups);
}
public function getWidget(ServerRequestInterface $request): ResponseInterface
{
$widgetIdentifier = (string)($request->getQueryParams()['widget'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$widgets = [];
foreach ($availableDashboards as $dashboard) {
$dashboard->initializeWidgets($request);
foreach ($dashboard->getWidgets() as $dashboardEntry) {
$widgets[$dashboardEntry->getIdentifier()] = $dashboardEntry;
}
}
$dashboardWidget = $widgets[$widgetIdentifier] ?? null;
if ($dashboardWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget does not exist!',
]);
}
return new JsonResponse([
'status' => 'ok',
'widget' => $dashboardWidget->getTransferWidgetData()->jsonSerialize(),
]);
}
public function getWidgetSettings(ServerRequestInterface $request): ResponseInterface
{
$widgetIdentifier = (string)($request->getQueryParams()['widget'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$widgets = [];
foreach ($availableDashboards as $dashboard) {
$dashboard->initializeWidgets($request);
foreach ($dashboard->getWidgets() as $dashboardEntry) {
$widgets[$dashboardEntry->getIdentifier()] = $dashboardEntry;
}
}
$dashboardWidget = $widgets[$widgetIdentifier] ?? null;
if ($dashboardWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget does not exist!',
]);
}
$categories = [
new Category(
key: $dashboardWidget->getType(),
label: $this->getLanguageService()->sL($dashboardWidget->getTitle()),
description: $this->getLanguageService()->sL($dashboardWidget->getDescription()),
icon: $dashboardWidget->getIconIdentifier(),
settings: array_map(
fn(SettingDefinition $definition): EditableSetting => new EditableSetting(
definition: $this->resolveSettingLabels($definition),
value: $dashboardWidget->getSettings()->get($definition->key),
systemDefault: $definition->default,
typeImplementation: $this->settingsTypeRegistry->get($definition->type)->getJavaScriptModule(),
),
array_values(array_filter($dashboardWidget->getSettingsDefinitions(), fn(SettingDefinition $settingDefinition) => !$settingDefinition->readonly))
),
),
];
return new JsonResponse([
'status' => 'ok',
'categories' => json_encode($categories),
]);
}
public function updateWidgetSettings(ServerRequestInterface $request): ResponseInterface
{
// Check if identifier is available
$widgetIdentifier = trim((string)($request->getParsedBody()['widget'] ?? ''));
if ($widgetIdentifier === '') {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget is not available!',
]);
}
// Check for widget
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$targetDashboard = null;
$targetWidget = null;
foreach ($availableDashboards as $dashboard) {
$dashboard->initializeWidgets($request);
if ($dashboard->getWidget($widgetIdentifier)) {
$targetDashboard = $dashboard;
$targetWidget = $dashboard->getWidget($widgetIdentifier);
break;
}
}
if ($targetWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget does not exist!',
]);
}
if ($targetDashboard === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard does not exist!',
]);
}
$rawSettings = $request->getParsedBody()['settings'] ?? [];
$widgetData = [];
foreach ($targetDashboard->getWidgets() as $widget) {
$widgetData[$widget->getIdentifier()] = $widget->getRawConfig();
if ($targetWidget->getIdentifier() === $widget->getIdentifier()) {
$currentSettings = $widget->getRawConfig()['settings'] ?? [];
$newSettings = $this->widgetSettingsFactory->createSettingsFromFormData($rawSettings, $widget->getSettingsDefinitions());
$defaultSettings = $this->widgetSettingsFactory->createSettings($widget->getType(), [], $widget->getSettingsDefinitions());
$diff = SettingsDiff::create(
$currentSettings,
$newSettings,
$defaultSettings,
);
if ($diff->changes === [] && $diff->deletions === []) {
return new JsonResponse([
'status' => 'info',
'message' => $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.settings.unchanged'),
]);
}
$widgetData[$widget->getIdentifier()]['settings'] = $diff->settings;
}
}
$this->dashboardRepository->updateWidgetConfig($targetDashboard, $widgetData);
// Fetch updated Dashboard
$dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($targetDashboard->getIdentifier());
$dashboardEntity->initializeWidgets($request);
$returnWidget = $dashboardEntity->getWidget($targetWidget->getIdentifier());
if ($returnWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget does not exist!',
]);
}
return new JsonResponse([
'status' => 'ok',
]);
}
public function addWidget(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['dashboard'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboard, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$widgetType = (string)($request->getParsedBody()['type'] ?? '');
if ($widgetType === '') {
throw new \InvalidArgumentException('Argument "widget" not set.', 1714987384);
}
$widgets = $dashboard->getWidgetConfig();
$widgetIdentifier = sha1($widgetType . '-' . time());
$widgets[$widgetIdentifier] = ['identifier' => $widgetType];
$this->dashboardRepository->updateWidgetConfig($dashboard, $widgets);
// Fetch updated Dashboard
$dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
$dashboard->initializeWidgets($request);
$widgets = [];
foreach ($dashboard->getWidgets() as $dashboardEntry) {
$widgets[$dashboardEntry->getIdentifier()] = $dashboardEntry;
}
$dashboardWidget = $widgets[$widgetIdentifier] ?? null;
if ($dashboardWidget === null) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget is not available!',
]);
}
return new JsonResponse([
'status' => 'ok',
'widget' => $dashboardWidget->getTransferWidgetData()->jsonSerialize(),
]);
}
public function removeWidget(ServerRequestInterface $request): ResponseInterface
{
$dashboardIdentifier = (string)($request->getParsedBody()['dashboard'] ?? '');
$availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId());
$dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier);
if (!in_array($dashboard, $availableDashboards)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Dashboard is not available!',
]);
}
$widgetIdentifier = (string)($request->getParsedBody()['identifier'] ?? '');
$widgets = $dashboard->getWidgetConfig();
if ($widgetIdentifier === '' || !array_key_exists($widgetIdentifier, $widgets)) {
return new JsonResponse([
'status' => 'error',
'message' => 'Widget is not available!',
]);
}
unset($widgets[$widgetIdentifier]);
$this->dashboardRepository->updateWidgetConfig($dashboard, $widgets);
return new JsonResponse([
'status' => 'ok',
]);
}
private function resolveSettingLabels(SettingDefinition $definition): SettingDefinition
{
$languageService = $this->getLanguageService();
return new SettingDefinition(...[
...get_object_vars($definition),
'label' => $languageService->sL($definition->label),
'description' => $definition->description !== null ? $languageService->sL($definition->description) : null,
'enum' => array_map(static fn(string $label): string => $languageService->sL($label), $definition->enum),
]);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -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\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Dashboard\DashboardInitializationService;
/**
* @internal
*/
#[AsController]
readonly class DashboardController
{
public function __construct(
protected PageRenderer $pageRenderer,
protected DashboardInitializationService $dashboardInitializationService,
protected ModuleTemplateFactory $moduleTemplateFactory,
) {}
public function mainAction(ServerRequestInterface $request): ResponseInterface
{
$this->dashboardInitializationService->initializeDashboards($request, $this->getBackendUser());
$view = $this->moduleTemplateFactory->create($request);
$this->preparePageRenderer();
$this->addFrontendResources();
$view->setTitle($this->getLanguageService()->translate('title', 'dashboard.module'));
$view->getDocHeaderComponent()->disable();
return $view->renderResponse('Dashboard/Main');
}
/**
* Adds CSS and JS files that are necessary for widgets to the page renderer
*/
protected function addFrontendResources(): void
{
$javaScriptRenderer = $this->pageRenderer->getJavaScriptRenderer();
foreach ($this->dashboardInitializationService->getJavaScriptModuleInstructions() as $instruction) {
$javaScriptRenderer->addJavaScriptModuleInstruction($instruction);
}
foreach ($this->dashboardInitializationService->getCssFiles() as $cssFile) {
$this->pageRenderer->addCssFile($cssFile);
}
foreach ($this->dashboardInitializationService->getJsFiles() as $jsFile) {
$this->pageRenderer->addJsFile($jsFile);
}
}
/**
* Add the CSS and JS of the dashboard module to the page renderer
*/
protected function preparePageRenderer(): void
{
$this->pageRenderer->loadJavaScriptModule('@typo3/dashboard/dashboard.js');
$this->pageRenderer->addCssFile('EXT:dashboard/Resources/Public/Css/dashboard.css');
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+159
View File
@@ -0,0 +1,159 @@
<?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;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\Settings;
use TYPO3\CMS\Dashboard\Dto\Dashboard as TransferDashboard;
use TYPO3\CMS\Dashboard\Dto\WidgetConfiguration as TransferWidgetConfiguration;
use TYPO3\CMS\Dashboard\Factory\WidgetSettingsFactory;
use TYPO3\CMS\Dashboard\Widgets\WidgetContext;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
/**
* @internal
*/
class Dashboard
{
/**
* @var array<string,DashboardEntry>
*/
protected array $widgets = [];
protected ?object $widgetPositions = null;
/**
* @param array<string,array<string,string|array>> $widgetConfig
*/
public function __construct(
protected readonly string $identifier,
protected readonly string $title,
protected readonly array $widgetConfig,
protected readonly WidgetRegistry $widgetRegistry,
protected readonly WidgetSettingsFactory $widgetSettingsFactory,
protected readonly ContainerInterface $container,
) {}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getTitle(): string
{
return $this->getLanguageService()->sL($this->title) ?: $this->title;
}
public function getWidgetConfig(): array
{
return $this->widgetConfig;
}
/**
* @return array<string,DashboardEntry>
*/
public function getWidgets(): array
{
return $this->widgets;
}
public function getWidget(string $identifier): ?DashboardEntry
{
return $this->widgets[$identifier] ?? null;
}
public function getWidgetPositions(): object
{
return $this->widgetPositions ?? new \stdClass();
}
/**
* This will return a list of all widgets of the current dashboard object. It will only include available
* widgets and will add the initialized object of the widget itself
*/
public function initializeWidgets(ServerRequestInterface $request): void
{
$availableWidgets = $this->widgetRegistry->getAvailableWidgets();
$this->widgetPositions = new \stdClass();
foreach ($this->widgetConfig as $hash => $widgetConfig) {
$widgetConfigIdentifier = $widgetConfig['identifier'] ?? '';
if ($widgetConfigIdentifier !== '' && array_key_exists($widgetConfigIdentifier, $availableWidgets)) {
// Widget (Renderer) Instance
$widgetRenderer = $this->widgetRegistry->getAvailableWidget($request, $widgetConfigIdentifier);
// Dashboard Entry with Widget Context
$this->widgets[$hash] = new DashboardEntry(
context: new WidgetContext(
identifier: $hash,
rawData: $widgetConfig,
configuration: $availableWidgets[$widgetConfigIdentifier],
settings: $widgetRenderer instanceof WidgetRendererInterface ? $this->widgetSettingsFactory->createSettings(
$widgetConfigIdentifier,
$widgetConfig['settings'] ?? [],
$widgetRenderer->getSettingsDefinitions(),
) : new Settings([]),
request: $request,
),
renderer: $widgetRenderer,
);
// Widget Positions
$positions = $widgetConfig['positions'] ?? [];
foreach ($positions as $columnCount => $position) {
if (!isset($position['height']) || !isset($position['width']) || !isset($position['x']) || !isset($position['y'])) {
continue;
}
if (!isset($this->widgetPositions->{$columnCount})) {
$this->widgetPositions->{$columnCount} = [];
}
$this->widgetPositions->{$columnCount}[] = [
'identifier' => $hash,
'height' => (int)$position['height'],
'width' => (int)$position['width'],
'x' => (int)$position['x'],
'y' => (int)$position['y'],
];
}
}
}
foreach (array_keys(get_object_vars($this->widgetPositions)) as $columnCount) {
usort(
$this->widgetPositions->{$columnCount},
static fn(array $a, array $b): int => $a['y'] !== $b['y'] ? $a['y'] - $b['y'] : $a['x'] - $b['x']
);
}
}
public function getTransferData(): TransferDashboard
{
return new TransferDashboard(
identifier: $this->getIdentifier(),
title: $this->getTitle(),
widgets: array_values(array_map(fn(DashboardEntry $entry): TransferWidgetConfiguration => $entry->getTransferWidgetConfiguration(), $this->getWidgets())),
widgetPositions: $this->getWidgetPositions(),
);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+172
View File
@@ -0,0 +1,172 @@
<?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;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsInterface;
use TYPO3\CMS\Dashboard\Dto\WidgetConfiguration as TransferWidgetConfiguration;
use TYPO3\CMS\Dashboard\Dto\WidgetData as TransferWidgetData;
use TYPO3\CMS\Dashboard\Widgets\EventDataInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetContext;
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetResult;
/**
* Dashboard entry representing a widget instance within a dashboard.
*
* This class encapsulates a dashboard widget instance, providing access to its context,
* configuration, settings, and rendering capabilities. It serves as a bridge between
* the dashboard system and individual widget implementations, handling both legacy
* WidgetInterface and new WidgetRendererInterface widgets.
*
* Each dashboard entry maintains its own widget context with instance-specific settings
* and provides methods for rendering and configuration management.
*
* @internal
*/
final readonly class DashboardEntry
{
public function __construct(
private WidgetContext $context,
private WidgetRendererInterface|WidgetInterface $renderer,
) {}
public function getIdentifier(): string
{
return $this->context->identifier;
}
public function getType(): string
{
return $this->context->configuration->getIdentifier();
}
public function getTitle(): string
{
return $this->context->configuration->getTitle();
}
public function getDescription(): string
{
return $this->context->configuration->getDescription();
}
public function getIconIdentifier(): string
{
return $this->context->configuration->getIconIdentifier();
}
public function getHeight(): string
{
return $this->context->configuration->getHeight();
}
public function getWidth(): string
{
return $this->context->configuration->getWidth();
}
public function getSettings(): SettingsInterface
{
return $this->context->settings;
}
public function getRequest(): ServerRequestInterface
{
return $this->context->request;
}
public function getEventData(): array
{
return ($this->renderer instanceof EventDataInterface) ? $this->renderer->getEventData() : [];
}
public function getTransferWidgetConfiguration(): TransferWidgetConfiguration
{
return new TransferWidgetConfiguration(
identifier: $this->getIdentifier(),
type: $this->getType(),
height: $this->getHeight(),
width: $this->getWidth(),
);
}
public function getTransferWidgetData(): TransferWidgetData
{
$result = $this->render();
return new TransferWidgetData(
identifier: $this->getIdentifier(),
type: $this->getType(),
height: $this->getHeight(),
width: $this->getWidth(),
label: $result->label ?? $this->getLanguageService()->sL($this->getTitle()),
content: $result->content,
eventdata: $this->getEventData(),
refreshable: $result->refreshable,
configurable: (
$this->renderer instanceof WidgetRendererInterface
&& array_filter($this->renderer->getSettingsDefinitions(), fn($definition) => !$definition->readonly) !== []
)
);
}
/**
* @return SettingDefinition[]
*/
public function getSettingsDefinitions(): array
{
if ($this->renderer instanceof WidgetRendererInterface) {
return $this->renderer->getSettingsDefinitions();
}
return [];
}
public function getRawConfig(): array
{
return $this->context->rawData;
}
private function render(): WidgetResult
{
try {
if ($this->renderer instanceof WidgetRendererInterface) {
return $this->renderer->renderWidget($this->context);
}
// Map legacy WidgetInterface to "new" WidgetResult
return new WidgetResult(
content: $this->renderer->renderWidgetContent(),
refreshable: $this->renderer->getOptions()['refreshAvailable'] ?? false,
);
} catch (\Exception) {
return new WidgetResult(
content: sprintf('<div class="widget-content-main">%s</div>', $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.error')),
refreshable: true,
);
}
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+184
View File
@@ -0,0 +1,184 @@
<?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;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\Repository\DashboardRepository;
use TYPO3\CMS\Dashboard\Widgets\AdditionalCssInterface;
use TYPO3\CMS\Dashboard\Widgets\AdditionalJavaScriptInterface;
use TYPO3\CMS\Dashboard\Widgets\JavaScriptInterface;
/**
* @internal
*/
class DashboardInitializationService
{
protected const MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER = 'dashboard/current_dashboard/';
/**
* @var list<JavaScriptModuleInstruction>
*/
protected array $javaScriptModuleInstructions = [];
private array $jsFiles = [];
private array $cssFiles = [];
public function __construct(
private readonly DashboardRepository $dashboardRepository,
private readonly DashboardPresetRegistry $dashboardPresetRegistry,
private readonly WidgetRegistry $widgetRegistry,
) {}
public function initializeDashboards(ServerRequestInterface $request, BackendUserAuthentication $user): void
{
$this->defineCurrentDashboard($user);
$this->defineResourcesOfWidgets();
}
protected function defineCurrentDashboard(BackendUserAuthentication $user): Dashboard
{
$currentDashboard = $this->dashboardRepository->getDashboardByIdentifier($this->loadCurrentDashboard($user));
if ($currentDashboard === null) {
$dashboards = $this->getDashboardsForUser($user);
/** @var Dashboard $currentDashboard */
$currentDashboard = reset($dashboards);
$this->saveCurrentDashboard($user, $currentDashboard->getIdentifier());
}
return $currentDashboard;
}
protected function createDefaultDashboards(BackendUserAuthentication $user): array
{
$dashboardsForUser = [];
$userConfig = $user->getTSConfig();
$dashboardsToCreate = GeneralUtility::trimExplode(
',',
$userConfig['options.']['dashboard.']['dashboardPresetsForNewUsers'] ?? 'default'
);
foreach ($this->dashboardPresetRegistry->getDashboardPresets() as $dashboardPreset) {
if (in_array($dashboardPreset->getIdentifier(), $dashboardsToCreate, true)) {
$dashboard = $this->dashboardRepository->create(
$dashboardPreset,
(int)$user->user['uid']
);
if ($dashboard === null) {
continue;
}
$dashboardsForUser[$dashboard->getIdentifier()] = $dashboard;
}
}
return $dashboardsForUser;
}
/**
* @return Dashboard[]
*/
public function getDashboardsForUser(BackendUserAuthentication $user): array
{
$dashboards = [];
foreach ($this->dashboardRepository->getDashboardsForUser((int)$user->user['uid']) as $dashboard) {
$dashboards[$dashboard->getIdentifier()] = $dashboard;
}
if ($dashboards === []) {
$dashboards = $this->createDefaultDashboards($user);
}
return $dashboards;
}
protected function defineResourcesOfWidgets(): void
{
foreach ($this->widgetRegistry->getAvailableWidgets() as $widget) {
$concreteInstance = GeneralUtility::makeInstance($widget->getServiceName());
if ($concreteInstance instanceof JavaScriptInterface) {
$this->defineJavaScriptInstructions($concreteInstance);
}
if ($concreteInstance instanceof AdditionalCssInterface) {
$this->defineCssFiles($concreteInstance);
}
if ($concreteInstance instanceof AdditionalJavaScriptInterface) {
$this->defineJsFiles($concreteInstance);
}
}
}
protected function defineJavaScriptInstructions(JavaScriptInterface $widgetInstance): void
{
foreach ($widgetInstance->getJavaScriptModuleInstructions() as $instruction) {
$this->javaScriptModuleInstructions[] = $instruction;
}
}
/**
* Define the correct path of the JS files of a widget and add them to the list of JS files that needs to be
* included
*/
protected function defineJsFiles(AdditionalJavaScriptInterface $widgetInstance): void
{
foreach ($widgetInstance->getJsFiles() as $jsFile) {
$this->jsFiles[$jsFile] = $jsFile;
}
}
/**
* Define the correct path of the CSS files of a widget and add them to the list of CSS files that needs to be
* included
*/
protected function defineCssFiles(AdditionalCssInterface $widgetInstance): void
{
foreach ($widgetInstance->getCssFiles() as $cssFile) {
$this->cssFiles[$cssFile] = $cssFile;
}
}
protected function loadCurrentDashboard(BackendUserAuthentication $user): string
{
return $user->getModuleData(self::MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER) ?? '';
}
protected function saveCurrentDashboard(BackendUserAuthentication $user, string $identifier): void
{
$user->pushModuleData(self::MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER, $identifier);
}
/**
* @return list<JavaScriptModuleInstruction>
*/
public function getJavaScriptModuleInstructions(): array
{
return $this->javaScriptModuleInstructions;
}
public function getJsFiles(): array
{
return $this->jsFiles;
}
public function getCssFiles(): array
{
return $this->cssFiles;
}
}
+88
View File
@@ -0,0 +1,88 @@
<?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;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* @internal
*/
readonly class DashboardPreset implements \JsonSerializable
{
/**
* @param list<array{identifier: string, settings?: array<string, mixed>}> $defaultWidgets
*/
public function __construct(
protected string $identifier,
protected string $title,
protected string $description,
protected string $iconIdentifier = 'content-dashboard',
protected array $defaultWidgets = [],
protected bool $showInWizard = true
) {}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getIconIdentifier(): string
{
return $this->iconIdentifier;
}
public function getTitle(): string
{
return $this->getLanguageService()->sL($this->title) ?: $this->title;
}
public function getDescription(): string
{
return $this->getLanguageService()->sL($this->description) ?: $this->description;
}
/**
* @return list<array{identifier: string, settings?: array<string, mixed>}>
*/
public function getDefaultWidgets(): array
{
return $this->defaultWidgets;
}
public function isShowInWizard(): bool
{
return $this->showInWizard;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
public function jsonSerialize(): array
{
return [
'identifier' => $this->getIdentifier(),
'title' => $this->getTitle(),
'description' => $this->getDescription(),
'icon' => $this->getIconIdentifier(),
'widgets' => $this->getDefaultWidgets(),
'showInWizard' => $this->isShowInWizard(),
];
}
}
+54
View File
@@ -0,0 +1,54 @@
<?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;
/**
* @internal
*/
class DashboardPresetRegistry
{
/**
* @var DashboardPreset[]
*/
private array $dashboardPresets = [];
public function getDashboardPresets(): array
{
if (empty($this->dashboardPresets)) {
$fallbackDashboardPreset = new DashboardPreset(
'dashboardPreset-fallback',
'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default',
'',
'content-dashboard',
[],
false
);
return [
'dashboardPreset-fallback' => $fallbackDashboardPreset,
];
}
return $this->dashboardPresets;
}
public function registerDashboardPreset(DashboardPreset $dashboardPreset): void
{
$this->dashboardPresets[$dashboardPreset->getIdentifier()] = $dashboardPreset;
}
}
@@ -0,0 +1,109 @@
<?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\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\WidgetRegistry;
use TYPO3\CMS\Dashboard\Widgets\AdminOnlyWidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetConfiguration;
/**
* @internal
*/
final readonly class DashboardWidgetPass implements CompilerPassInterface
{
public function __construct(private string $tagName) {}
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition(WidgetRegistry::class)) {
return;
}
$widgetRegistryDefinition = $container->findDefinition(WidgetRegistry::class);
foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) {
$definition = $container->findDefinition($serviceName);
$definition->setPublic(true);
foreach ($tags as $attributes) {
$identifier = $attributes['identifier'] ?? $serviceName;
$attributes['identifier'] = $identifier;
$attributes['serviceName'] = $serviceName;
$attributes['adminOnly'] = is_a($definition->getClass(), AdminOnlyWidgetInterface::class, true);
$attributes = $this->convertAttributes($attributes);
$configurationServiceName = $this->registerWidgetConfigurationService(
$container,
$identifier,
$attributes
);
$definition->setArgument('$configuration', new Reference($configurationServiceName));
$widgetRegistryDefinition->addMethodCall('registerWidget', [$identifier . 'WidgetConfiguration']);
}
}
}
private function convertAttributes(array $attributes): array
{
$attributes = array_merge([
'iconIdentifier' => 'content-dashboard',
'height' => 'small',
'width' => 'small',
], $attributes);
if (isset($attributes['groupNames'])) {
$attributes['groupNames'] = GeneralUtility::trimExplode(',', $attributes['groupNames'], true);
} else {
$attributes['groupNames'] = [];
}
return $attributes;
}
private function registerWidgetConfigurationService(
ContainerBuilder $container,
string $widgetIdentifier,
array $arguments
): string {
$serviceName = $widgetIdentifier . 'WidgetConfiguration';
$definition = new Definition(
WidgetConfiguration::class,
$this->adjustArgumentsForDi($arguments)
);
$definition->setPublic(true);
$container->addDefinitions([$serviceName => $definition]);
return $serviceName;
}
private function adjustArgumentsForDi(array $arguments): array
{
foreach ($arguments as $key => $value) {
$arguments['$' . $key] = $value;
unset($arguments[$key]);
}
return $arguments;
}
}
+41
View File
@@ -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\Dto;
/**
* @internal
*/
final readonly class Dashboard implements \JsonSerializable
{
public function __construct(
private string $identifier,
private string $title,
private array $widgets,
private object $widgetPositions,
) {}
public function jsonSerialize(): array
{
return [
'identifier' => $this->identifier,
'title' => $this->title,
'widgets' => $this->widgets,
'widgetPositions' => $this->widgetPositions,
];
}
}
+41
View File
@@ -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\Dto;
/**
* @internal
*/
final readonly class WidgetConfiguration implements \JsonSerializable
{
public function __construct(
private string $identifier,
private string $type,
private string $height,
private string $width,
) {}
public function jsonSerialize(): array
{
return [
'identifier' => $this->identifier,
'type' => $this->type,
'height' => $this->height,
'width' => $this->width,
];
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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\Dto;
/**
* @internal
*/
final readonly class WidgetData implements \JsonSerializable
{
public function __construct(
private string $identifier,
private string $type,
private string $height,
private string $width,
private string $label,
private string $content,
private array $eventdata,
private bool $refreshable,
private bool $configurable,
) {}
public function jsonSerialize(): array
{
return [
'identifier' => $this->identifier,
'type' => $this->type,
'height' => $this->height,
'width' => $this->width,
'label' => $this->label,
'content' => $this->content,
'eventdata' => $this->eventdata,
'refreshable' => $this->refreshable,
'configurable' => $this->configurable,
];
}
}
+43
View File
@@ -0,0 +1,43 @@
<?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\Dto;
/**
* @internal
*/
final readonly class WidgetPosition implements \JsonSerializable
{
public function __construct(
private string $identifier,
private int $height,
private int $width,
private int $y,
private int $x,
) {}
public function jsonSerialize(): array
{
return [
'identifier' => $this->identifier,
'height' => $this->height,
'width' => $this->width,
'y' => $this->y,
'x' => $this->x,
];
}
}
@@ -0,0 +1,36 @@
<?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\EventListener;
use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Page\PageRenderer;
/**
* Adds custom CSS needed for EXT:dashboard in the "outer" backend scaffold
*/
final readonly class AfterBackendPageRenderEventListener
{
public function __construct(private PageRenderer $pageRenderer) {}
#[AsEventListener(event: AfterBackendPageRenderEvent::class)]
public function __invoke(): void
{
$this->pageRenderer->addCssFile('EXT:dashboard/Resources/Public/Css/Modal/style.css');
}
}
@@ -0,0 +1,22 @@
<?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\Exception;
use TYPO3\CMS\Core\Exception;
class InvalidRssFeedException extends Exception {}
+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\Factory;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingDefinitionValidation;
use TYPO3\CMS\Core\Settings\SettingsFactory;
use TYPO3\CMS\Core\Settings\SettingsInterface;
use TYPO3\CMS\Core\Settings\SettingsProvider;
/**
* Factory for creating widget settings instances.
*
* This factory creates Settings objects for dashboard widgets, combining default
* values from setting definitions with instance-specific values. It handles
* validation of setting definitions and provides utilities for creating settings
* from form data submitted through the widget configuration interface.
*
* The factory supports creating settings with various options:
* - Respecting readonly settings
* - Omitting default values when needed
* - Processing form data for widget configuration updates
*/
#[Autoconfigure(public: true)]
readonly class WidgetSettingsFactory
{
public function __construct(
protected SettingsFactory $settingsFactory,
protected SettingDefinitionValidation $settingDefinitionValidation,
) {}
/**
* @param SettingDefinition[] $definitions
*/
public function createSettings(
string $name,
array $settings,
array $definitions,
bool $respectReadonly = false,
bool $omitDefaults = false,
): SettingsInterface {
$defaultSettings = [];
foreach ($definitions as $definition) {
$this->settingDefinitionValidation->validate($definition);
if (!$omitDefaults) {
$defaultSettings[$definition->key] = $definition->default;
}
}
return $this->settingsFactory->resolveSettings(
new SettingsProvider($name, $defaultSettings, $definitions),
new SettingsProvider($name . ':instance', $settings, []),
);
}
public function createSettingsFromFormData(array $settings, array $definitions): SettingsInterface
{
return $this->settingsFactory->createSettingsFromFormData($settings, $definitions);
}
}
@@ -0,0 +1,50 @@
<?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\Persistence;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Fill the "cruser_id" for new dashboards.
*
* @internal this is a TYPO3-internal specific hook implementation and not part of TYPO3's Public API
*/
class DashboardCreationEnricher
{
/**
* @param array $incomingFieldArray
* @param string $table
* @param string $id
*/
public function processDatamap_preProcessFieldArray(&$incomingFieldArray, $table, $id, DataHandler $dataHandler)
{
// Not within be_dashboards
if ($table !== 'be_dashboards') {
return;
}
// Existing record, nothing to change
if (MathUtility::canBeInterpretedAsInteger($id)) {
return;
}
if (isset($incomingFieldArray['cruser_id'])) {
return;
}
$incomingFieldArray['cruser_id'] = $dataHandler->BE_USER->user['uid'] ?? 0;
}
}
+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\Repository;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Dashboard\Dashboard;
use TYPO3\CMS\Dashboard\DashboardPreset;
use TYPO3\CMS\Dashboard\Factory\WidgetSettingsFactory;
use TYPO3\CMS\Dashboard\WidgetRegistry;
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
/**
* @internal
*/
class DashboardRepository
{
private const string TABLE = 'be_dashboards';
/**
* @var string[]
*/
protected array $allowedFields = ['title'];
/**
* @var list<WidgetRendererInterface|WidgetInterface>
*/
protected array $widgets = [];
public function __construct(
protected readonly ConnectionPool $connectionPool,
protected readonly WidgetRegistry $widgetRegistry,
protected readonly WidgetSettingsFactory $widgetSettingsFactory,
protected readonly ContainerInterface $container
) {}
/**
* @return Dashboard[]
*/
public function getDashboardsForUser(int $userId): array
{
$queryBuilder = $this->getQueryBuilder();
$rows = $queryBuilder
->select('*')
->from(self::TABLE)
->where(
$queryBuilder->expr()->eq('cruser_id', $queryBuilder->createNamedParameter($userId))
)
->executeQuery()
->fetchAllAssociative();
$results = [];
foreach ($rows as $row) {
$results[] = $this->createFromRow($row);
}
return $results;
}
public function create(DashboardPreset $dashboardPreset, int $userId, string $title = ''): ?Dashboard
{
$widgets = [];
$title = $title ?: $dashboardPreset->getTitle();
foreach ($dashboardPreset->getDefaultWidgets() as $defaultWidget) {
$hash = sha1(StringUtility::getUniqueId('widget_') . '-' . $defaultWidget['identifier']);
$widgets[$hash] = $defaultWidget;
}
$identifier = sha1($dashboardPreset->getIdentifier() . '-' . time());
$this->getQueryBuilder()
->insert(self::TABLE)
->values([
'identifier' => $identifier,
'title' => $title,
'tstamp' => time(),
'crdate' => time(),
'cruser_id' => $userId,
'widgets' => json_encode($widgets),
])
->executeStatement();
return $this->getDashboardByIdentifier($identifier);
}
/**
* @return int|null
*/
public function updateDashboardSettings(string $identifier, array $values)
{
$checkedValues = $this->checkAllowedFields($values);
if (empty($checkedValues)) {
return null;
}
$queryBuilder = $this->getQueryBuilder();
$queryBuilder->update(self::TABLE)
->where(
$queryBuilder->expr()->eq(
'identifier',
$queryBuilder->createNamedParameter($identifier)
)
);
foreach ($checkedValues as $field => $value) {
$queryBuilder->set($field, $value);
}
return $queryBuilder->executeStatement();
}
protected function checkAllowedFields(array $values): array
{
$allowedFields = [];
foreach ($values as $field => $value) {
if (!empty($value) && in_array((string)$field, $this->allowedFields, true)) {
$allowedFields[$field] = $value;
}
}
return $allowedFields;
}
public function getDashboardByIdentifier(string $identifier): ?Dashboard
{
$queryBuilder = $this->getQueryBuilder();
$row = $queryBuilder
->select('*')
->from(self::TABLE)
->where($queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($identifier)))
->executeQuery()
->fetchAllAssociative();
if (count($row)) {
return $this->createFromRow($row[0]);
}
return null;
}
/**
* @param array<array<string, mixed>> $widgets
*/
public function updateWidgetConfig(Dashboard $dashboard, array $widgets): void
{
$queryBuilder = $this->getQueryBuilder();
$queryBuilder
->update(self::TABLE)
->set('widgets', json_encode($widgets))
->where($queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($dashboard->getIdentifier())))
->executeStatement();
}
public function delete(Dashboard $dashboard): void
{
$queryBuilder = $this->getQueryBuilder();
$queryBuilder
->update(self::TABLE)
->set('deleted', 1)
->where($queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($dashboard->getIdentifier())))
->executeStatement();
}
protected function createFromRow(array $row): Dashboard
{
return GeneralUtility::makeInstance(
Dashboard::class,
$row['identifier'] ?? '',
$row['title'] ?? '',
json_decode((string)($row['widgets'] ?? ''), true) ?? [],
$this->widgetRegistry,
$this->widgetSettingsFactory,
$this->container
);
}
protected function getQueryBuilder(): QueryBuilder
{
return $this->connectionPool->getQueryBuilderForTable(self::TABLE);
}
}
+231
View File
@@ -0,0 +1,231 @@
<?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;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
use TYPO3\CMS\Core\Package\AbstractServiceProvider;
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
class ServiceProvider extends AbstractServiceProvider
{
private const string CACHE_IDENTIFIER_PREFIX = 'Dashboard_';
protected static function getPackagePath(): string
{
return __DIR__ . '/../';
}
protected static function getPackageName(): string
{
return 'typo3/cms-dashboard';
}
public function getFactories(): array
{
return [
'dashboard.presets' => self::getDashboardPresets(...),
'dashboard.widgetGroups' => self::getWidgetGroups(...),
'dashboard.widgets' => self::getWidgets(...),
'dashboard.configuration.warmer' => self::getConfigurationWarmer(...),
];
}
public function getExtensions(): array
{
return [
DashboardPresetRegistry::class => self::configureDashboardPresetRegistry(...),
ListenerProvider::class => self::addEventListeners(...),
WidgetGroupRegistry::class => self::configureWidgetGroupRegistry(...),
'dashboard.presets' => self::configureDashboardPresets(...),
'dashboard.widgetGroups' => self::configureWidgetGroups(...),
'dashboard.widgets' => self::configureWidgets(...),
] + parent::getExtensions();
}
public static function getDashboardPresets(ContainerInterface $container): \ArrayObject
{
return new \ArrayObject();
}
public static function getWidgetGroups(ContainerInterface $container): \ArrayObject
{
return new \ArrayObject();
}
public static function getWidgets(ContainerInterface $container): \ArrayObject
{
return new \ArrayObject();
}
public static function configureDashboardPresetRegistry(
ContainerInterface $container,
?DashboardPresetRegistry $dashboardPresetRegistry = null
): DashboardPresetRegistry {
$dashboardPresetRegistry = $dashboardPresetRegistry ?? self::new($container, DashboardPresetRegistry::class);
$cache = $container->get('cache.core');
$cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class)->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'Presets')->toString();
if (!$dashboardPresetsFromPackages = $cache->require($cacheIdentifier)) {
$dashboardPresetsFromPackages = $container->get('dashboard.presets')->getArrayCopy();
$cache->set($cacheIdentifier, 'return ' . var_export($dashboardPresetsFromPackages, true) . ';');
}
foreach ($dashboardPresetsFromPackages as $identifier => $options) {
// Compatibility layer for presets using the widget identifier only
$defaultWidgets = array_map(
static fn(string|array $defaultWidget): array => is_string($defaultWidget) ? ['identifier' => $defaultWidget] : $defaultWidget,
$options['defaultWidgets'] ?? []
);
$preset = new DashboardPreset(
$identifier,
$options['title'],
$options['description'],
$options['iconIdentifier'],
$defaultWidgets,
$options['showInWizard']
);
$dashboardPresetRegistry->registerDashboardPreset($preset);
}
return $dashboardPresetRegistry;
}
public static function configureWidgetGroupRegistry(
ContainerInterface $container,
?WidgetGroupRegistry $widgetGroupRegistry = null
): WidgetGroupRegistry {
$widgetGroupRegistry = $widgetGroupRegistry ?? self::new($container, WidgetGroupRegistry::class);
$cache = $container->get('cache.core');
$cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class)->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'WidgetGroups')->toString();
if (!$widgetGroupsFromPackages = $cache->require($cacheIdentifier)) {
$widgetGroupsFromPackages = $container->get('dashboard.widgetGroups')->getArrayCopy();
$cache->set($cacheIdentifier, 'return ' . var_export($widgetGroupsFromPackages, true) . ';');
}
foreach ($widgetGroupsFromPackages as $identifier => $options) {
$group = new WidgetGroup(
$identifier,
$options['title']
);
$widgetGroupRegistry->registerWidgetGroup($group);
}
return $widgetGroupRegistry;
}
public static function configureDashboardPresets(ContainerInterface $container, \ArrayObject $presets): \ArrayObject
{
$paths = self::getPathsOfInstalledPackages();
foreach ($paths as $pathOfPackage) {
$dashboardPresetsFileNameForPackage = $pathOfPackage . 'Configuration/Backend/DashboardPresets.php';
if (file_exists($dashboardPresetsFileNameForPackage)) {
$definedPresetsInPackage = self::requireFile($dashboardPresetsFileNameForPackage);
if (is_array($definedPresetsInPackage)) {
$presets->exchangeArray(array_merge($presets->getArrayCopy(), $definedPresetsInPackage));
}
}
}
return $presets;
}
/**
* @param string|null $path supplied when invoked internally through PseudoServiceProvider
*/
public static function configureWidgetGroups(ContainerInterface $container, \ArrayObject $widgetGroups, ?string $path = null): \ArrayObject
{
$paths = self::getPathsOfInstalledPackages();
foreach ($paths as $pathOfPackage) {
$widgetGroupsFileNameForPackage = $pathOfPackage . 'Configuration/Backend/DashboardWidgetGroups.php';
if (file_exists($widgetGroupsFileNameForPackage)) {
$definedGroupsInPackage = self::requireFile($widgetGroupsFileNameForPackage);
if (is_array($definedGroupsInPackage)) {
$widgetGroups->exchangeArray(array_merge($widgetGroups->getArrayCopy(), $definedGroupsInPackage));
}
}
}
return $widgetGroups;
}
/**
* @param string|null $path supplied when invoked internally through PseudoServiceProvider
*/
public static function configureWidgets(ContainerInterface $container, \ArrayObject $widgets, ?string $path = null): \ArrayObject
{
$paths = self::getPathsOfInstalledPackages();
foreach ($paths as $pathOfPackage) {
$widgetsFileNameForPackage = $pathOfPackage . 'Configuration/Backend/DashboardWidgets.php';
if (file_exists($widgetsFileNameForPackage)) {
$definedWidgetsInPackage = self::requireFile($widgetsFileNameForPackage);
if (is_array($definedWidgetsInPackage)) {
$widgets->exchangeArray(array_merge($widgets->getArrayCopy(), $definedWidgetsInPackage));
}
}
}
return $widgets;
}
protected static function getPathsOfInstalledPackages(): array
{
$paths = [];
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
foreach ($packageManager->getActivePackages() as $package) {
$paths[] = $package->getPackagePath();
}
return $paths;
}
public static function getConfigurationWarmer(ContainerInterface $container): \Closure
{
$cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class);
$presetsCacheIdentifier = $cacheIdentifier->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'Presets')->toString();
$widgetGroupsCacheIdentifier = $cacheIdentifier->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'WidgetGroups')->toString();
return static function (CacheWarmupEvent $event) use ($container, $presetsCacheIdentifier, $widgetGroupsCacheIdentifier) {
if ($event->hasGroup('system')) {
$cache = $container->get('cache.core');
$dashboardPresetsFromPackages = $container->get('dashboard.presets')->getArrayCopy();
$cache->set($presetsCacheIdentifier, 'return ' . var_export($dashboardPresetsFromPackages, true) . ';');
$widgetGroupsFromPackages = $container->get('dashboard.widgetGroups')->getArrayCopy();
$cache->set($widgetGroupsCacheIdentifier, 'return ' . var_export($widgetGroupsFromPackages, true) . ';');
}
};
}
public static function addEventListeners(ContainerInterface $container, ListenerProvider $listenerProvider): ListenerProvider
{
$listenerProvider->addListener(CacheWarmupEvent::class, 'dashboard.configuration.warmer');
return $listenerProvider;
}
}
@@ -0,0 +1,74 @@
<?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\ViewHelpers\Widget;
use TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface;
use TYPO3\CMS\Dashboard\Widgets\ElementAttributesInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* @internal
*
* Renders a dashboard button
*
* Examples
* ========
*
* ::
*
* <dashboard:widget.button button="{button}" class="widget-cta">
* {f:translate(id: button.title, default: button.title)}
* </dashboard:widget.button>
*/
final class ButtonViewHelper extends AbstractTagBasedViewHelper
{
/**
* @var string
*/
protected $tagName = 'a';
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('button', ButtonProviderInterface::class, 'Dashboard widget button', true);
}
public function render(): string
{
$button = $this->arguments['button'];
$this->tag->addAttribute('href', $button->getLink() ?: '#');
$target = $button->getTarget();
if ($target !== '') {
$this->tag->addAttribute('target', $target);
if ($target === '_blank') {
$this->tag->addAttribute('rel', 'noreferrer');
}
}
if ($button instanceof ElementAttributesInterface) {
$this->tag->addAttributes($button->getElementAttributes());
}
$this->tag->setContent((string)$this->renderChildren());
$this->tag->forceClosingTag(true);
return $this->tag->render();
}
}
+40
View File
@@ -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;
/**
* Provides API for widgets.
*/
class WidgetApi
{
/**
* Provides default colors to use for charts.
*
* @return array{0:string, 1:string, 2:string, 3:string, 4:string} Hex codes of default colors.
*/
public static function getDefaultChartColors(): array
{
return [
'#ff8700',
'#a4276a',
'#1a568f',
'#4c7e3a',
'#69bbb5',
];
}
}
+43
View File
@@ -0,0 +1,43 @@
<?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;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* @internal
*/
readonly class WidgetGroup
{
public function __construct(protected string $identifier, protected string $title) {}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getTitle(): string
{
return $this->getLanguageService()->sL($this->title) ?: $this->title;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,74 @@
<?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;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* @internal
*/
readonly class WidgetGroupInitializationService
{
public function __construct(
private WidgetGroupRegistry $widgetGroupRegistry,
private WidgetRegistry $widgetRegistry,
) {}
/**
* Define the different groups of widgets as shown in the modal when adding a widget to the current dashboard
*/
public function buildWidgetGroupsConfiguration(): array
{
$groupConfigurations = [];
foreach ($this->widgetGroupRegistry->getWidgetGroups() as $widgetGroup) {
$widgets = [];
$widgetGroupIdentifier = $widgetGroup->getIdentifier();
$widgetsForGroup = $this->widgetRegistry->getAvailableWidgetsForWidgetGroup($widgetGroupIdentifier);
foreach ($widgetsForGroup as $widgetConfiguration) {
$widgetIdentifier = $widgetConfiguration->getIdentifier();
$widgets[] = [
'identifier' => $widgetIdentifier,
'icon' => $widgetConfiguration->getIconIdentifier(),
'label' => $this->getLanguageService()->sL($widgetConfiguration->getTitle()),
'description' => $this->getLanguageService()->sL($widgetConfiguration->getDescription()),
'requestType' => 'event',
'event' => 'typo3:dashboard:widget:add',
];
}
if ($widgets === []) {
continue;
}
$groupConfigurations[$widgetGroupIdentifier] = [
'identifier' => $widgetGroupIdentifier,
'label' => $widgetGroup->getTitle(),
'items' => $widgets,
];
}
return $groupConfigurations;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+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;
/**
* @internal
*/
class WidgetGroupRegistry
{
/**
* @var WidgetGroup[]
*/
private array $widgetGroups = [];
public function getWidgetGroups(): array
{
return $this->widgetGroups;
}
public function registerWidgetGroup(WidgetGroup $widgetGroup): void
{
$this->widgetGroups[$widgetGroup->getIdentifier()] = $widgetGroup;
}
}
+139
View File
@@ -0,0 +1,139 @@
<?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;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Dashboard\Factory\WidgetSettingsFactory;
use TYPO3\CMS\Dashboard\Widgets\RequestAwareWidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetConfiguration;
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
/**
* @internal
*/
#[Autoconfigure(public: true)]
class WidgetRegistry
{
/**
* @var array<string,WidgetConfigurationInterface>
*/
private $widgets = [];
/**
* @var array<string, WidgetConfigurationInterface[]>
*/
private $widgetsPerWidgetGroup = [];
public function __construct(
protected readonly ContainerInterface $container,
protected readonly WidgetSettingsFactory $widgetSettingsFactory,
) {}
/**
* @return WidgetConfigurationInterface[]
*/
public function getAvailableWidgets(): array
{
return $this->checkPermissionOfWidgets($this->widgets);
}
/**
* @return WidgetConfigurationInterface[]
*/
public function getAllWidgets(): array
{
return $this->widgets;
}
/**
* @throws \InvalidArgumentException If requested identifier does not exist.
*/
public function getAvailableWidget(ServerRequestInterface $request, string $identifier): WidgetRendererInterface|WidgetInterface
{
if (array_key_exists($identifier, $this->getAvailableWidgets())) {
$widget = $this->container->get($this->widgets[$identifier]->getServiceName());
if ($widget instanceof RequestAwareWidgetInterface) {
$widget->setRequest($request);
}
return $widget;
}
throw new \InvalidArgumentException('Requested widget "' . $identifier . '" does not exist.', 1584777201);
}
/**
* @return WidgetConfigurationInterface[]
*/
public function getAvailableWidgetsForWidgetGroup(string $widgetGroupIdentifier): array
{
if (!array_key_exists($widgetGroupIdentifier, $this->widgetsPerWidgetGroup)) {
return [];
}
return $this->checkPermissionOfWidgets($this->widgetsPerWidgetGroup[$widgetGroupIdentifier]);
}
public function registerWidget(string $serviceName): void
{
$widgetConfiguration = $this->container->get($serviceName);
$this->widgets[$widgetConfiguration->getIdentifier()] = $widgetConfiguration;
foreach ($widgetConfiguration->getGroupNames() as $groupIdentifier) {
$this->widgetsPerWidgetGroup = ArrayUtility::setValueByPath(
$this->widgetsPerWidgetGroup,
$groupIdentifier . '/' . $widgetConfiguration->getIdentifier(),
$widgetConfiguration
);
}
}
/**
* @param WidgetConfigurationInterface[] $widgets
* @return WidgetConfigurationInterface[]
*/
protected function checkPermissionOfWidgets(array $widgets): array
{
return array_filter($widgets, function ($widget, $identifier) {
return $this->getBackendUser()->check('available_widgets', $identifier)
&& (!$widget instanceof WidgetConfiguration || !$widget->isAdminOnly() || $this->getBackendUser()->isAdmin());
}, ARRAY_FILTER_USE_BOTH);
}
public function widgetItemsProcFunc(array &$parameters): void
{
foreach ($this->widgets as $widget) {
if ($widget instanceof WidgetConfiguration && $widget->isAdminOnly()) {
continue;
}
$parameters['items'][] = [
'label' => $widget->getTitle(),
'value' => $widget->getIdentifier(),
'icon' => $widget->getIconIdentifier(),
'description' => $widget->getDescription(),
];
}
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -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,
) {}
}