Files
liceo-ariosto/plugins/system/cache/src/Extension/Cache.php
2025-06-17 11:53:18 +02:00

383 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @package Joomla.Plugin
* @subpackage System.cache
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Cache\Extension;
use Joomla\CMS\Cache\CacheController;
use Joomla\CMS\Cache\CacheControllerFactoryInterface;
use Joomla\CMS\Document\FactoryInterface as DocumentFactoryInterface;
use Joomla\CMS\Event\AbstractEvent;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Profiler\Profiler;
use Joomla\CMS\Router\SiteRouter;
use Joomla\CMS\Uri\Uri;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\Event;
use Joomla\Event\Priority;
use Joomla\Event\SubscriberInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Page Cache Plugin.
*
* @since 1.5
*/
final class Cache extends CMSPlugin implements SubscriberInterface
{
/**
* Cache instance.
*
* @var CacheController
* @since 1.5
*/
private $cache;
/**
* The application's document factory interface
*
* @var DocumentFactoryInterface
* @since 4.2.0
*/
private $documentFactory;
/**
* Cache controller factory interface
*
* @var CacheControllerFactoryInterface
* @since 4.2.0
*/
private $cacheControllerFactory;
/**
* The application profiler, used when Debug Site is set to Yes in Global Configuration.
*
* @var Profiler|null
* @since 4.2.0
*/
private $profiler;
/**
* The frontend router, injected by the service provider.
*
* @var SiteRouter|null
* @since 4.2.0
*/
private $router;
/**
* Constructor
*
* @param DispatcherInterface $dispatcher The object to observe
* @param array $config An optional associative
* array of configuration
* settings. Recognized key
* values include 'name',
* 'group', 'params',
* 'language'
* (this list is not meant
* to be comprehensive).
* @param DocumentFactoryInterface $documentFactory The application's
* document factory
* @param CacheControllerFactoryInterface $cacheControllerFactory Cache controller factory
* @param Profiler|null $profiler The application profiler
* @param SiteRouter|null $router The frontend router
*
* @since 4.2.0
*/
public function __construct(
DispatcherInterface $dispatcher,
array $config,
DocumentFactoryInterface $documentFactory,
CacheControllerFactoryInterface $cacheControllerFactory,
?Profiler $profiler,
?SiteRouter $router
) {
parent::__construct($dispatcher, $config);
$this->documentFactory = $documentFactory;
$this->cacheControllerFactory = $cacheControllerFactory;
$this->profiler = $profiler;
$this->router = $router;
}
/**
* Returns an array of CMS events this plugin will listen to and the respective handlers.
*
* @return array
*
* @since 4.2.0
*/
public static function getSubscribedEvents(): array
{
/**
* Note that onAfterRender and onAfterRespond must be the last handlers to run for this
* plugin to operate as expected. These handlers put pages into cache. We must make sure
* that a. the page SHOULD be cached and b. we are caching the complete page, as it's
* output to the browser.
*/
return [
'onAfterRoute' => 'onAfterRoute',
'onAfterRender' => ['onAfterRender', Priority::LOW],
'onAfterRespond' => ['onAfterRespond', Priority::LOW],
];
}
/**
* Returns a cached page if the current URL exists in the cache.
*
* @param Event $event The Joomla event being handled
*
* @return void
*
* @since 4.0.0
*/
public function onAfterRoute(Event $event)
{
if (!$this->appStateSupportsCaching()) {
return;
}
// If any `pagecache` plugins return false for onPageCacheSetCaching, do not use the cache.
PluginHelper::importPlugin('pagecache');
$results = $this->getApplication()->triggerEvent('onPageCacheSetCaching');
$this->getCacheController()->setCaching(!\in_array(false, $results, true));
$data = $this->getCacheController()->get($this->getCacheKey());
if ($data === false) {
// No cached data.
return;
}
// Set the page content from the cache and output it to the browser.
$this->getApplication()->setBody($data);
echo $this->getApplication()->toString((bool) $this->getApplication()->get('gzip'));
// Mark afterCache in debug and run debug onAfterRespond events, e.g. show Joomla Debug Console if debug is active.
if (JDEBUG) {
// Create a document instance and load it into the application.
$document = $this->documentFactory
->createDocument($this->getApplication()->getInput()->get('format', 'html'));
$this->getApplication()->loadDocument($document);
if ($this->profiler) {
$this->profiler->mark('afterCache');
}
$this->getDispatcher()->dispatch('onAfterRespond', AbstractEvent::create(
'onAfterRespond',
[
'subject' => $this->getApplication(),
]
));
}
// Closes the application.
$this->getApplication()->close();
}
/**
* Does the current application state allow for caching?
*
* The following conditions must be met:
* * This is the frontend application. This plugin does not apply to other applications.
* * This is a GET request. This plugin does not apply to POST, PUT etc.
* * There is no currently logged in user (pages might have userspecific content).
* * The message queue is empty.
*
* The first two tests are cached to make early returns possible; these conditions cannot change
* throughout the lifetime of the request.
*
* The other two tests MUST NOT be cached because autologin plugins may fire anytime within
* the application lifetime logging in a user and messages can be generated anytime within the
* application's lifetime.
*
* @return boolean
* @since 4.2.0
*/
private function appStateSupportsCaching(): bool
{
static $isSite = null;
static $isGET = null;
if ($isSite === null) {
$isSite = $this->getApplication()->isClient('site');
$isGET = $this->getApplication()->getInput()->getMethod() === 'GET';
}
// Boolean shortcircuit evaluation means this returns fast false when $isSite is false.
return $isSite
&& $isGET
&& $this->getApplication()->getIdentity()->guest
&& empty($this->getApplication()->getMessageQueue());
}
/**
* Get the cache controller
*
* @return CacheController
* @since 4.2.0
*/
private function getCacheController(): CacheController
{
if (!empty($this->cache)) {
return $this->cache;
}
// Set the cache options.
$options = [
'defaultgroup' => 'page',
'browsercache' => $this->params->get('browsercache', 0),
'caching' => false,
];
// Instantiate cache with previous options.
$this->cache = $this->cacheControllerFactory->createCacheController('page', $options);
return $this->cache;
}
/**
* Get a cache key for the current page based on the url and possible other factors.
*
* @return string
*
* @since 3.7
*/
private function getCacheKey(): string
{
static $key;
if (!$key) {
PluginHelper::importPlugin('pagecache');
$parts = $this->getApplication()->triggerEvent('onPageCacheGetKey');
$parts[] = Uri::getInstance()->toString();
$key = md5(serialize($parts));
}
return $key;
}
/**
* After Render Event. Check whether the current page is excluded from cache.
*
* @param Event $event The CMS event we are handling.
*
* @return void
*
* @since 3.9.12
*/
public function onAfterRender(Event $event)
{
if (!$this->appStateSupportsCaching() || $this->getCacheController()->getCaching() === false) {
return;
}
if ($this->isExcluded() === true) {
$this->getCacheController()->setCaching(false);
return;
}
// Disable compression before caching the page.
$this->getApplication()->set('gzip', false);
}
/**
* Check if the page is excluded from the cache or not.
*
* @return boolean True if the page is excluded else false
*
* @since 3.5
*/
private function isExcluded(): bool
{
// Check if menu items have been excluded.
$excludedMenuItems = $this->params->get('exclude_menu_items', []);
if ($excludedMenuItems) {
// Get the current menu item.
$active = $this->getApplication()->getMenu()->getActive();
if ($active && $active->id && \in_array((int) $active->id, (array) $excludedMenuItems)) {
return true;
}
}
// Check if regular expressions are being used.
$exclusions = $this->params->get('exclude', '');
if ($exclusions) {
// Convert the exclusions into a normalised array
$exclusions = str_replace(["\r\n", "\r"], "\n", $exclusions);
$exclusions = explode("\n", $exclusions);
$filterExpression = function ($x) {
return $x !== '';
};
$exclusions = array_filter($exclusions, $filterExpression);
// Gets the internal (non-SEF) and the external (possibly SEF) URIs.
$internalUrl = '/index.php?'
. Uri::getInstance()->buildQuery($this->router->getVars());
$externalUrl = Uri::getInstance()->toString();
$reduceCallback
= function (bool $carry, string $exclusion) use ($internalUrl, $externalUrl) {
// Test both external and internal URIs
return $carry && preg_match(
'#' . $exclusion . '#i',
$externalUrl . ' ' . $internalUrl,
$match
);
};
$excluded = array_reduce($exclusions, $reduceCallback, false);
if ($excluded) {
return true;
}
}
// If any pagecache plugins return true for onPageCacheIsExcluded, exclude.
PluginHelper::importPlugin('pagecache');
$results = $this->getApplication()->triggerEvent('onPageCacheIsExcluded');
return \in_array(true, $results, true);
}
/**
* After Respond Event. Stores page in cache.
*
* @param Event $event The application event we are handling.
*
* @return void
*
* @since 1.5
*/
public function onAfterRespond(Event $event)
{
if (!$this->appStateSupportsCaching() || $this->getCacheController()->getCaching() === false) {
return;
}
// Saves current page in cache.
$this->getCacheController()->store($this->getApplication()->getBody(), $this->getCacheKey());
}
}