name = 'litespeedcache'; // self::MODULE_NAME was rejected by validator $this->tab = 'administration'; $this->author = 'LiteSpeedTech'; $this->version = self::getVersion(); $this->need_instance = 0; $this->module_key = '2a93f81de38cad872010f09589c279ba'; $this->ps_versions_compliancy = [ 'min' => '1.6', // support both 1.6 and 1.7 'max' => _PS_VERSION_, ]; $this->controllers = ['esi']; $this->bootstrap = true; parent::__construct(); $this->displayName = $this->l('LiteSpeed Cache Plugin'); $this->description = $this->l('Integrates with LiteSpeed Full Page Cache on LiteSpeed Server.'); $this->config = LiteSpeedCacheConfig::getInstance(); // instantiate cache even when module not enabled, because may still need purge cache. $this->cache = new LiteSpeedCacheCore($this->config); $this->esiInjection = ['tracker' => [], 'marker' => [], ]; self::$ccflag |= $this->config->moduleEnabled(); if (!defined('_LITESPEED_CACHE_')) { define('_LITESPEED_CACHE_', 1); } if (!defined('_LITESPEED_DEBUG_')) { define('_LITESPEED_DEBUG_', 0); } if (self::isActiveForUser()) { require_once _PS_MODULE_DIR_ . 'litespeedcache/thirdparty/lsc_include.php'; } } public static function getVersion() { return '1.4.0'; } public static function isActive() { return (self::$ccflag & (self::CCBM_MOD_ACTIVE | self::CCBM_MOD_ALLOWIP)) != 0; } public static function isActiveForUser() { return (self::$ccflag & self::CCBM_MOD_ACTIVE) != 0; } public static function isRestrictedIP() { return (self::$ccflag & self::CCBM_MOD_ALLOWIP) != 0; } public static function isCacheable() { return ((self::$ccflag & self::CCBM_NOT_CACHEABLE) == 0) && ((self::$ccflag & self::CCBM_CACHEABLE) != 0); } public static function isEsiRequest() { return (self::$ccflag & self::CCBM_ESI_REQ) != 0; } public static function canInjectEsi() { return (self::$ccflag & self::CCBM_CAN_INJECT_ESI) != 0; } public static function getCCFlag() { return self::$ccflag; } public static function getCCFlagDebugInfo() { $flag = self::$ccflag; $info = []; if ($flag & self::CCBM_MOD_ALLOWIP) { $info[] = 'Allowed IP'; } if ($flag & self::CCBM_FRONT_CONTROLLER) { $info[] = 'FrontController'; } if ($flag & self::CCBM_GUEST) { $info[] = 'Guest'; } if ($flag & self::CCBM_CACHEABLE) { $info[] = 'Cacheable'; } if (self::$no_cache_reason) { $info[] = 'NO CACHE reason: ' . self::$no_cache_reason; } return implode('; ', $info); } public function setEsiOn() { self::$ccflag |= self::CCBM_ESI_ON; } private static function myInstance() { return Module::getInstanceByName(self::MODULE_NAME); } public function hookActionDispatcher($params) { if (!self::isActiveForUser()) { return; } $controllerType = $params['controller_type']; $controllerClass = $params['controller_class']; if (_LITESPEED_DEBUG_ > 0) { $notprinted = ['AdminDashboardController', 'AdminGamificationController', 'AdminAjaxFaviconBOController']; if (in_array($controllerClass, $notprinted)) { LiteSpeedCacheLog::setDebugLevel(0); // disable logging for current request } } $status = $this->checkDispatcher($controllerType, $controllerClass); LscIntegration::preDispatchAction(); if (_LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_CACHE_ROUTE) { LiteSpeedCacheLog::log(__FUNCTION__ . ' type=' . $controllerType . ' controller=' . $controllerClass . ' req=' . $_SERVER['REQUEST_URI'] . ' :' . $status, LiteSpeedCacheLog::LEVEL_CACHE_ROUTE); } } public function hookOverrideLayoutTemplate($params) { if (self::isCacheable()) { if ($this->cache->hasNotification()) { $this->setNotCacheable('Has private notification'); } elseif ((self::$ccflag & self::CCBM_ESI_REQ) == 0) { $this->cache->initCacheTagsByController($params); } } } public function hookDisplayOverrideTemplate($params) { //if (self::isCacheable() && isset($params['entity']) && isset($params['id'])) { if (!self::isCacheable()) { return; } $this->cache->initCacheTagsByController($params); } public function hookActionProductSearchAfter($params) { // Hook::exec('actionProductSearchAfter', $searchVariables); if (self::isCacheable() && isset($params['products'])) { foreach ($params['products'] as $p) { if (!empty($p['specific_prices'])) { $this->cache->checkSpecificPrices($p['specific_prices']); } } } } public function hookFilterCategoryContent($params) { if (self::isCacheable()) { if (isset($params['object']['id'])) { $this->cache->addCacheTags(LiteSpeedCacheConfig::TAG_PREFIX_CATEGORY . $params['object']['id']); } } return $params; } public function hookFilterProductContent($params) { if (self::isCacheable()) { if (isset($params['object']['id'])) { $this->cache->addCacheTags(LiteSpeedCacheConfig::TAG_PREFIX_PRODUCT . $params['object']['id']); } if (!empty($params['object']['specific_prices'])) { $this->cache->checkSpecificPrices($params['object']['specific_prices']); } } return $params; } public function hookFilterCmsCategoryContent($params) { if (self::isCacheable()) { // any cms page update, will purge all cmscategory pages, as the assignment may change, // so we do not distinguish by cms category id $this->cache->addCacheTags(LiteSpeedCacheConfig::TAG_PREFIX_CMS); } return $params; } public function hookFilterCmsContent($params) { if (self::isCacheable() && isset($params['object']['id'])) { $this->cache->addCacheTags(LiteSpeedCacheConfig::TAG_PREFIX_CMS . $params['object']['id']); } return $params; } // this is catchall function for purge events public function __call($method, $args) { if (self::isActive()) { $keys = array_keys($args); if (count($keys) == 1 && $keys[0] == 0) { $args = $args[0]; } $this->cache->purgeByCatchAllMethod($method, $args); } } /* our own hook * Required field $params['from'] * $params['public'] & $params['private'] one has to exist, array of tags * $params['ALL'] - entire cache storage */ public function hookLitespeedCachePurge($params) { $msg = __FUNCTION__ . ' '; $err = ''; if (!isset($params['from'])) { $err = $msg . 'Illegal entrance - missing from'; } else { $msg .= 'from ' . $params['from']; if (self::isActive()) { if (isset($params['public'])) { $this->cache->purgeByTags($params['public'], false, $msg); } elseif (isset($params['private'])) { $this->cache->purgeByTags($params['private'], true, $msg); } elseif (isset($params['ALL'])) { $this->cache->purgeEntireStorage($msg); } else { $err = $msg . 'Illegal - missing public or private'; } } else { // only allow purge all PS data if not active if (isset($params['public']) && $params['public'] == '*') { $this->cache->purgeByTags('*', false, $msg); } else { $err = $msg . 'Illegal tags - module not activated, can only take *'; } } } if ($err && _LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_PURGE_EVENT) { LiteSpeedCacheLog::log($err, LiteSpeedCacheLog::LEVEL_PURGE_EVENT); } } // allow other modules to set public function hookLitespeedNotCacheable($params) { if (!self::isActiveForUser()) { return; } $reason = ''; if (isset($params['reason'])) { $reason = $params['reason']; } if (isset($params['from'])) { $reason .= ' from ' . $params['from']; } $this->setNotCacheable($reason); } // if debug enabled, show generation timestamp in comments public function hookDisplayFooterAfter($params) { if (self::isCacheable() && _LITESPEED_DEBUG_) { $comment = ''; if (_LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_FOOTER_COMMENT) { LiteSpeedCacheLog::log( 'Add html comments in footer ' . $comment, LiteSpeedCacheLog::LEVEL_FOOTER_COMMENT ); } return $comment; } } // called by Media override addJsDef public static function filterJsDef(&$jsDef) { if ((self::$ccflag & self::CCBM_CAN_INJECT_ESI) == 0) { return; } $injected = LscIntegration::filterJSDef($jsDef); if (!empty($injected)) { $lsc = self::myInstance(); foreach ($injected as $id => $item) { if (!isset($lsc->esiInjection['marker'][$id])) { $lsc->esiInjection['marker'][$id] = $item; } } } } // return status private function checkDispatcher($controllerType, $controllerClass) { if (!self::isActiveForUser()) { // check for ip restriction return 'not active'; } if (!defined('_LITESPEED_CALLBACK_')) { define('_LITESPEED_CALLBACK_', 1); ob_start('LiteSpeedCache::callbackOutputFilter'); } if ($controllerType == DispatcherCore::FC_FRONT) { self::$ccflag |= self::CCBM_FRONT_CONTROLLER; } if ($controllerClass == 'litespeedcacheesiModuleFrontController') { self::$ccflag |= self::CCBM_ESI_REQ; return 'esi request'; } // here also check purge controller if (($reason = $this->cache->isCacheableRoute($controllerType, $controllerClass)) != '') { $this->setNotCacheable($reason); return $reason; } if (isset($_SERVER['LSCACHE_VARY_VALUE']) && ($_SERVER['LSCACHE_VARY_VALUE'] == 'guest' || $_SERVER['LSCACHE_VARY_VALUE'] == 'guestm')) { self::$ccflag |= self::CCBM_CACHEABLE | self::CCBM_GUEST; // no ESI allowed return 'cacheable guest'; } self::$ccflag |= (self::CCBM_CACHEABLE | self::CCBM_CAN_INJECT_ESI); return 'cacheable & allow esiInject'; } public function addCacheControlByEsiModule($item) { if (!self::isActive()) { $this->cache->purgeByTags('*', false, 'request esi while module is not active'); return; } $ttl = $item->getTTL(); if ($item->onlyCacheEmtpy() && $item->getContent() !== '') { $ttl = 0; } if ($ttl === 0 || $ttl === '0') { self::$ccflag |= self::CCBM_NOT_CACHEABLE; self::$no_cache_reason .= 'Set by ESIModule ' . $item->getConf()->getModuleName(); } else { $this->cache->addCacheTags($item->getTags()); self::$ccflag |= self::CCBM_CACHEABLE; if ($item->isPrivate()) { self::$ccflag |= self::CCBM_PRIVATE; } if ($ttl > 0) { $this->cache->setTTL($ttl); } } } // return changed public static function setVaryCookie() { if ((self::$ccflag & self::CCBM_VARY_CHECKED) == 0) { if (LiteSpeedCacheVaryCookie::setVary()) { self::$ccflag |= self::CCBM_VARY_CHANGED; } self::$ccflag |= self::CCBM_VARY_CHECKED; } return (self::$ccflag & self::CCBM_VARY_CHANGED) != 0; } public static function callbackOutputFilter($buffer) { $lsc = self::myInstance(); if ((self::$ccflag & self::CCBM_FRONT_CONTROLLER) > 0) { self::setVaryCookie(); } /** retired old logic, when vary cookie change, still cacheable * if ((self::$ccflag & self::CCBM_FRONT_CONTROLLER) > 0 && self::setVaryCookie() && self::isCacheable()) { * //condition order is fixed * $lsc->setNotCacheable('Env change'); * } **/ $code = http_response_code(); if ($code == 404) { self::$ccflag |= self::CCBM_ERROR_CODE; if (LiteSpeedCacheHelper::isStaticResource($_SERVER['REQUEST_URI'])) { $buffer = ''; self::$ccflag &= ~self::CCBM_CAN_INJECT_ESI; } } elseif ($code != 200) { self::$ccflag |= self::CCBM_ERROR_CODE; $lsc->setNotCacheable('Response code is ' . $code); } if (self::canInjectEsi() && (count($lsc->esiInjection['marker']) || self::isCacheable())) { // if no injection, but cacheable, still need to check token $buffer = $lsc->replaceEsiMarker($buffer); } $lsc->cache->setCacheControlHeader(); /* for testing * // $tname = tempnam('/tmp/t','A'); * // file_put_contents($tname, $buffer); */ return $buffer; } private function registerEsiMarker($params, $conf) { $item = new LiteSpeedCacheEsiItem($params, $conf); $id = $item->getId(); if (!isset($this->esiInjection['marker'][$id])) { $this->esiInjection['marker'][$id] = $item; } return '_LSCESI-' . $id . '-START_'; } private function replaceEsiMarker($buf) { if (count($this->esiInjection['marker'])) { // U :ungreedy s: dotall m: multiline $nb = preg_replace_callback( ['/_LSC(ESI)-(.+)-START_(.*)_LSCESIEND_/Usm', '/(\'|\")_LSCESIJS-(.+)-START__LSCESIEND_(\'|\")/Usm', ], function ($m) { // inject ESI even it's not cacheable $id = $m[2]; $lsc = self::myInstance(); if (!isset($lsc->esiInjection['marker'][$id])) { $id = stripslashes($id); } if (!isset($lsc->esiInjection['marker'][$id])) { // should not happen if (_LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_UNEXPECTED) { LiteSpeedCacheLog::log('Lost Injection ' . $id, LiteSpeedCacheLog::LEVEL_UNEXPECTED); } return ''; } $item = $lsc->esiInjection['marker'][$id]; $esiInclude = $item->getInclude(); if ($esiInclude === false) { if ($item->getParam('pt') == $item::ESI_JSDEF) { LscIntegration::processJsDef($item); // content set inside } else { $item->setContent($m[3]); } $esiInclude = $item->getInclude(); } return $esiInclude; }, $buf ); } else { // log here, shouldn't happen $nb = $buf; } $bufInline = ''; // Tools::getToken() is not really used for cacheable pages // Tools::getToken(false) is used -------------- caninject $static_token = Tools::getToken(false); $tkparam = ['pt' => LiteSpeedCacheEsiItem::ESI_TOKEN, 'm' => LscToken::NAME, 'd' => 'static']; $tkitem = new LiteSpeedCacheEsiItem($tkparam, $this->config->getEsiModuleConf(LscToken::NAME)); $tkitem->setContent($static_token); // we always add to inline $this->esiInjection['marker'][$tkitem->getId()] = $tkitem; $envparam = ['pt' => LiteSpeedCacheEsiItem::ESI_ENV, 'm' => LscEnv::NAME]; $envitem = new LiteSpeedCacheEsiItem($envparam, $this->config->getEsiModuleConf(LscEnv::NAME)); $envitem->setContent(''); $this->esiInjection['marker'][$envitem->getId()] = $envitem; if (self::isCacheable()) { // only if cacheable, do global replacement if (strpos($nb, $static_token)) { $tokenInc = $tkitem->getInclude(); $nb = str_replace($static_token, $tokenInc, $nb); } $nb = $envitem->getInclude() . $nb; // must be first one } $allPrivateItems = []; // last adding esi:inline, which needs to be in front of esi:include foreach ($this->esiInjection['marker'] as $item) { $inline = $item->getInline(); if ($inline !== false) { // for ajax call, it's possible no inline content $bufInline .= $inline; if ($item->isPrivate()) { $allPrivateItems[] = $item; } } } if ($bufInline) { if (!empty($allPrivateItems)) { LiteSpeedCacheHelper::syncItemCache($allPrivateItems); } self::$ccflag |= self::CCBM_ESI_ON; } if ($bufInline && _LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_ESI_OUTPUT) { LiteSpeedCacheLog::log('ESI inline output ' . $bufInline, LiteSpeedCacheLog::LEVEL_ESI_OUTPUT); } return $bufInline . $nb; } public function hookLitespeedEsiBegin($params) { if ((self::$ccflag & self::CCBM_CAN_INJECT_ESI) == 0) { return ''; } $err = 0; $err_field = ''; $m = $f = 'NA'; $pt = LiteSpeedCacheEsiItem::ESI_SMARTYFIELD; if (isset($params['m'])) { $m = $params['m']; } else { $err |= 1; $err_field .= 'm '; } if (isset($params['field'])) { $f = $params['field']; } else { $err |= 1; $err_field .= 'field '; } if (count($this->esiInjection['tracker']) > 0) { $err |= 2; } $esiParam = ['pt' => $pt, 'm' => $m, 'f' => $f]; if ($f == 'widget' && isset($params['hook'])) { $esiParam['h'] = $params['hook']; } elseif ($f == 'widget_block') { if (isset($params['tpl'])) { $esiParam['t'] = $params['tpl']; } else { $err |= 1; $err_field .= 'tpl '; } } $conf = $this->config->canInjectEsi($m, $esiParam); if ($conf == false) { $err |= 4; } array_push($this->esiInjection['tracker'], $err); // check here for template name if ($err) { if (_LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_CUST_SMARTY) { $msg = ''; if ($err & 1) { $msg .= 'Missing hookLitespeedEsiBegin param (' . $err_field . '). '; } if ($err & 2) { $msg .= 'Ignore due to nested hookLitespeedEsiBegin. '; } if ($err & 4) { $msg .= 'Cannot inject ESI for ' . $m; } LiteSpeedCacheLog::log(__FUNCTION__ . ' ' . $msg, LiteSpeedCacheLog::LEVEL_CUST_SMARTY); } return ''; } return $this->registerEsiMarker($esiParam, $conf); } public function hookLitespeedEsiEnd($params) { if ((self::$ccflag & self::CCBM_CAN_INJECT_ESI) == 0) { return ''; } $res = array_pop($this->esiInjection['tracker']); if ($res === 0) { // begin has no error, output end marker // here simply output marker return self::ESI_MARKER_END; } if (_LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_CUST_SMARTY) { // check here for template name $err = ($res === null) ? ' Mismatched hookLitespeedEsiEnd detected' : ' Ignored hookLitespeedEsiEnd due to error in hookLitespeedEsiBegin'; LiteSpeedCacheLog::log(__FUNCTION__ . $err, LiteSpeedCacheLog::LEVEL_CUST_SMARTY); } return ''; } // used by override hook, return false or marker public static function injectRenderWidget($module, $hook_name) { if ((self::$ccflag & self::CCBM_CAN_INJECT_ESI) == 0) { return false; } $lsc = self::myInstance(); $m = $module->name; $pt = LiteSpeedCacheEsiItem::ESI_RENDERWIDGET; $esiParam = ['pt' => $pt, 'm' => $m, 'h' => $hook_name]; $conf = $lsc->config->canInjectEsi($m, $esiParam); if ($conf == false) { return false; } if (_LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_ESI_INCLUDE) { LiteSpeedCacheLog::log(__FUNCTION__ . " $m : $hook_name", LiteSpeedCacheLog::LEVEL_ESI_INCLUDE); } return $lsc->registerEsiMarker($esiParam, $conf); } // used by override hook public static function injectCallHook($module, $method) { if ((self::$ccflag & self::CCBM_CAN_INJECT_ESI) == 0) { return false; } $lsc = self::myInstance(); $m = $module->name; $pt = LiteSpeedCacheEsiItem::ESI_CALLHOOK; $esiParam = ['pt' => $pt, 'm' => $m, 'mt' => $method]; $conf = $lsc->config->canInjectEsi($m, $esiParam); if ($conf == false) { return false; } if (_LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_ESI_INCLUDE) { LiteSpeedCacheLog::log(__FUNCTION__ . " $m : $method", LiteSpeedCacheLog::LEVEL_ESI_INCLUDE); } return $lsc->registerEsiMarker($esiParam, $conf); } public static function forceNotCacheable($reason) { $lsc = self::myInstance(); $lsc->setNotCacheable($reason); } // allow other plugins to set current response not cacheable private function setNotCacheable($reason = '') { if (!self::isActive()) { // not check ip for force nocache return; } self::$ccflag |= self::CCBM_NOT_CACHEABLE; self::$no_cache_reason .= $reason; if ($reason && _LITESPEED_DEBUG_ >= LiteSpeedCacheLog::LEVEL_NOCACHE_REASON) { LiteSpeedCacheLog::log(__FUNCTION__ . ' - ' . $reason, LiteSpeedCacheLog::LEVEL_NOCACHE_REASON); } } public function getContent() { Tools::redirectAdmin($this->context->link->getAdminLink('AdminLiteSpeedCacheConfig')); } public function install() { $this->installTab(); if (Shop::isFeatureActive()) { Shop::setContext(Shop::CONTEXT_ALL); } if (parent::install()) { $all = json_encode($this->config->getDefaultConfData(LiteSpeedCacheConfig::ENTRY_ALL)); $shop = json_encode($this->config->getDefaultConfData(LiteSpeedCacheConfig::ENTRY_SHOP)); $mod = json_encode($this->config->getDefaultConfData(LiteSpeedCacheConfig::ENTRY_MODULE)); Configuration::updateValue(LiteSpeedCacheConfig::ENTRY_ALL, $all); Configuration::updateValue(LiteSpeedCacheConfig::ENTRY_SHOP, $shop); Configuration::updateValue(LiteSpeedCacheConfig::ENTRY_MODULE, $mod); LiteSpeedCacheHelper::htAccessBackup('b4lsc'); return $this->installHooks(); } else { return false; } } public function uninstall() { $this->uninstallTab(); LiteSpeedCacheHelper::htAccessUpdate(0, 0, 0); $this->cache->purgeByTags('*', false, 'from uninstall'); Configuration::deleteByName(LiteSpeedCacheConfig::ENTRY_ALL); Configuration::deleteByName(LiteSpeedCacheConfig::ENTRY_SHOP); Configuration::deleteByName(LiteSpeedCacheConfig::ENTRY_MODULE); return parent::uninstall(); } private function installHooks() { $hooks = $this->config->getReservedHooks(); foreach ($hooks as $hook) { if (!$this->registerHook($hook)) { return false; } } return true; } private function uninstallTab() { $definedtabs = $this->initTabs(); if (version_compare(_PS_VERSION_, '1.7.1.0', '>=')) { $this->tabs = $definedtabs; return null; } foreach ($definedtabs as $t) { if ($id_tab = (int) Tab::getIdFromClassName($t['class_name'])) { $tab = new Tab($id_tab); $tab->delete(); } } return $definedtabs; } private function installTab() { $definedtabs = $this->uninstallTab(); if ($definedtabs == null) { return; } foreach ($definedtabs as $t) { $tab = new Tab(); $tab->active = 1; $tab->class_name = $t['class_name']; $tab->name = []; foreach (Language::getLanguages(true) as $lang) { $tab->name[$lang['id_lang']] = $t['name']; } $tab->id_parent = is_int($t['ParentClassName']) ? $t['ParentClassName'] : (int) Tab::getIdFromClassName($t['ParentClassName']); $tab->module = $this->name; $tab->add(); } } private function initTabs() { $root_node = version_compare(_PS_VERSION_, '1.7.1.0', '>=') ? 'AdminAdvancedParameters' : 0; $definedtabs = [ [ 'class_name' => 'AdminLiteSpeedCache', 'name' => $this->l('LiteSpeed Cache'), // this will use the default admin lang 'visible' => 1, 'icon' => 'flash_on', 'ParentClassName' => $root_node, ], [ 'class_name' => 'AdminLiteSpeedCacheManage', 'name' => $this->l('Manage'), 'visible' => 1, 'ParentClassName' => 'AdminLiteSpeedCache', ], [ 'class_name' => 'AdminLiteSpeedCacheConfig', 'name' => $this->l('Configuration'), 'visible' => 1, 'ParentClassName' => 'AdminLiteSpeedCache', ], [ 'class_name' => 'AdminLiteSpeedCacheCustomize', 'name' => $this->l('Customization'), 'visible' => 1, 'ParentClassName' => 'AdminLiteSpeedCache', ], ]; return $definedtabs; } }