Your IP : 216.73.216.220


Current Path : /var/www/surf/TYPO3/vendor/typo3/cms-frontend/Classes/Hooks/
Upload File :
Current File : /var/www/surf/TYPO3/vendor/typo3/cms-frontend/Classes/Hooks/TreelistCacheUpdateHooks.php

<?php

/*
 * 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\Frontend\Hooks;

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Class that hooks into DataHandler and listens for updates to pages to update the
 * treelist cache
 * @internal this is a concrete TYPO3 hook implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
 */
class TreelistCacheUpdateHooks
{
    /**
     * Should not be manipulated from others except through the
     * configuration provided @see __construct()
     *
     * @var array
     */
    private $updateRequiringFields = [
        'pid',
        'php_tree_stop',
        'extendToSubpages',
    ];

    /**
     * Constructor, adds update requiring fields to the default ones
     */
    public function __construct()
    {
        // As enableFields can be set dynamically we add them here
        $pagesEnableFields = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns'];
        foreach ($pagesEnableFields as $pagesEnableField) {
            $this->updateRequiringFields[] = $pagesEnableField;
        }
        $this->updateRequiringFields[] = $GLOBALS['TCA']['pages']['ctrl']['delete'];
        // Extension can add fields to the pages table that require an
        // update of the treelist cache, too; so we also add those
        // example: $TYPO3_CONF_VARS['BE']['additionalTreelistUpdateFields'] .= ',my_field';
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['additionalTreelistUpdateFields'])) {
            $additionalTreelistUpdateFields = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['BE']['additionalTreelistUpdateFields'], true);
            $this->updateRequiringFields = array_merge($this->updateRequiringFields, $additionalTreelistUpdateFields);
        }
    }

    /**
     * waits for DataHandler commands and looks for changed pages, if found further
     * changes take place to determine whether the cache needs to be updated
     *
     * @param string $status DataHandler operation status, either 'new' or 'update'
     * @param string $table The DB table the operation was carried out on
     * @param mixed $recordId The record's uid for update records, a string to look the record's uid up after it has been created
     * @param array $updatedFields Array of changed fields and their new values
     * @param DataHandler $dataHandler DataHandler parent object
     */
    public function processDatamap_afterDatabaseOperations($status, $table, $recordId, array $updatedFields, DataHandler $dataHandler)
    {
        if ($table === 'pages' && $this->requiresUpdate($updatedFields)) {
            $affectedPagePid = 0;
            $affectedPageUid = 0;
            if ($status === 'new') {
                // Detect new pages
                // Resolve the uid
                $affectedPageUid = $dataHandler->substNEWwithIDs[$recordId];
                $affectedPagePid = $updatedFields['pid'];
            } elseif ($status === 'update') {
                // Detect updated pages
                $affectedPageUid = $recordId;
                // When updating a page the pid is not directly available so we
                // need to retrieve it ourselves.
                $fullPageRecord = BackendUtility::getRecord($table, $recordId);
                $affectedPagePid = $fullPageRecord['pid'];
            }
            $clearCacheActions = $this->determineClearCacheActions($status, $updatedFields);
            $this->processClearCacheActions($affectedPageUid, $affectedPagePid, $updatedFields, $clearCacheActions);
        }
    }

    /**
     * Waits for DataHandler commands and looks for deleted pages or published pages, if found
     * further changes take place to determine whether the cache needs to be updated
     *
     * @param string $command The TCE command
     * @param string $table The record's table
     * @param int $recordId The record's uid
     * @param array $commandValue The commands value, typically an array with more detailed command information
     * @param DataHandler $dataHandler The DataHandler parent object
     */
    public function processCmdmap_postProcess($command, $table, $recordId, $commandValue, DataHandler $dataHandler)
    {
        $action = (is_array($commandValue) && isset($commandValue['action'])) ? (string)$commandValue['action'] : '';
        if ($table === 'pages' && ($command === 'delete' || ($command === 'version' && $action === 'publish'))) {
            $affectedRecord = BackendUtility::getRecord($table, $recordId, '*', '', false);
            if (!is_array($affectedRecord)) {
                return;
            }
            $affectedPageUid = $affectedRecord['uid'];
            $affectedPagePid = $affectedRecord['pid'];

            // Faking the updated fields
            $updatedFields = [];
            if ($command === 'delete') {
                $updatedFields['deleted'] = 1;
            } else {
                // page was published to live
                $updatedFields['t3ver_wsid'] = 0;
            }
            $clearCacheActions = $this->determineClearCacheActions(
                'update',
                $updatedFields
            );

            $this->processClearCacheActions($affectedPageUid, $affectedPagePid, $updatedFields, $clearCacheActions);
        }
    }

    /**
     * waits for DataHandler commands and looks for moved pages, if found further
     * changes take place to determine whether the cache needs to be updated
     *
     * @param string $table Table name of the moved record
     * @param int $recordId The record's uid
     * @param int $destinationPid The record's destination page id
     * @param array $movedRecord The record that moved
     * @param array $updatedFields Array of changed fields
     * @param DataHandler $dataHandler DataHandler parent object
     */
    public function moveRecord_firstElementPostProcess($table, $recordId, $destinationPid, array $movedRecord, array $updatedFields, DataHandler $dataHandler)
    {
        if ($table === 'pages' && $this->requiresUpdate($updatedFields)) {
            $affectedPageUid = $recordId;
            $affectedPageOldPid = $movedRecord['pid'];
            $affectedPageNewPid = $updatedFields['pid'];
            $clearCacheActions = $this->determineClearCacheActions('update', $updatedFields);
            // Clear treelist entries for old parent page
            $this->processClearCacheActions($affectedPageUid, $affectedPageOldPid, $updatedFields, $clearCacheActions);
            // Clear treelist entries for new parent page
            $this->processClearCacheActions($affectedPageUid, $affectedPageNewPid, $updatedFields, $clearCacheActions);
        }
    }

    /**
     * Waits for DataHandler commands and looks for moved pages, if found further
     * changes take place to determine whether the cache needs to be updated
     *
     * @param string $table Table name of the moved record
     * @param int $recordId The record's uid
     * @param int $destinationPid The record's destination page id
     * @param int $originalDestinationPid (negative) page id th page has been moved after
     * @param array $movedRecord The record that moved
     * @param array $updatedFields Array of changed fields
     * @param DataHandler $dataHandler DataHandler parent object
     */
    public function moveRecord_afterAnotherElementPostProcess($table, $recordId, $destinationPid, $originalDestinationPid, array $movedRecord, array $updatedFields, DataHandler $dataHandler)
    {
        if ($table === 'pages' && $this->requiresUpdate($updatedFields)) {
            $affectedPageUid = $recordId;
            $affectedPageOldPid = $movedRecord['pid'];
            $affectedPageNewPid = $updatedFields['pid'];
            $clearCacheActions = $this->determineClearCacheActions('update', $updatedFields);
            // Clear treelist entries for old parent page
            $this->processClearCacheActions($affectedPageUid, $affectedPageOldPid, $updatedFields, $clearCacheActions);
            // Clear treelist entries for new parent page
            $this->processClearCacheActions($affectedPageUid, $affectedPageNewPid, $updatedFields, $clearCacheActions);
        }
    }

    /**
     * Checks whether the change requires an update of the treelist cache
     *
     * @param array $updatedFields Array of changed fields
     * @return bool TRUE if the treelist cache needs to be updated, FALSE if no update to the cache is required
     */
    protected function requiresUpdate(array $updatedFields)
    {
        $requiresUpdate = false;
        $updatedFieldNames = array_keys($updatedFields);
        foreach ($updatedFieldNames as $updatedFieldName) {
            if (in_array($updatedFieldName, $this->updateRequiringFields, true)) {
                $requiresUpdate = true;
                break;
            }
        }
        return $requiresUpdate;
    }

    /**
     * Calls the cache maintenance functions according to the determined actions
     *
     * @param int $affectedPage uid of the affected page
     * @param int $affectedParentPage parent uid of the affected page
     * @param array $updatedFields Array of updated fields and their new values
     * @param array $actions Array of actions to carry out
     */
    protected function processClearCacheActions($affectedPage, $affectedParentPage, $updatedFields, array $actions)
    {
        $actionNames = array_keys($actions);
        foreach ($actionNames as $actionName) {
            switch ($actionName) {
                case 'allParents':
                    $this->clearCacheForAllParents($affectedParentPage);
                    break;
                case 'setExpiration':
                    // Only used when setting an end time for a page
                    $expirationTime = $updatedFields['endtime'];
                    $this->setCacheExpiration($affectedPage, $expirationTime);
                    break;
                case 'uidInTreelist':
                    $this->clearCacheWhereUidInTreelist($affectedPage);
                    break;
            }
        }
        // From time to time clean the cache from expired entries
        // (theoretically every 1000 calls)
        $randomNumber = random_int(1, 1000);
        if ($randomNumber === 500) {
            $this->removeExpiredCacheEntries();
        }
    }

    /**
     * Clears the treelist cache for all parents of a changed page.
     * gets called after creating a new page and after moving a page
     *
     * @param int $affectedParentPage Parent page id of the changed page, the page to start clearing from
     */
    protected function clearCacheForAllParents($affectedParentPage)
    {
        $rootLine = BackendUtility::BEgetRootLine($affectedParentPage);
        $rootLineIds = [];
        foreach ($rootLine as $page) {
            if ($page['uid'] != 0) {
                $rootLineIds[] = $page['uid'];
            }
        }
        if (!empty($rootLineIds)) {
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
                ->getQueryBuilderForTable('cache_treelist');
            $queryBuilder
                ->delete('cache_treelist')
                ->where(
                    $queryBuilder->expr()->in(
                        'pid',
                        $queryBuilder->createNamedParameter($rootLineIds, Connection::PARAM_INT_ARRAY)
                    )
                )
                ->executeStatement();
        }
    }

    /**
     * Clears the treelist cache for all pages where the affected page is found
     * in the treelist
     *
     * @param int $affectedPage ID of the changed page
     */
    protected function clearCacheWhereUidInTreelist($affectedPage)
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('cache_treelist');
        $queryBuilder
            ->delete('cache_treelist')
            ->where(
                $queryBuilder->expr()->inSet('treelist', $queryBuilder->quote($affectedPage))
            )
            ->executeStatement();
    }

    /**
     * Sets an expiration time for all cache entries having the changed page in
     * the treelist.
     *
     * @param int $affectedPage Uid of the changed page
     * @param int $expirationTime
     */
    protected function setCacheExpiration($affectedPage, $expirationTime)
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('cache_treelist');
        $queryBuilder
            ->update('cache_treelist')
            ->where(
                $queryBuilder->expr()->inSet('treelist', $queryBuilder->quote($affectedPage))
            )
            ->set('expires', $expirationTime)
            ->executeStatement();
    }

    /**
     * Removes all expired treelist cache entries
     */
    protected function removeExpiredCacheEntries()
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('cache_treelist');
        $queryBuilder
            ->delete('cache_treelist')
            ->where(
                $queryBuilder->expr()->lte(
                    'expires',
                    $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
                )
            )
            ->executeStatement();
    }

    /**
     * Determines what happened to the page record, this is necessary to clear
     * as less cache entries as needed later
     *
     * @param string $status DataHandler operation status, either 'new' or 'update'
     * @param array $updatedFields Array of updated fields
     * @return array List of actions that happened to the page record
     */
    protected function determineClearCacheActions($status, $updatedFields): array
    {
        $actions = [];
        if ($status === 'new') {
            // New page
            $actions['allParents'] = true;
        } elseif ($status === 'update') {
            $updatedFieldNames = array_keys($updatedFields);
            foreach ($updatedFieldNames as $updatedFieldName) {
                switch ($updatedFieldName) {
                    case 'pid':

                    case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled']:

                    case $GLOBALS['TCA']['pages']['ctrl']['delete']:

                    case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['starttime']:

                    case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['fe_group']:

                    case 'extendToSubpages':

                    case 't3ver_wsid':

                    case 'php_tree_stop':
                        // php_tree_stop
                        $actions['allParents'] = true;
                        $actions['uidInTreelist'] = true;
                        break;
                    case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['endtime']:
                        // end time set/unset
                        // When setting an end time the cache entry needs an
                        // expiration time. When unsetting the end time the
                        // page must become listed in the treelist again.
                        if ($updatedFields['endtime'] > 0) {
                            $actions['setExpiration'] = true;
                        } else {
                            $actions['uidInTreelist'] = true;
                        }
                        break;
                    default:
                        if (in_array($updatedFieldName, $this->updateRequiringFields, true)) {
                            $actions['uidInTreelist'] = true;
                        }
                }
            }
        }
        return $actions;
    }
}