* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ use Defuse\Crypto\Key; use PrestaShop\PrestaShop\Core\Exception\CoreException; use PrestaShop\PrestaShop\Core\Session\SessionInterface; /** * @property bool $detect_language * @property int $id_customer * @property int $id_employee * @property int $id_lang * @property int $id_guest * @property int|null $id_connections * @property bool $is_guest * @property bool $logged * @property string $passwd * @property int $session_id * @property string $session_token * @property string $shopContext * @property int $last_activity */ class CookieCore { public const SAMESITE_NONE = 'None'; public const SAMESITE_LAX = 'Lax'; public const SAMESITE_STRICT = 'Strict'; public const SAMESITE_AVAILABLE_VALUES = [ self::SAMESITE_NONE => self::SAMESITE_NONE, self::SAMESITE_LAX => self::SAMESITE_LAX, self::SAMESITE_STRICT => self::SAMESITE_STRICT, ]; /** @var array Contain cookie content in a key => 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 */ 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(); } 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)) { die(Tools::displayError('Cookie value can\'t be an array.')); } if (preg_match('/¤|\|/', $key . $value)) { throw new Exception('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 links to the customer * but leave there affiliate's informations. * 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; } public function makeNewLog() { unset( $this->_content['id_customer'], $this->_content['id_guest'] ); Guest::setNewGuest($this); $this->_modified = true; } /** * Get cookie content. */ 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 * * @since 1.7.0 */ 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.'); } /* * The alternative signature supporting an options array is only available since * PHP 7.3.0, before there is no support for SameSite attribute. */ if (PHP_VERSION_ID < 70300) { return setcookie( $this->_name, $content, $time, $this->_path, $this->_domain . '; SameSite=' . $this->_sameSite, $this->_secure, true ); } 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, static::SAMESITE_AVAILABLE_VALUES) ? (string) $this->_sameSite : static::SAMESITE_NONE, ] ); } 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 (e.g. "filter_"). */ 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; } public function unsetFamily($origin) { $family = $this->getFamily($origin); foreach (array_keys($family) as $member) { unset($this->$member); } } public function getAll() { return $this->_content; } /** * @return string name of cookie */ public function getName() { return $this->_name; } /** * Check if the cookie exists. * * @since 1.5.0 * * @return bool */ public function exists() { return isset($_COOKIE[$this->_name]); } /** * Register a new session * * @param SessionInterface $session */ 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 session * * @return bool */ 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 this session is still alive * * @return bool */ 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 * * @return SessionInterface|null */ 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; } }