bool. * * @param array $opts Optional overrides (mostly for tests). * @return array */ 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' ); } } }