includes_pro() ? ':pro' : ':core' ); } public const VERSION_OPTION = 'ipscmi_order_cart_version'; public const INSTALLED_OPTION = 'ipscmi_order_cart_installed'; public const LEGACY_INSTALLED_OPTION = 'ips_wooordecart_installed'; public const REDIRECT_OPTION = 'ipscmi_order_cart_do_activation_redirect'; /** * Run additive install or upgrade tasks. * * The version is deliberately persisted last. A failed earlier step can * therefore be retried on the next request instead of looking complete. */ public function run( bool $is_activation = true ): void { $this->migrate_owned_options(); self::add_settings(); $this->create_cartgate_tables(); $this->migrate_local_pickup_posts(); if ( $is_activation ) { $this->add_redirection_flag(); } $this->clear_transients(); update_option( self::SCHEMA_OPTION, $this->schema_version(), false ); $this->add_version(); } /** * Return whether the stored schema/version needs reconciliation. */ public function needs_upgrade(): bool { $stored_version = (string) get_option( self::VERSION_OPTION, '' ); $missing = new \stdClass(); $installed = get_option( self::INSTALLED_OPTION, $missing ); if ( $missing === $installed ) { return true; } return (string) get_option( self::SCHEMA_OPTION, '' ) !== $this->schema_version() || '' === $stored_version || version_compare( $stored_version, $this->current_version(), '!=' ); } /** Copy only known IPS-owned keys. Retry safely after failure and never overwrite a canonical value. */ private function migrate_owned_options(): void { $missing = new \stdClass(); foreach ( CartDataManifest::legacy_options() as $legacy => $canonical ) { if ( $missing !== get_option( $canonical, $missing ) ) { continue; } $value = get_option( $legacy, $missing ); if ( $missing === $value ) { continue; } if ( ! add_option( $canonical, $value, '', false ) && $missing === get_option( $canonical, $missing ) ) { throw new \RuntimeException( 'Could not migrate an IPS Order Cart option.' ); } } } /** Preserve IDs and metadata; never claim another plugin's generic posts. */ private function migrate_local_pickup_posts(): void { global $wpdb; $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT p.ID FROM {$wpdb->posts} p WHERE p.post_type = %s AND EXISTS (SELECT 1 FROM {$wpdb->postmeta} m WHERE m.post_id = p.ID AND m.meta_key = %s)", 'localpickup', CartDataManifest::LOCAL_PICKUP_META_KEY ) ); if ( '' !== (string) $wpdb->last_error ) { throw new \RuntimeException( 'Could not read IPS Order Cart pickup locations for migration.' ); } foreach ( $post_ids as $post_id ) { $updated = $wpdb->update( $wpdb->posts, [ 'post_type' => CartDataManifest::LOCAL_PICKUP_POST_TYPE ], [ 'ID' => (int) $post_id, 'post_type' => 'localpickup' ], [ '%s' ], [ '%d', '%s' ] ); if ( false === $updated ) { throw new \RuntimeException( 'Could not migrate an IPS Order Cart pickup location.' ); } clean_post_cache( (int) $post_id ); } } /** * Reconcile additive install tasks after a normal WordPress plugin update. */ public function maybe_upgrade(): bool { if ( ! $this->needs_upgrade() ) { return false; } $this->run( false ); return true; } /** * Persist the current plugin version. This must remain the final run step. */ public function add_version(): void { $this->migrate_installation_marker(); update_option( self::VERSION_OPTION, $this->current_version(), false ); } /** * Seed SettingsManager values without replacing merchant configuration. */ public static function add_settings(): void { SettingsManager::instance()->add_setting( 'enable', 'no' ); SettingsManager::instance()->add_setting( 'allow_uninstall', 'no' ); } /** * Clear version-sensitive asset manifests. */ public function clear_transients(): void { global $wpdb; foreach ( [ 'frontend', 'backend' ] as $type ) { delete_transient( 'ipscmi_order_cart_manifest_' . $type ); } $manifest_prefix = 'ipscmi_order_cart_manifest_'; $transient_names = []; foreach ( [ '_transient_', '_transient_timeout_' ] as $storage_prefix ) { $option_names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( $storage_prefix . $manifest_prefix ) . '%' ) ); foreach ( (array) $option_names as $option_name ) { $transient_names[] = substr( (string) $option_name, strlen( $storage_prefix ) ); } } foreach ( array_unique( $transient_names ) as $transient_name ) { delete_transient( $transient_name ); } } /** * Flag a genuine activation, but not a background version upgrade. */ public function add_redirection_flag(): void { add_option( self::REDIRECT_OPTION, true, '', false ); } /** * Install every table registered as owned by this plugin. */ public function create_cartgate_tables(): void { Database_Tables::install_tables(); } /** * Backward-compatible entry point used by older integrations. */ public static function drop_cartgate_tables(): void { Database_Tables::drop_tables(); } /** * Move the historic misspelled marker to the canonical option. * * The legacy value remains available until an explicit full uninstall. */ private function migrate_installation_marker(): void { $missing = new \stdClass(); $installed = get_option( self::INSTALLED_OPTION, $missing ); if ( $missing !== $installed ) { return; } $legacy_installed = get_option( self::LEGACY_INSTALLED_OPTION, $missing ); $installed_at = $missing !== $legacy_installed ? $legacy_installed : time(); update_option( self::INSTALLED_OPTION, $installed_at, false ); } /** * Resolve the authoritative plugin version for activation and upgrades. */ private function current_version(): string { if ( defined( 'IPSCMI_ORDER_CART_VERSION' ) ) { return (string) IPSCMI_ORDER_CART_VERSION; } if ( defined( 'IPSCMI_ORDER_CART_VERSION' ) ) { return (string) IPSCMI_ORDER_CART_VERSION; } $plugin_file = defined( 'IPSCMI_ORDER_CART_RUNTIME_FILE' ) ? IPSCMI_ORDER_CART_RUNTIME_FILE : dirname( __DIR__, 2 ) . '/ips-order-cart.php'; $headers = function_exists( 'get_file_data' ) ? get_file_data( $plugin_file, [ 'Version' => 'Version' ] ) : []; return ! empty( $headers['Version'] ) ? (string) $headers['Version'] : '0.0.0'; } }