_customer_id = self::$next_key; $this->table = $wpdb->prefix . 'woocommerce_sessions'; $this->expiry = time() + max( AgentAdapt_Sessions::IDLE_LIFETIME, AgentAdapt_Cart_Continuity::retention_days() * DAY_IN_SECONDS ); } /** * Set a value. When WooCommerce stores a cart whose lines or quantities * differ from the stored one, stamp the time (CHANGED). Totals that * WooCommerce recalculates on a read are not a change. * * @param string $key Key. * @param mixed $value Value. */ public function set( $key, $value ) { if ( 'cart' === sanitize_key( (string) $key ) && self::contents( $value ) !== self::contents( $this->get( 'cart' ) ) ) { parent::set( self::CHANGED, time() ); } parent::set( $key, $value ); } /** * A stored cart's contents: line key → quantity, in key order. * * @param mixed $cart Cart as WooCommerce stores it. * @return array */ private static function contents( $cart ): array { $out = array(); foreach ( (array) $cart as $key => $line ) { if ( is_array( $line ) ) { $out[ (string) $key ] = (int) ( $line['quantity'] ?? 0 ); } } ksort( $out ); return $out; } /** * Stored session data for several keys at once: key → data, each value * unserialised (WC_Session keeps every value serialised once more inside * the serialised row). Keys with no row are left out. * * @param string[] $keys WooCommerce session keys. * @return array> */ public static function stored( array $keys ): array { $keys = array_values( array_unique( array_filter( array_map( 'strval', $keys ) ) ) ); if ( ! $keys ) { return array(); } global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,PluginCheck.Security.DirectDB.UnescapedDBParameter -- one %s placeholder per key; the table name is %i. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT session_key, session_value FROM %i WHERE session_key IN (' . implode( ',', array_fill( 0, count( $keys ), '%s' ) ) . ')', array_merge( array( $wpdb->prefix . 'woocommerce_sessions' ), $keys ) ), ARRAY_A ); // phpcs:enable $out = array(); foreach ( (array) $rows as $row ) { $data = maybe_unserialize( (string) $row['session_value'] ); $vals = array(); foreach ( is_array( $data ) ? $data : array() as $k => $v ) { $vals[ (string) $k ] = maybe_unserialize( $v ); } $out[ (string) $row['session_key'] ] = $vals; } return $out; } /** * Delete the stored session data for several keys at once: the carts of * Sessions that can no longer be used (AgentAdapt_Cart_Continuity::purge()) * or that WordPress's personal-data eraser erases (AgentAdapt_Privacy). * * @param string[] $keys WooCommerce session keys. * @return int Rows deleted. */ public static function delete_stored( array $keys ): int { global $wpdb; $n = 0; foreach ( array_chunk( array_values( array_unique( array_filter( array_map( 'strval', $keys ) ) ) ), 200 ) as $chunk ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,PluginCheck.Security.DirectDB.UnescapedDBParameter -- one %s placeholder per key; the table name is %i. $n += (int) $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE session_key IN (' . implode( ',', array_fill( 0, count( $chunk ), '%s' ) ) . ')', array_merge( array( $wpdb->prefix . 'woocommerce_sessions' ), $chunk ) ) ); // phpcs:enable } return $n; } /** * Load data. On the API side AgentAdapt_Cart saves explicitly when a * capability finishes, so a fatal half-way never persists half a change; * on the web side WooCommerce's forms change the cart, so it saves at * shutdown like WooCommerce's own handler. */ public function init() { $this->_data = $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array(); $this->_dirty = false; if ( self::$save_on_shutdown ) { add_action( 'shutdown', array( $this, 'save_data' ), 20 ); } } /** * Whether a key is bound. */ public function has_session() { return '' !== (string) $this->_customer_id; } /** * The guest key. */ public function get_customer_unique_id() { return (string) $this->_customer_id; } /** * All session data (WC_Cart_Session reads it when merging carts). * * @return array */ public function get_session_data() { return (array) $this->_data; } /** * Session data for a key. * * @param string $customer_id Key. * @param mixed $default_value Default. * @return mixed */ public function get_session( $customer_id, $default_value = false ) { global $wpdb; $value = $wpdb->get_var( $wpdb->prepare( 'SELECT session_value FROM %i WHERE session_key = %s', $this->table, $customer_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery return null === $value ? $default_value : maybe_unserialize( $value ); } /** * Persist when something changed. */ public function save_data() { if ( ! $this->_dirty || ! $this->has_session() ) { return; } global $wpdb; $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prepare( 'INSERT INTO %i (session_key, session_value, session_expiry) VALUES (%s, %s, %d) ON DUPLICATE KEY UPDATE session_value = VALUES(session_value), session_expiry = VALUES(session_expiry)', $this->table, $this->_customer_id, maybe_serialize( $this->_data ), $this->expiry ) ); $this->_dirty = false; } /** * No cookie: there is no browser on this side. * * @param bool $set Ignored. */ public function set_customer_session_cookie( $set ) {} /** * Forget in-memory state. */ public function forget_session() { $this->_data = array(); $this->_dirty = false; } /** * Delete the stored session. */ public function destroy_session() { global $wpdb; $wpdb->delete( $this->table, array( 'session_key' => $this->_customer_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $this->forget_session(); } }