diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fb62b9f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Tests and developer tools +/Build/ export-ignore +/Tests/ export-ignore +/.ddev/ export-ignore + +# Git and CI metadata +/.gitattributes export-ignore +/.gitignore export-ignore +/.github/ export-ignore + +# Config, versioned but not part of the package +/.claude/ export-ignore +/CLAUDE.md export-ignore + +# Internal documents, versioned but not part of the package +/.internal/ export-ignore diff --git a/Build/phpstan-baseline.neon b/Build/phpstan-baseline.neon index b4fa326..967d5ac 100644 --- a/Build/phpstan-baseline.neon +++ b/Build/phpstan-baseline.neon @@ -1,45 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Call to method assignMultiple\\(\\) on an unknown class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Call to method hasTemplate\\(\\) on an unknown class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Call to method render\\(\\) on an unknown class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Call to method setLayoutRootPaths\\(\\) on an unknown class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Call to method setPartialRootPaths\\(\\) on an unknown class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Call to method setTemplate\\(\\) on an unknown class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Call to method setTemplateRootPaths\\(\\) on an unknown class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView not found\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" count: 1 @@ -54,12 +14,3 @@ parameters: message: "#^Parameter \\#1 \\$row of method B13\\\\Backendpreviews\\\\Service\\\\DatabaseRowService\\:\\:extendRow\\(\\) expects array, TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface given\\.$#" count: 1 path: ../Classes/Listener/PageContentPreviewRendering.php - - - - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\:\\:add\\(\\)\\.$#" - count: 1 - path: ../Classes/ViewHelpers/GetDatabaseRecordViewHelper.php - - - message: "#^Class TYPO3\\\\CMS\\\\Core\\\\Imaging\\\\IconProvider\\\\FontawesomeIconProvider not found\\.$#" - count: 2 - path: ../Classes/Listener/BootCompleted.php diff --git a/Build/phpstan-baseline12.neon b/Build/phpstan-baseline12.neon deleted file mode 100644 index c3a7f0a..0000000 --- a/Build/phpstan-baseline12.neon +++ /dev/null @@ -1,87 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Backend\\\\View\\\\PageLayoutContext\\:\\:getCurrentRequest\\(\\)\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Call to method create\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\View\\\\ViewFactoryInterface\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Call to method getPid\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Call to method getRecordType\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Class TYPO3\\\\CMS\\\\Core\\\\View\\\\ViewFactoryInterface not found\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Instantiated class TYPO3\\\\CMS\\\\Core\\\\View\\\\ViewFactoryData not found\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Parameter \\$record of method B13\\\\Backendpreviews\\\\Backend\\\\Preview\\\\ContentPreview\\:\\:render\\(\\) has invalid type TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface\\.$#" - count: 1 - path: ../Classes/Backend/Preview/ContentPreview.php - - - - message: "#^Class TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface not found\\.$#" - count: 1 - path: ../Classes/Backend/Preview/StandardContentPreviewRenderer.php - - - - message: "#^Instanceof between array and TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface will always evaluate to false\\.$#" - count: 1 - path: ../Classes/Backend/Preview/StandardContentPreviewRenderer.php - - - message: "#^Call to method get\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface\\.$#" - count: 1 - path: ../Classes/Service/DatabaseRowService.php - - - - message: "#^Call to method getMainType\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface\\.$#" - count: 2 - path: ../Classes/Service/DatabaseRowService.php - - - - message: "#^Call to method getPid\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface\\.$#" - count: 1 - path: ../Classes/Service/DatabaseRowService.php - - - - message: "#^Call to method getRawRecord\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface\\.$#" - count: 1 - path: ../Classes/Service/DatabaseRowService.php - - - - message: "#^Call to method getUid\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface\\.$#" - count: 4 - path: ../Classes/Service/DatabaseRowService.php - - - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Authentication\\\\BackendUserAuthentication\\:\\:checkRecordEditAccess\\(\\)\\.$#" - count: 1 - path: ../Classes/Service/DatabaseRowService.php - - - - message: "#^Parameter \\$record of method B13\\\\Backendpreviews\\\\Service\\\\DatabaseRowService\\:\\:getAdditionalDataForView\\(\\) has invalid type TYPO3\\\\CMS\\\\Core\\\\Domain\\\\RecordInterface\\.$#" - count: 1 - path: ../Classes/Service/DatabaseRowService.php - - - message: "#^Class TYPO3\\\\CMS\\\\Core\\\\Imaging\\\\IconProvider\\\\FontawesomeIconProvider not found\\.$#" - count: 2 - path: ../Classes/Listener/BootCompleted.php - - - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Backend\\\\View\\\\PageLayoutContext\\:\\:getReturnUrl\\(\\)\\.$#" - count: 1 - path: ../Classes/Service/DatabaseRowService.php \ No newline at end of file diff --git a/Build/phpstan-baseline13.neon b/Build/phpstan-baseline13.neon index cd5a79f..e4987ad 100644 --- a/Build/phpstan-baseline13.neon +++ b/Build/phpstan-baseline13.neon @@ -8,11 +8,3 @@ parameters: message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Authentication\\\\BackendUserAuthentication\\:\\:checkRecordEditAccess\\(\\)\\.$#" count: 1 path: ../Classes/Service/DatabaseRowService.php - - - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\:\\:add\\(\\)\\.$#" - count: 1 - path: ../Classes/ViewHelpers/GetDatabaseRecordViewHelper.php - - - message: "#^Class TYPO3\\\\CMS\\\\Core\\\\Imaging\\\\IconProvider\\\\FontawesomeIconProvider not found\\.$#" - count: 2 - path: ../Classes/Listener/BootCompleted.php diff --git a/Build/phpstan.neon b/Build/phpstan.neon index d27b0d2..34cb13a 100644 --- a/Build/phpstan.neon +++ b/Build/phpstan.neon @@ -3,6 +3,4 @@ includes: parameters: level: 5 paths: - - %currentWorkingDirectory%/Classes - excludePaths: - - %currentWorkingDirectory%/Classes/Hooks/BackendPreviewRenderer.php \ No newline at end of file + - %currentWorkingDirectory%/Classes \ No newline at end of file diff --git a/Build/phpstan12.neon b/Build/phpstan12.neon deleted file mode 100644 index 2ec6711..0000000 --- a/Build/phpstan12.neon +++ /dev/null @@ -1,8 +0,0 @@ -includes: - - phpstan-baseline12.neon -parameters: - level: 5 - paths: - - %currentWorkingDirectory%/Classes - excludePaths: - - %currentWorkingDirectory%/Classes/Hooks/BackendPreviewRenderer.php \ No newline at end of file diff --git a/Build/phpstan13.neon b/Build/phpstan13.neon index 0ebc213..27c0da6 100644 --- a/Build/phpstan13.neon +++ b/Build/phpstan13.neon @@ -3,6 +3,4 @@ includes: parameters: level: 5 paths: - - %currentWorkingDirectory%/Classes - excludePaths: - - %currentWorkingDirectory%/Classes/Hooks/BackendPreviewRenderer.php \ No newline at end of file + - %currentWorkingDirectory%/Classes \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8964747 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,111 @@ +# Changelog + +All notable changes to `b13/backendpreviews` are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +This section becomes 2.0.0 when the release is tagged. + +### Removed + +- **Support for TYPO3 v10.4, v11.5, and v12.4 is dropped**, together with PHP + below 8.2. The extension now requires TYPO3 v13.4 or v14 and PHP 8.2. If you + are on an older TYPO3, stay on the 1.5 releases—they keep working, they just + do not get new features. +- **The `tt_content_drawItem` hook is gone.** `PageLayoutViewDrawItemHookInterface` + no longer exists in the TYPO3 versions this release supports, so + `B13\Backendpreviews\Hooks\BackendPreviewRenderer` and its registration were + removed. Previews are rendered through the `previewRenderer` registered on + `tt_content` plus the `PageContentPreviewRenderingEvent` listener, which is + how it already worked on v12 and above. Only relevant if your own code + referenced that class. +- **`ext_tables.php` is gone.** It only registered the pre-v12 backend skin; + the stylesheet is registered unconditionally in `ext_localconf.php` now. +- **The Fontawesome icon registration is gone.** `exclamation-triangle` and + `exclamation-circle` were registered but never used—the shipped warning + partials use the core icons `actions-exclamation-*-alt`. + +### Changed + +- **Both render paths use `ViewFactoryInterface`.** `StandaloneView`, which is + removed in TYPO3 v14, is no longer referenced anywhere. Preview templates are + unaffected; the same templates, layouts, and partials keep rendering. +- **Metadata corrected:** the `authors` block in `composer.json` and the + `author` in `ext_emconf.php` now name b13 GmbH, with the extension's creator + credited in the README instead. The extension category changed from `fe` to + `be`, which is what it actually is. + +### Documentation + +- **README brought in line with the code.** It described a hook that no longer + exists, still listed TYPO3 v10.4 and PHP 7.4 as supported, pointed at + `settings.yaml` instead of `config.yaml` for the site set dependency, and used + a `previewRenderer` example (`textpic`) that has not applied for several major + versions—`fluid_styled_content` registers none at all on v13 and v14. +- **A new "What Your Template Gets" section** spells out that v14 assigns a + single `{record}` object while v13 assigns the row's fields individually, and + which variables this extension adds on each version. Templates that read + element fields directly need both spellings to work on v13 and v14; see + [Breaking-92434](https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/14.0/Breaking-92434-UseRecordAPIInPageModulePreviewRendering.html) + for the core migration path. +- **`SECURITY.md` added** with the reporting route and a note on what a preview + template can expose—in particular that `b13:getDatabaseRecord` reads whatever + table it is pointed at without applying backend user permissions. +- **`CHANGELOG.md` added**, which is this file. + +## [1.5.3] — 2026-08-26 + +### Added + +- **Non-image media files get a fallback icon** in the preview instead of a + broken thumbnail: video, audio, and everything else render the matching + mimetype icon. + +### Documentation + +- **README completed** with a requirements section, the site set as an + alternative to importing the PageTsConfig, and documentation for the three + shipped ViewHelpers (`b13:renderBodytext`, `b13:getDatabaseRecord`, + `b13:explodeList`). + +## [1.5.2] — 2026-04-24 + +### Added + +- **Contextual edit URL on TYPO3 v14.** Previews link through + `record_edit_contextual`, so clicking one opens the element in the page + module's own editing context instead of navigating away. + +### Fixed + +- Compatibility with TYPO3 14.3 and a v14 deprecation. + +## [1.5.1] — 2026-02-26 + +### Fixed + +- **The icon registration no longer runs in `ext_localconf.php`.** Using + `IconRegistry` that early broke on some installations. + +## [1.5.0] — 2026-02-13 + +### Added + +- **TYPO3 v14 and Fluid 5 support**, alongside the existing versions. + +### Changed + +- **List group items are separated more clearly** in the preview markup. + +### Fixed + +- Several fixes to the shipped partials, the `Link` partial on v14, and + dependency injection for `DatabaseRowService`. + +## [1.4.3] and earlier + +Released between 2021 and 2024, covering the TYPO3 v10 to v13 era. See the +[README](README.md) for what the extension does and how it is configured. diff --git a/Classes/Backend/Preview/ContentPreview.php b/Classes/Backend/Preview/ContentPreview.php index 2468ab1..7723444 100644 --- a/Classes/Backend/Preview/ContentPreview.php +++ b/Classes/Backend/Preview/ContentPreview.php @@ -19,43 +19,23 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\View\ViewFactoryData; use TYPO3\CMS\Core\View\ViewFactoryInterface; -use TYPO3\CMS\Fluid\View\StandaloneView; +use TYPO3\CMS\Core\View\ViewInterface; use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException; class ContentPreview { public function render(RecordInterface $record, PageLayoutContext $context): ?string { - $previewConfiguration = BackendUtility::getPagesTSconfig($record->getPid())['mod.']['web_layout.']['tt_content.']['preview.'] ?? []; - if (!$previewConfiguration) { - // Early return in case no preview configuration can be found + $previewConfiguration = $this->getPreviewConfiguration((int)$record->getPid()); + if ($previewConfiguration === null) { return null; } - $fluidConfiguration = $previewConfiguration['view.'] ?? []; - if (!$fluidConfiguration) { - // Early return in case no fluid template configuration can be found - return null; - } - - $templateConfiguration = $previewConfiguration['template.'] ?? []; $cType = $record->getRecordType(); - if (!empty($templateConfiguration[$cType])) { - $fluidTemplateName = $templateConfiguration[$cType]; - } else { - $fluidTemplateName = $cType; - } - $viewFactory = GeneralUtility::makeInstance(ViewFactoryInterface::class); - $view = $viewFactory->create( - new ViewFactoryData( - $fluidConfiguration['templateRootPaths.'] ?? null, - $fluidConfiguration['partialRootPaths.'] ?? null, - $fluidConfiguration['layoutRootPaths.'] ?? null, - null, - $context->getCurrentRequest() - ) - ); + $templateConfiguration = $previewConfiguration['template.'] ?? []; + $fluidTemplateName = !empty($templateConfiguration[$cType]) ? $templateConfiguration[$cType] : $cType; + $view = $this->createView($previewConfiguration['view.'], $context); $data = GeneralUtility::makeInstance(DatabaseRowService::class)->getAdditionalDataForView($record, $context); $view->assignMultiple($data); $view->assign('record', $record); @@ -66,17 +46,16 @@ public function render(RecordInterface $record, PageLayoutContext $context): ?st return null; } - public function renderLegacy(array $row): ?string + /** + * Render path for TYPO3 v13, where the page module still passes the content element as an array + * instead of a RecordInterface (see Breaking-92434). + * + * @param array $row + */ + public function renderLegacy(array $row, PageLayoutContext $context): ?string { - $previewConfiguration = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? []; - if (!$previewConfiguration) { - // Early return in case no preview configuration can be found - return null; - } - - $fluidConfiguration = $previewConfiguration['view.'] ?? []; - if (!$fluidConfiguration) { - // Early return in case no fluid template configuration can be found + $previewConfiguration = $this->getPreviewConfiguration((int)$row['pid']); + if ($previewConfiguration === null) { return null; } @@ -89,16 +68,42 @@ public function renderLegacy(array $row): ?string $fluidTemplateName = $row['CType']; } - $view = GeneralUtility::makeInstance(StandaloneView::class); - $view->setLayoutRootPaths($fluidConfiguration['layoutRootPaths.'] ?? []); - $view->setPartialRootPaths($fluidConfiguration['partialRootPaths.'] ?? []); - $view->setTemplateRootPaths($fluidConfiguration['templateRootPaths.'] ?? []); - $view->setTemplate($fluidTemplateName); + $view = $this->createView($previewConfiguration['view.'], $context); $view->assignMultiple($row); - - if ($view->hasTemplate()) { - return $view->render(); + try { + return $view->render($fluidTemplateName); + } catch (InvalidTemplateResourceException) { } return null; } + + /** + * @return array|null + */ + protected function getPreviewConfiguration(int $pid): ?array + { + $previewConfiguration = BackendUtility::getPagesTSconfig($pid)['mod.']['web_layout.']['tt_content.']['preview.'] ?? []; + // Early return in case no preview (or fluid template) configuration can be found + if (!$previewConfiguration || empty($previewConfiguration['view.'])) { + return null; + } + return $previewConfiguration; + } + + /** + * @param array $fluidConfiguration + */ + protected function createView(array $fluidConfiguration, PageLayoutContext $context): ViewInterface + { + $viewFactory = GeneralUtility::makeInstance(ViewFactoryInterface::class); + return $viewFactory->create( + new ViewFactoryData( + $fluidConfiguration['templateRootPaths.'] ?? null, + $fluidConfiguration['partialRootPaths.'] ?? null, + $fluidConfiguration['layoutRootPaths.'] ?? null, + null, + $context->getCurrentRequest() + ) + ); + } } diff --git a/Classes/Backend/Preview/StandardContentPreviewRenderer.php b/Classes/Backend/Preview/StandardContentPreviewRenderer.php index 0d37168..09ec9d2 100644 --- a/Classes/Backend/Preview/StandardContentPreviewRenderer.php +++ b/Classes/Backend/Preview/StandardContentPreviewRenderer.php @@ -36,7 +36,7 @@ public function renderPageModulePreviewContent(GridColumnItem $item): string if ($record instanceof RecordInterface) { $content = $contentPreview->render($record, $context); } else { - $content = $contentPreview->renderLegacy($record); + $content = $contentPreview->renderLegacy($record, $context); } if ($content !== null) { return $content; diff --git a/Classes/Hooks/BackendPreviewRenderer.php b/Classes/Hooks/BackendPreviewRenderer.php deleted file mode 100644 index c13e9e2..0000000 --- a/Classes/Hooks/BackendPreviewRenderer.php +++ /dev/null @@ -1,62 +0,0 @@ -databaseRowService = $databaseRowService; - } - - /** - * Preprocesses the preview rendering of a content element of any type - * - * @param \TYPO3\CMS\Backend\View\PageLayoutView $parentObject Calling parent object - * @param bool $drawItem Whether to draw the item using the default functionality - * @param string $headerContent Header content - * @param string $itemContent Item content - * @param array $row Record row of tt_content - */ - public function preProcess( - PageLayoutView &$parentObject, - &$drawItem, - &$headerContent, - &$itemContent, - array &$row - ) { - $row = $this->databaseRowService->extendRow($row); - if ((GeneralUtility::makeInstance(Features::class))->isFeatureEnabled('fluidBasedPageModule') === false) { - $contentPreview = GeneralUtility::makeInstance(ContentPreview::class); - $content = $contentPreview->renderLegacy($row); - if ($content !== null) { - $itemContent .= $content; - $drawItem = false; - } - } - } -} diff --git a/Classes/Listener/BootCompleted.php b/Classes/Listener/BootCompleted.php deleted file mode 100644 index 4d797c9..0000000 --- a/Classes/Listener/BootCompleted.php +++ /dev/null @@ -1,45 +0,0 @@ -packageManager->isPackageActive('fontawesome_provider')) { - return; - } - $this->iconRegistry->registerIcon( - 'exclamation-triangle', - \TYPO3\CMS\Core\Imaging\IconProvider\FontawesomeIconProvider::class, - [ - 'name' => 'exclamation-triangle', - ] - ); - $this->iconRegistry->registerIcon( - 'exclamation-circle', - \TYPO3\CMS\Core\Imaging\IconProvider\FontawesomeIconProvider::class, - [ - 'name' => 'exclamation-circle', - ] - ); - } -} diff --git a/Classes/Service/DatabaseRowService.php b/Classes/Service/DatabaseRowService.php index 1e5c0bf..4da1f90 100644 --- a/Classes/Service/DatabaseRowService.php +++ b/Classes/Service/DatabaseRowService.php @@ -19,7 +19,6 @@ use TYPO3\CMS\Core\Domain\RecordInterface; use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Core\Localization\LanguageService; -use TYPO3\CMS\Core\Resource\FileReference; use TYPO3\CMS\Core\Resource\FileRepository; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -45,7 +44,7 @@ public function getAdditionalDataForView(RecordInterface $record, ?PageLayoutCon if ($context === null) { trigger_error('not passing context is deprecated', E_USER_DEPRECATED); } - if ($context === null || (new Typo3Version())->getMajorVersion() < 13) { + if ($context === null) { $returnUrl = GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $record->getUid(); } else { $returnUrl = $context->getReturnUrl() . '#element-tt_content-' . $record->getUid(); @@ -120,38 +119,17 @@ public function extendRow(array $row): array $row['pi_flexform_transformed'] = $flexFormService->convertFlexFormContentToArray($row['pi_flexform']); } - // return all sys_file_reference rows - // automatic resolving of all type=file, only works with TYPO3 v12+ - $foundOne = false; + // return all sys_file_reference rows by automatically resolving all type=file fields foreach ($GLOBALS['TCA']['tt_content']['columns'] as $fieldName => $fieldConfig) { - if ($fieldConfig['config']['type'] === 'file') { - $variableName = GeneralUtility::underscoredToUpperCamelCase($fieldName); - if ($fieldName === 'image') { - $variableName = 'Images'; - } - if ($row[$fieldName] ?? false) { - $row['all' . $variableName] = $this->fileRepository->findByRelation('tt_content', $fieldName, $row['uid']); - $row['all' . $variableName . '-visible'] = $this->countVisibleFileReferences($row['all' . $variableName]); - } - $foundOne = true; - } - } - if (!$foundOne) { - if ($row['assets'] ?? false) { - $row['allAssets'] = $this->fileRepository->findByRelation('tt_content', 'assets', $row['uid']); - $row['allAssets-visible'] = $this->countVisibleFileReferences($row['allAssets']); + if (($fieldConfig['config']['type'] ?? '') !== 'file') { + continue; } - if ($row['assets2'] ?? false) { - $row['allAssets2'] = $this->fileRepository->findByRelation('tt_content', 'assets2', $row['uid']); - $row['allAssets2-visible'] = $this->countVisibleFileReferences($row['allAssets2']); + $variableName = GeneralUtility::underscoredToUpperCamelCase($fieldName); + if ($fieldName === 'image') { + $variableName = 'Images'; } - if ($row['media'] ?? false) { - $row['allMedia'] = $this->fileRepository->findByRelation('tt_content', 'media', $row['uid']); - $row['allMedia-visible'] = $this->countVisibleFileReferences($row['allMedia']); - } - if ($row['image'] ?? false) { - $row['allImages'] = $this->fileRepository->findByRelation('tt_content', 'image', $row['uid']); - $row['allImages-visible'] = $this->countVisibleFileReferences($row['allImages']); + if ($row[$fieldName] ?? false) { + $row['all' . $variableName] = $this->fileRepository->findByRelation('tt_content', $fieldName, $row['uid']); } } return $row; @@ -166,16 +144,4 @@ protected function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } - - protected function countVisibleFileReferences(array $references): int - { - $cnt = 0; - /** @var FileReference $reference */ - foreach ($references as $reference) { - if ((int)$reference->getProperty('hidden') === 0) { - $cnt++; - } - } - return $cnt; - } } diff --git a/Classes/ViewHelpers/GetDatabaseRecordViewHelper.php b/Classes/ViewHelpers/GetDatabaseRecordViewHelper.php index ad80011..ef115e4 100644 --- a/Classes/ViewHelpers/GetDatabaseRecordViewHelper.php +++ b/Classes/ViewHelpers/GetDatabaseRecordViewHelper.php @@ -17,7 +17,6 @@ use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction; -use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; @@ -73,11 +72,12 @@ public function render(): array ->where( $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY)) ); - if ((new Typo3Version())->getMajorVersion() > 12) { - $queryBuilder->getConcreteQueryBuilder()->addOrderBy('FIELD(uid,' . implode(',', $uids) . ')'); - } else { - $queryBuilder->add('orderBy', 'FIELD(uid,' . implode(',', $uids) . ')'); + $orderByCase = 'CASE ' . $queryBuilder->quoteIdentifier('uid'); + foreach (array_values($uids) as $position => $uid) { + $orderByCase .= ' WHEN ' . $uid . ' THEN ' . $position; } + $orderByCase .= ' END'; + $queryBuilder->getConcreteQueryBuilder()->addOrderBy($orderByCase); return $queryBuilder ->executeQuery() ->fetchAllAssociative(); diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 6d456a7..2ed931b 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -6,8 +6,6 @@ services: B13\Backendpreviews\: resource: '../Classes/*' - B13\Backendpreviews\Hooks\BackendPreviewRenderer: - public: true B13\Backendpreviews\Service\DatabaseRowService: public: true B13\Backendpreviews\Listener\PageContentPreviewRendering: @@ -15,7 +13,3 @@ services: - name: event.listener event: TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent identifier: 'b13-backendpreview-page-content-preview-rendering' - B13\Backendpreviews\Listener\BootCompleted: - tags: - - name: event.listener - identifier: 'b13-backendpreview-boot-completed' diff --git a/README.md b/README.md index c70039b..90e1b87 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,38 @@ # Nice Backend Previews for Content Elements in TYPO3 -## About this extension +## About This Extension -This extension adds a hook for rendering content element previews for TYPO3's backend view in the page module, -adding the ability to use Fluid Partials and Layouts to enable consistent preview markup. +This extension takes over the preview rendering for content elements in TYPO3's page module, +adding the ability to use Fluid templates, layouts, and partials to enable consistent preview markup. ## Requirements -* TYPO3 v10.4, v11.5, v12.4, v13.4 or v14 -* PHP 7.4 or higher +* TYPO3 v13.4 or v14 +* PHP 8.2 or higher + +Version 2.0 dropped support for TYPO3 v10.4, v11.5, and v12.4 together with the compatibility layer +for those versions. If you are still on one of them, stay on the 1.5 releases. + +Release notes live in [CHANGELOG.md](CHANGELOG.md). ## Installation -Use composer to add this content element to your project +Use composer to add this extension to your project `composer require b13/backendpreviews` -and install the extension using the Extension Manager in your TYPO3 backend. +and set it up with `vendor/bin/typo3 extension:setup`. -## Add configuration +## Add Configuration -Add this to your PageTsConfig to include the default Fluid Templates provided with this extension: +Add this to your PageTsConfig to include the default Fluid templates provided with this extension: ``` @import 'EXT:backendpreviews/Configuration/PageTs/PageTs.tsconfig' ``` -On TYPO3 v13 and v14 you can alternatively include the shipped site set `b13/backendpreviews` -as a dependency of your own site set (`Configuration/Sets//settings.yaml`) instead of -importing the PageTsConfig manually: +Alternatively, include the shipped site set `b13/backendpreviews` as a dependency of your own site +set (`Configuration/Sets//config.yaml`) instead of importing the PageTsConfig manually: ```yaml dependencies: @@ -55,25 +59,67 @@ You can set a different templateName explicitly like this: mod.web_layout.tt_content.preview.template.mytype = Myowntemplate ``` -For plugins a template name for a specific plugin can be specified like this: +## What Your Template Gets -``` -mod.web_layout.tt_content.preview.template.list.mylist_type = Listtypetemplate -``` +TYPO3 v14 hands the content element to a preview renderer as a record object, while v13 still passes +a plain array (see +[Breaking-92434](https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/14.0/Breaking-92434-UseRecordAPIInPageModulePreviewRendering.html), +which is also the migration guide for your own templates). The variables in a preview template +therefore differ between the two versions. -All flexform data of the plugin are available in `{pi_flexform_transformed}` to create meaningful previews: +On **v14**, everything from the element itself hangs off a single `{record}`: +```html +

{record.header}

+

{record.bodytext}

+Image UID: {record.image.uid} +{record.pi_flexform.sheets.s_messages.settings.welcome_header} ``` -Page: {pi_flexform_transformed.settings.page} -``` -## Use custom backend previews for default CTypes +Relations are already resolved on that object, and flexform values come as a `FlexFormFieldValues` +object grouped by sheet. + +On **v13**, the fields of the `tt_content` row are assigned individually, so the same template reads +`{header}`, `{bodytext}`, and `{image}`. This extension adds three things that only exist on that +version, because v14 covers them through the record: + +* `{pi_flexform_transformed}` – all flexform data of the plugin as a flat array: + + ``` + Page: {pi_flexform_transformed.settings.page} + ``` + +* `{allImages}`, and `{all}` for every other TCA field of type `file` – the file + references of that field, which is what the shipped `Images` partial expects. +* `{list_type-label}`, plus a template name per plugin for elements with CType `list`: + + ``` + mod.web_layout.tt_content.preview.template.list.mylist_type = Listtypetemplate + ``` + + There is no v14 counterpart—`list_type` is gone from the core. + +On **both** versions this extension assigns: -Default CTypes for `fluid_styled_content` define dedicated `previewRenderer` classes. If you want to use `EXT:backendpreviews` instead, -remove the configuration for each of these CTypes in your extension's `ext_localconf.php`: +* `{editLink.url}` and `{editLink.title}` – the edit link for the element, if the user may edit it. + On v14 there is also `{editLink.contextual}`, which the shipped layouts use to render a + `typo3-backend-contextual-record-edit-trigger`. +* `{CType-label}` – the resolved label of the element's CType. + +If your templates build on the shipped layouts and partials, the version switch is handled there. +A template that reads element fields directly needs both spellings to work on v13 and v14. + +## Use Custom Backend Previews for Default CTypes + +This extension registers its preview renderer for the whole `tt_content` table. A renderer that is +registered for a single type wins over that, so an element whose type brings its own +`previewRenderer` is not rendered by this extension. The CTypes of `fluid_styled_content` no longer +register one, but other extensions still do—`EXT:form`, for instance, for `form_formframework`. +To use `EXT:backendpreviews` for such a type, remove that configuration in your own extension's +`Configuration/TCA/Overrides/tt_content.php`: ``` -unset($GLOBALS['TCA']['tt_content']['types']['textpic']['previewRenderer']); +unset($GLOBALS['TCA']['tt_content']['types']['form_formframework']['previewRenderer']); ``` ## ViewHelpers @@ -103,15 +149,22 @@ namespace `B13\Backendpreviews\ViewHelpers`. Register them in your template like * `b13:explodeList` – splits a list value into an array you can iterate over with `f:for`, either by a character (`splitChar`, default `,`) or by newlines (`splitNL`). +## Security + +Please report security issues to [security@b13.com](mailto:security@b13.com). See +[SECURITY.md](SECURITY.md) for the reporting process and what to expect. + ## License -As TYPO3 Core, _backendpreviews_ is licensed under GPL2 or later. See the LICENSE file for more details. +As TYPO3 Core, _backendpreviews_ is licensed under GPL-2.0-or-later. See the [LICENSE](LICENSE) file +for more details. -## Background, Authors & Further Maintenance +## Credits -`EXT:backendpreviews` was initially created by David Steeb in 2021 for [b13, Stuttgart](https://b13.com). We use this as -a basis to add consistent previews for our custom content element types. +`EXT:backendpreviews` was created by David Steeb and is maintained by [b13 GmbH](https://b13.com), +Stuttgart, Germany. We use it as a basis to add consistent previews for our custom content element +types. -[Find more TYPO3 extensions we have developed](https://b13.com/useful-typo3-extensions-from-b13-to-you) that help us -deliver value in client projects. As part of the way we work, we focus on testing and best practices to ensure long-term -performance, reliability, and results in all our code. +[Find more TYPO3 extensions we have developed](https://b13.com/useful-typo3-extensions-from-b13-to-you?utm_source=backendpreviews&utm_medium=readme) +that help us deliver value in client projects. As part of our work, we focus on testing and best +practices to ensure long-term performance, reliability, and results in all our code. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f3f53a4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,64 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security issues to **[security@b13.com](mailto:security@b13.com)**, +not through the public issue tracker. + +Helpful in a report: + +- The extension version and the TYPO3 version +- What an attacker can do, and what access they need to do it +- Steps to reproduce, or a proof of concept +- Whether the issue is already public anywhere + +If you would like to encrypt your report, ask us for a key first. + +## What to Expect + +We read every report and reply to it. You will hear back from us with an +assessment of the finding, and we agree a disclosure date with you before +anything is published. + +We do not publish fixed response times. We would rather answer you quickly than +name a deadline we might miss—if a date matters for your own disclosure +process, say so in your report and we will agree one with you. + +We publish a fix before we describe the issue. Advisories appear next to the +[changelog](CHANGELOG.md) once a fixed version is available, and we credit +reporters who want to be credited. + +## Supported Versions + +Security fixes go into the current minor release of each supported major +version. There are no backports to majors that have reached their end of +support. + +The extension's own support window follows the TYPO3 versions it declares in +`composer.json`. See the [changelog](CHANGELOG.md) for the version history. + +## Data This Extension Handles + +Worth knowing when you assess the impact of a finding: + +- **The extension holds no data of its own.** It has no database tables, no + configuration of its own, and writes nothing. It renders content that is + already in `tt_content`. +- **It renders in the backend only**, inside the page module. Nothing it + produces is reachable from the frontend. +- **Preview templates decide what is shown.** The templates shipped here render + fields of the content element that the editor is already allowed to see in + that module. A template added in a project can widen that—the + `b13:getDatabaseRecord` ViewHelper reads any table it is pointed at, without + applying backend user permissions to the result. Treat a preview template as + code, and do not use it to surface records an editor may not see otherwise. +- **The edit link is permission-checked.** It is only assigned when the backend + user may edit the record. + +## Scope + +In scope: the code in this repository. + +Out of scope: vulnerabilities in TYPO3 core (report those to the +[TYPO3 Security Team](https://typo3.org/help/security-advisories)) or in a +third-party extension. diff --git a/composer.json b/composer.json index ecf9e78..9783560 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "typo3/cms": { "web-dir": ".Build/Web", "extension-key": "backendpreviews", - "version": "1.5.3", + "version": "2.0.0", "Package": { "providesPackages": {} } @@ -15,9 +15,21 @@ "license": [ "GPL-2.0-or-later" ], + "authors": [ + { + "name": "b13 GmbH", + "email": "typo3@b13.com", + "homepage": "https://b13.com" + } + ], + "homepage": "https://b13.com", + "support": { + "issues": "https://github.com/b13/backendpreviews/issues", + "source": "https://github.com/b13/backendpreviews" + }, "require": { - "php": "^7.4 || ~8.0", - "typo3/cms-backend": "^10.4 || ^11.5 || ^12.4 || ^13.4 || ^14.3" + "php": "^8.2", + "typo3/cms-backend": "^13.4 || ^14.3" }, "require-dev": { "phpstan/phpstan": "^1.10", diff --git a/ext_emconf.php b/ext_emconf.php index f980c1e..997b637 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -3,16 +3,16 @@ $EM_CONF[$_EXTKEY] = [ 'title' => 'Enhanced Fluid based backend element previews', 'description' => 'Adds full Fluid Templates/Layouts/Partials to backend element previews to enable consistent use of partials.', - 'category' => 'fe', - 'author' => 'David Steeb', + 'category' => 'be', + 'author' => 'b13 GmbH', 'author_email' => 'typo3@b13.com', 'state' => 'stable', 'clearCacheOnLoad' => true, 'author_company' => 'b13 GmbH, Stuttgart', - 'version' => '1.5.3', + 'version' => '2.0.0', 'constraints' => [ 'depends' => [ - 'typo3' => '10.4.0-14.99.99', + 'typo3' => '13.4.0-14.99.99', ], ], ]; diff --git a/ext_localconf.php b/ext_localconf.php index e25159a..8aa9adc 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -2,32 +2,4 @@ defined('TYPO3') or die(); -if ((\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Information\Typo3Version::class))->getMajorVersion() < 12) { - // extends database row and render Backend-Preview if fluidBasedPageModule is not used - // (if fluidBasedPageModule is used "$GLOBALS['TCA']['tt_content']['ctrl']['previewRenderer']" takes effect for Backend-Preview-Rendering) - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']['tx_backendpreviews'] = - \B13\Backendpreviews\Hooks\BackendPreviewRenderer::class; -} -// if TYPO3 > 11 "$GLOBALS['TCA']['tt_content']['ctrl']['previewRenderer']" is always used for Backend-Preview-Rendering -// extending the database row is handled by B13\Backendpreviews\Listener\PageContentPreviewRendering - -if ((new \TYPO3\CMS\Core\Information\Typo3Version())->getMajorVersion() < 12) { - $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class); - $iconRegistry->registerIcon( - 'exclamation-triangle', - \TYPO3\CMS\Core\Imaging\IconProvider\FontawesomeIconProvider::class, - [ - 'name' => 'exclamation-triangle', - ] - ); - $iconRegistry->registerIcon( - 'exclamation-circle', - \TYPO3\CMS\Core\Imaging\IconProvider\FontawesomeIconProvider::class, - [ - 'name' => 'exclamation-circle', - ] - ); -} -if ((\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Information\Typo3Version::class))->getMajorVersion() >= 12) { - $GLOBALS['TYPO3_CONF_VARS']['BE']['stylesheets']['b13/backendpreviews'] = 'EXT:backendpreviews/Resources/Public/Backend/Css/Skin/'; -} +$GLOBALS['TYPO3_CONF_VARS']['BE']['stylesheets']['b13/backendpreviews'] = 'EXT:backendpreviews/Resources/Public/Backend/Css/Skin/'; diff --git a/ext_tables.php b/ext_tables.php deleted file mode 100644 index 0983654..0000000 --- a/ext_tables.php +++ /dev/null @@ -1,12 +0,0 @@ -getMajorVersion() < 12) { - $GLOBALS['TBE_STYLES']['skins']['backendpreviews']['name'] = 'backendpreviews'; - $GLOBALS['TBE_STYLES']['skins']['backendpreviews']['stylesheetDirectories'] = [ - 'EXT:backendpreviews/Resources/Public/Backend/Css/Skin/', - ]; - } -})();