value format */ protected $_content = []; /** @var string Crypted cookie name for setcookie() */ protected $_name; /** @var int expiration date for setcookie() */ protected $_expire; /** @var bool|string Website domain for setcookie() */ protected $_domain; /** @var string|bool SameSite for setcookie() */ protected $_sameSite; /** @var string Path for setcookie() */ protected $_path; /** @var PhpEncryption cipher tool instance */ protected $cipherTool; protected $_modified = false; protected $_allow_writing; protected $_salt; protected $_standalone; /** @var bool */ protected $_secure = false; /** @var SessionInterface|null */ protected $session = null; /** * Get data if the cookie exists and else initialize an new one. * * @param string $name Cookie name before encrypting * @param string $path Cookie path * @param int|null $expire Cookie expiration time (default: 20 days from now) * @param array|null $shared_urls Array of shared URLs for domain calculation * @param bool $standalone Whether this is a standalone cookie (ie. the cookie is self-contained and not dependent on PrestaShop's context) * @param bool $secure Whether the cookie should be secure (HTTPS only) */ public function __construct($name, $path = '', $expire = null, $shared_urls = null, $standalone = false, $secure = false) { $this->_content = []; $this->_standalone = $standalone; $this->_expire = null === $expire ? time() + 1728000 : (int) $expire; $this->_path = trim(($this->_standalone ? '' : Context::getContext()->shop->physical_uri) . $path, '/\\') . '/'; if ($this->_path[0] != '/') { $this->_path = '/' . $this->_path; } $this->_path = rawurlencode($this->_path); $this->_path = str_replace(['%2F', '%7E', '%2B', '%26'], ['/', '~', '+', '&'], $this->_path); $this->_domain = $this->getDomain($shared_urls); $this->_sameSite = Configuration::get('PS_COOKIE_SAMESITE'); $this->_name = 'PrestaShop-' . md5(($this->_standalone ? '' : _PS_VERSION_) . $name . $this->_domain); $this->_allow_writing = true; $this->_salt = $this->_standalone ? str_pad('', 32, md5('ps' . __FILE__)) : _COOKIE_IV_; if ($this->_standalone) { $asciiSafeString = Defuse\Crypto\Encoding::saveBytesToChecksummedAsciiSafeString(Key::KEY_CURRENT_VERSION, str_pad($name, Key::KEY_BYTE_SIZE, md5(__FILE__))); $this->cipherTool = new PhpEncryption($asciiSafeString); } else { $this->cipherTool = new PhpEncryption(_NEW_COOKIE_KEY_); } $this->_secure = (bool) $secure; $this->update(); } /** * Disable cookie writing. * Prevents the cookie from being written to the browser. */ public function disallowWriting() { $this->_allow_writing = false; } /** * @param array|null $shared_urls * * @return bool|string */ protected function getDomain($shared_urls = null) { $httpHost = Tools::getHttpHost(false, false); if (!$httpHost) { return false; } $r = '!(?:(\w+)://)?(?:(\w+)\:(\w+)@)?([^/:]+)?(?:\:(\d*))?([^#?]+)?(?:\?([^#]+))?(?:#(.+$))?!i'; if (!preg_match($r, $httpHost, $out)) { return false; } if (preg_match('/^(((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]{1}[0-9]|[1-9]).)' . '{1}((25[0-5]|2[0-4][0-9]|[1]{1}[0-9]{2}|[1-9]{1}[0-9]|[0-9]).)' . '{2}((25[0-5]|2[0-4][0-9]|[1]{1}[0-9]{2}|[1-9]{1}[0-9]|[0-9]){1}))$/', $out[4])) { return false; } if (!strstr($httpHost, '.')) { return false; } $domain = false; if ($shared_urls !== null) { foreach ($shared_urls as $shared_url) { if ($shared_url != $out[4]) { continue; } if (preg_match('/^(?:.*\.)?([^.]*(?:.{2,4})?\..{2,3})$/Ui', $shared_url, $res)) { $domain = '.' . $res[1]; break; } } } if (!$domain) { $domain = $out[4]; } return $domain; } /** * Set expiration date. * * @param int $expire Expiration time from now */ public function setExpire($expire) { $this->_expire = (int) $expire; } /** * Magic method wich return cookie data from _content array. * * @param string $key key wanted * * @return string value corresponding to the key */ public function __get($key) { return isset($this->_content[$key]) ? $this->_content[$key] : false; } /** * Magic method which check if key exists in the cookie. * * @param string $key key wanted * * @return bool key existence */ public function __isset($key) { return isset($this->_content[$key]); } /** * Magic method which adds data into _content array. * * @param string $key Access key for the value * @param mixed $value Value corresponding to the key * * @throws Exception */ public function __set($key, $value) { if (is_array($value)) { throw new PrestaShopException('Cookie value can\'t be an array.'); } if (preg_match('/¤|\|/', $key . $value)) { throw new PrestaShopException('Forbidden chars in cookie'); } if (!$this->_modified && (!array_key_exists($key, $this->_content) || $this->_content[$key] != $value)) { $this->_modified = true; } $this->_content[$key] = $value; } /** * Magic method which delete data into _content array. * * @param string $key key wanted */ public function __unset($key) { if (isset($this->_content[$key])) { $this->_modified = true; } unset($this->_content[$key]); } /** * Delete cookie * As of version 1.5 don't call this function, use Customer::logout() or Employee::logout() instead;. */ public function logout() { $this->deleteSession(); $this->_content = []; $this->encryptAndSetCookie(); unset($_COOKIE[$this->_name]); $this->_modified = true; } /** * Soft logout, delete everything linked to the customer * but leave their affiliate's informations intact. * As of version 1.5 don't call this function, use Customer::mylogout() instead;. */ public function mylogout() { $this->deleteSession(); unset( $this->_content['id_customer'], $this->_content['id_guest'], $this->_content['is_guest'], $this->_content['id_connections'], $this->_content['customer_lastname'], $this->_content['customer_firstname'], $this->_content['passwd'], $this->_content['logged'], $this->_content['email'], $this->_content['id_cart'], $this->_content['id_address_invoice'], $this->_content['id_address_delivery'] ); $this->_modified = true; } /** * Create a new guest log entry. * Removes current customer and guest IDs and creates a new guest session. */ public function makeNewLog() { unset( $this->_content['id_customer'], $this->_content['id_guest'] ); Guest::setNewGuest($this); $this->_modified = true; } /** * Get cookie content and update internal data. * Decrypts and validates the cookie content, handles checksum verification. * * @param bool $nullValues Whether to handle null values */ public function update($nullValues = false) { if (isset($_COOKIE[$this->_name])) { /* Decrypt cookie content */ $content = $this->cipherTool->decrypt($_COOKIE[$this->_name]); // printf("\$content = %s
", $content); /* Get cookie checksum */ $tmpTab = explode('¤', $content); // remove the checksum which is the last element array_pop($tmpTab); $content_for_checksum = implode('¤', $tmpTab) . '¤'; $checksum = hash('sha256', $this->_salt . $content_for_checksum); // printf("\$checksum = %s
", $checksum); /* Unserialize cookie content */ $tmpTab = explode('¤', $content); foreach ($tmpTab as $keyAndValue) { $tmpTab2 = explode('|', $keyAndValue); if (count($tmpTab2) == 2) { $this->_content[$tmpTab2[0]] = $tmpTab2[1]; } } /* Check if cookie has not been modified */ if (!isset($this->_content['checksum']) || $this->_content['checksum'] != $checksum) { $this->logout(); } if (!isset($this->_content['date_add'])) { $this->_content['date_add'] = date('Y-m-d H:i:s'); } } else { $this->_content['date_add'] = date('Y-m-d H:i:s'); } // checks if the language exists, if not choose the default language if (!$this->_standalone && !Language::getLanguage((int) $this->id_lang)) { $this->id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); // set detect_language to force going through Tools::setCookieLanguage to figure out browser lang $this->detect_language = true; } } /** * Encrypt and set the Cookie. * * @param string|null $cookie Cookie content * * @return bool Indicates whether the Cookie was successfully set */ protected function encryptAndSetCookie($cookie = null) { if ($cookie) { $content = $this->cipherTool->encrypt($cookie); $time = $this->_expire; } else { $content = 0; $time = 1; } /* * We need to check if the new cookie will be compliant with RFC 2965, maximum of 4096 bytes * per cookie. Major browsers follow this very closely and will refuse to save this cookie. * * If we exceed this value, some module is saving something to cookie that it shouldn't save, * and overflowing the cookie. It's absolutely critical that this does not happen because * it breaks for example all cart functionality. * * We are using strlen because it calculates the byte count, we don't care about character * count in case of multi-byte characters. */ if (strlen($this->_name . $content) > 4096) { throw new PrestaShopException('Error during setting a cookie. Combined size of name and value cannot exceed 4096 characters. Larger cookie is not compliant with RFC 2965 and will not be accepted by the browser.'); } return setcookie( $this->_name, $content, [ 'expires' => $time, 'path' => $this->_path, 'domain' => (string) $this->_domain, 'secure' => $this->_secure, 'httponly' => true, 'samesite' => in_array((string) $this->_sameSite, CookieOptions::SAMESITE_AVAILABLE_VALUES) ? (string) $this->_sameSite : CookieOptions::SAMESITE_NONE, ] ); } /** * Destructor. * Automatically saves the cookie when the object is destroyed. */ public function __destruct() { $this->write(); } /** * Save cookie with setcookie(). */ public function write() { if (!$this->_modified || headers_sent() || !$this->_allow_writing) { return; } $previousChecksum = $cookie = ''; /* Serialize cookie content */ if (isset($this->_content['checksum'])) { $previousChecksum = $this->_content['checksum']; unset($this->_content['checksum']); } foreach ($this->_content as $key => $value) { $cookie .= $key . '|' . $value . '¤'; } /* Add checksum to cookie */ $newChecksum = hash('sha256', $this->_salt . $cookie); // do not set cookie if the checksum is the same: it means the content has not changed! if ($previousChecksum === $newChecksum) { return; } $cookie .= 'checksum|' . $newChecksum; $this->_modified = false; /* Cookies are encrypted for evident security reasons */ return $this->encryptAndSetCookie($cookie); } /** * Get a family of variables with a common prefix (e.g. "filter_"). * * @param string $origin The prefix to search for * * @return array Array of key-value pairs matching the prefix */ public function getFamily($origin) { $result = []; if (count($this->_content) == 0) { return $result; } foreach ($this->_content as $key => $value) { if (strncmp($key, $origin, strlen($origin)) == 0) { $result[$key] = $value; } } return $result; } /** * Remove a family of variables with a common prefix. * * @param string $origin The prefix of variables to remove */ public function unsetFamily($origin) { $family = $this->getFamily($origin); foreach (array_keys($family) as $member) { unset($this->$member); } } /** * Get all cookie content. * * @return array All cookie data as key-value pairs */ public function getAll() { return $this->_content; } /** * @return string name of cookie */ public function getName() { return $this->_name; } /** * Check if the cookie exists. * * @return bool */ public function exists() { return isset($_COOKIE[$this->_name]); } /** * Register a new session for the current user. * * @param SessionInterface $session The session object to register * * @throws CoreException If no valid user ID is found */ public function registerSession(SessionInterface $session) { if (isset($this->id_employee)) { $session->setUserId((int) $this->id_employee); } elseif (isset($this->id_customer)) { $session->setUserId((int) $this->id_customer); } else { throw new CoreException('Invalid user id'); } $session->setToken(sha1(time() . uniqid())); $session->add(); $this->session_id = $session->getId(); $this->session_token = $session->getToken(); } /** * Delete the current session. * Removes the session if it exists. * * @return bool True if session was deleted, false if no session exists */ public function deleteSession() { if (!isset($this->session_id)) { return false; } $session = $this->getSession($this->session_id); if ($session !== null) { $session->delete(); return true; } return false; } /** * Check if the current session is still alive and valid. * Verifies session ID, token, and user ID match. * * @return bool True if session is valid and alive */ public function isSessionAlive() { if (!isset($this->session_id) || !isset($this->session_token)) { return false; } $session = $this->getSession($this->session_id); return $session !== null && $session->getToken() === $this->session_token && ( (int) $this->id_employee === $session->getUserId() || (int) $this->id_customer === $session->getUserId() ) ; } /** * Retrieve session based on a session ID and the employee or customer ID * Creates appropriate session object (Employee or Customer) and updates its timestamp. * * @param int $sessionId The session ID to retrieve * * @return SessionInterface|null The session object or null if not found */ public function getSession($sessionId) { if ($this->session !== null) { return $this->session; } if (isset($this->id_employee)) { $this->session = new EmployeeSession($sessionId); } elseif (isset($this->id_customer)) { $this->session = new CustomerSession($sessionId); } if (isset($this->session) && Validate::isLoadedObject($this->session)) { // Update session date_upd $this->session->save(); } return $this->session; } }