Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -t ${{ matrix.TYPO3 }} -s acceptance -- --fail-fast
if: matrix.TYPO3 != '14'
- name: Acceptance Tests 14
run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -t ${{ matrix.TYPO3 }} -s acceptance -- --fail-fast --skip-group=content_defender
run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -t ${{ matrix.TYPO3 }} -s acceptance -- --fail-fast
if: matrix.TYPO3 == '14'
- name: Archive acceptance tests results
uses: actions/upload-artifact@v4
Expand Down
2 changes: 2 additions & 0 deletions Build/phpstan11-7.4.neon
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ parameters:
- %currentWorkingDirectory%/Classes/Listener/PageTsConfig.php
- %currentWorkingDirectory%/Classes/Listener/PageContentPreviewRendering.php
- %currentWorkingDirectory%/Classes/Domain/Core/RecordWithRenderedGrid.php
- %currentWorkingDirectory%/Classes/Listener/ManipulateBackendLayoutColPosConfigurationForPage.php
- %currentWorkingDirectory%/Classes/Hooks/Datahandler/ContentElementRestriction

2 changes: 2 additions & 0 deletions Build/phpstan11.neon
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ parameters:
- %currentWorkingDirectory%/Classes/Listener/PageTsConfig.php
- %currentWorkingDirectory%/Classes/Listener/PageContentPreviewRendering.php
- %currentWorkingDirectory%/Classes/Domain/Core/RecordWithRenderedGrid.php
- %currentWorkingDirectory%/Classes/Listener/ManipulateBackendLayoutColPosConfigurationForPage.php
- %currentWorkingDirectory%/Classes/Hooks/Datahandler/ContentElementRestriction

2 changes: 2 additions & 0 deletions Build/phpstan12.neon
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ parameters:
- %currentWorkingDirectory%/Tests/Unit/Hooks/UsedRecordsTest.php
- %currentWorkingDirectory%/Classes/Domain/Core/RecordWithRenderedGrid.php
- %currentWorkingDirectory%/Classes/Listener/PageContentPreviewRendering.php
- %currentWorkingDirectory%/Classes/Listener/ManipulateBackendLayoutColPosConfigurationForPage.php
- %currentWorkingDirectory%/Classes/Hooks/Datahandler/ContentElementRestriction


2 changes: 2 additions & 0 deletions Build/phpstan13.neon
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ parameters:
- %currentWorkingDirectory%/Classes/Hooks/WizardItems.php
- %currentWorkingDirectory%/Classes/Listener/LegacyPageTsConfig.php
- %currentWorkingDirectory%/Classes/Listener/PageContentPreviewRendering.php
- %currentWorkingDirectory%/Classes/Listener/ManipulateBackendLayoutColPosConfigurationForPage.php
- %currentWorkingDirectory%/Classes/Hooks/Datahandler/ContentElementRestriction

2 changes: 2 additions & 0 deletions Classes/Hooks/Datahandler/CommandMapPostProcessingHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ protected function copyOrMoveChildren(int $origUid, int $newId, int $containerId
// when moving or copy a container into other language the other language is returned
$container = $this->containerFactory->buildContainer($origUid);
(GeneralUtility::makeInstance(DatahandlerProcess::class))->startContainerProcess($origUid);
(GeneralUtility::makeInstance(DatahandlerProcess::class))->lockContentElementRestrictions();
$children = [];
$colPosVals = $container->getChildrenColPos();
foreach ($colPosVals as $colPos) {
Expand Down Expand Up @@ -177,6 +178,7 @@ protected function copyOrMoveChildren(int $origUid, int $newId, int $containerId
}
}
(GeneralUtility::makeInstance(DatahandlerProcess::class))->endContainerProcess($origUid);
(GeneralUtility::makeInstance(DatahandlerProcess::class))->unlockContentElementRestrictions();
} catch (Exception $e) {
// nothing todo
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<?php

declare(strict_types=1);

namespace B13\Container\Hooks\Datahandler\ContentElementRestriction;

/*
* This file is part of TYPO3 CMS-based extension "container" by b13.
*
* 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.
*/

use B13\Container\Domain\Factory\ContainerFactory;
use B13\Container\Domain\Factory\Exception;
use B13\Container\Hooks\Datahandler\DatahandlerProcess;
use B13\Container\Tca\Registry;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;

#[Autoconfigure(public: true)]
class DataHandlerHook
{
protected $lockDatamapHook = false;

public function __construct(
private BackendLayoutView $backendLayoutView,
private DatahandlerProcess $datahandlerProcess,
private ContainerFactory $containerFactory,
private Registry $registry
) {
}

public function processCmdmap_beforeStart(DataHandler $dataHandler): void
{
$cmdmap = $dataHandler->cmdmap;
if (isset($cmdmap['pages'])) {
$this->lockDatamapHook = true;
}
if (empty($cmdmap['tt_content']) || $dataHandler->bypassAccessCheckForRecords) {
return;
}
$this->lockDatamapHook = true;
if ($this->datahandlerProcess->areContentElementRestrictionsLooked()) {
return;
}
foreach ($cmdmap['tt_content'] as $id => $incomingFieldArray) {
foreach ($incomingFieldArray as $command => $value) {
if (!in_array($command, ['copy', 'move'], true)) {
continue;
}
$currentRecord = BackendUtility::getRecord('tt_content', $id);

// EXT:container start
if (
(!empty($value['update'])) &&
isset($value['update']['colPos']) &&
$value['update']['colPos'] > 0 &&
isset($value['update']['tx_container_parent']) &&
$value['update']['tx_container_parent'] > 0 &&
MathUtility::canBeInterpretedAsInteger($id)
) {
$colPos = (int)$value['update']['colPos'];
if (!empty($currentRecord['CType'])) {
if ($this->checkContainerCType((int)$value['update']['tx_container_parent'], $currentRecord['CType'], (int)$value['update']['colPos']) === false) {
// 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.
unset($dataHandler->cmdmap['tt_content'][$id]);
$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]);
}
}
$useChildId = null;
if ($command === 'move') {
$useChildId = $id;
}
if ($this->checkContainerMaxItems((int)$value['update']['tx_container_parent'], (int)$value['update']['colPos'], $useChildId)) {
unset($dataHandler->cmdmap['tt_content'][$id]);
$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]);
}
return;
}
// EXT:container end

if (empty($currentRecord['CType'] ?? '')) {
continue;
}
if (is_array($value) && !empty($value['action']) && $value['action'] === 'paste' && isset($value['update']['colPos'])) {
// Moving / pasting to a new colPos on a potentially different page
$pageId = (int)$value['target'];
$colPos = (int)$value['update']['colPos'];
} else {
$pageId = (int)$value;
$colPos = (int)$currentRecord['colPos'];
}
if ($pageId < 0) {
$targetRecord = BackendUtility::getRecord('tt_content', abs($pageId));
$pageId = (int)$targetRecord['pid'];
$colPos = (int)$targetRecord['colPos'];
}

$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageId);
$columnConfiguration = $this->backendLayoutView->getColPosConfigurationForPage($backendLayout, $colPos, $pageId);
$allowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'] ?? '', true);
$disallowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'] ?? '', true);
if ((!empty($allowedContentElementsInTargetColPos) && !in_array($currentRecord['CType'], $allowedContentElementsInTargetColPos, true))
|| (!empty($disallowedContentElementsInTargetColPos) && in_array($currentRecord['CType'], $disallowedContentElementsInTargetColPos, true))
) {
// 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.
unset($dataHandler->cmdmap['tt_content'][$id]);
$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]);
}
}
}
}

protected function checkContainerMaxItems(int $containerId, int $colPos, ?int $childUid = null): bool
{
try {
$container = $this->containerFactory->buildContainer($containerId);
$columnConfiguration = $this->registry->getContentDefenderConfiguration($container->getCType(), $colPos);
if (($columnConfiguration['maxitems'] ?? 0) === 0) {
return false;
}
$childrenOfColumn = $container->getChildrenByColPos($colPos);
$count = count($childrenOfColumn);
if ($childUid !== null && $container->hasChildInColPos($colPos, $childUid)) {
$count--;
}
return $count >= $columnConfiguration['maxitems'];
} catch (Exception) {
// not a container;
}
return false;
}

protected function checkContainerCType(int $containerId, string $cType, int $colPos): bool
{
try {
$container = $this->containerFactory->buildContainer($containerId);
$columnConfiguration = $this->registry->getContentDefenderConfiguration($container->getCType(), $colPos);
$allowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'] ?? '', true);
$disallowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'] ?? '', true);
if ((!empty($allowedContentElementsInTargetColPos) && !in_array($cType, $allowedContentElementsInTargetColPos, true))
|| (!empty($disallowedContentElementsInTargetColPos) && in_array($cType, $disallowedContentElementsInTargetColPos, true))
) {
return false;
}
} catch (Exception) {
// not a container;
}
return true;
}

public function processCmdmap_postProcess(string $command, string $table, $id, $value, DataHandler $dataHandler, $pasteUpdate, $pasteDatamap): void
{
$this->lockDatamapHook = false;
}

public function processDatamap_beforeStart(DataHandler $dataHandler): void
{
if ($this->lockDatamapHook === true) {
return;
}
$datamap = $dataHandler->datamap;
if (empty($datamap['tt_content']) || $dataHandler->bypassAccessCheckForRecords) {
return;
}
foreach ($datamap['tt_content'] as $id => $incomingFieldArray) {
if (MathUtility::canBeInterpretedAsInteger($id)) {
$record = BackendUtility::getRecord('tt_content', $id);
if (!is_array($record)) {
// Skip this if the record could not be determined for whatever reason
continue;
}
$recordData = array_merge($record, $incomingFieldArray);
} else {
$recordData = array_merge($dataHandler->defaultValues['tt_content'] ?? [], $incomingFieldArray);
}
// EXT:container start
if ((int)($recordData['tx_container_parent'] ?? 0) > 0 && (int)($recordData['colPos'] ?? 0) > 0) {
if ($this->checkContainerMaxItems((int)$recordData['tx_container_parent'], (int)$recordData['colPos'])) {
if (MathUtility::canBeInterpretedAsInteger($id)) {
// edit
continue;
}
unset($dataHandler->datamap['tt_content'][$id]);
$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, [$id, $recordData['colPos']]);
}
}
// EXT:container end
if (empty($recordData['CType']) || !array_key_exists('colPos', $recordData)) {
// No idea what happened here, but we stop with this record if there is no CType or colPos
continue;
}
$pageId = (int)$recordData['pid'];
if ($pageId < 0) {
$previousRecord = BackendUtility::getRecord('tt_content', abs($pageId), 'pid');
if ($previousRecord === null) {
// Broken target data. Stop here and let DH handle this mess.
continue;
}
$pageId = (int)$previousRecord['pid'];
}
$colPos = (int)$recordData['colPos'];
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageId);
$columnConfiguration = $this->backendLayoutView->getColPosConfigurationForPage($backendLayout, $colPos, $pageId);
$allowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'] ?? '', true);
$disallowedContentElementsInTargetColPos = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'] ?? '', true);
if ((!empty($allowedContentElementsInTargetColPos) && !in_array($recordData['CType'], $allowedContentElementsInTargetColPos, true))
|| (!empty($disallowedContentElementsInTargetColPos) && in_array($recordData['CType'], $disallowedContentElementsInTargetColPos, true))
) {
// 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.
unset($dataHandler->datamap['tt_content'][$id]);
$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]);
}
}
}
}
19 changes: 19 additions & 0 deletions Classes/Hooks/Datahandler/DatahandlerProcess.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ class DatahandlerProcess implements SingletonInterface
{
protected $containerInProcess = [];

protected $contentElementRestrictionsLook = false;

protected $stack = 0;

public function areContentElementRestrictionsLooked(): bool
{
return $this->stack > 0;
}

public function unlockContentElementRestrictions(): void
{
$this->stack--;
}

public function lockContentElementRestrictions(): void
{
$this->stack++;
}

public function isContainerInProcess(int $containerId): bool
{
return in_array($containerId, $this->containerInProcess, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace B13\Container\Listener;

/*
* This file is part of TYPO3 CMS-based extension "container" by b13.
*
* 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.
*/

use B13\Container\Domain\Factory\ContainerFactory;
use B13\Container\Domain\Factory\Exception;
use B13\Container\Tca\Registry;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\Event\ManipulateBackendLayoutColPosConfigurationForPageEvent;

class ManipulateBackendLayoutColPosConfigurationForPage
{
/**
* @var Registry
*/
protected $tcaRegistry;

/**
* @var ContainerFactory
*/
protected $containerFactory;

public function __construct(ContainerFactory $containerFactory, Registry $tcaRegistry)
{
$this->containerFactory = $containerFactory;
$this->tcaRegistry = $tcaRegistry;
}

public function __invoke(ManipulateBackendLayoutColPosConfigurationForPageEvent $e)
{
$parent = $this->getParentUid($e->request);
if ($parent === null) {
return;
}

try {
$container = $this->containerFactory->buildContainer($parent);
} catch (Exception $e) {
// not a container
return;
}
$cType = $container->getCType();
$configuration = $this->tcaRegistry->getContentDefenderConfiguration($cType, $e->colPos);
$e->configuration = [
'allowedContentTypes' => $configuration['allowedContentTypes'],
'disallowedContentTypes' => $configuration['disallowedContentTypes'],
];
}

private function getParentUid(?ServerRequestInterface $request): ?int
{
if ($request === null) {
return null;
}
$queryParams = $request->getQueryParams();
if (isset($queryParams['tx_container_parent']) && $queryParams['tx_container_parent'] > 0) {
// new content elemment wizard
return (int)$queryParams['tx_container_parent'];
}
if (
isset($queryParams['defVals']['tt_content']['tx_container_parent']) &&
$queryParams['defVals']['tt_content']['tx_container_parent'] > 0
) {
// TcaCTypeItems: new record
return (int)$queryParams['defVals']['tt_content']['tx_container_parent'];
}
if (isset($queryParams['edit']['tt_content'])) {
$recordUid = array_keys($queryParams['edit']['tt_content'])[0];
$recordUid = (int)abs($recordUid);
// TcaCTypeItems: edit record
$record = BackendUtility::getRecord('tt_content', $recordUid, 'tx_container_parent');
if (isset($record['tx_container_parent'])) {
return (int)$record['tx_container_parent'];
}
}
return null;
}
}
Loading