Skip to content

Commit

Permalink
[!!!][TASK] Extract record access checks from TSFE
Browse files Browse the repository at this point in the history
Record access checks are moved from TSFE to the
new RecordAccessVoter class. This encapsulates
corresponding logic at a central place.

In addition, the existing hook
$GLOBALS[TYPO3_CONF_VARS][SC_OPTIONS][tslib/class.tslib_fe.php][hook_checkEnableFields]
is removed in favor of the new PSR-14 RecordAccessGrantedEvent.

Resolves: #96996
Releases: main
Change-Id: Ic056eb4c62d9792ee62198ae346db5231576d1bb
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/73638
Tested-by: core-ci <typo3@b13.com>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Benni Mack <benni@typo3.org>
  • Loading branch information
bmack committed Feb 23, 2022
1 parent 4c50bff commit 6efe63b
Show file tree
Hide file tree
Showing 12 changed files with 561 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?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\Core\Domain\Access;

use Psr\EventDispatcher\StoppableEventInterface;
use TYPO3\CMS\Core\Context\Context;

/**
* Event to modify records to be checked against "enableFields".
* Listeners are able to grant access or to modify the record itself to
* continue to use the native access check functionality with a modified dataset.
*/
final class RecordAccessGrantedEvent implements StoppableEventInterface
{
private ?bool $accessGranted = null;

public function __construct(
private readonly string $tableName,
private array $record,
private readonly Context $context
) {
}

public function isPropagationStopped(): bool
{
return $this->accessGranted !== null;
}

/**
* @internal
*/
public function accessGranted(): bool
{
if ($this->accessGranted === null) {
throw new \RuntimeException('Access was not yet defined.', 1645506529);
}

return $this->accessGranted;
}

public function setAccessGranted(bool $accessGranted): void
{
$this->accessGranted = $accessGranted;
}

public function getTable(): string
{
return $this->tableName;
}

public function getRecord(): array
{
return $this->record;
}

public function updateRecord(array $record): void
{
$this->record = $record;
}

public function getContext(): Context
{
return $this->context;
}
}
123 changes: 123 additions & 0 deletions typo3/sysext/core/Classes/Domain/Access/RecordAccessVoter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?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\Core\Domain\Access;

use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Context\Context;

/**
* Checks if a record can be accessed (usually in TYPO3 Frontend) due to various "enableFields" or group access checks.
*
* Not related to "write permissions" etc.
*/
class RecordAccessVoter
{
public function __construct(
protected readonly EventDispatcherInterface $eventDispatcher
) {
}

/**
* Checks page record for enableFields
* Returns TRUE if enableFields does not disable the page record.
* Takes notice of the includeHiddenPages visibility aspect flag and uses SIM_ACCESS_TIME for start/endtime evaluation
*
* @param string $table the TCA table to check for
* @param array $record The record to evaluate (needs fields: hidden, starttime, endtime, fe_group)
* @param Context $context Context API to check against
* @return bool TRUE, if record is viewable.
*/
public function accessGranted(string $table, array $record, Context $context): bool
{
$event = new RecordAccessGrantedEvent($table, $record, $context);
$this->eventDispatcher->dispatch($event);
if ($event->isPropagationStopped()) {
return $event->accessGranted();
}
$record = $event->getRecord();

$configuration = $this->getEnableFieldsConfigurationForTable($table);
$visibilityAspect = $context->getAspect('visibility');
$includeHidden = $table === 'pages'
? $visibilityAspect->includeHiddenPages()
: $visibilityAspect->includeHiddenContent();

// Hidden field is active and hidden records should not be included
if (($record[$configuration['disabled'] ?? null] ?? false) && !$includeHidden) {
return false;
}
// Records' starttime set AND is HIGHER than the current access time
if (isset($configuration['starttime'], $record[$configuration['starttime']])
&& (int)$record[$configuration['starttime']] > $GLOBALS['SIM_ACCESS_TIME']
) {
return false;
}
// Records' endtime is set AND NOT "0" AND LOWER than the current access time
if (isset($configuration['endtime'], $record[$configuration['endtime']])
&& ((int)$record[$configuration['endtime']] !== 0)
&& ((int)$record[$configuration['endtime']] < $GLOBALS['SIM_ACCESS_TIME'])
) {
return false;
}
// Insufficient group access
if ($this->groupAccessGranted($table, $record, $context) === false) {
return false;
}
// Record is available
return true;
}

/**
* Check group access against a record, if the current users' groups match the fe_group values of the record.
*
* @param string $table the TCA table to check for
* @param array $record The record to evaluate (needs enableField: fe_group)
* @param Context $context Context API to check against
* @return bool TRUE, if group access is granted.
*/
public function groupAccessGranted(string $table, array $record, Context $context): bool
{
if (!$context->hasAspect('frontend.user')) {
return true;
}
$configuration = $this->getEnableFieldsConfigurationForTable($table);
if (!isset($configuration['fe_group']) || !($record[$configuration['fe_group']] ?? false)) {
return true;
}
$pageGroupList = explode(',', (string)$record[$configuration['fe_group']]);
return count(array_intersect($context->getAspect('frontend.user')->getGroupIds(), $pageGroupList)) > 0;
}

/**
* Checks if the current page of the root line is visible.
*
* If the field extendToSubpages is 0, access is granted,
* else the fields hidden, starttime, endtime, fe_group are evaluated.
*
* @internal this is a special use case and should only be used with care, not part of TYPO3's Public API.
*/
public function accessGrantedForPageInRootLine(array $pageRecord, Context $context): bool
{
return !($pageRecord['extendToSubpages'] ?? false) || $this->accessGranted('pages', $pageRecord, $context);
}

protected function getEnableFieldsConfigurationForTable(string $table): array
{
return $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'] ?? [];
}
}
3 changes: 3 additions & 0 deletions typo3/sysext/core/Configuration/Services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ services:
TYPO3\CMS\Core\Resource\Security\SvgTypeCheck:
public: true

TYPO3\CMS\Core\Domain\Access\RecordAccessVoter:
public: true

# Core caches, cache.core and cache.assets are injected as early
# entries in TYPO3\CMS\Core\Core\Bootstrap and therefore omitted here
cache.hash:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.. include:: ../../Includes.txt

===================================================
Breaking: #96996 - Hook "checkEnableFields" removed
===================================================

See :issue:`96996`

Description
===========

The previous TYPO3 Hook "hook_checkEnableFields" registered via
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_checkEnableFields']`
has been removed in favor of a new PSR-14 Event
:php:`TYPO3\CMS\Core\Domain\Access\RecordAccessGrantedEvent`.

Impact
======

Hooks in third-party extensions will not be executed anymore.

Affected Installations
======================

TYPO3 installations with custom extensions using this hook. The
extension scanner will notify about usages.

Migration
=========

Register a new PSR-14 event listener for :php:`RecordAccessGrantedEvent`
in the extensions' :file:`Services.yaml` to keep TYPO3 v12+ compatibility.

Extensions can then provide compatibility with TYPO3 v11 and TYPO3 v12 at
the same time.

.. index:: Frontend, PHP-API, FullyScanned, ext:frontend
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.. include:: ../../Includes.txt

===============================================================================
Deprecation: #96996 - Deprecate TypoScriptFrontendController->checkEnableFields
===============================================================================

See :issue:`96996`

Description
===========

The :php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->checkEnableFields()`
method has been deprecated in favour of the new :php:`TYPO3\CMS\Core\Domain\Access\RecordAccessVoter`
component.

Impact
======

:php:`TypoScriptFrontendController->checkEnableFields()` will raise a
deprecation level log entry when called. The extension scanner will
report usages as weak match.

Affected Installations
======================

All installations calling :php:`TypoScriptFrontendController->checkEnableFields()`
in custom extension code.

Migration
=========

Replace all usages of the deprecated method. Use the :php:`RecordAccessVoter`
component instead, e.g. :php:`RecordAccessVoter->accessGranted()`.

.. index:: Frontend, PHP-API, FullyScanned, ext:frontend
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.. include:: ../../Includes.txt

=====================================================================
Feature: #96996 - PSR-14 Event for modifying record access evaluation
=====================================================================

See :issue:`96996`

Description
===========

A new PSR-14 event :php:`RecordAccessGrantedEvent` has been added. It serves
as replacement for the now removed hook
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_checkEnableFields']`.

The new PSR-14 event can be used to either define whether record access is granted
for a user, or to even modify the record in question. In case the `$accessGranted`
property is set (either :php:`true` or :php:`false`), the defined settings is
directly used, skipping any further event listener as well as any further
evaluation.

Example
=======

Registration of the Event in your extensions' :file:`Services.yaml`:

.. code-block:: yaml
MyVendor\MyPackage\MyEventListener:
tags:
- name: event.listener
identifier: 'my-package/set-access-granted'
The corresponding event listener class:

.. code-block:: php
use TYPO3\CMS\Core\Domain\Access\RecordAccessGrantedEvent;
class MyEventListener {
public function __invoke(RecordAccessGrantedEvent $event): void
{
// Manually set access granted
if ($event->getTable() === 'my_table' && ($event->getRecord()['custom_access_field'] ?? false)) {
$event->setAccessGranted(true);
}
// Update the record to be checked
$record = $event->getRecord();
$record['some_field'] = true;
$event->updateRecord($record);
}
}
Impact
======

With the new PSR-14 :php:`RecordAccessGrantedEvent`, it's
now possible to manipulate the record access evaluation by
either directly granting access or by modifying the record
to be evaluated.

.. index:: Frontend, PHP-API, ext:frontend
Loading

0 comments on commit 6efe63b

Please sign in to comment.