Files
2025-06-24 14:14:35 +02:00

313 lines
11 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-2020 LiteSpeed Technologies, Inc. (https://www.litespeedtech.com)
* @license https://opensource.org/licenses/GPL-3.0
*/
use LiteSpeedCacheLog as LSLog;
class LiteSpeedCacheVaryCookie extends CookieCore
{
const BM_HAS_VARYCOOKIE = 1;
const BM_VARYCOOKIE_CHANGED = 2;
const BM_HAS_VARYVALUE = 4;
const BM_VARYVALUE_CHANGED = 8;
const BM_IS_GUEST = 16;
const BM_IS_MOBILEVIEW = 32;
const BM_UPDATE_FAILED = 128;
const DEFAULT_VARY_COOKIE_NAME = '_lscache_vary'; // system default
const AMP_VARY_COOKIE_NAME = '_lscache_vary_amp';
const PRIVATE_SESSION_COOKIE = 'lsc_private';
private $vd;
private $name;
private $debug_header;
private $status = 0;
public function __construct($name = '', $path = '')
{
if ($name == '') {
$name = self::DEFAULT_VARY_COOKIE_NAME;
}
// have to extend CookieCore in order to retrieve the internal variables.
parent::__construct($name, $path);
$this->_modified = false; // disallow others to call write
$this->_allow_writing = false;
$context = Context::getContext();
$psCookie = $context->cookie;
$this->_path = $psCookie->_path;
$this->_domain = $psCookie->_domain;
$this->_secure = $psCookie->_secure;
$this->name = $name;
if ($name == self::DEFAULT_VARY_COOKIE_NAME) {
$this->init($context, $psCookie);
}
}
private function envChanged()
{
if ($this->vd['vv']['ov'] !== $this->vd['vv']['nv']) {
return true;
}
if ($this->status & self::BM_IS_GUEST) {
if (($this->vd['cv']['ov'] === null)
&& (($this->vd['vv']['nv'] === 'guest' && $this->vd['cv']['nv'] === null)
|| ($this->vd['vv']['nv'] === 'guestm' && $this->vd['cv']['nv'] === 'mobile~1~'))) {
return false;
} else {
return true;
}
} else { // non guest
return $this->vd['cv']['ov'] !== $this->vd['cv']['nv'];
}
}
public static function setVary()
{
// this will only be called when all vary value determined
$vary = new LiteSpeedCacheVaryCookie();
$vary->writeVary();
$changed = $vary->envChanged();
$debug_info = '';
if ($changed) {
$debug_info = 'changed ' . json_encode($vary->vd);
if ( _LITESPEED_DEBUG_ >= LSLog::LEVEL_ENVCOOKIE_CHANGE) {
LSLog::log($debug_info, LSLog::LEVEL_ENVCOOKIE_CHANGE);
}
} elseif (($vary->status & (self::BM_HAS_VARYCOOKIE | self::BM_HAS_VARYVALUE)) > 0) {
$debug_info = 'found & match ' . json_encode($vary->vd);
if (_LITESPEED_DEBUG_ >= LSLog::LEVEL_ENVCOOKIE_DETAIL) {
LSLog::log($debug_info, LSLog::LEVEL_ENVCOOKIE_DETAIL);
}
}
if ($debug_info && $vary->debug_header) {
header("X-LSCACHE-Debug-Vary: $debug_info");
}
return $changed;
}
public static function setAmpVary($value)
{
// this will be called by Amp module from third-party integration
$amp = new LiteSpeedCacheVaryCookie(self::AMP_VARY_COOKIE_NAME);
$amp->writeAmpVary($value);
}
private function writeAmpVary($value)
{
if (headers_sent()) {
$this->status |= self::BM_UPDATE_FAILED;
return;
}
// check lscache vary cookie, not default PS cookie, workaround validator
$cookies = ${'_COOKIE'};
$ov = null;
if (isset($cookies[self::AMP_VARY_COOKIE_NAME])) {
$ov = $cookies[self::AMP_VARY_COOKIE_NAME];
}
if ($ov != $value) {
setcookie(self::AMP_VARY_COOKIE_NAME, $value, 0, $this->_path, $this->_domain, $this->_secure, true);
//LiteSpeedCache::forceNotCacheable('Amp vary change'); not needed any more, lscache engine can save with proper key.
}
}
private function getPrivateId()
{
$len = 32;
if (function_exists('random_bytes')) {
$id = bin2hex(random_bytes($len));
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$id = bin2hex(openssl_random_pseudo_bytes($len));
} else {
$id = uniqid();
}
$val = $_SERVER['REMOTE_ADDR'] . $_SERVER['REMOTE_PORT'] . microtime() . $id;
return md5($val);
}
private function writeVary()
{
if (headers_sent()) {
$this->status |= self::BM_UPDATE_FAILED;
return;
}
if (($this->status & self::BM_IS_GUEST) > 0
&& ($this->status & self::BM_VARYVALUE_CHANGED) == 0
&& LiteSpeedCache::isCacheable()) {
// no cookie set for guest mode and only if for cacheable response.
// for non-cacheable route, like ajax request, can set vary cookie
return;
}
// always check private session cookie
if ($this->vd['ps']['ov'] == null) {
$privateId = $this->getPrivateId();
$this->vd['ps']['nv'] = $privateId;
setcookie(self::PRIVATE_SESSION_COOKIE, $privateId, 0, $this->_path, $this->_domain, $this->_secure, true);
}
if ($this->status & self::BM_VARYCOOKIE_CHANGED) {
$val = $this->vd['cv']['nv'];
$time = 0; // end of session expire
if ($val === null) { // delete cookie
$val = '';
$time = 1000;
}
if (!setcookie($this->vd['cv']['name'], $val, $time, $this->_path, $this->_domain, $this->_secure, true)) {
$this->status |= self::BM_UPDATE_FAILED;
}
}
if ($this->status & self::BM_VARYVALUE_CHANGED) {
header('X-LiteSpeed-Vary: value=' . $this->vd['vv']['nv']);
}
}
private function init($context, $psCookie)
{
$this->vd = [
'cv' => ['name' => $this->name, 'ov' => null, 'nv' => null], // cookieVary
'vv' => ['ov' => null, 'nv' => null], // valueVary
'ps' => ['ov' => null, 'nv' => null], // private session
];
$conf = LiteSpeedCacheConfig::getInstance();
// $diffCustomerGroup 0: No; 1: Yes; 2: login_out
$diffCustomerGroup = $conf->getDiffCustomerGroup();
// $diffMobile 0: no; 1: yes
$diffMobile = $conf->get(LiteSpeedCacheConfig::CFG_DIFFMOBILE);
$isMobile = $diffMobile ? $context->getMobileDevice() : false;
$bypass = $conf->getContextBypass();
$this->debug_header = $conf->get(LiteSpeedCacheConfig::CFG_DEBUG_HEADER);
// check lscache vary cookie, not default PS cookie, workaround validator
$cookies = ${'_COOKIE'};
$data = [];
if (LiteSpeedCache::isRestrictedIP()) {
$data['dev'] = 1;
}
if (!in_array('ctry', $bypass) && isset($psCookie->iso_code_country)) {
$iso = $psCookie->iso_code_country;
$id_country = (int)Configuration::get('PS_COUNTRY_DEFAULT');
$default_iso = Country::getIsoById($id_country);
if ($iso != $default_iso) {
$data['ctry'] = $iso;
}
}
if (!in_array('curr', $bypass) && isset($psCookie->id_currency)) {
$configuration_curr = Configuration::get('PS_CURRENCY_DEFAULT');
if ($psCookie->id_currency != $configuration_curr) {
$data['curr'] = $psCookie->id_currency;
}
}
if (!in_array('lang', $bypass) && isset($psCookie->id_lang) && Language::isMultiLanguageActivated()) {
$configuration_id_lang = Configuration::get('PS_LANG_DEFAULT');
if ($psCookie->id_lang != $configuration_id_lang) {
$data['lang'] = $psCookie->id_lang;
}
}
if ($diffMobile && $isMobile) {
$data['mobile'] = 1;
$this->status |= self::BM_IS_MOBILEVIEW;
}
// customer maybe null
if (($diffCustomerGroup != 0) && ($context->customer != null) && $context->customer->isLogged()) {
// 1: every group, 2: inout
if ($diffCustomerGroup == 1) {
$data['cg'] = $context->customer->getGroups()[0];
} else {
$data['cg'] = 1;
}
}
if (!empty($data)) {
ksort($data); // data is array, key sorted
$newVal = '';
foreach ($data as $k => $v) {
$newVal .= $k . '~' . $v . '~';
}
$this->vd['cv']['nv'] = $newVal;
$this->vd['cv']['data'] = $data;
$this->status |= self::BM_HAS_VARYCOOKIE;
}
if (isset($cookies[$this->vd['cv']['name']])) {
$oldVal = $cookies[$this->vd['cv']['name']];
if ($oldVal == 'deleted') {
$oldVal = null;
}
$this->vd['cv']['ov'] = $oldVal;
}
if ($this->vd['cv']['ov'] !== $this->vd['cv']['nv']) {
$this->status |= self::BM_VARYCOOKIE_CHANGED;
}
// check vary value
if (isset($_SERVER['LSCACHE_VARY_VALUE'])) {
$ov = $_SERVER['LSCACHE_VARY_VALUE'];
$this->vd['vv']['ov'] = $this->vd['vv']['nv'] = $ov;
$this->status |= self::BM_HAS_VARYVALUE;
if ($diffMobile) { // check if mismatch
if ($ov == 'guest' && $isMobile) {
$this->vd['vv']['nv'] = 'guestm';
} elseif ($ov == 'guestm' && !$isMobile) {
$this->vd['vv']['nv'] = 'guest';
}
if ($this->vd['vv']['ov'] !== $this->vd['vv']['nv']) {
$this->status |= self::BM_VARYVALUE_CHANGED;
}
}
if ($ov == 'guest' || $ov == 'guestm') {
$this->status |= self::BM_IS_GUEST;
}
}
if (isset($cookies[self::PRIVATE_SESSION_COOKIE])) {
$this->vd['ps']['ov'] = $cookies[self::PRIVATE_SESSION_COOKIE];
}
}
}