Migrator
**Site relevance:** Antichrist.net
**Memory type:** plugin source memory
**Source path:** Antichrist.net/wp-content/plugins/uaix-locale-router/src/Migrator.php
**Size:** 8 KB
Summary
namespace UAIXLocaleRouter;
Source Preview
This source file is short enough to preview directly on its source-memory page.
<?php
namespace UAIXLocaleRouter;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use FilesystemIterator;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Versioned compatibility migrations from the old NS12 Locale Router.
*/
final class Migrator {
/**
* Source-to-target option map.
*
* @return array
*/
private static function option_map() {
return array(
'ns12lr_settings' => 'uaixlr_settings',
'ns12lr_supported_locales' => 'uaixlr_supported_locales',
'ns12lr_default_locale' => 'uaixlr_default_locale',
'ns12lr_dictionary_manifest' => 'uaixlr_dictionary_manifest',
'ns12lr_install_version' => 'uaixlr_install_version',
'ns12lr_bootstrap_notice' => 'uaixlr_bootstrap_notice',
'ns12lr_bootstrap_notice_raw' => 'uaixlr_bootstrap_notice_raw',
);
}
/**
* Run all idempotent migrations.
*
* @return array
*/
public static function run() {
$status = array(
'version' => UAIXLR_VERSION,
'ran_at' => time(),
'options_copied' => array(),
'dictionary_files' => array(),
'dictionary_backups' => array(),
'validation_table' => 'not-needed',
'legacy_data_deleted' => false,
'errors' => array(),
);
try {
$status['options_copied'] = self::migrate_options();
} catch ( \Throwable $throwable ) {
$status['errors'][] = 'options: ' . $throwable->getMessage();
}
try {
$file_status = self::migrate_dictionary_files();
$status['dictionary_files'] = $file_status['copied'];
$status['dictionary_backups'] = $file_status['backups'];
} catch ( \Throwable $throwable ) {
$status['errors'][] = 'dictionaries: ' . $throwable->getMessage();
}
try {
$status['validation_table'] = self::migrate_validation_table();
} catch ( \Throwable $throwable ) {
$status['errors'][] = 'validation table: ' . $throwable->getMessage();
}
update_option( 'uaixlr_migration_status', $status, false );
if ( self::did_migrate_anything( $status ) && empty( $status['errors'] ) ) {
Plugin::set_flash_notice(
__( 'UAIX Locale Router migrated existing NS12 Locale Router settings, dictionaries, or validation history. The old NS12 data was left in place for rollback safety.', 'uaix-locale-router' ),
'success'
);
}
if ( ! empty( $status['errors'] ) ) {
Plugin::set_flash_notice(
__( 'UAIX Locale Router finished with migration warnings. Open Status for details.', 'uaix-locale-router' ),
'warning',
array(
'details' => array(
'Errors' => implode( '; ', $status['errors'] ),
),
)
);
}
return $status;
}
/**
* Return the last migration status payload.
*
* @return array
*/
public static function status() {
$status = get_option( 'uaixlr_migration_status', array() );
return is_array( $status ) ? $status : array();
}
/**
* Copy old NS12 options to UAIX names when the UAIX option does not exist.
*
* @return array
*/
private static function migrate_options() {
$copied = array();
foreach ( self::option_map() as $old_option => $new_option ) {
if ( false !== get_option( $new_option, false ) ) {
continue;
}
$old_value = get_option( $old_option, false );
if ( false === $old_value ) {
continue;
}
add_option( $new_option, $old_value, '', false );
$copied[] = $old_option . ' -> ' . $new_option;
}
return $copied;
}
/**
* Copy old upload dictionaries forward without destroying rollback files.
*
* @return array
*/
private static function migrate_dictionary_files() {
$upload_dir = wp_upload_dir();
$base_dir = isset( $upload_dir['basedir'] ) ? trailingslashit( $upload_dir['basedir'] ) : '';
$old_dir = $base_dir . 'ns12-locale-router/dictionaries';
$new_dir = $base_dir . 'uaix-locale-router/dictionaries';
$result = array(
'copied' => array(),
'backups' => array(),
);
if ( '' === $base_dir || ! is_dir( $old_dir ) ) {
return $result;
}
if ( ! is_dir( $new_dir ) ) {
wp_mkdir_p( $new_dir );
}
if ( ! is_dir( $new_dir ) || ! is_writable( $new_dir ) ) {
throw new \RuntimeException( 'The UAIX dictionary directory is not writable.' );
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $old_dir, FilesystemIterator::SKIP_DOTS )
);
foreach ( $iterator as $file_info ) {
if ( ! $file_info->isFile() ) {
continue;
}
$source = (string) $file_info->getPathname();
if ( ! self::is_safe_dictionary_filename( basename( $source ) ) ) {
continue;
}
$target = trailingslashit( $new_dir ) . basename( $source );
if ( file_exists( $target ) ) {
$source_mtime = filemtime( $source );
$target_mtime = filemtime( $target );
if ( false !== $source_mtime && false !== $target_mtime && $target_mtime >= $source_mtime ) {
continue;
}
$backup = $target . '.bak-' . gmdate( 'YmdHis' );
if ( ! @copy( $target, $backup ) ) {
throw new \RuntimeException( 'Could not create a backup for ' . basename( $target ) . '.' );
}
$result['backups'][] = $backup;
}
if ( ! @copy( $source, $target ) ) {
throw new \RuntimeException( 'Could not copy dictionary ' . basename( $source ) . '.' );
}
$result['copied'][] = basename( $target );
}
return $result;
}
/**
* Copy the old validation table to the UAIX table when needed.
*
* @return string
*/
private static function migrate_validation_table() {
global $wpdb;
$old_table = self::safe_table_name( $wpdb->prefix . 'ns12lr_dictionary_validation' );
$new_table = self::safe_table_name( $wpdb->prefix . 'uaixlr_dictionary_validation' );
if ( ! self::table_exists( $old_table ) ) {
return 'not-needed';
}
if ( self::table_exists( $new_table ) ) {
return 'already-present';
}
if ( class_exists( __NAMESPACE__ . '\Plugin' ) ) {
Plugin::create_validation_table();
}
if ( ! self::table_exists( $new_table ) ) {
return 'failed';
}
$columns = array(
'ValidationId',
'LocaleTag',
'FileHash',
'TemplateVersion',
'Status',
'IssueCount',
'IssuesJson',
'CreatedUtc',
'CreatedByUserId',
);
$sql_columns = '`' . implode( '`, `', $columns ) . '`';
$wpdb->query( "INSERT INTO `{$new_table}` ({$sql_columns}) SELECT {$sql_columns} FROM `{$old_table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return self::table_exists( $new_table ) ? 'copied' : 'failed';
}
/**
* Determine whether a database table exists.
*
* @param string $table_name Table name.
* @return bool
*/
private static function table_exists( $table_name ) {
global $wpdb;
$table_name = self::safe_table_name( $table_name );
$found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) );
return $found === $table_name;
}
/**
* Whitelist generated table names before interpolating them into DDL.
*
* @param string $table_name Table name.
* @return string
*/
private static function safe_table_name( $table_name ) {
return preg_replace( '/[^A-Za-z0-9_]/', '', (string) $table_name );
}
/**
* Validate legacy dictionary file names before copying.
*
* @param string $filename File name.
* @return bool
*/
private static function is_safe_dictionary_filename( $filename ) {
if ( ! is_string( $filename ) || strlen( $filename ) > 190 ) {
return false;
}
if ( ! preg_match( '/^[A-Za-z0-9._-]+\.json$/', $filename ) ) {
return false;
}
return false === strpos( $filename, '..' );
}
/**
* Determine whether this migration did real work.
*
* @param array $status Migration status.
* @return bool
*/
private static function did_migrate_anything( array $status ) {
return ! empty( $status['options_copied'] )
|| ! empty( $status['dictionary_files'] )
|| 'copied' === $status['validation_table'];
}
}