= self::VERSION ) { return true; } add_action( 'sitelang_continue_key_migration', array( __CLASS__, 'run' ) ); for ( $pass = 0; $pass < 3; $pass++ ) { if ( self::run() ) { return true; } } return false; } /** Process one resumable pass. Legacy rows are deliberately never deleted. */ public static function run() { global $wpdb; if ( ! defined( 'SITELANG_DIRECTORY_BUILD' ) || ! SITELANG_DIRECTORY_BUILD || self::current_version() >= self::VERSION ) { return true; } if ( ! self::ensure_migration_map() ) { self::schedule_continuation(); return false; } $progress = get_option( 'sitelang_identifier_copy_progress', array() ); $progress = is_array( $progress ) ? $progress : array(); $complete = true; $old = self::legacy_prefix(); if ( empty( $progress['options'] ) ) { $cursor = absint( $progress['options_cursor'] ?? 0 ); $rows = self::legacy_option_batch( $cursor, $old ); if ( false === $rows ) { $complete = false; $rows = array(); } foreach ( $rows as $row ) { $new_name = str_replace( $old, 'sitelang_', (string) $row['option_name'] ); $inserted = $wpdb->query( $wpdb->prepare( 'INSERT IGNORE INTO %i (option_name, option_value, autoload) VALUES (%s, %s, %s)', $wpdb->options, $new_name, $row['option_value'], $row['autoload'] ) ); if ( false === $inserted ) { $complete = false; break; } $cursor = max( $cursor, absint( $row['option_id'] ) ); } $progress['options_cursor'] = $cursor; if ( false !== $rows && count( $rows ) < self::BATCH_SIZE && $complete ) { $progress['options'] = 1; } else { $complete = false; } } if ( empty( $progress['actionscheduler'] ) ) { $action_result = self::copy_action_scheduler_batch( $old ); if ( false === $action_result ) { $complete = false; } elseif ( $action_result < self::BATCH_SIZE ) { $progress['actionscheduler'] = 1; } else { $complete = false; } } $meta_tables = array( 'postmeta' => array( $wpdb->postmeta, 'post_id', 'meta_id' ), 'commentmeta' => array( $wpdb->commentmeta, 'comment_id', 'meta_id' ), 'termmeta' => array( $wpdb->termmeta, 'term_id', 'meta_id' ), 'usermeta' => array( $wpdb->usermeta, 'user_id', 'umeta_id' ), 'order_itemmeta' => array( $wpdb->prefix . 'woocommerce_order_itemmeta', 'order_item_id', 'meta_id' ), ); foreach ( $meta_tables as $label => $definition ) { if ( ! empty( $progress[ $label ] ) ) { continue; } if ( ! self::table_exists( $definition[0] ) ) { $progress[ $label ] = 1; continue; } $result = self::copy_meta_batch( $label, $definition[0], $definition[1], $definition[2], $old ); if ( false === $result ) { $complete = false; } elseif ( $result < self::BATCH_SIZE ) { $progress[ $label ] = 1; } else { $complete = false; } } if ( empty( $progress['translations_table'] ) ) { $table_complete = self::copy_translation_table_batch(); if ( $table_complete ) { $progress['translations_table'] = 1; } else { $complete = false; } } if ( empty( $progress['cron'] ) ) { self::copy_cron_hooks( $old ); $progress['cron'] = 1; } update_option( 'sitelang_identifier_copy_progress', $progress, false ); $required = array( 'options', 'postmeta', 'commentmeta', 'termmeta', 'usermeta', 'order_itemmeta', 'translations_table', 'actionscheduler', 'cron' ); if ( $complete && count( array_intersect_key( array_filter( $progress ), array_flip( $required ) ) ) === count( $required ) ) { update_option( 'sitelang_db_version', self::VERSION, false ); update_option( 'sitelang_identifier_copy_version', self::VERSION, false ); return true; } self::schedule_continuation(); return false; } /** Show administrators why SiteLang is temporarily waiting during a large copy. */ public static function render_pending_notice() { if ( current_user_can( 'manage_options' ) ) { echo '

' . esc_html__( 'SiteLang is safely copying existing settings and translation mappings for this update. No legacy data is deleted. The language features will return automatically when the copy finishes.', 'sitelang' ) . '

'; } } private static function current_version() { return absint( get_option( 'sitelang_identifier_copy_version', 0 ) ); } /** Reconstruct the previous identifier without shipping its short prefix. */ private static function legacy_prefix() { return chr( 115 ) . chr( 108 ) . chr( 119 ) . '_'; } private static function ensure_migration_map() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = $wpdb->prefix . 'sitelang_migration_map'; $sql = "CREATE TABLE {$table} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, source_table varchar(32) NOT NULL, source_id bigint(20) unsigned NOT NULL, target_id bigint(20) unsigned NULL, PRIMARY KEY (id), UNIQUE KEY source_record (source_table, source_id) ) {$wpdb->get_charset_collate()};"; dbDelta( $sql ); return self::table_exists( $table ); } private static function schedule_continuation() { if ( ! wp_next_scheduled( 'sitelang_continue_key_migration' ) ) { wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'sitelang_continue_key_migration' ); } } private static function legacy_option_batch( $cursor, $old ) { global $wpdb; $pattern = '%' . $wpdb->esc_like( $old ) . '%'; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT option_id, option_name, option_value, autoload FROM {$wpdb->options} WHERE option_id > %d AND option_name LIKE %s ORDER BY option_id ASC LIMIT %d", $cursor, $pattern, self::BATCH_SIZE ), ARRAY_A ); return ! empty( $wpdb->last_error ) ? false : (array) $rows; } private static function copy_meta_batch( $label, $table, $object_column, $id_column, $old ) { global $wpdb; $pattern = '%' . $wpdb->esc_like( $old ) . '%'; $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT source.%i, source.%i, source.meta_key, source.meta_value FROM %i AS source LEFT JOIN %i AS copied ON copied.source_table = %s AND copied.source_id = source.%i WHERE source.meta_key LIKE %s AND copied.id IS NULL ORDER BY source.%i ASC LIMIT %d', $id_column, $object_column, $table, $wpdb->prefix . 'sitelang_migration_map', $label, $id_column, $pattern, $id_column, self::BATCH_SIZE ), ARRAY_A ); if ( ! empty( $wpdb->last_error ) ) { return false; } $rows = (array) $rows; foreach ( $rows as $row ) { $wpdb->query( 'START TRANSACTION' ); $copy = $row; unset( $copy[ $id_column ] ); $copy[ $object_column ] = absint( $row[ $object_column ] ); $copy['meta_key'] = str_replace( $old, 'sitelang_', (string) $row['meta_key'] ); if ( ( '_' . $old . 'ai_job_action_id' ) === (string) $row['meta_key'] && ctype_digit( (string) $row['meta_value'] ) ) { $mapped_action = $wpdb->get_var( $wpdb->prepare( 'SELECT target_id FROM %i WHERE source_table = %s AND source_id = %d', $wpdb->prefix . 'sitelang_migration_map', 'actionscheduler_actions', absint( $row['meta_value'] ) ) ); if ( $mapped_action ) { $copy['meta_value'] = (string) absint( $mapped_action ); } } if ( false === $wpdb->insert( $table, $copy ) || false === $wpdb->insert( $wpdb->prefix . 'sitelang_migration_map', array( 'source_table' => $label, 'source_id' => absint( $row[ $id_column ] ) ), array( '%s', '%d' ) ) ) { $wpdb->query( 'ROLLBACK' ); return false; } $wpdb->query( 'COMMIT' ); } return count( $rows ); } private static function copy_translation_table_batch() { global $wpdb; $old_table = $wpdb->prefix . self::legacy_prefix() . 'translations'; $new_table = $wpdb->prefix . 'sitelang_translations'; if ( ! self::table_exists( $old_table ) ) { return true; } if ( ! self::table_exists( $new_table ) ) { if ( false === $wpdb->query( $wpdb->prepare( 'CREATE TABLE %i LIKE %i', $new_table, $old_table ) ) || ! self::table_exists( $new_table ) ) { return false; } } $cursor = absint( get_option( 'sitelang_identifier_table_cursor', 0 ) ); $rows = $wpdb->get_col( $wpdb->prepare( 'SELECT id FROM %i WHERE id > %d ORDER BY id ASC LIMIT %d', $old_table, $cursor, self::BATCH_SIZE ) ); if ( ! empty( $wpdb->last_error ) ) { return false; } $rows = (array) $rows; if ( ! $rows ) { return true; } $last = absint( end( $rows ) ); if ( false === $wpdb->query( $wpdb->prepare( 'INSERT IGNORE INTO %i SELECT * FROM %i WHERE id > %d AND id <= %d ORDER BY id ASC', $new_table, $old_table, $cursor, $last ) ) ) { return false; } update_option( 'sitelang_identifier_table_cursor', $last, false ); return count( $rows ) < self::BATCH_SIZE; } /** Copy pending/complete Action Scheduler records without changing old rows. */ private static function copy_action_scheduler_batch( $old ) { global $wpdb; $table = $wpdb->prefix . 'actionscheduler_actions'; if ( ! self::table_exists( $table ) ) { return 0; } $pattern = '%' . $wpdb->esc_like( $old ) . '%'; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT source.* FROM %i AS source LEFT JOIN %i AS copied ON copied.source_table = %s AND copied.source_id = source.action_id WHERE source.hook LIKE %s AND copied.id IS NULL ORDER BY source.action_id ASC LIMIT %d", $table, $wpdb->prefix . 'sitelang_migration_map', 'actionscheduler_actions', $pattern, self::BATCH_SIZE ), ARRAY_A ); if ( ! empty( $wpdb->last_error ) ) { return false; } $rows = (array) $rows; foreach ( $rows as $row ) { $source_id = absint( $row['action_id'] ); unset( $row['action_id'] ); $row['hook'] = str_replace( $old, 'sitelang_', (string) $row['hook'] ); if ( 'pending' === $row['status'] && ! empty( $row['claim_id'] ) ) { $row['claim_id'] = 0; } $wpdb->query( 'START TRANSACTION' ); if ( false === $wpdb->insert( $table, $row ) ) { $wpdb->query( 'ROLLBACK' ); return false; } $target_id = absint( $wpdb->insert_id ); if ( ! $target_id || false === $wpdb->insert( $wpdb->prefix . 'sitelang_migration_map', array( 'source_table' => 'actionscheduler_actions', 'source_id' => $source_id, 'target_id' => $target_id ), array( '%s', '%d', '%d' ) ) ) { $wpdb->query( 'ROLLBACK' ); return false; } $wpdb->query( 'COMMIT' ); } return count( $rows ); } private static function copy_cron_hooks( $old ) { $cron = _get_cron_array(); if ( ! is_array( $cron ) ) { return; } foreach ( $cron as $timestamp => $hooks ) { foreach ( $hooks as $hook => $events ) { if ( false === strpos( $hook, $old ) ) { continue; } $new_hook = str_replace( $old, 'sitelang_', $hook ); foreach ( $events as $event_key => $event ) { $args = self::replace_cron_args( (array) ( $event['args'] ?? array() ), $old ); $key = md5( serialize( $args ) ); if ( ! isset( $cron[ $timestamp ][ $new_hook ][ $key ] ) ) { $event['args'] = $args; if ( ! empty( $event['schedule'] ) ) { $event['schedule'] = str_replace( $old, 'sitelang_', $event['schedule'] ); } $cron[ $timestamp ][ $new_hook ][ $key ] = $event; } unset( $event_key ); } } } _set_cron_array( $cron ); } private static function replace_cron_args( array $args, $old ) { foreach ( $args as $key => $value ) { $new_key = is_string( $key ) ? str_replace( $old, 'sitelang_', $key ) : $key; if ( is_string( $value ) ) { $args[ $new_key ] = str_replace( $old, 'sitelang_', $value ); } elseif ( is_array( $value ) ) { $args[ $new_key ] = self::replace_cron_args( $value, $old ); } elseif ( $new_key !== $key ) { $args[ $new_key ] = $value; } if ( $new_key !== $key ) { unset( $args[ $key ] ); } } return $args; } private static function table_exists( $table ) { global $wpdb; return $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ); } }