new. */ private const OPTION_MAP = array( // The encryption key must travel with the settings blob it unlocks, // otherwise the stored integration key decrypts to an empty string. 'fluxserp_enc_key' => 'ranktim_enc_key', 'fluxserp_settings' => 'ranktim_settings', 'fluxserp_last_catchup_run' => 'ranktim_last_catchup_run', ); /** Post meta keys to rename, old => new. */ private const META_MAP = array( '_fluxserp_item_id' => '_ranktim_item_id', '_fluxserp_synced_at' => '_ranktim_synced_at', '_fluxserp_meta_title' => '_ranktim_meta_title', '_fluxserp_meta_description' => '_ranktim_meta_description', '_fluxserp_primary_keyword' => '_ranktim_primary_keyword', '_fluxserp_secondary_keywords' => '_ranktim_secondary_keywords', '_fluxserp_cta' => '_ranktim_cta', '_fluxserp_internal_link_suggestions' => '_ranktim_internal_link_suggestions', '_fluxserp_image_suggestions' => '_ranktim_image_suggestions', '_fluxserp_image_url' => '_ranktim_image_url', '_fluxserp_image_alt' => '_ranktim_image_alt', '_fluxserp_outline' => '_ranktim_outline', '_fluxserp_json_ld' => '_ranktim_json_ld', ); /** * Run any migration steps this install still needs. * * Cheap enough to call on every admin request: the common case is one * autoloaded option read that matches CURRENT_VERSION and returns. */ public static function maybe_run(): void { $version = (int) get_option( self::VERSION_OPTION, 0 ); if ( $version >= self::CURRENT_VERSION ) { return; } if ( $version < 2 ) { self::migrate_fluxserp_prefix(); } update_option( self::VERSION_OPTION, self::CURRENT_VERSION, true ); } /** * Move options, post meta and the scheduled event off the fluxserp prefix. */ private static function migrate_fluxserp_prefix(): void { self::migrate_options(); self::migrate_post_meta(); self::unschedule_legacy_cron(); self::purge_legacy_transients(); } /** * Copy legacy options across. The originals are left in place so a rollback * to an older plugin build keeps working; they are inert otherwise. */ private static function migrate_options(): void { foreach ( self::OPTION_MAP as $legacy => $current ) { // Never clobber a value the new-format plugin already wrote. if ( false !== get_option( $current, false ) ) { continue; } $value = get_option( $legacy, null ); if ( null === $value ) { continue; } // Settings and the catch-up stamp are read on demand; only the // version stamp needs to autoload. update_option( $current, $value, false ); } } /** * Rename post meta keys in a single UPDATE per key. * * A meta_key rename is one indexed UPDATE on wp_postmeta, so even a site * with thousands of imported articles finishes inside a normal request. */ private static function migrate_post_meta(): void { global $wpdb; foreach ( self::META_MAP as $legacy => $current ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time key rename; no core API renames meta keys in bulk. $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = %s WHERE meta_key = %s", $current, $legacy ) ); } // Meta was rewritten underneath the object cache. wp_cache_flush(); } /** * Drop the old scheduled event. The new hook is (re)scheduled by * {@see Ranktim_Cron::ensure_scheduled()} on the same request. */ private static function unschedule_legacy_cron(): void { while ( $timestamp = wp_next_scheduled( self::LEGACY_CRON_HOOK ) ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition wp_unschedule_event( $timestamp, self::LEGACY_CRON_HOOK ); } } /** * Delete short-lived fluxserp transients (sync lock, queue cache, ack * throttles, admin notices). They all expire on their own, but clearing the * lock immediately means the first sync after the upgrade is not skipped. */ private static function purge_legacy_transients(): void { global $wpdb; delete_transient( 'fluxserp_sync_lock' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time cleanup of prefixed transients; there is no core API for prefix deletion. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_fluxserp_' ) . '%', $wpdb->esc_like( '_transient_timeout_fluxserp_' ) . '%' ) ); } }