= 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 ) ) );
}
}