Files
interblue.pl/modules/litespeedcache/classes/Cache.php
2024-10-25 14:16:28 +02:00

727 lines
25 KiB
PHP

<?php
/**
* LiteSpeed Cache for Prestashop.
*
* NOTICE OF LICENSE
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see https://opensource.org/licenses/GPL-3.0 .
*
* @author LiteSpeed Technologies
* @copyright Copyright (c) 2017-2018 LiteSpeed Technologies, Inc. (https://www.litespeedtech.com)
* @license https://opensource.org/licenses/GPL-3.0
*/
use LiteSpeedCache as LSC;
use LiteSpeedCacheLog as LSLog;
use LiteSpeedCacheConfig as Conf;
class LiteSpeedCacheCore
{
const LSHEADER_PURGE = 'X-Litespeed-Purge2';
const LSHEADER_CACHE_CONTROL = 'X-Litespeed-Cache-Control';
const LSHEADER_CACHE_TAG = 'X-Litespeed-Tag';
const LSHEADER_CACHE_VARY = 'X-Litespeed-Vary';
private $cacheTags = array();
private $purgeTags;
private $config;
private $esiTtl;
private $specificPrices = array();
public function __construct(LiteSpeedCacheConfig $config)
{
$this->config = $config;
$this->purgeTags = array('pub' => array(), 'priv' => array());
}
public function setEsiTtl($ttl)
{
$this->esiTtl = $ttl; // todo: may retire
}
public function isCacheableRoute($controllerType, $controllerClass)
{
$reason = '';
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
$reason = 'Not GET request';
} elseif ($controllerType != DispatcherCore::FC_FRONT) {
$reason = 'Not FC_FRONT';
} else {
$tag = $this->config->isControllerCacheable($controllerClass);
if ($tag === false) {
$reason = 'Not in defined cacheable controllers';
} elseif (!$this->inDoNotCacheRules($reason) && $tag) {
$this->addCacheTags($tag);
}
}
if ($reason) {
$reason = 'Route not cacheable: ' . $controllerClass . ' - ' . $reason;
} elseif (defined('_LITESPEED_DEBUG_') && _LITESPEED_DEBUG_ >= LSLog::LEVEL_CACHE_ROUTE) {
LSLog::log('route in defined cacheable controllers ' . $controllerClass, LSLog::LEVEL_CACHE_ROUTE);
}
// check purge by controller
if ($ptags = $this->config->isPurgeController($controllerClass)) {
// usually won't happen for both pub and priv together
if (!empty($ptags['pub'])) {
$this->purgeByTags($ptags['pub'], false, $controllerClass);
}
if (!empty($ptags['priv'])) {
$this->purgeByTags($ptags['priv'], true, $controllerClass);
}
}
return $reason;
}
public function inDoNotCacheRules(&$reason)
{
$nocache = $this->config->getNoCacheConf();
$requrl = $_SERVER['REQUEST_URI'];
foreach ($nocache[Conf::CFG_NOCACHE_URL] as $url) {
$url1 = rtrim($url, '*');
if ($url1 !== $url) { // contains *
if (strpos($requrl, $url1) !== false) {
$reason = 'disabled url (partial match) ' . $url;
return true;
}
} elseif ($url == $requrl) {
$reason = 'disabled url (exact match) ' . $url;
return true;
}
}
foreach ($nocache[Conf::CFG_NOCACHE_VAR] as $var) {
if (isset($_REQUEST[$var])) {
$reason = 'contains param ' . $var;
return true;
}
}
return false;
}
public function hasNotification()
{
if (($smarty = Context::getContext()->smarty) !== null) {
$notification = $smarty->getTemplateVars('notifications');
if (is_array($notification)) {
if (!empty($notification['error'])
|| !empty($notification['warning'])
|| !empty($notification['success'])
|| !empty($notification['info'])) {
return true;
}
}
}
return false;
}
public function initCacheTagsByController($params)
{
if (!empty($this->cacheTags)) {
return; // already initialized
}
if (!isset($params['controller'])) {
if (_LITESPEED_DEBUG_ >= LSLog::LEVEL_UNEXPECTED) {
LSLog::log('initCacheTagsByController - no controller in param', LSLog::LEVEL_UNEXPECTED);
}
return;
}
$controller = $params['controller'];
$tag = null;
$entity = isset($params['entity']) ?
$params['entity'] // PS 1.7
: $controller->php_self; // PS 1.6
switch ($entity) {
case 'product':
if (method_exists($controller, 'getProduct')
&& ($p = $controller->getProduct()) != null) {
$tag = Conf::TAG_PREFIX_PRODUCT . $p->id;
}
break;
case 'category':
if (method_exists($controller, 'getCategory')
&& ($c = $controller->getCategory()) != null) {
$tag = Conf::TAG_PREFIX_CATEGORY . $c->id;
}
break;
}
if (!$tag && isset($params['id'])) {
$id = $params['id'];
switch ($controller->php_self) {
case 'product':
$tag = Conf::TAG_PREFIX_PRODUCT . $id;
break;
case 'category':
$tag = Conf::TAG_PREFIX_CATEGORY . $id;
break;
case 'manufacturer':
$tag = Conf::TAG_PREFIX_MANUFACTURER . $id;
break;
case 'supplier':
$tag = Conf::TAG_PREFIX_SUPPLIER . $id;
break;
case 'cms':
$tag = Conf::TAG_PREFIX_CMS . $id;
break;
}
}
if ($tag) {
$this->addCacheTags($tag);
} elseif (_LITESPEED_DEBUG_ >= LSLog::LEVEL_UNEXPECTED) {
LSLog::log('check what we have here - initCacheTagsByController', LSLog::LEVEL_UNEXPECTED);
}
}
public function addCacheTags($tag)
{
$old = count($this->cacheTags);
if (is_array($tag)) {
$this->cacheTags = array_unique(array_merge($this->cacheTags, $tag));
} elseif (!in_array($tag, $this->cacheTags)) {
$this->cacheTags[] = $tag;
}
return (count($this->cacheTags) > $old);
}
// return 1: added, 0: already exists, 2: already has purgeall
public function addPurgeTags($tag, $isPrivate)
{
$returnCode = 0;
$type = $isPrivate ? 'priv' : 'pub';
if (in_array('*', $this->purgeTags[$type])) {
return 2;
}
if (is_array($tag)) {
$oldcount = count($this->purgeTags[$type]);
$this->purgeTags[$type] = array_unique(array_merge($this->purgeTags[$type], $tag));
if (count($this->purgeTags[$type]) > $oldcount) {
$returnCode = 1;
}
} elseif (!in_array($tag, $this->purgeTags[$type])) {
$this->purgeTags[$type][] = $tag;
$returnCode = 1;
}
if (in_array('*', $this->purgeTags[$type])) {
$this->purgeTags[$type] = array('*'); // purge all
}
return $returnCode;
}
private function isNewPurgeTag($tag, $isPrivate)
{
$type = $isPrivate ? 'priv' : 'pub';
if (in_array('*', $this->purgeTags['pub'])
|| in_array($tag, $this->purgeTags[$type])
|| ($isPrivate && in_array('*', $this->purgeTags[$type]))) {
return false;
} else {
return true;
}
}
public function purgeByTags($tags, $isPrivate, $from)
{
if ($from && _LITESPEED_DEBUG_ >= LSLog::LEVEL_PURGE_EVENT) {
LSLog::log('purgeByTags from ' . $from, LSLog::LEVEL_PURGE_EVENT);
}
$this->addPurgeTags($tags, $isPrivate);
}
private function getPurgeTagsByProduct($id_product, $product, $isupdate)
{
$tags = array();
$pid = Conf::TAG_PREFIX_PRODUCT . $id_product;
if (!$this->isNewPurgeTag($pid, false)) {
return $tags; // has purge all or already added
}
$tags['pub'] = $this->config->getDefaultPurgeTagsByProduct();
$tags['pub'][] = $pid;
if ($product === null) {
// populate product
$product = new Product((int) $id_product);
}
$tags['pub'][] = Conf::TAG_PREFIX_MANUFACTURER . $product->id_manufacturer;
$tags['pub'][] = Conf::TAG_PREFIX_SUPPLIER . $product->id_supplier;
if (!$isupdate) {
// new or delete, also purge all suppliers and manufacturers list, as it shows # of products in it
$tags['pub'][] = Conf::TAG_PREFIX_MANUFACTURER;
$tags['pub'][] = Conf::TAG_PREFIX_SUPPLIER;
}
$cats = $product->getCategories();
if (!empty($cats)) {
foreach ($cats as $catid) {
$tags['pub'][] = Conf::TAG_PREFIX_CATEGORY . $catid;
}
}
return $tags;
}
private function getPurgeTagsByProductOrder($id_product, $includeCategory)
{
$tags = array();
$pid = Conf::TAG_PREFIX_PRODUCT . $id_product;
if (!$this->isNewPurgeTag($pid, false)) {
return $tags; // has purge all or already added
}
$tags[] = $pid;
if (!$includeCategory) {
return $tags;
}
$product = new Product((int) $id_product);
$tags[] = Conf::TAG_PREFIX_MANUFACTURER . $product->id_manufacturer;
$tags[] = Conf::TAG_PREFIX_SUPPLIER . $product->id_supplier;
$cats = $product->getCategories();
if (!empty($cats)) {
foreach ($cats as $catid) {
$tags[] = Conf::TAG_PREFIX_CATEGORY . $catid;
}
}
return $tags;
}
private function getPurgeTagsByWebService()
{
$method = $_SERVER['REQUEST_METHOD'];
if ($method == 'GET' || $method == 'HEAD') {
// possible methods are POST, PUT, DELETE
return;
}
$resources = explode('/', $_REQUEST['url']);
if (empty($resources) || count($resources) != 2 || intval($resources[1]) != $resources[1]) {
if (_LITESPEED_DEBUG_ >= LSLog::LEVEL_WEBSERVICE_DETAIL)
LSLog::log("WebService Purge - Ignored $method " . var_export($resources, 1), LSLog::LEVEL_WEBSERVICE_DETAIL);
return;
}
$pubtags = [];
$restype = $resources[0];
$resid = $resources[1];
switch ($resources[0]) {
case 'categories':
$pubtags[] = Conf::TAG_PREFIX_CATEGORY . $resid;
break;
case 'stock_availables':
if ($prod_id = $this->getProdidByStockAvailId($resid)) {
$pubtags[] = Conf::TAG_PREFIX_PRODUCT . $prod_id;
}
break;
default:
return;
}
$tags = ['pub' => $pubtags];
return $tags;
}
private function getProdidByStockAvailId($stockavail_id)
{
if ($stockavail_id <= 0) {
return null;
}
$query = new DbQuery();
$query->select('id_product');
$query->from('stock_available');
$query->where('id_stock_available = ' . (int) $stockavail_id);
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
}
private function getPurgeTagsByOrder($order)
{
if ($order == null) { // $order is null, happened in bankwire module
return;
}
$tags = array();
$pubtags = array();
$hasStockStatusChange = false;
$flushOption = $this->config->get(Conf::CFG_FLUSH_PRODCAT);
$list = $order->getOrderDetailList();
foreach ($list as $product) {
$includeProd = $includeCats = false;
$outstock = ((int) $product['product_quantity_in_stock'] <= 0);
if ($outstock) {
$hasStockStatusChange = true;
}
switch ($flushOption) { // Flush Product and Categories When Product Qty Change
case 0: // 0: default - Flush product when qty change, flush categories when stock status change
$includeProd = true;
$includeCats = $outstock;
break;
case 1: // 1: Only flush product and categories when stock status change
$includeProd = $outstock;
$includeCats = $outstock;
break;
case 2: // 2: Flush product when stock status change, do not flush categories
$includeProd = $outstock;
$includeCats = false;
break;
case 3: // 3: Always flush product and categories when qty/stock status change
$includeProd = true;
$includeCats = true;
break;
}
if ($includeProd) {
$tags = $this->getPurgeTagsByProductOrder($product['product_id'], $includeCats);
if (!empty($tags)) {
$pubtags = array_merge($pubtags, $tags);
}
}
}
if (!empty($pubtags)) {
if ($hasStockStatusChange) {
$pubtags = array_merge($pubtags, $this->config->getDefaultPurgeTagsByProduct());
}
$tags['pub'] = array_unique($pubtags);
}
return $tags;
}
private function getPurgeTagsByCategory($category)
{
$tags = array();
if ($category == null) {
return $tags; // extra proctection. happened on a client BigBuySynchronizer.php
}
$cid = Conf::TAG_PREFIX_CATEGORY . $category->id_category;
if (!$this->isNewPurgeTag($cid, false)) {
return $tags; // has purge all or already added
}
$tags['pub'] = $this->config->getDefaultPurgeTagsByCategory();
$tags['pub'][] = $cid;
if (!$category->is_root_category) {
$cats = $category->getParentsCategories();
if (!empty($cats)) {
foreach ($cats as $catid) {
$tags['pub'][] = Conf::TAG_PREFIX_CATEGORY . $catid;
}
}
}
return $tags;
}
public function purgeByCatchAllMethod($method, $args)
{
$tags = $this->getPurgeTagsByHookMethod($method, $args);
if (empty($tags)) {
if (($tags === null)
&& (strcasecmp($method, 'hookaddWebserviceResources') != 0)
&& (_LITESPEED_DEBUG_ >= LSLog::LEVEL_UNEXPECTED)) {
LSLog::log('Unexpected hook function called' . $method, LSLog::LEVEL_UNEXPECTED);
}
return;
}
if (!defined('_LITESPEED_CALLBACK_')) {
define('_LITESPEED_CALLBACK_', 1);
ob_start('LiteSpeedCache::callbackOutputFilter');
}
$res = 0;
if (!empty($tags['pub'])) {
$res |= $this->addPurgeTags($tags['pub'], false);
}
if (!empty($tags['priv'])) {
$res |= $this->addPurgeTags($tags['priv'], true);
}
if (_LITESPEED_DEBUG_ >= LSLog::LEVEL_PURGE_EVENT) {
LSLog::log('purgeByCatchAll ' . $method . ' res=' . $res, LSLog::LEVEL_PURGE_EVENT);
}
}
private function getPurgeTagsByHookMethod($method, $args)
{
if (strncmp($method, 'hook', 4) != 0) {
return null;
}
$event = Tools::strtolower(Tools::substr($method, 4));
$tags = array();
switch ($event) {
case 'actioncustomerlogoutafter':
case 'actionauthentication':
case 'actioncustomeraccountadd':
$tags['priv'] = array('*');
break;
case 'actionproductadd':
case 'actionproductsave':
case 'actionproductupdate':
case 'actionproductdelete':
return $this->getPurgeTagsByProduct($args['id_product'], $args['product'], false);
case 'actionobjectspecificpricecoreaddafter':
case 'actionobjectspecificpricecoredeleteafter':
return $this->getPurgeTagsByProduct($args['object']->id_product, null, true);
case 'actionwatermark':
case 'updateproduct':
case 'actionupdatequantity':
if (isset($args['id_product'])) {
return $this->getPurgeTagsByProduct($args['id_product'], null, true);
}
break;
case 'displayorderconfirmation':
return $this->getPurgeTagsByOrder($args['order']);
case 'categoryupdate':
case 'actioncategoryupdate':
case 'actioncategoryadd':
case 'actioncategorydelete':
if (isset($args['category'])) {
return $this->getPurgeTagsByCategory($args['category']);
}
break;
case 'actionobjectcmsupdateafter':
case 'actionobjectcmsdeleteafter':
case 'actionobjectcmsaddafter':
$tags['pub'] = array(Conf::TAG_PREFIX_CMS . $args['object']->id,
Conf::TAG_PREFIX_CMS, // cmscategory
Conf::TAG_SITEMAP,
);
break;
case 'actionobjectsupplierupdateafter':
case 'actionobjectsupplierdeleteafter':
case 'actionobjectsupplieraddafter':
$tags['pub'] = array(Conf::TAG_PREFIX_SUPPLIER . $args['object']->id,
Conf::TAG_PREFIX_SUPPLIER, // all supplier
Conf::TAG_SITEMAP,
);
break;
case 'actionobjectmanufacturerupdateafter':
case 'actionobjectmanufacturerdeleteafter':
case 'actionobjectmanufactureraddAfter':
$tags['pub'] = array(
Conf::TAG_PREFIX_MANUFACTURER . $args['object']->id,
Conf::TAG_PREFIX_MANUFACTURER,
Conf::TAG_SITEMAP, // allbrands
);
break;
case 'actionobjectstoreupdateafter':
$tags['pub'] = array(Conf::TAG_STORES);
break;
case 'addwebserviceresources':
return $this->getPurgeTagsByWebService();
default: // custom defined events
return $this->config->getPurgeTagsByEvent($event);
}
return $tags;
}
private function getPurgeHeader()
{
$hasPub = !empty($this->purgeTags['pub']);
$hasPriv = !empty($this->purgeTags['priv']);
if (!$hasPub && !$hasPriv) {
return '';
}
$purgeHeader = '';
$pre = 'tag=' . LiteSpeedCacheHelper::getTagPrefix();
if (in_array('*', $this->purgeTags['pub'])) {
// when purge all public, also purge all private block
$purgeHeader .= $pre . ',' . $pre . '_PRIV';
LiteSpeedCacheHelper::clearInternalCache();
} else {
$pre .= '_';
if ($hasPub) {
$purgeHeader .= $pre . implode(",$pre", $this->purgeTags['pub']);
}
if ($hasPriv) {
if ($purgeHeader) {
$purgeHeader .= ';'; // has public
}
$purgeHeader .= 'private,';
if (in_array('*', $this->purgeTags['priv'])) {
$purgeHeader .= '*';
} else {
$purgeHeader .= $pre . implode(",$pre", $this->purgeTags['priv']);
}
}
}
if ($purgeHeader) {
$purgeHeader = self::LSHEADER_PURGE . ': ' . $purgeHeader;
}
return $purgeHeader;
}
public function purgeEntireStorage($from)
{
$purgeHeader = self::LSHEADER_PURGE . ': *';
header($purgeHeader);
LiteSpeedCacheHelper::clearInternalCache();
LSLog::log('Set header ' . $purgeHeader . ' (' . $from . ')', LSLog::LEVEL_FORCE);
}
public function checkSpecificPrices($specific_prices)
{
// LSLog::log('specific price ' . var_export($specific_prices, 1));
if ($specific_prices['from'] == $specific_prices['to'] && $specific_prices['to'] == '0000-00-00 00:00:00') {
// no date range
return;
}
if (is_array($specific_prices) && isset($specific_prices['to'])) {
$this->specificPrices[] = array('from' => $specific_prices['from'],
'to' => $specific_prices['to'],
);
}
}
private function adjustTtlBySpecificPrices($ttl)
{
if (empty($this->specificPrices)) {
return $ttl;
}
$ttl0 = $ttl;
$now = time();
foreach ($this->specificPrices as $sp) {
if ($sp['from'] != '0000-00-00 00:00:00') { // from is set
$from = strtotime($sp['from']);
if ($from > $now) { // not start yet, go by from date
if (($from - $now) < $ttl) {
$ttl = $from - $now;
}
continue;
}
}
if ($sp['to'] != '0000-00-00 00:00:00') { // to is set
$to = strtotime($sp['to']);
if (($to > $now) && (($to - $now) < $ttl)) {
$ttl = $to - $now;
}
}
}
if (defined('_LITESPEED_DEBUG_') && _LITESPEED_DEBUG_ >= LSLog::LEVEL_SPECIFIC_PRICE) {
if ($ttl0 != $ttl) {
LSLog::log("TTL adjusted due to specific prices from $ttl0 to $ttl", LSLog::LEVEL_SETHEADER);
} else {
LSLog::log('Detected specific prices, but no TTL adjustment', LSLog::LEVEL_SPECIFIC_PRICE);
}
}
return $ttl;
}
public function setCacheControlHeader()
{
if (headers_sent()) {
return;
}
$headers = array();
if (($purgeHeader = $this->getPurgeHeader()) != '') {
$headers[] = $purgeHeader;
}
$cacheControlHeader = '';
$ccflag = LSC::getCCFlag();
if ((($ccflag & LSC::CCBM_NOT_CACHEABLE) == 0) && (($ccflag & LSC::CCBM_CACHEABLE) != 0)) {
$prefix = LiteSpeedCacheHelper::getTagPrefix();
$tags = array();
$ttl = (($ccflag & LSC::CCBM_ESI_REQ) == 0) ? '' : $this->esiTtl;
if ($ttl == '') {
$ttl = (($ccflag & LSC::CCBM_PRIVATE) == 0) ?
$this->config->get(Conf::CFG_PUBLIC_TTL) : $this->config->get(Conf::CFG_PRIVATE_TTL);
}
$ttl = $this->adjustTtlBySpecificPrices($ttl);
if (($ccflag & LSC::CCBM_PRIVATE) == 0) {
$cacheControlHeader .= 'public,max-age=' . $ttl;
$tags[] = $prefix; // for purge all PS cache
$shopId = Context::getContext()->shop->id; // todo: should have in env
$tags[] = $prefix . '_' . Conf::TAG_PREFIX_SHOP . $shopId; // for purge one shop
} else {
$cacheControlHeader .= 'private,no-vary,max-age=' . $ttl;
$tags[] = 'public:' . $prefix . '_PRIV'; // in private cache, use public:tag_name_PRIV
}
foreach ($this->cacheTags as $tag) {
$tags[] = $prefix . '_' . $tag;
}
$headers[] = self::LSHEADER_CACHE_TAG . ': ' . implode(',', $tags);
if (($ccflag & LSC::CCBM_GUEST) != 0) {
$guestmode = $this->config->get(Conf::CFG_GUESTMODE);
if ($guestmode == 0 || $guestmode == 2) {
// if disabled (shouldnot happen, meaning htaccess out of sync), or first page only
$headers[] = 'LSC-Cookie: PrestaShop-lsc=guest'; //set pass-through cookie
}
}
} else {
$cacheControlHeader = 'no-cache';
}
if (($ccflag & LSC::CCBM_ESI_ON) != 0) {
$cacheControlHeader .= ',esi=on';
}
if (($ccflag & LSC::CCBM_VARY_CHANGED) && ($ccflag & LSC::CCBM_ESI_REQ)) {
$cacheControlHeader .= ',reload-after';
}
$headers[] = self::LSHEADER_CACHE_CONTROL . ': ' . $cacheControlHeader;
foreach ($headers as $header) {
header($header);
}
if (defined('_LITESPEED_DEBUG_') && _LITESPEED_DEBUG_ >= LSLog::LEVEL_SETHEADER) {
LSLog::log('SetHeader ' . implode("\n", $headers), LSLog::LEVEL_SETHEADER);
}
}
}