Skip to content

Commit 61c1c4c

Browse files
committed
[FEATURE] ce restriction datahandler hook
1 parent af683b0 commit 61c1c4c

File tree

11 files changed

+240
-8
lines changed

11 files changed

+240
-8
lines changed

Build/phpstan11-7.4.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ parameters:
1818
- %currentWorkingDirectory%/Classes/Listener/PageContentPreviewRendering.php
1919
- %currentWorkingDirectory%/Classes/Domain/Core/RecordWithRenderedGrid.php
2020
- %currentWorkingDirectory%/Classes/Listener/ManipulateBackendLayoutColPosConfigurationForPage.php
21+
- %currentWorkingDirectory%/Classes/Hooks/Datahandler/ContentElementRestriction
2122

Build/phpstan11.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ parameters:
1818
- %currentWorkingDirectory%/Classes/Listener/PageContentPreviewRendering.php
1919
- %currentWorkingDirectory%/Classes/Domain/Core/RecordWithRenderedGrid.php
2020
- %currentWorkingDirectory%/Classes/Listener/ManipulateBackendLayoutColPosConfigurationForPage.php
21+
- %currentWorkingDirectory%/Classes/Hooks/Datahandler/ContentElementRestriction
2122

Build/phpstan12.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ parameters:
1515
- %currentWorkingDirectory%/Classes/Domain/Core/RecordWithRenderedGrid.php
1616
- %currentWorkingDirectory%/Classes/Listener/PageContentPreviewRendering.php
1717
- %currentWorkingDirectory%/Classes/Listener/ManipulateBackendLayoutColPosConfigurationForPage.php
18+
- %currentWorkingDirectory%/Classes/Hooks/Datahandler/ContentElementRestriction
1819

1920

Build/phpstan13.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ parameters:
1616
- %currentWorkingDirectory%/Classes/Listener/LegacyPageTsConfig.php
1717
- %currentWorkingDirectory%/Classes/Listener/PageContentPreviewRendering.php
1818
- %currentWorkingDirectory%/Classes/Listener/ManipulateBackendLayoutColPosConfigurationForPage.php
19+
- %currentWorkingDirectory%/Classes/Hooks/Datahandler/ContentElementRestriction
1920

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace B13\Container\Hooks\Datahandler\ContentElementRestriction;
6+
7+
/*
8+
* This file is part of TYPO3 CMS-based extension "container" by b13.
9+
*
10+
* It is free software; you can redistribute it and/or modify it under
11+
* the terms of the GNU General Public License, either version 2
12+
* of the License, or any later version.
13+
*/
14+
15+
use B13\Container\Domain\Factory\ContainerFactory;
16+
use B13\Container\Domain\Factory\Exception;
17+
use B13\Container\Hooks\Datahandler\DatahandlerProcess;
18+
use B13\Container\Tca\Registry;
19+
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
20+
use TYPO3\CMS\Backend\Utility\BackendUtility;
21+
use TYPO3\CMS\Backend\View\BackendLayoutView;
22+
use TYPO3\CMS\Core\DataHandling\DataHandler;
23+
use TYPO3\CMS\Core\Utility\GeneralUtility;
24+
use TYPO3\CMS\Core\Utility\MathUtility;
25+
26+
#[Autoconfigure(public: true)]
27+
class DataHandlerHook
28+
{
29+
protected $lockDatamapHook = false;
30+
31+
public function __construct(
32+
private BackendLayoutView $backendLayoutView,
33+
private DatahandlerProcess $datahandlerProcess,
34+
private ContainerFactory $containerFactory,
35+
private Registry $registry
36+
) {
37+
}
38+
39+
public function processCmdmap_beforeStart(DataHandler $dataHandler): void
40+
{
41+
$cmdmap = $dataHandler->cmdmap;
42+
if (empty($cmdmap['tt_content']) || $dataHandler->bypassAccessCheckForRecords) {
43+
return;
44+
}
45+
$this->lockDatamapHook = true;
46+
foreach ($cmdmap['tt_content'] as $id => $incomingFieldArray) {
47+
foreach ($incomingFieldArray as $command => $value) {
48+
if (!in_array($command, ['copy', 'move'], true)) {
49+
continue;
50+
}
51+
$currentRecord = BackendUtility::getRecord('tt_content', $id);
52+
if (empty($currentRecord['CType'] ?? '')) {
53+
continue;
54+
}
55+
// EXT:container start
56+
if ((int)($currentRecord['tx_container_parent'] ?? 0) > 0 && $this->datahandlerProcess->isContainerInProcess((int)($currentRecord['tx_container_parent']))) {
57+
continue;
58+
}
59+
// EXT:container end
60+
if (is_array($value) && !empty($value['action']) && $value['action'] === 'paste' && isset($value['update']['colPos'])) {
61+
// Moving / pasting to a new colPos on a potentially different page
62+
$pageId = (int)$value['target'];
63+
$colPos = (int)$value['update']['colPos'];
64+
} else {
65+
$pageId = (int)$value;
66+
$colPos = (int)$currentRecord['colPos'];
67+
}
68+
if ($pageId < 0) {
69+
$targetRecord = BackendUtility::getRecord('tt_content', abs($pageId));
70+
$pageId = (int)$targetRecord['pid'];
71+
$colPos = (int)$targetRecord['colPos'];
72+
}
73+
// EXT:container start
74+
if (
75+
(!empty($value['update'])) &&
76+
isset($value['update']['colPos']) &&
77+
$value['update']['colPos'] > 0 &&
78+
isset($value['update']['tx_container_parent']) &&
79+
$value['update']['tx_container_parent'] > 0 &&
80+
MathUtility::canBeInterpretedAsInteger($id)
81+
) {
82+
if ($this->checkContainerCType((int)$value['update']['tx_container_parent'], $currentRecord['CType'], (int)$value['update']['colPos']) === false) {
83+
// Not allowed to move or copy to target. Unset this command and create a log entry which may be turned into a notification when called by BE.
84+
unset($dataHandler->cmdmap['tt_content'][$id]);
85+
$dataHandler->log('tt_content', $id, 1, null, 1, 'The command "%s" for record "tt_content:%s" with CType "%s" to colPos "%s" couldn\'t be executed due to disallowed value(s).', null, [$command, $id, $currentRecord['CType'], $colPos]);
86+
}
87+
$useChildId = null;
88+
if ($command === 'move') {
89+
$useChildId = $id;
90+
}
91+
if ($this->checkContainerMaxItems((int)$value['update']['tx_container_parent'], (int)$value['update']['colPos'], $useChildId)) {
92+
//unset($dataHandler->cmdmap['tt_content'][$id]);
93+
//$dataHandler->log('tt_content', $id, 1, null, 1, 'The command "%s" for record "tt_content:%s" to colPos "%s" couldn\'t be executed due to maxitems reached.', null, [$command, $id, $colPos]);
94+
}
95+
return;
96+
}
97+
// EXT:container end
98+
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageId);
99+
$columnConfiguration = $this->backendLayoutView->getColPosConfigurationForPage($backendLayout, $colPos, $pageId);
100+
$allowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'] ?? '', true);
101+
$disallowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'] ?? '', true);
102+
if ((!empty($allowedContentElementsInTargetColPos) && !in_array($currentRecord['CType'], $allowedContentElementsInTargetColPos, true))
103+
|| (!empty($disallowedContentElementsInTargetColPos) && in_array($currentRecord['CType'], $disallowedContentElementsInTargetColPos, true))
104+
) {
105+
// Not allowed to move or copy to target. Unset this command and create a log entry which may be turned into a notification when called by BE.
106+
unset($dataHandler->cmdmap['tt_content'][$id]);
107+
$dataHandler->log('tt_content', $id, 1, null, 1, 'The command "%s" for record "tt_content:%s" with CType "%s" to colPos "%s" couldn\'t be executed due to disallowed value(s).', null, [$command, $id, $currentRecord['CType'], $colPos]);
108+
}
109+
}
110+
}
111+
}
112+
113+
protected function checkContainerMaxItems(int $containerId, int $colPos, ?int $childUid = null): bool
114+
{
115+
try {
116+
$container = $this->containerFactory->buildContainer($containerId);
117+
$columnConfiguration = $this->registry->getContentDefenderConfiguration($container->getCType(), $colPos);
118+
if (($columnConfiguration['maxitems'] ?? 0) === 0) {
119+
return true;
120+
}
121+
$childrenOfColumn = $container->getChildrenByColPos($colPos);
122+
$count = count($childrenOfColumn);
123+
if ($childUid !== null && $container->hasChildInColPos($colPos, $childUid)) {
124+
$count--;
125+
}
126+
return $count >= $columnConfiguration['maxitems'];
127+
} catch (Exception) {
128+
// not a container;
129+
}
130+
return true;
131+
}
132+
133+
protected function checkContainerCType(int $containerId, string $cType, int $colPos): bool
134+
{
135+
try {
136+
$container = $this->containerFactory->buildContainer($containerId);
137+
$columnConfiguration = $this->registry->getContentDefenderConfiguration($container->getCType(), $colPos);
138+
$allowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'] ?? '', true);
139+
$disallowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'] ?? '', true);
140+
if ((!empty($allowedContentElementsInTargetColPos) && !in_array($cType, $allowedContentElementsInTargetColPos, true))
141+
|| (!empty($disallowedContentElementsInTargetColPos) && in_array($cType, $disallowedContentElementsInTargetColPos, true))
142+
) {
143+
return false;
144+
}
145+
} catch (Exception) {
146+
// not a container;
147+
}
148+
return true;
149+
}
150+
151+
public function processCmdmap_postProcess(string $command, string $table, $id, $value, DataHandler $dataHandler, $pasteUpdate, $pasteDatamap): void
152+
{
153+
$this->lockDatamapHook = false;
154+
}
155+
156+
public function processDatamap_beforeStart(DataHandler $dataHandler): void
157+
{
158+
if ($this->lockDatamapHook === true) {
159+
return;
160+
}
161+
$datamap = $dataHandler->datamap;
162+
if (empty($datamap['tt_content']) || $dataHandler->bypassAccessCheckForRecords) {
163+
return;
164+
}
165+
foreach ($datamap['tt_content'] as $id => $incomingFieldArray) {
166+
if (MathUtility::canBeInterpretedAsInteger($id)) {
167+
$record = BackendUtility::getRecord('tt_content', $id);
168+
if (!is_array($record)) {
169+
// Skip this if the record could not be determined for whatever reason
170+
continue;
171+
}
172+
$recordData = array_merge($record, $incomingFieldArray);
173+
} else {
174+
$recordData = array_merge($dataHandler->defaultValues['tt_content'] ?? [], $incomingFieldArray);
175+
}
176+
if (empty($recordData['CType']) || !array_key_exists('colPos', $recordData)) {
177+
// No idea what happened here, but we stop with this record if there is no CType or colPos
178+
continue;
179+
}
180+
// EXT:container start
181+
if ((int)($recordData['tx_container_parent'] ?? 0) > 0 && $this->datahandlerProcess->isContainerInProcess((int)($recordData['tx_container_parent']))) {
182+
continue;
183+
}
184+
// EXT:container end
185+
$pageId = (int)$recordData['pid'];
186+
if ($pageId < 0) {
187+
$previousRecord = BackendUtility::getRecord('tt_content', abs($pageId), 'pid');
188+
if ($previousRecord === null) {
189+
// Broken target data. Stop here and let DH handle this mess.
190+
continue;
191+
}
192+
$pageId = (int)$previousRecord['pid'];
193+
}
194+
$colPos = (int)$recordData['colPos'];
195+
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageId);
196+
$columnConfiguration = $this->backendLayoutView->getColPosConfigurationForPage($backendLayout, $colPos, $pageId);
197+
$allowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'] ?? '', true);
198+
$disallowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'] ?? '', true);
199+
if ((!empty($allowedContentElementsInTargetColPos) && !in_array($recordData['CType'], $allowedContentElementsInTargetColPos, true))
200+
|| (!empty($disallowedContentElementsInTargetColPos) && in_array($recordData['CType'], $disallowedContentElementsInTargetColPos, true))
201+
) {
202+
// Not allowed to create in this colPos on this page. Unset this command and create a log entry which may be turned into a notification when called by BE.
203+
unset($dataHandler->datamap['tt_content'][$id]);
204+
$dataHandler->log('tt_content', $id, 1, null, 1, 'The record "tt_content:%s" with CType "%s" in colPos "%s" couldn\'t be saved due to disallowed value(s).', null, [$id, $recordData['CType'], $colPos]);
205+
}
206+
}
207+
}
208+
}

Classes/Tca/Registry.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,15 @@ public function getContentDefenderConfiguration(string $cType, int $colPos): arr
124124
foreach ($rows as $columns) {
125125
foreach ($columns as $column) {
126126
if ((int)$column['colPos'] === $colPos) {
127-
$contentDefenderConfiguration['allowedContentTypes'] = $column['allowedContentTypes'] ?? [];
128-
$contentDefenderConfiguration['disallowedContentTypes'] = $column['disallowedContentTypes'] ?? [];
127+
$contentDefenderConfiguration['allowedContentTypes'] = $column['allowedContentTypes'] ?? '';
128+
$contentDefenderConfiguration['disallowedContentTypes'] = $column['disallowedContentTypes'] ?? '';
129129
$contentDefenderConfiguration['allowed.'] = $column['allowed'] ?? [];
130130
$contentDefenderConfiguration['disallowed.'] = $column['disallowed'] ?? [];
131131
$contentDefenderConfiguration['maxitems'] = $column['maxitems'] ?? 0;
132-
if ($contentDefenderConfiguration['allowedContentTypes'] === [] && $contentDefenderConfiguration['allowed.'] !== []) {
132+
if ($contentDefenderConfiguration['allowedContentTypes'] === '' && $contentDefenderConfiguration['allowed.'] !== []) {
133133
$contentDefenderConfiguration['allowedContentTypes'] = $contentDefenderConfiguration['allowed.']['CType'] ?? '';
134134
}
135-
if ($contentDefenderConfiguration['disallowedContentTypes'] === [] && $contentDefenderConfiguration['disallowed.'] !== []) {
135+
if ($contentDefenderConfiguration['disallowedContentTypes'] === '' && $contentDefenderConfiguration['disallowed.'] !== []) {
136136
$contentDefenderConfiguration['disallowedContentTypes'] = $contentDefenderConfiguration['disallowed.']['CType'] ?? '';
137137
}
138138
return $contentDefenderConfiguration;

Tests/Functional/Datahandler/ContentDefender/CopyContainerTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* of the License, or any later version.
1313
*/
1414

15+
use TYPO3\CMS\Core\Information\Typo3Version;
16+
1517
class CopyContainerTest extends AbstractContentDefender
1618
{
1719
/**
@@ -20,11 +22,13 @@ class CopyContainerTest extends AbstractContentDefender
2022
protected array $testExtensionsToLoad = [
2123
'typo3conf/ext/container',
2224
'typo3conf/ext/container_example',
23-
'typo3conf/ext/content_defender',
2425
];
2526

2627
protected function setUp(): void
2728
{
29+
if ((new Typo3Version())->getMajorVersion() < 14) {
30+
$this->testExtensionsToLoad[] = 'typo3conf/ext/content_defender';
31+
}
2832
parent::setUp();
2933
$this->importCSVDataSet(__DIR__ . '/Fixtures/copy_container.csv');
3034
}

Tests/Functional/Datahandler/ContentDefender/DefaultLanguageTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* of the License, or any later version.
1313
*/
1414

15+
use TYPO3\CMS\Core\Information\Typo3Version;
16+
1517
class DefaultLanguageTest extends AbstractContentDefender
1618
{
1719
/**
@@ -20,11 +22,13 @@ class DefaultLanguageTest extends AbstractContentDefender
2022
protected array $testExtensionsToLoad = [
2123
'typo3conf/ext/container',
2224
'typo3conf/ext/container_example',
23-
'typo3conf/ext/content_defender',
2425
];
2526

2627
protected function setUp(): void
2728
{
29+
if ((new Typo3Version())->getMajorVersion() < 14) {
30+
$this->testExtensionsToLoad[] = 'typo3conf/ext/content_defender';
31+
}
2832
parent::setUp();
2933
$this->importCSVDataSet(__DIR__ . '/Fixtures/DefaultLanguage/setup.csv');
3034
}

Tests/Functional/Datahandler/ContentDefender/LocalizationTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* of the License, or any later version.
1313
*/
1414

15+
use TYPO3\CMS\Core\Information\Typo3Version;
16+
1517
class LocalizationTest extends AbstractContentDefender
1618
{
1719
/**
@@ -20,11 +22,13 @@ class LocalizationTest extends AbstractContentDefender
2022
protected array $testExtensionsToLoad = [
2123
'typo3conf/ext/container',
2224
'typo3conf/ext/container_example',
23-
'typo3conf/ext/content_defender',
2425
];
2526

2627
protected function setUp(): void
2728
{
29+
if ((new Typo3Version())->getMajorVersion() < 14) {
30+
$this->testExtensionsToLoad[] = 'typo3conf/ext/content_defender';
31+
}
2832
parent::setUp();
2933
$this->importCSVDataSet(__DIR__ . '/Fixtures/Localization/setup.csv');
3034
}

Tests/Functional/Datahandler/ContentDefender/MaxItemsTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* of the License, or any later version.
1313
*/
1414

15+
use TYPO3\CMS\Core\Information\Typo3Version;
1516
use TYPO3\CMS\Core\Utility\StringUtility;
1617

1718
class MaxItemsTest extends AbstractContentDefender
@@ -22,11 +23,13 @@ class MaxItemsTest extends AbstractContentDefender
2223
protected array $testExtensionsToLoad = [
2324
'typo3conf/ext/container',
2425
'typo3conf/ext/container_example',
25-
'typo3conf/ext/content_defender',
2626
];
2727

2828
protected function setUp(): void
2929
{
30+
if ((new Typo3Version())->getMajorVersion() < 14) {
31+
$this->testExtensionsToLoad[] = 'typo3conf/ext/content_defender';
32+
}
3033
parent::setUp();
3134
$this->linkSiteConfigurationIntoTestInstance();
3235
}

0 commit comments

Comments
 (0)