get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- current blog's core options table. 'orderguard_data_migration_version' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- cache-independent compatibility control read. if ( '' !== $wpdb->last_error ) { if ( empty( $logged_error[ $blog_id ] ) ) { orderguard_log( 'OrderGuard: stored-data migration stamp read failed: ' . $wpdb->last_error ); $logged_error[ $blog_id ] = true; } return 'error'; } $states[ $blog_id ] = ( null === $value ) ? 'absent' : 'present'; return $states[ $blog_id ]; } /** * Read one current option with the optional bounded private fallback. * * @param string $new_name Current option name. * @param mixed $default Value returned when no permitted row exists. * @param bool $ok False only when the value could not be determined. * @return mixed */ function orderguard_get_option( $new_name, $default = false, &$ok = null ) { global $wpdb; $ok = true; $absent = new stdClass(); $value = get_option( $new_name, $absent ); if ( $absent !== $value ) { return $value; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- OrderGuard's own tables or exact transactional reads/writes; no WordPress API covers these; must read current database state (locks, claims, delete checks); caching would be wrong $raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- current blog's core options table. $new_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- cache-healing read after an Options API miss. if ( '' !== $wpdb->last_error ) { $ok = false; return $default; } if ( null !== $raw ) { orderguard_invalidate_option_caches( $new_name ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applies an existing WordPress or WooCommerce filter; not a new hook return apply_filters( "option_{$new_name}", maybe_unserialize( $raw ), $new_name ); } $legacy_name = orderguard_legacy_option_name( $new_name ); if ( '' === $legacy_name ) { return $default; } $stamp_state = orderguard_data_migration_stamp_state(); if ( 'error' === $stamp_state ) { $ok = false; return $default; } if ( 'present' === $stamp_state ) { return $default; } $value = get_option( $legacy_name, $absent ); if ( $absent !== $value ) { return $value; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- OrderGuard's own tables or exact transactional reads/writes; no WordPress API covers these; must read current database state (locks, claims, delete checks); caching would be wrong $raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- current blog's core options table. $legacy_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- cache-healing compatibility read. if ( '' !== $wpdb->last_error ) { $ok = false; return $default; } if ( null === $raw ) { return $default; } orderguard_invalidate_option_caches( $legacy_name ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applies an existing WordPress or WooCommerce filter; not a new hook return apply_filters( "option_{$legacy_name}", maybe_unserialize( $raw ), $legacy_name ); } /** * Write only the current option name. * * @param string $new_name Current option name. * @param mixed $value Option value. * @param bool|null $autoload Optional autoload intent. * @return bool */ function orderguard_update_option( $new_name, $value, $autoload = null ) { if ( null === $autoload ) { return update_option( $new_name, $value ); } return update_option( $new_name, $value, $autoload ); } /** * Delete the current option and any permitted private predecessor. * * @param string $new_name Current option name. * @return bool Whether either row was deleted. */ function orderguard_delete_option( $new_name ) { $deleted = delete_option( $new_name ); $legacy_name = orderguard_legacy_option_name( $new_name ); if ( '' !== $legacy_name ) { $deleted = delete_option( $legacy_name ) || $deleted; } return $deleted; } /** * Read current order meta, with an optional private predecessor. * * @param WC_Order $order Order object. * @param string $new_name Current meta key. * @param mixed $default Value when neither generation exists. * @return mixed */ function orderguard_get_order_meta( $order, $new_name, $default = '' ) { if ( ! $order instanceof WC_Order ) { return $default; } if ( $order->meta_exists( $new_name ) ) { return $order->get_meta( $new_name ); } $legacy_name = apply_filters( 'orderguard_legacy_order_meta_name', '', $new_name ); if ( is_string( $legacy_name ) && '' !== $legacy_name && $order->meta_exists( $legacy_name ) ) { return $order->get_meta( $legacy_name ); } return $default; } /** Normalize the early flat courier-provenance shape into a source-keyed map. */ function orderguard_normalize_courier_provenance( $value ) { if ( ! is_array( $value ) ) { return array(); } if ( isset( $value['courier'] ) && is_string( $value['courier'] ) ) { $source = sanitize_key( $value['courier'] ); return '' === $source ? array() : array( $source => $value ); } return $value; } /** * Merge map-shaped meta across generations without dropping per-source data. * * @param WC_Order $order Order object. * @param string $new_name Current meta key. * @param string $mode new_wins or earliest. * @return array */ function orderguard_get_order_meta_map( $order, $new_name, $mode = 'new_wins' ) { if ( ! $order instanceof WC_Order ) { return array(); } $current = $order->meta_exists( $new_name ) ? $order->get_meta( $new_name ) : array(); $current = is_array( $current ) ? $current : array(); $legacy = array(); $old = apply_filters( 'orderguard_legacy_order_meta_name', '', $new_name ); if ( is_string( $old ) && '' !== $old && $order->meta_exists( $old ) ) { $legacy = $order->get_meta( $old ); $legacy = is_array( $legacy ) ? $legacy : array(); } if ( '_orderguard_courier_provenance' === $new_name ) { $legacy = orderguard_normalize_courier_provenance( $legacy ); $current = orderguard_normalize_courier_provenance( $current ); } if ( 'earliest' !== $mode ) { return array_merge( $legacy, $current ); } $merged = $legacy; foreach ( $current as $source => $timestamp ) { $timestamp = (int) $timestamp; $previous = isset( $merged[ $source ] ) ? (int) $merged[ $source ] : 0; if ( $timestamp > 0 && ( $previous <= 0 || $timestamp < $previous ) ) { $merged[ $source ] = $timestamp; } } return $merged; } /** * Delete current order meta and any permitted private predecessor. * * @param WC_Order $order Order object. * @param string $new_name Current meta key. */ function orderguard_delete_order_meta( $order, $new_name ) { if ( ! $order instanceof WC_Order ) { return; } $order->delete_meta_data( $new_name ); $legacy_name = apply_filters( 'orderguard_legacy_order_meta_name', '', $new_name ); if ( is_string( $legacy_name ) && '' !== $legacy_name ) { $order->delete_meta_data( $legacy_name ); } } /** * Return current and permitted compatibility keys for separate queries. * * @param string $new_name Current meta key. * @return string[] */ function orderguard_order_meta_query_keys( $new_name ) { $keys = array( $new_name ); $legacy_name = apply_filters( 'orderguard_legacy_order_meta_name', '', $new_name ); if ( is_string( $legacy_name ) && '' !== $legacy_name ) { $keys[] = $legacy_name; } return array_values( array_unique( $keys ) ); } /** * Check current scheduled work plus an optional private predecessor hook. * * @param string $new_hook Current hook. * @param array|null $args Action arguments, or null for any arguments. * @param string $group Action Scheduler group. * @return int|bool */ function orderguard_has_scheduled_action( $new_hook, $args = null, $group = '' ) { if ( ! function_exists( 'as_has_scheduled_action' ) ) { return false; } $action_id = as_has_scheduled_action( $new_hook, $args, $group ); if ( $action_id ) { return $action_id; } $legacy_hook = apply_filters( 'orderguard_legacy_scheduled_hook', '', $new_hook ); return ( is_string( $legacy_hook ) && '' !== $legacy_hook ) ? as_has_scheduled_action( $legacy_hook, $args, $group ) : false; } /** * Read current user meta and copy a private predecessor forward on access. * * @param int $user_id User ID. * @param string $new_name Current meta key. * @return mixed */ function orderguard_get_user_meta( $user_id, $new_name ) { if ( metadata_exists( 'user', $user_id, $new_name ) ) { return get_user_meta( $user_id, $new_name, true ); } $legacy_name = apply_filters( 'orderguard_legacy_user_meta_name', '', $new_name ); if ( ! is_string( $legacy_name ) || '' === $legacy_name ) { return ''; } if ( ! metadata_exists( 'user', $user_id, $legacy_name ) ) { return ''; } $value = get_user_meta( $user_id, $legacy_name, true ); update_user_meta( $user_id, $new_name, $value ); return $value; } /** * Read a current transient with an optional bounded private predecessor. * * @param string $new_name Current transient name. * @return mixed */ function orderguard_get_transient( $new_name ) { $value = get_transient( $new_name ); if ( false !== $value ) { return $value; } $legacy_name = apply_filters( 'orderguard_legacy_transient_name', '', $new_name ); return ( is_string( $legacy_name ) && '' !== $legacy_name ) ? get_transient( $legacy_name ) : false; } /** * Delete a current transient and any permitted private predecessor. * * @param string $new_name Current transient name. */ function orderguard_delete_transient( $new_name ) { delete_transient( $new_name ); $legacy_name = apply_filters( 'orderguard_legacy_transient_name', '', $new_name ); if ( is_string( $legacy_name ) && '' !== $legacy_name ) { delete_transient( $legacy_name ); } } /** * Read a migration control row directly. * * @param string $name Control option name. * @param bool $found Whether the row exists. * @param bool $ok False on a database error. * @return string|null */ function orderguard_migration_control_read( $name, &$found, &$ok ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- OrderGuard's own tables or exact transactional reads/writes; no WordPress API covers these; must read current database state (locks, claims, delete checks); caching would be wrong $value = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- current blog's core options table. $name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- cache-independent control read. $ok = ( '' === $wpdb->last_error ); $found = $ok && null !== $value; return $ok ? $value : null; } /** * Confirm absence before starting a one-time workflow. * * @param string $new_name Current option name. * @param mixed $blocking Existing value, when present. * @param string $blocking_name Existing row name, when present. * @param bool $ok False on a database error or stale stamp. * @return bool */ function orderguard_option_absent_for_fresh_start( $new_name, &$blocking = null, &$blocking_name = '', &$ok = null ) { $blocking = null; $blocking_name = ''; $ok = false; $legacy_name = orderguard_legacy_option_name( $new_name ); $stamp_found = false; $stamp_ok = false; $stamp = orderguard_migration_control_read( 'orderguard_data_migration_version', $stamp_found, $stamp_ok ); if ( ! $stamp_ok ) { return false; } if ( $stamp_found && defined( 'ORDERGUARD_DATA_MIGRATION_VERSION' ) && ORDERGUARD_DATA_MIGRATION_VERSION !== (string) $stamp ) { return false; } $found = false; $raw = orderguard_migration_control_read( $new_name, $found, $ok ); if ( ! $ok || $found ) { if ( $found ) { $blocking = maybe_unserialize( $raw ); $blocking_name = $new_name; } return false; } if ( ! $stamp_found && '' !== $legacy_name ) { $raw = orderguard_migration_control_read( $legacy_name, $found, $ok ); if ( ! $ok || $found ) { if ( $found ) { $blocking = maybe_unserialize( $raw ); $blocking_name = $legacy_name; } return false; } } $ok = true; return true; } // Source path is pinned by build.sh; the public artifact omits this directory. $orderguard_compat_runtime = __DIR__ . '/compat/runtime.php'; if ( is_readable( $orderguard_compat_runtime ) ) { require_once $orderguard_compat_runtime; } unset( $orderguard_compat_runtime );