In-memory payload mirrored to a transient. */ private $session_data = []; /** @var bool Whether the existing cookie (if any) has been read. */ private $loaded = false; /** * Singleton instance holder. * * @var self|null */ private static $instance = null; /** * Get or create the singleton instance. * * @return self */ public static function instance() { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } /** * Read an existing checkout cookie into memory. Does not create a cookie. */ private function load(): void { if ($this->loaded) { return; } $this->loaded = true; $cookie_session_id = Request::token('cookie', $this->cookie_name); if ($cookie_session_id === '') { $this->session_id = null; $this->session_data = []; return; } $this->session_id = $cookie_session_id; $stored = get_transient('bizdex_uplisting_session_' . $this->session_id); $this->session_data = is_array($stored) ? $stored : []; } /** * Generate a unique opaque identifier. */ private function generate_session_id() { return wp_generate_password(32, false, false); } /** * Persist the ID to an httponly, secure (when SSL) cookie. * * Called only from {@see set_all()} during uncached REST checkout. * * @return void */ private function set_cookie() { if (headers_sent() || empty($this->session_id)) { return; } $secure = is_ssl(); setcookie( $this->cookie_name, $this->session_id, time() + DAY_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN, $secure, true ); } /** * Return the current checkout ID, or empty when none exists. * * Does not create a cookie. * * @since 1.0.0 */ public function get_id(): string { $this->load(); return (string) ($this->session_id ?? ''); } /** * Read a single value from the checkout payload. * * @param string $key Array key (e.g. `property_id`, `dates`). * @param mixed $default Returned when the key is not set. * @return mixed */ public function get($key, $default = null) { $this->load(); return isset($this->session_data[$key]) ? $this->session_data[$key] : $default; } /** * Write a single value when a checkout cookie already exists. * * Does not create a cookie for incidental writes. * * @param string $key Array key. * @param mixed $value Value to store (scalar or array). * @return void */ public function set($key, $value) { $this->load(); if (empty($this->session_id)) { return; } $this->session_data[$key] = $value; $this->save(); } /** * Return the full checkout payload. * * @return array */ public function get_all() { $this->load(); return is_array($this->session_data) ? $this->session_data : []; } /** * Replace the checkout payload and set the cookie. * * Used only from create-booking REST (not page-cacheable). * * @param array $data Complete array to store. * @return void */ public function set_all($data) { $this->load(); if (!is_array($data)) { $data = []; } if (empty($this->session_id)) { $this->session_id = $this->generate_session_id(); } $this->session_data = $data; $this->set_cookie(); $this->save(); } /** * Clear checkout data, delete the transient, and expire the cookie. * * Called after a successful booking from REST (not page-cacheable). * * @return void */ public function destroy() { $this->load(); if ($this->session_id) { delete_transient('bizdex_uplisting_session_' . $this->session_id); } $this->session_id = null; $this->session_data = []; if (headers_sent()) { return; } $secure = is_ssl(); setcookie( $this->cookie_name, '', time() - YEAR_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN, $secure, true ); } /** * Write `$session_data` to transient `bizdex_uplisting_session_{id}` (24h TTL). * * @return void */ private function save() { if ($this->session_id) { set_transient('bizdex_uplisting_session_' . $this->session_id, $this->session_data, DAY_IN_SECONDS); } } }