.part to * and forgets the password. The only absolute server path in the manifest is content_abs, the source content * directory, which the importer needs to rewrite stored file paths (spec ยง4.4; the archive holds the whole database). * * A re-run after a crash reopens the archive at the saved state (before the manifest) and writes it again; a re-run * after the rename finds the finished archive and only records it. * * @package Mench_Migrate */ defined( 'ABSPATH' ) || exit; // phpcs:disable WordPress.WP.AlternativeFunctions -- Streams backups, uploads and job files (often several GB) chunk by chunk inside the plugin's own storage folder and the site being restored; WP_Filesystem has no streaming or locking API. class Mench_Migrate_Step_Finalize_Export extends Mench_Migrate_Step_Export_Base { /** Most administrator logins listed for session handling at import. */ private const MAX_LOGINS = 50; public function label(): string { return __( 'Finishing the archive', 'mench-migrate' ); } public function run( Mench_Migrate_Job $job, float $deadline ): Mench_Migrate_Step_Result { self::remove_db_dir( $job ); $part = self::part_path( $job ); $archive = self::archive_path( $job ); if ( is_file( $part ) || ! is_file( $archive ) ) { $writer = self::writer( $job ); $writer->add_string( 'manifest.json', (string) wp_json_encode( self::manifest( $job ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT ), 'manifest' ); $writer->close(); unset( $writer ); if ( ! rename( $part, $archive ) ) { throw new Mench_Migrate_Storage_Exception( 'Cannot rename the finished archive.', 'finalize-export' ); } } self::remove_index( $job ); // Only after the rename: a re-run before it reopens the writer from it. Mench_Migrate_Secrets::forget( $job->id ); $list = self::list_path( $job ); is_file( $list ) && unlink( $list ); clearstatcache( true, $archive ); $bytes = (int) filesize( $archive ); $job->log( 'info', 'Archive ' . $job->data['archive'] . ' is ready (' . Mench_Migrate_Admin::size( $bytes ) . ').' ); return Mench_Migrate_Step_Result::done( [ 'bytes' => $bytes, 'totals' => self::totals( $job ), 'writer' => null, ] ); } private static function totals( Mench_Migrate_Job $job ): array { return [ 'files' => (int) ( $job->data['files']['count'] ?? 0 ), 'bytes' => (int) ( $job->data['files']['bytes'] ?? 0 ), 'tables' => (int) ( $job->data['db']['tables'] ?? 0 ), 'rows' => (int) ( $job->data['db']['rows'] ?? 0 ), ]; } /** Exactly the keys the importer reads. */ public static function manifest( Mench_Migrate_Job $job ): array { global $wpdb, $wp_version; $options = (array) ( $job->data['options'] ?? [] ); $logins = get_users( [ 'capability' => 'manage_options', 'fields' => 'user_login', 'number' => self::MAX_LOGINS, 'orderby' => 'ID', ] ); return [ 'plugin_version' => MENCH_MIGRATE_VERSION, 'wp_version' => (string) $wp_version, 'php_version' => PHP_VERSION, 'db_version' => (string) $wpdb->db_server_info(), 'charset' => (string) $wpdb->charset, 'collate' => (string) $wpdb->collate, 'prefix' => (string) $wpdb->prefix, 'home' => home_url(), 'siteurl' => site_url(), 'uploads_rel' => self::uploads_rel(), 'content_abs' => untrailingslashit( (string) apply_filters( 'mench_migrate_content_dir', WP_CONTENT_DIR ) ), 'active_plugins' => array_values( (array) get_option( 'active_plugins', [] ) ), 'theme' => get_template(), 'stylesheet' => get_stylesheet(), 'excludes' => array_values( array_merge( Mench_Migrate_Excludes::defaults(), (array) ( $options['excludes'] ?? [] ) ) ), 'user_pairs' => (object) (array) ( $options['replace'] ?? [] ), 'totals' => self::totals( $job ), 'created_gmt' => gmdate( 'Y-m-d\TH:i:s\Z' ), 'user_logins' => array_values( array_map( 'strval', (array) $logins ) ), ]; } /** The uploads folder relative to the content directory ('uploads' normally), or '' when it lives elsewhere. */ private static function uploads_rel(): string { $uploads = wp_upload_dir( null, false ); $basedir = realpath( (string) ( $uploads['basedir'] ?? '' ) ); $basedir = false === $basedir ? untrailingslashit( (string) ( $uploads['basedir'] ?? '' ) ) : $basedir; $content = self::content_dir(); return 0 === strpos( $basedir . '/', $content . '/' ) && $basedir !== $content ? ltrim( substr( $basedir, strlen( $content ) ), '/' ) : ''; } }