get_row( $wpdb->prepare( 'SELECT option_value FROM %i WHERE option_name = %s', $wpdb->options, $name ), ARRAY_A ); if ( $wpdb->last_error ) { throw new \RuntimeException( 'Welcome choices could not be read. No new optional action was authorized.' ); } return null === $row ? $default : maybe_unserialize( $row['option_value'] ); } /** Recheck the saved intent and its live lease immediately before optional transport. */ public static function currentOption( string $name, array $value ): bool { global $wpdb; $owner = self::$owners[$wpdb->options] ?? ''; if ( 'owwc_consent' !== $name || ! is_string( $owner ) || ! preg_match( '/^[0-9]+:[a-f0-9]{48}$/D', $owner ) ) { return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- A fresh exact state and unexpired owner must authorize dispatch; cached choices cannot fence a delayed worker after withdrawal. $saved = $wpdb->get_var( $wpdb->prepare( 'SELECT welcome_state.option_value FROM %i AS welcome_state INNER JOIN %i AS connection_lock ON connection_lock.option_name = %s WHERE welcome_state.option_name = %s AND BINARY connection_lock.option_value = BINARY %s AND %d >= UNIX_TIMESTAMP()', $wpdb->options, $wpdb->options, self::LOCK, $name, $owner, (int) $owner ) ); return ! $wpdb->last_error && is_string( $saved ) && $saved === maybe_serialize( $value ); } /** Every write is fenced by the current, unexpired owner in the same SQL statement. */ public static function saveOption( string $name, $value ): void { global $wpdb; $owner = self::$owners[$wpdb->options] ?? ''; if ( 'owwc_consent' !== $name || ! is_string( $owner ) || ! preg_match( '/^[0-9]+:[a-f0-9]{48}$/D', $owner ) ) { throw new \RuntimeException( 'A current welcome connection lock is required before saving.' ); } $raw = maybe_serialize( $value ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Unique first state may be inserted only while this exact lease is current. Never upsert over a competing state. $inserted = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO %i (option_name, option_value, autoload) SELECT %s, %s, 'no' FROM %i AS connection_lock WHERE connection_lock.option_name = %s AND BINARY connection_lock.option_value = BINARY %s AND %d >= UNIX_TIMESTAMP()", $wpdb->options, $name, $raw, $wpdb->options, self::LOCK, $owner, (int) $owner ) ); if ( false === $inserted ) { throw new \RuntimeException( 'Welcome choices could not be persisted.' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The joined lease check fences a worker paused beyond expiry or replaced after withdrawal; the Options API has no conditional write primitive. $changed = $wpdb->query( $wpdb->prepare( 'UPDATE %i AS welcome_state INNER JOIN %i AS connection_lock ON connection_lock.option_name = %s SET welcome_state.option_value = %s WHERE welcome_state.option_name = %s AND BINARY connection_lock.option_value = BINARY %s AND %d >= UNIX_TIMESTAMP()', $wpdb->options, $wpdb->options, self::LOCK, $raw, $name, $owner, (int) $owner ) ); self::clear( $name ); if ( false === $changed ) { throw new \RuntimeException( 'Welcome choices could not be persisted.' ); } // Verify even unchanged writes while the same owner is still current; stale cached equality is not success. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Exact current state and lease required to acknowledge persistence; no reusable read cache is safe here. $saved = $wpdb->get_var( $wpdb->prepare( 'SELECT welcome_state.option_value FROM %i AS welcome_state INNER JOIN %i AS connection_lock ON connection_lock.option_name = %s WHERE welcome_state.option_name = %s AND BINARY connection_lock.option_value = BINARY %s AND %d >= UNIX_TIMESTAMP()', $wpdb->options, $wpdb->options, self::LOCK, $name, $owner, (int) $owner ) ); if ( $wpdb->last_error || ! is_string( $saved ) || $saved !== $raw ) { throw new \RuntimeException( 'The welcome connection changed or its save lease expired. Review the current choices before retrying.' ); } } public function lock( string $name, int $seconds = 30 ): ?string { global $wpdb; $owner = ( time() + max( 30, min( 600, $seconds ) ) ) . ':' . bin2hex( random_bytes( 24 ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- A unique option plus exact-value compare-and-swap provides a cross-worker lock; cached add_option may overwrite on duplicate key. $inserted = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO %i (option_name, option_value, autoload) VALUES (%s, %s, 'no')", $wpdb->options, self::LOCK, $owner ) ); if ( false === $inserted ) { return null; } if ( 1 === $inserted ) { self::$owners[$wpdb->options] = $owner; self::clear( self::LOCK ); return $owner; } $old = self::freshOption( self::LOCK, '' ); if ( ! is_string( $old ) || ! preg_match( '/^[0-9]+:[a-f0-9]{48}$/D', $old ) || (int) $old >= time() ) { return null; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Replace exactly the expired owner, never delete a newer lock acquired by another request. $changed = $wpdb->query( $wpdb->prepare( 'UPDATE %i SET option_value = %s WHERE option_name = %s AND BINARY option_value = BINARY %s', $wpdb->options, $owner, self::LOCK, $old ) ); self::clear( self::LOCK ); if ( 1 === $changed ) { self::$owners[$wpdb->options] = $owner; return $owner; } return null; } public function unlock( string $name, string $owner ): void { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Only this exact owner may release its lock; caches are invalidated below. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name = %s AND BINARY option_value = BINARY %s', $wpdb->options, self::LOCK, $owner ) ); self::clear( self::LOCK ); if ( ( self::$owners[$wpdb->options] ?? null ) === $owner ) { unset( self::$owners[$wpdb->options] ); } } }