parse_cookie(); if ( $cookie ) { list( $this->customer_id, $this->expiration ) = $cookie; $this->has_cookie = true; } else { $this->customer_id = $this->generate_customer_id(); $this->set_expiration(); } add_action( 'wp_loaded', array( $this, 'load_data' ) ); add_action( 'shutdown', array( $this, 'save_data' ), 20 ); add_action( 'clear_auth_cookie', array( $this, 'destroy' ) ); } /** * Load session payload from the table. */ public function load_data() { global $wpdb; $table = $wpdb->prefix . 'bbquote_sessions'; $value = $wpdb->get_var( $wpdb->prepare( 'SELECT session_value FROM %i WHERE session_key = %s', $table, $this->customer_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own table, per-visitor row, object cache would serve stale lists. $this->data = $value ? (array) maybe_unserialize( $value ) : array(); } /** * Get a value from the session. * * @param string $key Key. * @param mixed $fallback Default. * @return mixed */ public function get( $key, $fallback = null ) { return array_key_exists( $key, $this->data ) ? $this->data[ $key ] : $fallback; } /** * Unix timestamp when the session (and its cookie) expires. * * @return int */ public function get_expiration() { return $this->expiration; } /** * Set a value. Marks the session dirty and ensures the cookie exists. * * @param string $key Key. * @param mixed $value Value. */ public function set( $key, $value ) { if ( null === $value ) { unset( $this->data[ $key ] ); } else { $this->data[ $key ] = $value; } $this->dirty = true; $this->maybe_set_cookie(); } /** * Persist dirty data on shutdown. */ public function save_data() { if ( ! $this->dirty ) { return; } global $wpdb; $table = $wpdb->prefix . 'bbquote_sessions'; $wpdb->replace( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own sessions table. $table, array( 'session_key' => $this->customer_id, 'session_value' => maybe_serialize( $this->data ), 'session_expiry' => $this->expiration, ), array( '%s', '%s', '%d' ) ); $this->dirty = false; } /** * Drop the session and its cookie. */ public function destroy() { global $wpdb; $table = $wpdb->prefix . 'bbquote_sessions'; $wpdb->delete( $table, array( 'session_key' => $this->customer_id ), array( '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own sessions table. if ( ! headers_sent() ) { wc_setcookie( self::COOKIE, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true ); } $this->data = array(); $this->dirty = false; $this->has_cookie = false; $this->customer_id = $this->generate_customer_id(); $this->set_expiration(); } /** * Delete expired sessions. Hooked to daily cron. */ public static function cleanup() { global $wpdb; $table = $wpdb->prefix . 'bbquote_sessions'; $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE session_expiry < %d', $table, time() ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own sessions table. } /** * Issue the signed cookie when missing/invalid, and roll the expiry forward for active shoppers * (otherwise the list would vanish exactly 48 h after the first add). */ public function maybe_set_cookie() { if ( headers_sent() ) { return; } $renew = ( $this->expiration - time() ) < (int) apply_filters( 'bbquote_session_renew_threshold', DAY_IN_SECONDS ); if ( $this->has_cookie && ! $renew ) { return; } if ( $renew ) { $this->set_expiration(); $this->dirty = true; } $to_hash = $this->customer_id . '|' . $this->expiration; $hash = hash_hmac( 'sha256', $to_hash, wp_hash( $to_hash ) ); $value = $this->customer_id . '||' . $this->expiration . '||' . $hash; wc_setcookie( self::COOKIE, $value, $this->expiration, $this->use_secure_cookie(), true ); $_COOKIE[ self::COOKIE ] = $value; $this->has_cookie = true; } /** * Parse and validate the cookie. * * @return array|false [customer_id, expiration] or false. */ protected function parse_cookie() { if ( empty( $_COOKIE[ self::COOKIE ] ) ) { return false; } $parts = explode( '||', sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE ] ) ) ); if ( 3 !== count( $parts ) ) { return false; } list( $customer_id, $expiration, $hash ) = $parts; if ( (int) $expiration < time() ) { return false; } $to_hash = $customer_id . '|' . $expiration; $expected = hash_hmac( 'sha256', $to_hash, wp_hash( $to_hash ) ); if ( ! hash_equals( $expected, $hash ) ) { return false; } return array( $customer_id, (int) $expiration ); } /** * User id for logged-in users, random hash for guests. * * @return string */ protected function generate_customer_id() { if ( is_user_logged_in() ) { return 'u' . get_current_user_id(); } return md5( wp_generate_password( 32, false ) ); } /** * Expiration: 48h, filterable. */ protected function set_expiration() { $this->expiration = time() + (int) apply_filters( 'bbquote_session_expiration', 48 * HOUR_IN_SECONDS ); } /** * Whether to flag the cookie as secure. * * @return bool */ protected function use_secure_cookie() { return (bool) apply_filters( 'bbquote_session_use_secure_cookie', is_ssl() ); } }