, password ?string, storage string ('local'), label ?string. Job::create has already moved the * password to Mench_Migrate_Secrets and left options.encrypted in its place. * * @package Mench_Migrate */ defined( 'ABSPATH' ) || exit; class Mench_Migrate_Step_Export_Init extends Mench_Migrate_Step_Export_Base { public const INCLUDE_KEYS = [ 'db', 'uploads', 'plugins', 'themes', 'mu_plugins', 'others' ]; public function label(): string { return __( 'Preparing the export', 'mench-migrate' ); } public function run( Mench_Migrate_Job $job, float $deadline ): Mench_Migrate_Step_Result { // On a network the prefix matches every subsite's tables: refuse rather than export a mix (the Multisite add-on's job). if ( (bool) apply_filters( 'mench_migrate_is_multisite', is_multisite() ) ) { throw new Mench_Migrate_Exception( 'multisite_unsupported', 'export-init' ); } $raw = (array) ( $job->data['options'] ?? [] ); $options = self::normalise( $raw ); $storage = Mench_Migrate_Storages::get( $options['storage'] )->id(); if ( $options['encrypted'] && ! Mench_Migrate_Crypto::available() ) { throw new Mench_Migrate_Archive_Exception( 'crypto_unavailable', 'export-init' ); } // initiator_login and the options an add-on set through mench_migrate_job_options (a schedule id ...) stay next to // the core ones; any other key a caller sent is dropped, so a plain export cannot pose as an add-on's job. $keep = array_flip( array_merge( [ 'initiator_login' ], array_map( 'strval', (array) ( $job->data['addon_option_keys'] ?? [] ) ) ) ); $options = $options + array_intersect_key( array_diff_key( $raw, array_flip( [ 'password', 'encrypted' ] ) ), $keep ); $job->data['options'] = $options; // The name is saved before the archive is opened, so a re-run after a crash reuses it and leaves no stray .part. if ( empty( $job->data['archive'] ) ) { if ( 'cancelled' === Mench_Migrate_Job::disk_status( $job->id ) ) { return Mench_Migrate_Step_Result::done(); // The runner sees the cancel and merges nothing. } $job->data['archive'] = self::filtered_archive_name( $job ); $job->save(); } $list = self::list_path( $job ); wp_delete_file( $list ); self::remove_db_dir( $job ); $writer = Mench_Migrate_Archive_Writer::open( self::part_path( $job ), self::password( $job ), self::index_path( $job ) ); $state = $writer->state(); unset( $writer ); $job->log( 'info', 'Writing ' . $job->data['archive'] . ( $options['encrypted'] ? ' (encrypted)' : '' ) . '.' ); return Mench_Migrate_Step_Result::done( [ 'options' => $options, 'archive' => $job->data['archive'], 'storage' => $storage, 'writer' => $state, 'skipped_files' => 0, ] ); } /** Fills defaults and drops anything malformed (a stray password key included). */ public static function normalise( array $o ): array { $include = (array) ( $o['include'] ?? [] ); $inc = []; foreach ( self::INCLUDE_KEYS as $k ) { $inc[ $k ] = array_key_exists( $k, $include ) ? (bool) filter_var( $include[ $k ], FILTER_VALIDATE_BOOLEAN ) : true; } $excludes = []; foreach ( (array) ( $o['excludes'] ?? [] ) as $g ) { if ( is_string( $g ) && '' !== trim( $g ) ) { $excludes[] = trim( $g ); } } $replace = []; foreach ( (array) ( $o['replace'] ?? [] ) as $old => $new ) { $old = (string) $old; // A numeric-string key arrives as an int. if ( '' !== $old && is_scalar( $new ) ) { $replace[ $old ] = (string) $new; } } $storage = isset( $o['storage'] ) && is_string( $o['storage'] ) && '' !== $o['storage'] ? $o['storage'] : 'local'; $label = isset( $o['label'] ) && is_string( $o['label'] ) && '' !== trim( $o['label'] ) ? sanitize_text_field( $o['label'] ) : null; return [ 'encrypted' => ! empty( $o['encrypted'] ), // Set by Job::create when it stashed a password. 'include' => $inc, 'excludes' => array_values( array_unique( $excludes ) ), 'replace' => $replace, 'storage' => $storage, 'label' => $label, ]; } /** * The default name passed through mench_migrate_archive_name( string $name, Mench_Migrate_Job $job ) (1.1.0), so an * add-on can tag it (the Schedules add-on adds -sched-). The result is used only when it is still a valid .mmig * name that keeps the default's random suffix; anything else falls back to the default. */ public static function filtered_archive_name( Mench_Migrate_Job $job ): string { $name = self::archive_name(); $suffix = substr( $name, -21, 16 ); // <16 random>.mmig $filtered = apply_filters( 'mench_migrate_archive_name', $name, $job ); if ( is_string( $filtered ) && $filtered !== $name && Mench_Migrate_Storage_Local::valid_name( $filtered ) && '.mmig' === substr( $filtered, -5 ) && str_contains( $filtered, $suffix ) ) { return $filtered; } return $name; } /** --<16 random>.mmig; the suffix keeps the name unguessable where backups/ is web-reachable. */ public static function archive_name(): string { return self::archive_host() . '-' . gmdate( 'Ymd-His' ) . '-' . Mench_Migrate_Paths::random_suffix( 16 ) . '.mmig'; } /** 1.1.0: the sanitized host every archive name of this site starts with ('site' when there is none). */ public static function archive_host(): string { $host = strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ); $host = trim( (string) preg_replace( '/[^a-z0-9.-]+/', '-', $host ), '.-' ); return '' === $host ? 'site' : $host; } }