* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize * @license GNU/GPLv3, or later. See LICENSE file * * If LICENSE file missing, see . */ use CodeAlfa\Minify\Js; use JchOptimize\ContainerFactory; use JchOptimize\Core\Container\Container; use JchOptimize\Core\Helper; use JchOptimize\Core\PageCache\PageCache; use JchOptimize\Core\SystemUri; use Joomla\CMS\Application\CMSApplicationInterface; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Session\Session; use Joomla\Event\DispatcherInterface; defined('_JEXEC') or die ('Restricted access'); include_once JPATH_ADMINISTRATOR . '/components/com_jchoptimize/autoload.php'; class plgSystemJchoptimizepagecache extends CMSPlugin { /** * If plugin is enabled * * @var bool */ public bool $enabled = true; /** * Application object * * @var ?CMSApplicationInterface */ protected $app = null; /** * Container object * * @var Container */ private Container $container; /** * Page Cache object * * @var PageCache */ private PageCache $pageCache; /** * Constructor * * @param DispatcherInterface $subject The object to observe * @param array $config Optional associative array of configurations */ public function __construct(&$subject, $config = []) { parent::__construct($subject, $config); //Disable if the component is not installed or disabled if (!ComponentHelper::isEnabled('com_jchoptimize')) { $this->enabled = false; return; } //Disable if we can't get component's container try { $this->container = ContainerFactory::getContainer(); } catch (Exception $e) { $this->enabled = false; return; } //Disable if client is not Site if (!$this->app->isClient('site')) { $this->enabled = false; return; } //Disable if site offline if ($this->app->get('offline', '0')) { $this->enabled = false; return; } //Disable if there are messages enqueued if ($this->app->getMessageQueue()) { $this->enabled = false; return; } //Disable if jchnooptimize set if ($this->app->input->get('jchnooptimize') == '1' || $this->app->input->get('jchbackend') == '1') { $this->enabled = false; return; } //Disable if we couldn't get cache object try { $this->pageCache = $this->container->get(PageCache::class); } catch (Exception $e) { //didn't work, disable $this->enabled = false; return; } } public function onAfterInitialise() { //If already disabled return if (!$this->enabled) { return; } if (JDEBUG) { $this->pageCache->disableCaptureCache(); } $this->pageCache->initialize(); } /** * After route event, have to check for excluded menu items here */ public function onAfterRoute() { //If already disabled return if (!$this->enabled) { return; } //If we're forcing ssl on the front end but not serving https, disable caching if ($this->app->get('force_ssl') === 2 && SystemUri::currentUri()->getScheme() !== 'https') { $this->enabled = false; return; } try { $excludedMenus = $this->container->get('params')->get('cache_exclude_menu', []); $excludedComponents = $this->container->get('params')->get('cache_exclude_component', []); if (in_array($this->app->input->get('Itemid', '', 'int'), $excludedMenus) || in_array($this->app->input->get('option', ''), $excludedComponents) ) { $this->enabled = false; $this->pageCache->disableCaching(); return; } //Now may be a good time to set Caching $this->pageCache->setCaching(); } catch (Exception $e) { } } public function onAfterRender() { if (!$this->enabled) { return; } $html = $this->app->getBody(); if (!Helper::validateHtml($html)) { $this->pageCache->disableCaching(); return; } if ($this->pageCache->isCaptureCacheEnabled()) { $html = $this->addUpdateFormTokenAjax($html); } $this->app->setBody($html); //Disable gzip so the HTML can be cached later $this->app->set('gzip', false); } public function onAfterRespond() { if ($this->enabled) { //Still need to validate the HTMl here. We may be on a redirect. if (Helper::validateHtml($this->app->getBody())) { $this->pageCache->store($this->app->getBody()); } } } /** * If Page Cache plugin is already disabled then this will disable the Page Cache object when it is constructed * * @return bool */ public function onPageCacheSetCaching(): bool { return $this->enabled; } public function onPageCacheGetKey() { return Factory::getLanguage()->getTag(); } private function addUpdateFormTokenAjax($html) { $url = SystemUri::baseFull() . 'index.php?option=com_ajax&format=json&plugin=getformtoken'; /** @see plgSystemJchoptimizepagecache::onAjaxGetformtoken() */ $script = << { const response = await fetch('$url'); if (response.ok) { const jsonValue = await response.json(); return Promise.resolve(jsonValue); } } updateFormToken().then(data => { const formRegex = new RegExp('[0-9a-f]{32}'); for (let formToken of document.querySelectorAll('input[type=hidden]')){ if (formToken.value == '1' && formRegex.test(formToken.name)){ formToken.name = data.data[0]; } } const jsonRegex = new RegExp('"csrf\.token":"[^"]+"'); for(let scriptToken of document.querySelectorAll('script[type="application/json"]')){ if(scriptToken.classList.contains('joomla-script-options')){ let json = scriptToken.textContent; if(jsonRegex.test(json)){ scriptToken.textContent = json.replace(jsonRegex, '"csrf.token":"' + data.data[0] + '"'); } } } }); JS; $htmlScript = ''; $html = str_replace('', $htmlScript . "\n" . '', $html); return $html; } public function onAjaxGetformtoken(): string { return Session::getFormToken(); } }