prepare(). // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter const ORDERPING_CONFIRMATION_PATH = '/wp-json/orderping/v1/confirmation'; const ORDERPING_CONFIRM_META = '_orderping_confirmation'; /** * State table name. * * @return string */ function orderping_confirmation_table() { global $wpdb; return $wpdb->prefix . 'orderping_confirmation'; } /** * Action table name. * * @return string */ function orderping_confirm_action_table() { global $wpdb; return $wpdb->prefix . 'orderping_confirm_action'; } /** * Creates the confirmation tables (called from orderping_install_tables()). */ function orderping_install_confirmation_tables() { global $wpdb; $charset = $wpdb->get_charset_collate(); $state = orderping_confirmation_table(); $action = orderping_confirm_action_table(); dbDelta( "CREATE TABLE {$state} ( order_id bigint(20) unsigned NOT NULL, version int(10) unsigned NOT NULL DEFAULT 1, confirmation_version int(10) unsigned NOT NULL, state varchar(16) NOT NULL, snapshot_hash varchar(64) NOT NULL, action_id varchar(100) NOT NULL, updated_at bigint(20) unsigned NOT NULL, PRIMARY KEY (order_id) ) {$charset};" ); dbDelta( "CREATE TABLE {$action} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, order_id bigint(20) unsigned NOT NULL, action_id varchar(100) NOT NULL, note_state varchar(12) NOT NULL DEFAULT 'none', created_at bigint(20) unsigned NOT NULL, PRIMARY KEY (id), UNIQUE KEY order_action (order_id,action_id) ) {$charset};" ); } /** * Order modified time as a Unix timestamp (0 when unknown). * * @param WC_Order $order Order. * @return int */ function orderping_order_modified( WC_Order $order ) { $date = $order->get_date_modified(); return $date ? (int) $date->getTimestamp() : 0; } /** * Freshness token finer than whole seconds: the modified time plus a hash of * the fields the snapshot covers (the order payload). * * @param WC_Order $order Order. * @return string */ function orderping_order_freshness( WC_Order $order ) { return orderping_order_modified( $order ) . ':' . hash( 'sha256', (string) wp_json_encode( orderping_order_payload( $order ) ) ); } /** * Loads the order bypassing object caches. * * @param int $order_id Order ID. * @return WC_Order|null */ function orderping_fresh_order( $order_id ) { $order_id = (int) $order_id; // HPOS: wc_get_order() can serve the OrderCache object, so evict it through WooCommerce's own cache class. if ( function_exists( 'wc_get_container' ) && class_exists( 'AutomatticWooCommerceInternalCachesOrderCache' ) ) { try { wc_get_container()->get( AutomatticWooCommerceInternalCachesOrderCache::class )->remove( $order_id ); } catch ( Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement // Container/class layout differs on this Woo version; the fresh read below still bypasses the cache. } } // Legacy posts storage: drop the cached post and its meta. clean_post_cache( $order_id ); wp_cache_delete( 'order-' . $order_id, 'orders' ); wp_cache_delete( $order_id, 'posts' ); wp_cache_delete( $order_id, 'post_meta' ); // Build a new object and read it from the data store (HPOS or posts, whichever is active), // never through wc_get_order(), which may return a cached instance. try { $order = new WC_Order(); $order->set_id( $order_id ); $order->get_data_store()->read( $order ); } catch ( Throwable $e ) { return null; } return $order->get_id() === $order_id ? $order : null; } /** * Takes a row lock on the order's storage row inside the open transaction. * * @param int $order_id Order ID. */ function orderping_lock_order_row( $order_id ) { global $wpdb; $hpos = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); $table = $hpos ? $wpdb->prefix . 'wc_orders' : $wpdb->posts; $col = $hpos ? 'id' : 'ID'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->get_var( $wpdb->prepare( "SELECT {$col} FROM {$table} WHERE {$col} = %d FOR UPDATE", $order_id ) ); } /** * Current stored state for an order. * * @param int $order_id Order ID. * @return array|null */ function orderping_confirmation_row( $order_id ) { global $wpdb; $table = orderping_confirmation_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE order_id = %d", $order_id ), ARRAY_A ); return is_array( $row ) ? $row : null; } /** * Whether an action row exists. * * @param int $order_id Order ID. * @param string $action_id Action ID. * @return bool */ function orderping_confirm_action_known( $order_id, $action_id ) { global $wpdb; $table = orderping_confirm_action_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return 1 === (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE order_id = %d AND action_id = %s", $order_id, $action_id ) ); } /** * Whether the outbox holds this order's placed event (it was captured). * * @param int $order_id Order ID. * @return bool */ function orderping_order_was_captured( $order_id ) { global $wpdb; $table = orderping_outbox_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return 1 === (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE event_key = %s", 'placed-' . $order_id ) ); } /** * Mirrors the stored confirmation to order meta. Returns the JSON value. * * @param WC_Order $order Order. * @param array $row State row. * @return string */ function orderping_mirror_confirmation_meta( WC_Order $order, array $row ) { // The state table is the truth: mirror its current row, not the caller's possibly stale copy. $fresh_row = orderping_confirmation_row( $order->get_id() ); if ( null !== $fresh_row ) { $row = $fresh_row; } $value = wp_json_encode( array( 'version' => (int) $row['confirmation_version'], 'snapshotHash' => (string) $row['snapshot_hash'], 'state' => (string) $row['state'], 'actionId' => (string) $row['action_id'], ) ); $existing = $order->get_meta( ORDERPING_CONFIRM_META, true ); $decoded = is_string( $existing ) ? json_decode( $existing, true ) : null; // Never let an older mirror overwrite a newer confirmation. $newer = is_array( $decoded ) && (int) ( $decoded['version'] ?? 0 ) > (int) $row['confirmation_version']; if ( ! $newer && $existing !== $value ) { $order->update_meta_data( ORDERPING_CONFIRM_META, $value ); $order->save(); } // Report what is actually persisted, read fresh from the order. $reloaded = wc_get_order( $order->get_id() ); $stored = $reloaded instanceof WC_Order ? $reloaded->get_meta( ORDERPING_CONFIRM_META, true ) : ''; return is_string( $stored ) ? $stored : ''; } /** * Records the note bookkeeping at most once per action (the visible note itself is written by the * order-state mirror). The claim is a * conditional UPDATE none -> attempted; only the claimer records it. A * claim without a recorded result means the add may or may not have happened, * which is reported as "unknown" and never retried. * * @param WC_Order $order Order. * @param string $action_id Action ID. * @return string added|unknown */ function orderping_confirmation_note( WC_Order $order, $action_id ) { global $wpdb; $table = orderping_confirm_action_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $claimed = $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET note_state = 'attempted' WHERE order_id = %d AND action_id = %s AND note_state = 'none'", $order->get_id(), $action_id ) ); if ( 1 === (int) $claimed ) { // No order note here: the order-state mirror (order-state.php) writes the one "customer // confirmed" note when the backend syncs the confirmed state. Writing it here too made two. // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET note_state = 'added' WHERE order_id = %d AND action_id = %s AND note_state = 'attempted'", $order->get_id(), $action_id ) ); return 'added'; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $state = $wpdb->get_var( $wpdb->prepare( "SELECT note_state FROM {$table} WHERE order_id = %d AND action_id = %s", $order->get_id(), $action_id ) ); return 'added' === $state ? 'added' : 'unknown'; } /** * Builds the read-back response body. * * @param WC_Order $order Order. * @param string $status applied|repeat|not_found. * @param string $note added|unknown|none. * @return array */ function orderping_confirmation_ok( WC_Order $order, $status, $note ) { $row = orderping_confirmation_row( $order->get_id() ); return array( 'status' => $status, 'state' => $row ? $row['state'] : null, 'confirmation_version' => $row ? (int) $row['confirmation_version'] : null, 'action_id' => $row ? $row['action_id'] : null, 'meta_value' => $row ? orderping_mirror_confirmation_meta( $order, $row ) : null, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- field name in the signed REST contract, not a query. 'note' => $note, 'modified' => orderping_order_modified( $order ), 'order' => orderping_order_payload( $order ), ); } /** * Builds a 409 response carrying fresh fields. * * @param WC_Order $order Order. * @param string $error order_modified|version_conflict. * @return WP_REST_Response */ function orderping_confirmation_conflict( WC_Order $order, $error ) { $row = orderping_confirmation_row( $order->get_id() ); return new WP_REST_Response( array( 'error' => $error, 'state' => $row ? $row['state'] : null, 'confirmation_version' => $row ? (int) $row['confirmation_version'] : null, 'action_id' => $row ? $row['action_id'] : null, 'modified' => orderping_order_modified( $order ), 'freshness' => orderping_order_freshness( $order ), 'order' => orderping_order_payload( $order ), ), 409 ); } /** * Inserts the action row and applies the conditional state change in one * transaction. UNIQUE(order_id, action_id) makes a replay a no-op ("repeat"); * the version-checked UPDATE rejects a concurrent or older action. * * @param int $order_id Order ID. * @param array $req Validated request. * @return string applied|repeat|conflict|order_modified|out_of_scope|error */ function orderping_confirmation_write( $order_id, array $req ) { global $wpdb; $state = orderping_confirmation_table(); $action = orderping_confirm_action_table(); $now = time(); $suppress = $wpdb->suppress_errors( true ); $wpdb->query( 'START TRANSACTION' ); // Freshness is part of the atomic acceptance: lock the order row (a concurrent // save UPDATEs it and waits until we commit), then re-read the order and // compare against what the caller observed. // Residual: if order storage is not InnoDB, or an extension writes the order // outside its row, the lock does not hold and the window is the re-read // below to COMMIT (milliseconds), not the whole request. orderping_lock_order_row( $order_id ); $fresh = orderping_fresh_order( $order_id ); if ( $fresh instanceof WC_Order && ! orderping_order_in_scope( $fresh ) ) { $wpdb->query( 'ROLLBACK' ); $wpdb->suppress_errors( $suppress ); return 'out_of_scope'; } if ( ! $fresh instanceof WC_Order || orderping_order_freshness( $fresh ) !== $req['freshness'] ) { $wpdb->query( 'ROLLBACK' ); $wpdb->suppress_errors( $suppress ); return 'order_modified'; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $inserted = $wpdb->query( $wpdb->prepare( "INSERT INTO {$action} (order_id, action_id, created_at) VALUES (%d, %s, %d)", $order_id, $req['action_id'], $now ) ); if ( 1 !== (int) $inserted ) { $wpdb->query( 'ROLLBACK' ); $wpdb->suppress_errors( $suppress ); // A duplicate key means this action was already applied. return orderping_confirm_action_known( $order_id, $req['action_id'] ) ? 'repeat' : 'error'; } $current = orderping_confirmation_row( $order_id ); if ( null === $current ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $changed = $wpdb->query( $wpdb->prepare( "INSERT INTO {$state} (order_id, version, confirmation_version, state, snapshot_hash, action_id, updated_at) VALUES (%d, 1, %d, %s, %s, %s, %d)", $order_id, $req['confirmation_version'], $req['state'], $req['snapshot_hash'], $req['action_id'], $now ) ); } elseif ( (int) $current['confirmation_version'] > $req['confirmation_version'] || ( (int) $current['confirmation_version'] === $req['confirmation_version'] && $current['state'] === $req['state'] ) ) { $changed = 0; } else { // Conditional UPDATE: only the writer that still sees the version it read wins. // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $changed = $wpdb->query( $wpdb->prepare( "UPDATE {$state} SET version = version + 1, confirmation_version = %d, state = %s, snapshot_hash = %s, action_id = %s, updated_at = %d WHERE order_id = %d AND version = %d", $req['confirmation_version'], $req['state'], $req['snapshot_hash'], $req['action_id'], $now, $order_id, (int) $current['version'] ) ); } if ( 1 !== (int) $changed ) { $wpdb->query( 'ROLLBACK' ); $wpdb->suppress_errors( $suppress ); return 'conflict'; } $committed = $wpdb->query( 'COMMIT' ); $wpdb->suppress_errors( $suppress ); return false === $committed ? 'error' : 'applied'; } /** * Handles a signed confirmation write or read-back. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ function orderping_rest_confirmation( WP_REST_Request $request ) { $conn = orderping_connection(); if ( null === $conn || $conn['id'] !== (string) $request->get_header( 'x_op_connection' ) ) { return new WP_Error( 'orderping_unauthorized', 'Unauthorized', array( 'status' => 401 ) ); } $body = $request->get_body(); $verdict = orderping_verify( $conn['secret'], 'cp-to-site', 'POST', ORDERPING_CONFIRMATION_PATH, (string) $request->get_header( 'x_op_timestamp' ), (string) $request->get_header( 'x_op_nonce' ), (string) $request->get_header( 'x_op_signature' ), $body, time() ); if ( true !== $verdict ) { return new WP_Error( 'orderping_unauthorized', 'Unauthorized', array( 'status' => 401 ) ); } // Claim the nonce only after the signature is valid. if ( ! orderping_claim_nonce( (string) $request->get_header( 'x_op_nonce' ) ) ) { return new WP_Error( 'orderping_replayed', 'Unauthorized', array( 'status' => 401 ) ); } $data = json_decode( $body, true ); if ( ! is_array( $data ) || 1 !== ( $data['contract_version'] ?? null ) || ! in_array( $data['mode'] ?? '', array( 'write', 'read_back' ), true ) || ! is_string( $data['action_id'] ?? null ) || ! preg_match( '/^[A-Za-z0-9:_-]{1,100}$/', $data['action_id'] ) || ! is_string( $data['order_id'] ?? null ) || ! preg_match( '/^\d{1,20}$/', $data['order_id'] ) || ! is_int( $data['confirmation_version'] ?? null ) || $data['confirmation_version'] < 1 || 'confirmed' !== ( $data['state'] ?? '' ) || ! is_string( $data['snapshot_hash'] ?? null ) || ! preg_match( '/^[a-f0-9]{64}$/', $data['snapshot_hash'] ) || ( null !== ( $data['observed_modified'] ?? null ) && ! is_int( $data['observed_modified'] ) ) || ( null !== ( $data['observed_freshness'] ?? null ) && ( ! is_string( $data['observed_freshness'] ) || strlen( $data['observed_freshness'] ) > 100 ) ) ) { return new WP_Error( 'orderping_invalid', 'Invalid request', array( 'status' => 400 ) ); } // Cache-bypassing read: the freshness check must see what is stored, not an object-cache copy. $order = orderping_fresh_order( (int) $data['order_id'] ); // Pilot scope first: nothing about an out-of-scope order is read, written or returned. if ( ! $order instanceof WC_Order || ! orderping_order_in_scope( $order ) ) { return new WP_Error( 'orderping_not_found', 'Order not found', array( 'status' => 404 ) ); } $order_id = $order->get_id(); $action_id = $data['action_id']; // Only orders this plugin captured for the backend: no order data for any other id. if ( ! orderping_order_was_captured( $order_id ) ) { return new WP_Error( 'orderping_not_found', 'Order not found', array( 'status' => 404 ) ); } if ( orderping_confirm_action_known( $order_id, $action_id ) ) { return new WP_REST_Response( orderping_confirmation_ok( $order, 'repeat', orderping_confirmation_note( $order, $action_id ) ), 200 ); } if ( 'read_back' === $data['mode'] ) { return new WP_REST_Response( orderping_confirmation_ok( $order, 'not_found', 'none' ), 200 ); } // One snapshot of the order drives both the check and the in-transaction re-verify. $freshness = orderping_order_freshness( $order ); if ( ( $data['observed_modified'] ?? null ) !== orderping_order_modified( $order ) ) { return orderping_confirmation_conflict( $order, 'order_modified' ); } // The backend accepted the fields it observed; refuse the write if they changed since. if ( isset( $data['observed_freshness'] ) && $data['observed_freshness'] !== $freshness ) { return orderping_confirmation_conflict( $order, 'order_modified' ); } $result = orderping_confirmation_write( $order_id, array( 'freshness' => $freshness, 'action_id' => $action_id, 'confirmation_version' => $data['confirmation_version'], 'state' => $data['state'], 'snapshot_hash' => $data['snapshot_hash'], ) ); if ( 'out_of_scope' === $result ) { return new WP_Error( 'orderping_not_found', 'Order not found', array( 'status' => 404 ) ); } if ( 'order_modified' === $result ) { $latest = orderping_fresh_order( $order_id ) ?? $order; if ( ! orderping_order_in_scope( $latest ) ) { return new WP_Error( 'orderping_not_found', 'Order not found', array( 'status' => 404 ) ); } return orderping_confirmation_conflict( $latest, 'order_modified' ); } if ( 'conflict' === $result ) { return orderping_confirmation_conflict( $order, 'version_conflict' ); } if ( 'error' === $result ) { return new WP_Error( 'orderping_storage', 'Storage error', array( 'status' => 503 ) ); } // Reload: the meta mirror and note write through a fresh order object. $order = wc_get_order( $order_id ); $note = orderping_confirmation_note( $order, $action_id ); return new WP_REST_Response( orderping_confirmation_ok( $order, 'repeat' === $result ? 'repeat' : 'applied', $note ), 200 ); }