bytes of the files restored, 'db' => bytes of the table entries] from the * archive index: the disk check then needs files + 2 × db + a margin (the staged files, * the row work files and the temp tables). Without it: 2.2 × the archive size. * @return array{blocking: array, warnings: array} */ public static function run( array $manifest, int $archive_bytes, bool $encrypted, ?Mench_Migrate_Import_Source $reader = null, ?array $sizes = null ): array { $env = self::env(); $blocking = []; $warnings = []; $add = static function ( array &$list, string $code, string $message ): void { $list[] = [ 'code' => $code, 'message' => $message ]; }; if ( $env['is_multisite'] ) { $add( $blocking, 'multisite', __( 'This site is a multisite network. Mench Migrate imports into single sites only.', 'mench-migrate' ) ); } if ( 'sqlite' === $env['db_engine'] ) { $add( $blocking, 'sqlite', __( 'This site runs on SQLite. Mench Migrate imports into MySQL or MariaDB only.', 'mench-migrate' ) ); } if ( $encrypted && ! $env['has_sodium'] ) { $add( $blocking, 'no_sodium', __( 'The archive is encrypted and this server lacks the PHP sodium extension needed to decrypt it.', 'mench-migrate' ) ); } $need = self::disk_needed( $archive_bytes, $sizes ); if ( null !== $env['free_bytes'] && $env['free_bytes'] < $need ) { $add( $blocking, 'disk_space', sprintf( /* translators: 1: free disk space, 2: space needed. */ __( 'Not enough free disk space: %1$s free, %2$s needed.', 'mench-migrate' ), Mench_Migrate_Admin::size( $env['free_bytes'] ), Mench_Migrate_Admin::size( $need ) ) ); } $src_php = self::version( (string) ( $manifest['php_version'] ?? '' ) ); $php = self::version( (string) $env['php_version'] ); if ( '' !== $src_php && '' !== $php ) { if ( (int) $php < (int) $src_php ) { $add( $blocking, 'php_too_old', sprintf( /* translators: 1: target PHP version, 2: source PHP version. */ __( 'This server runs PHP %1$s; the archive comes from PHP %2$s. Upgrade PHP to the same major version first.', 'mench-migrate' ), esc_html( $php ), esc_html( $src_php ) ) ); } elseif ( version_compare( $php, $src_php, '<' ) ) { $add( $warnings, 'php_downgrade', sprintf( /* translators: 1: target PHP version, 2: source PHP version. */ __( 'This server runs PHP %1$s, older than the source site’s PHP %2$s.', 'mench-migrate' ), esc_html( $php ), esc_html( $src_php ) ) ); } } $charset = strtolower( (string) ( $manifest['charset'] ?? '' ) . ' ' . (string) ( $manifest['collate'] ?? '' ) ); if ( 'sqlite' !== $env['db_engine'] && false !== strpos( $charset, 'utf8mb4' ) && ! $env['has_utf8mb4'] ) { $add( $blocking, 'collation_unsupported', __( 'The archive uses the utf8mb4 character set, which this database server does not support. Upgrade the database server first.', 'mench-migrate' ) ); } $src_db = (string) ( $manifest['db_version'] ?? '' ); if ( '' !== $src_db && 'sqlite' !== $env['db_engine'] && self::is_mariadb( $src_db ) === self::is_mariadb( (string) $env['db_version'] ) ) { $a = self::db_number( $src_db ); $b = self::db_number( (string) $env['db_version'] ); if ( '' !== $a && '' !== $b && version_compare( $b, $a, '<' ) ) { $add( $warnings, 'db_downgrade', sprintf( /* translators: 1: target database version, 2: source database version. */ __( 'This database server (%1$s) is older than the source site’s (%2$s).', 'mench-migrate' ), esc_html( $b ), esc_html( $a ) ) ); } } if ( $env['memory_limit_bytes'] > 0 && $env['memory_limit_bytes'] < self::MIN_MEMORY ) { $add( $warnings, 'low_memory', sprintf( /* translators: %s: PHP memory limit. */ __( 'PHP memory_limit is %s; 256 MB or more is recommended.', 'mench-migrate' ), Mench_Migrate_Admin::size( $env['memory_limit_bytes'] ) ) ); } $prefix = (string) ( $manifest['prefix'] ?? '' ); if ( '' !== $prefix && $prefix !== $env['table_prefix'] ) { $add( $warnings, 'prefix_differs', sprintf( /* translators: 1: archive table prefix, 2: this site's table prefix. */ __( 'The archive uses table prefix %1$s; this site uses %2$s. Table names and prefixed keys will be rewritten.', 'mench-migrate' ), esc_html( $prefix ), esc_html( (string) $env['table_prefix'] ) ) ); } if ( ! $env['same_device'] ) { // The switch only renames, so it would certainly fail: block before any work. $add( $blocking, 'cross_device', __( 'The Mench Migrate storage folder and wp-content are on different disks, so the site cannot be switched or rolled back safely. Put them on the same disk first.', 'mench-migrate' ) ); } if ( null !== $reader ) { foreach ( self::plugins_requiring_newer( $reader, $php, (string) $env['wp_version'] ) as $msg ) { $add( $warnings, 'plugin_requires_newer', $msg ); } $dropins = self::archive_dropins( $reader ); if ( $dropins ) { $add( $warnings, 'dropins_kept', sprintf( /* translators: %s: list of file names such as object-cache.php. */ __( 'The archive has these drop-ins, which are not restored; this site keeps its own: %s. They are tied to the server they came from (caching, database).', 'mench-migrate' ), esc_html( implode( ', ', $dropins ) ) ) ); } } return [ 'blocking' => $blocking, 'warnings' => $warnings ]; } /** Bytes of free disk the import needs (see run()). */ public static function disk_needed( int $archive_bytes, ?array $sizes ): float { if ( null === $sizes ) { return ceil( self::DISK_FACTOR * max( 0, $archive_bytes ) ); } $est = max( 0, (int) ( $sizes['files'] ?? 0 ) ) + 2 * max( 0, (int) ( $sizes['db'] ?? 0 ) ); return (float) ( $est + max( self::DISK_MARGIN_MIN, (int) ceil( 0.1 * $est ) ) ); } /** The target environment: real values, overridable through mench_migrate_preflight_env. */ public static function env(): array { global $wpdb, $wp_version; $content = (string) apply_filters( 'mench_migrate_content_dir', WP_CONTENT_DIR ); $free = function_exists( 'disk_free_space' ) ? @disk_free_space( $content ) : false; // phpcs:ignore WordPress.PHP.NoSilencedErrors $mem = trim( (string) ini_get( 'memory_limit' ) ); $info = method_exists( $wpdb, 'db_server_info' ) ? (string) $wpdb->db_server_info() : ''; $engine = ( defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ) ? 'sqlite' : ( self::is_mariadb( $info ) ? 'mariadb' : 'mysql' ); $root = @stat( Mench_Migrate_Paths::root() ); // phpcs:ignore WordPress.PHP.NoSilencedErrors $cstat = @stat( $content ); // phpcs:ignore WordPress.PHP.NoSilencedErrors $defaults = [ 'is_multisite' => is_multisite(), 'db_engine' => $engine, 'free_bytes' => false === $free ? null : (float) $free, 'php_version' => PHP_VERSION, 'db_version' => $info, 'memory_limit_bytes' => ( '' === $mem || '-1' === $mem ) ? -1 : (int) wp_convert_hr_to_bytes( $mem ), 'has_sodium' => Mench_Migrate_Crypto::available(), 'same_device' => ! $root || ! $cstat || $root['dev'] === $cstat['dev'], 'has_utf8mb4' => 'sqlite' === $engine || (bool) $wpdb->has_cap( 'utf8mb4' ), 'table_prefix' => (string) $wpdb->prefix, 'wp_version' => (string) $wp_version, ]; $env = apply_filters( 'mench_migrate_preflight_env', $defaults ); return array_merge( $defaults, is_array( $env ) ? $env : [] ); } /** * Messages for plugins in the archive whose main file asks for a newer PHP or WordPress than this site has. * * @return string[] */ private static function plugins_requiring_newer( Mench_Migrate_Import_Source $reader, string $php, string $wp ): array { $out = []; foreach ( $reader->each() as $e ) { if ( 'file' !== ( $e['kind'] ?? '' ) || ! preg_match( '#^plugins/([^/]+)/([^/]+)\.php$#', (string) $e['path'], $m ) || $m[1] !== $m[2] ) { continue; } try { $head = $reader->read_head( (string) $e['path'], self::HEADER_BYTES ); } catch ( Mench_Migrate_Archive_Exception $ex ) { continue; // The import itself will report a damaged entry. } $need_php = self::header( $head, 'Requires PHP' ); $need_wp = self::header( $head, 'Requires at least' ); $needs = []; if ( '' !== $need_php && '' !== $php && version_compare( $php, $need_php, '<' ) ) { /* translators: %s: PHP version. */ $needs[] = sprintf( __( 'PHP %s', 'mench-migrate' ), esc_html( $need_php ) ); } if ( '' !== $need_wp && '' !== $wp && version_compare( self::version( $wp ), $need_wp, '<' ) ) { /* translators: %s: WordPress version. */ $needs[] = sprintf( __( 'WordPress %s', 'mench-migrate' ), esc_html( $need_wp ) ); } if ( $needs ) { $out[] = sprintf( /* translators: 1: plugin folder name, 2: list of requirements. */ __( 'The plugin %1$s in the archive requires %2$s; it may not run on this site.', 'mench-migrate' ), esc_html( $m[1] ), implode( ', ', $needs ) ); } } return $out; } /** @return string[] Drop-in names at the archive's top level that the import leaves out. */ private static function archive_dropins( Mench_Migrate_Import_Source $reader ): array { $kept = Mench_Migrate_Step_Import_Files::kept_dropins(); $out = []; foreach ( $reader->each() as $e ) { $path = (string) ( $e['path'] ?? '' ); if ( 'file' === ( $e['kind'] ?? '' ) && in_array( $path, $kept, true ) ) { $out[] = $path; } } sort( $out ); return array_values( array_unique( $out ) ); } /** A plugin header value, as get_file_data() finds it, when it is a plain version number. */ private static function header( string $head, string $name ): string { if ( ! preg_match( '/^(?:[ \t]*<\?php)?[ \t\/*#@]*' . preg_quote( $name, '/' ) . ':(.*)$/mi', $head, $m ) ) { return ''; } $v = trim( (string) preg_replace( '/\s*(?:\*\/|\?>).*/', '', $m[1] ) ); return preg_match( '/^\d+(?:\.\d+){0,3}$/', $v ) ? $v : ''; } /** The leading x.y[.z] of a version string, or ''. */ private static function version( string $v ): string { return preg_match( '/^\s*(\d+(?:\.\d+){0,2})/', $v, $m ) ? $m[1] : ''; } private static function is_mariadb( string $info ): bool { return false !== stripos( $info, 'mariadb' ); } /** Version number of a server string; MariaDB's "5.5.5-" replication prefix is dropped. */ private static function db_number( string $info ): string { return self::version( (string) preg_replace( '/^5\.5\.5-/', '', trim( $info ) ) ); } }