d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
302 lines
9.6 KiB
PHP
302 lines
9.6 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_V2_Upgrader — atomic v1.3.x → v2.0.0 upgrade orchestrator (PR-7).
|
|
*
|
|
* Implements the Part F upgrade strategy from the master plan:
|
|
* 1. Pre-flight: PHP / WP / MySQL versions, free disk, no in-flight migrations
|
|
* 2. Backup wpdo_features option (rollback safety net)
|
|
* 3. dbDelta v2 tables (idempotent)
|
|
* 4. ALTER existing tables (skip if already migrated)
|
|
* 5. UAE import (when wp_uae_* present, otherwise no-op)
|
|
* 6. migrate_feature_flags_v2 — seed entity module states
|
|
* 7. update wpdo_db_version → 2.0.0
|
|
* 8. safely_deactivate_uae_plugin (when present)
|
|
* 9. Schedule cron to remove the UAE plugin directory (5s)
|
|
* 10. fire wpdo_v2_upgraded action
|
|
*
|
|
* Any throw → catch → write wpdo_v2_upgrade_error → restore features → admin
|
|
* notice. New tables are NOT dropped on failure (idempotent retry).
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 2.0.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Atomic upgrade orchestrator.
|
|
*/
|
|
final class TMDO_V2_Upgrader {
|
|
|
|
/** Target schema version after successful upgrade. */
|
|
public const TARGET_VERSION = '2.0.0';
|
|
|
|
/** Min PHP / WP / MySQL versions enforced by pre-flight. */
|
|
public const MIN_PHP_VERSION = '8.1';
|
|
public const MIN_WP_VERSION = '6.0';
|
|
public const MIN_MYSQL_VERSION = '5.7';
|
|
|
|
/** Free-disk threshold in MB. */
|
|
public const MIN_FREE_DISK_MB = 100;
|
|
|
|
/**
|
|
* Pre-flight check — returns map of check_name => bool.
|
|
*
|
|
* @param array $opts Optional overrides (mostly for tests).
|
|
* @return array<string,bool>
|
|
*/
|
|
public static function pre_flight_check( array $opts = array() ): array {
|
|
global $wpdb;
|
|
|
|
$wp_version = $opts['wp_version'] ?? ( defined( 'ABSPATH' ) ? ( get_bloginfo( 'version' ) ?: '6.0' ) : '6.0' );
|
|
$mysql_version = $opts['mysql_version'] ?? self::detect_mysql_version();
|
|
$disk_path = $opts['disk_path'] ?? sys_get_temp_dir();
|
|
|
|
$checks = array(
|
|
'php_version' => version_compare( PHP_VERSION, self::MIN_PHP_VERSION, '>=' ),
|
|
'wp_version' => version_compare( $wp_version, self::MIN_WP_VERSION, '>=' ),
|
|
'mysql_version' => version_compare( $mysql_version, self::MIN_MYSQL_VERSION, '>=' ),
|
|
'free_disk_mb' => self::detect_free_disk_mb( $disk_path ) >= self::MIN_FREE_DISK_MB,
|
|
'features_writable' => self::is_features_option_writable(),
|
|
'no_active_migration' => self::no_active_migration(),
|
|
);
|
|
|
|
return $checks;
|
|
}
|
|
|
|
/**
|
|
* Run the atomic upgrade. Returns true on success, false on failure.
|
|
*
|
|
* @return bool
|
|
* @throws \RuntimeException When UAE importer fails (caught internally and rolled back).
|
|
*/
|
|
public static function upgrade_to_v2(): bool {
|
|
// 1. Pre-flight check.
|
|
$checks = self::pre_flight_check();
|
|
if ( in_array( false, $checks, true ) ) {
|
|
update_option(
|
|
'wpdo_v2_upgrade_error',
|
|
'Pre-flight check failed: ' . wp_json_encode( $checks ),
|
|
false
|
|
);
|
|
update_option( 'wpdo_v2_upgrade_status', 'preflight_failed', false );
|
|
return false;
|
|
}
|
|
|
|
update_option( 'wpdo_v2_upgrade_started_at', time(), false );
|
|
update_option( 'wpdo_v2_upgrade_status', 'in_progress', false );
|
|
|
|
$features_backup = get_option( 'wpdo_features', array() );
|
|
update_option( 'wpdo_v2_features_backup', $features_backup, false );
|
|
|
|
try {
|
|
// 4. Install v2 tables.
|
|
TMDO_Installer::install_v2_tables();
|
|
|
|
// 5. UAE import (when applicable).
|
|
if ( self::detect_uae_data() ) {
|
|
$import_ok = self::run_uae_import();
|
|
if ( ! $import_ok ) {
|
|
throw new \RuntimeException( 'UAE importer failed' );
|
|
}
|
|
}
|
|
|
|
// 6. Migrate feature flags to include entity modules.
|
|
self::migrate_feature_flags_v2();
|
|
|
|
// 7. Bump db_version.
|
|
update_option( 'wpdo_db_version', self::TARGET_VERSION, false );
|
|
update_option( 'wpdo_v2_upgrade_status', 'complete', false );
|
|
update_option( 'wpdo_v2_upgraded_at', time(), false );
|
|
|
|
// 8. Safely deactivate UAE plugin.
|
|
self::safely_deactivate_uae_plugin();
|
|
|
|
// 9. Schedule plugin directory removal.
|
|
self::schedule_uae_dir_removal();
|
|
|
|
// 10. Fire hook for downstream consumers.
|
|
do_action( 'wpdo_v2_upgraded', '1.3.38', self::TARGET_VERSION );
|
|
|
|
return true;
|
|
|
|
} catch ( \Throwable $e ) {
|
|
// Rollback path: restore features option, leave new tables in place
|
|
// (idempotent retry on next attempt).
|
|
update_option( 'wpdo_features', $features_backup );
|
|
update_option( 'wpdo_v2_upgrade_error', $e->getMessage(), false );
|
|
update_option( 'wpdo_v2_upgrade_status', 'failed', false );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Roll back from a partially-completed v2 upgrade.
|
|
*
|
|
* @param bool $keep_data When true, retain wpdo_uni_* tables (for retry).
|
|
* @return bool
|
|
*/
|
|
public static function rollback_v2( bool $keep_data = true ): bool {
|
|
$backup = get_option( 'wpdo_v2_features_backup' );
|
|
if ( false !== $backup ) {
|
|
update_option( 'wpdo_features', $backup );
|
|
}
|
|
update_option( 'wpdo_db_version', '1.0.0', false );
|
|
update_option( 'wpdo_v2_upgrade_status', 'rolled_back', false );
|
|
|
|
if ( ! $keep_data ) {
|
|
global $wpdb;
|
|
foreach ( array( 'wpdo_audit', 'wpdo_shadow_diffs', 'wpdo_site_metrics', 'wpdo_uni_options' ) as $t ) {
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $t from hardcoded array, $wpdb->prefix sanitized by core.
|
|
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}{$t}`" );
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Detect whether wp_uae_* tables exist (S3 scenario).
|
|
*/
|
|
public static function detect_uae_data(): bool {
|
|
global $wpdb;
|
|
$count = (int) $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE %s',
|
|
$wpdb->prefix . 'uae_%'
|
|
)
|
|
);
|
|
return $count > 0;
|
|
}
|
|
|
|
/**
|
|
* Run the UAE → WPDO data importer. Stub for v2.0.0 — full implementation
|
|
* lives in includes/migration/class-tmdo-uae-importer.php (PR-7 follow-up).
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function run_uae_import(): bool {
|
|
if ( class_exists( 'TMDO_UAE_Importer' ) ) {
|
|
return TMDO_UAE_Importer::run(
|
|
array(
|
|
'auto' => true,
|
|
'keep_source' => true,
|
|
)
|
|
);
|
|
}
|
|
// No UAE importer yet — return true so dev10 (no UAE data) progresses.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Migrate the feature_flags option to include entity modules.
|
|
*/
|
|
public static function migrate_feature_flags_v2(): void {
|
|
$flags = get_option( 'wpdo_features', array() );
|
|
if ( ! is_array( $flags ) ) {
|
|
$flags = array();
|
|
}
|
|
foreach ( array( 'entity_user', 'entity_term', 'entity_comment', 'entity_options' ) as $module ) {
|
|
if ( ! isset( $flags[ $module ] ) ) {
|
|
$flags[ $module ] = 'idle';
|
|
}
|
|
}
|
|
update_option( 'wpdo_features', $flags );
|
|
}
|
|
|
|
/**
|
|
* Detect installed MySQL / MariaDB version via @@version.
|
|
*/
|
|
public static function detect_mysql_version(): string {
|
|
global $wpdb;
|
|
try {
|
|
$v = (string) $wpdb->get_var( 'SELECT VERSION()' );
|
|
// Strip trailing -MariaDB or similar tags for version_compare.
|
|
if ( preg_match( '/^([\d.]+)/', $v, $m ) ) {
|
|
return $m[1];
|
|
}
|
|
} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- intentional: fall through to safe default.
|
|
// Fall through to safe default.
|
|
}
|
|
return '5.7';
|
|
}
|
|
|
|
/**
|
|
* Detect free disk space in MB at the given path.
|
|
*
|
|
* @param string $path Filesystem path (typically sys_get_temp_dir()).
|
|
* @return int Free disk space in MB, or 0 when unreadable.
|
|
*/
|
|
public static function detect_free_disk_mb( string $path ): int {
|
|
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- @ suppresses E_WARNING when path is on a stat-fail filesystem; explicit false-check follows.
|
|
$free = @disk_free_space( $path );
|
|
if ( false === $free ) {
|
|
return 0;
|
|
}
|
|
return (int) ( $free / 1024 / 1024 );
|
|
}
|
|
|
|
/**
|
|
* Verify wpdo_features option is writable (autoload=no row exists or absent).
|
|
*/
|
|
private static function is_features_option_writable(): bool {
|
|
$probe_key = '_wpdo_v2_writability_probe_' . wp_generate_password( 12, false );
|
|
$ok = update_option( $probe_key, time(), false );
|
|
delete_option( $probe_key );
|
|
return $ok;
|
|
}
|
|
|
|
/**
|
|
* Verify no migration is currently in-flight (zero rows in 'in_progress' state).
|
|
*/
|
|
private static function no_active_migration(): bool {
|
|
global $wpdb;
|
|
try {
|
|
$count = (int) $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_migrations` WHERE state = %s",
|
|
'in_progress'
|
|
)
|
|
);
|
|
return 0 === $count;
|
|
} catch ( \Throwable $e ) {
|
|
// Table may not exist on truly fresh installs — treat as "no active migration".
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Quietly deactivate wp-universal-anti-eav plugin if it is currently active.
|
|
*/
|
|
private static function safely_deactivate_uae_plugin(): void {
|
|
if ( ! function_exists( 'deactivate_plugins' ) || ! function_exists( 'is_plugin_active' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
}
|
|
if ( ! function_exists( 'is_plugin_active' ) ) {
|
|
return;
|
|
}
|
|
$slug = 'wp-universal-anti-eav/wp-universal-anti-eav.php';
|
|
if ( is_plugin_active( $slug ) ) {
|
|
deactivate_plugins( $slug, true );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule a one-shot cron event to remove the UAE plugin directory.
|
|
*
|
|
* Filesystem permission failures fall back to admin notice for manual removal.
|
|
*/
|
|
private static function schedule_uae_dir_removal(): void {
|
|
if ( ! function_exists( 'wp_schedule_single_event' ) ) {
|
|
return;
|
|
}
|
|
if ( ! wp_next_scheduled( 'wpdo_remove_uae_plugin_dir' ) ) {
|
|
wp_schedule_single_event( time() + 5, 'wpdo_remove_uae_plugin_dir' );
|
|
}
|
|
}
|
|
}
|