' . esc_html__( 'Open', 'marchnote-site-migration' ) . '' ); return $links; } /** * True once WordPress.org knows this plugin, in which case core renders * "View details" from the official listing and we stay out of the way. */ private static function listed_on_wporg() { $updates = get_site_transient( 'update_plugins' ); return is_object( $updates ) && ( isset( $updates->response[ MARCHNOTE_BASENAME ] ) || isset( $updates->no_update[ MARCHNOTE_BASENAME ] ) ); } /** * Core only shows "View details" for plugins known to WordPress.org, * so add the same thickbox link ourselves for manual (zip) installs. */ public static function row_meta( $meta, $file ) { if ( MARCHNOTE_BASENAME !== $file || ! current_user_can( 'install_plugins' ) || self::listed_on_wporg() ) { return $meta; } $name = __( 'Marchnote Site Migration', 'marchnote-site-migration' ); $meta[] = sprintf( '%s', esc_url( network_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . self::SLUG . '&TB_iframe=true&width=600&height=550' ) ), /* translators: %s: plugin name */ esc_attr( sprintf( __( 'More information about %s', 'marchnote-site-migration' ), $name ) ), esc_attr( $name ), esc_html__( 'View details', 'marchnote-site-migration' ) ); return $meta; } /** * Supply the "View details" modal content for manual installs. Installs from * WordPress.org use the official listing instead. */ public static function plugin_info( $result, $action, $args ) { if ( 'plugin_information' !== $action || empty( $args->slug ) || self::SLUG !== $args->slug || self::listed_on_wporg() ) { return $result; } if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $data = get_plugin_data( MARCHNOTE_FILE, false, false ); $img = MARCHNOTE_URL . 'assets/images/'; $shots = array( 1 => __( 'Export your whole site in one click, with options to leave out media, plugins, themes, spam, revisions or any folder.', 'marchnote-site-migration' ), 2 => __( 'Exports run in short resumable steps, so slow hosts and short PHP time limits are not a problem.', 'marchnote-site-migration' ), 3 => __( 'Download the finished archive straight from the dialog.', 'marchnote-site-migration' ), 4 => __( 'Drop an archive to import it. Add your own find & replace pairs on top of the automatic URL and path rewrite.', 'marchnote-site-migration' ), 5 => __( 'Uploads are sent in small chunks, so there is no size limit and an interrupted upload resumes.', 'marchnote-site-migration' ), 6 => __( 'Check the source site before anything is overwritten.', 'marchnote-site-migration' ), 7 => __( 'Old URLs and paths are rewritten everywhere, including serialized and page-builder data.', 'marchnote-site-migration' ), 8 => __( 'Download, restore or delete your backups.', 'marchnote-site-migration' ), ); $gallery = '
    '; foreach ( $shots as $n => $caption ) { $src = esc_url( $img . "screenshot-$n.png" ); $gallery .= '
  1. ' . esc_attr( $caption ) . '

    ' . esc_html( $caption ) . '

  2. '; } $gallery .= '
'; return (object) array( 'name' => $data['Name'], 'slug' => self::SLUG, 'version' => MARCHNOTE_VERSION, 'author' => '' . esc_html( $data['Author'] ) . '', 'homepage' => $data['PluginURI'] ? $data['PluginURI'] : $data['AuthorURI'], 'banners' => array( 'low' => $img . 'banner-772x250.png', 'high' => $img . 'banner-1544x500.png', ), 'icons' => array( '1x' => $img . 'icon-128x128.png', '2x' => $img . 'icon-256x256.png', ), 'requires' => $data['RequiresWP'], 'requires_php' => $data['RequiresPHP'], 'tested' => get_bloginfo( 'version' ), 'last_updated' => gmdate( 'Y-m-d H:i:s', filemtime( MARCHNOTE_FILE ) ), 'download_link' => '', 'sections' => array( 'description' => '

' . esc_html( $data['Description'] ) . '

' . '', 'installation' => '
    ' . '
  1. Go to Plugins → Add New → Upload Plugin and upload marchnote-site-migration.zip.
  2. ' . '
  3. Activate it, then open Marchnote in the admin menu.
  4. ' . '
  5. On the old site click Export to file; on the new site drop the file on the Import tab.
  6. ' . '
  7. After importing a database, log in with an account from the imported site.
  8. ' . '
', 'screenshots' => $gallery, 'faq' =>'

Is there a size limit?

No. Very large files can also be copied by FTP into wp-content/marchnote-backups/ and restored from the Backups tab.

' . '

Can it import .wpress files?

No, it uses its own .mwp format.

' . '

Does it support multisite?

Not yet; single sites only.

', 'changelog' => '

1.0.1

1.0.0

', ), ); } public static function assets( $hook ) { if ( 'toplevel_page_marchnote' !== $hook ) { return; } wp_enqueue_style( 'marchnote-admin', MARCHNOTE_URL . 'assets/admin.css', array(), MARCHNOTE_VERSION ); wp_enqueue_script( 'marchnote-admin', MARCHNOTE_URL . 'assets/admin.js', array(), MARCHNOTE_VERSION, true ); wp_localize_script( 'marchnote-admin', 'MarchnoteData', array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::NONCE ), 'chunk' => self::chunk_size(), 'ext' => Marchnote_Archive::EXT, 'backups' => self::backup_rows(), ) ); } public static function chunk_size() { $max = (int) wp_max_upload_size(); return (int) max( 256 * KB_IN_BYTES, min( 8 * MB_IN_BYTES, floor( $max * 0.9 ) ) ); } public static function render() { $problems = self::environment_problems(); $backups = Marchnote_Storage::list_backups(); include MARCHNOTE_DIR . '/views/page.php'; } public static function environment_problems() { $out = array(); if ( is_multisite() ) { $out[] = __( 'Multisite networks are not supported.', 'marchnote-site-migration' ); } if ( PHP_INT_SIZE < 8 ) { $out[] = __( 'A 64-bit PHP build is required.', 'marchnote-site-migration' ); } try { Marchnote_Storage::ensure(); if ( ! wp_is_writable( Marchnote_Storage::backups_dir() ) ) { throw new RuntimeException(); } } catch ( Throwable $e ) { /* translators: %s: directory path */ $out[] = sprintf( __( 'The storage folder %s is not writable.', 'marchnote-site-migration' ), Marchnote_Storage::backups_dir() ); } return $out; } public static function download_url( $name ) { return wp_nonce_url( admin_url( 'admin-post.php?action=marchnote_download&file=' . rawurlencode( $name ) ), 'marchnote_download' ); } public static function backup_rows() { return array_map( function ( $b ) { return array( 'name' => $b['name'], 'size' => size_format( $b['size'], 1 ), 'date' => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $b['mtime'] ), 'download' => self::download_url( $b['name'] ), ); }, Marchnote_Storage::list_backups() ); } /* ------------------------------------------------------------------ */ /* AJAX */ /* ------------------------------------------------------------------ */ private static function guard() { if ( ! current_user_can( self::CAP ) || ! check_ajax_referer( self::NONCE, 'nonce', false ) ) { self::fail( __( 'You are not allowed to do that. Reload the page and try again.', 'marchnote-site-migration' ), 403 ); } } private static function fail( $message, $code = 400 ) { self::clean_buffers(); wp_send_json_error( array( 'message' => $message ), $code ); } private static function ok( $data ) { self::clean_buffers(); wp_send_json_success( $data ); } /** Stray output from other plugins would corrupt the JSON response. */ private static function clean_buffers() { while ( ob_get_level() > 0 ) { ob_end_clean(); } } /** * Raw request value. Callers verify the nonce (guard()) or the job token * first, and validate/sanitize each value for its own use. */ private static function post( $key ) { return isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } private static function job_response( Marchnote_Job $job ) { return array( 'job' => $job->id, 'token' => $job->data['token'], ); } public static function ajax_export_start() { self::guard(); ob_start(); try { $job = Marchnote_Export::start( (array) self::post( 'options' ) ); } catch ( Throwable $e ) { self::fail( $e->getMessage() ); } self::ok( self::job_response( $job ) ); } public static function ajax_import_start() { self::guard(); ob_start(); $path = Marchnote_Storage::backup_path( self::post( 'file' ) ); if ( ! $path ) { self::fail( __( 'Backup not found.', 'marchnote-site-migration' ), 404 ); } try { $job = Marchnote_Import::start( $path, (array) self::post( 'options' ) ); } catch ( Throwable $e ) { self::fail( $e->getMessage() ); } self::ok( self::job_response( $job ) ); } private static function job_from_request() { $job = Marchnote_Job::load( self::post( 'job' ) ); if ( ! $job || ! $job->check_token( self::post( 'token' ) ) ) { self::fail( __( 'This job no longer exists.', 'marchnote-site-migration' ), 404 ); } return $job; } public static function ajax_step() { ob_start(); $job = self::job_from_request(); self::ok( Marchnote_Runner::step( $job, array( 'confirm' => (bool) self::post( 'confirm' ) ) ) ); } public static function ajax_cancel() { ob_start(); $job = self::job_from_request(); Marchnote_Runner::abort( $job ); self::ok( array() ); } public static function ajax_backups() { self::guard(); self::ok( self::backup_rows() ); } public static function ajax_delete() { self::guard(); $path = Marchnote_Storage::backup_path( self::post( 'file' ) ); if ( $path ) { wp_delete_file( $path ); } if ( ! $path || file_exists( $path ) ) { self::fail( __( 'Could not delete the backup.', 'marchnote-site-migration' ) ); } self::ok( self::backup_rows() ); } /* ------------------------------------------------------------------ */ /* Chunked upload — sidesteps upload_max_filesize entirely */ /* ------------------------------------------------------------------ */ private static function upload_paths( $id ) { if ( ! is_string( $id ) || ! preg_match( '/^[a-z0-9]{24}$/', $id ) ) { self::fail( __( 'Invalid upload.', 'marchnote-site-migration' ) ); } $base = Marchnote_Storage::backups_dir() . "/.upload-$id"; return array( "$base.part", "$base.json" ); } public static function ajax_upload_init() { self::guard(); try { Marchnote_Storage::ensure(); } catch ( Throwable $e ) { self::fail( $e->getMessage() ); } $name = sanitize_file_name( (string) self::post( 'name' ) ); $size = (int) self::post( 'size' ); if ( ! preg_match( '/\.' . Marchnote_Archive::EXT . '$/i', $name ) ) { /* translators: %s: file extension */ self::fail( sprintf( __( 'Please choose a .%s file.', 'marchnote-site-migration' ), Marchnote_Archive::EXT ) ); } if ( $size <= 0 ) { self::fail( __( 'The file is empty.', 'marchnote-site-migration' ) ); } $free = function_exists( 'disk_free_space' ) ? @disk_free_space( Marchnote_Storage::backups_dir() ) : false; if ( false !== $free && $free < $size ) { self::fail( __( 'Not enough free disk space for this upload.', 'marchnote-site-migration' ) ); } $id = strtolower( wp_generate_password( 24, false, false ) ); list( $part, $meta ) = self::upload_paths( $id ); file_put_contents( $part, '' ); file_put_contents( $meta, wp_json_encode( compact( 'name', 'size' ) ) ); self::ok( array( 'upload' => $id ) ); } public static function ajax_upload_chunk() { self::guard(); list( $part, $meta ) = self::upload_paths( self::post( 'upload' ) ); $info = is_file( $meta ) ? json_decode( (string) file_get_contents( $meta ), true ) : null; if ( ! $info ) { self::fail( __( 'Upload expired. Please start again.', 'marchnote-site-migration' ) ); } clearstatcache( true, $part ); $have = (int) filesize( $part ); $offset = (int) self::post( 'offset' ); if ( $offset !== $have ) { self::ok( array( 'offset' => $have ) ); // client resyncs to what we really have } // Nonce and capability were checked in guard(); tmp_name is a PHP-generated // path, validated with is_uploaded_file() before use. // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $tmp = isset( $_FILES['chunk']['tmp_name'] ) ? (string) $_FILES['chunk']['tmp_name'] : ''; $err = isset( $_FILES['chunk']['error'] ) ? (int) $_FILES['chunk']['error'] : -1; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! $tmp || ! is_uploaded_file( $tmp ) || UPLOAD_ERR_OK !== $err ) { self::fail( __( 'A chunk failed to upload. Check post_max_size and upload_max_filesize.', 'marchnote-site-migration' ) ); } if ( $have + filesize( $tmp ) > $info['size'] ) { self::fail( __( 'Received more data than expected.', 'marchnote-site-migration' ) ); } $in = fopen( $tmp, 'rb' ); $out = fopen( $part, 'ab' ); stream_copy_to_stream( $in, $out ); fclose( $in ); fclose( $out ); clearstatcache( true, $part ); self::ok( array( 'offset' => (int) filesize( $part ) ) ); } public static function ajax_upload_finish() { self::guard(); list( $part, $meta ) = self::upload_paths( self::post( 'upload' ) ); $info = is_file( $meta ) ? json_decode( (string) file_get_contents( $meta ), true ) : null; clearstatcache( true, $part ); if ( ! $info || ! is_file( $part ) || filesize( $part ) !== (int) $info['size'] ) { self::fail( __( 'The upload is incomplete.', 'marchnote-site-migration' ) ); } if ( ! Marchnote_Archive::is_archive( $part ) ) { wp_delete_file( $part ); wp_delete_file( $meta ); self::fail( __( 'This is not a Marchnote archive.', 'marchnote-site-migration' ) ); } $name = wp_unique_filename( Marchnote_Storage::backups_dir(), $info['name'] ); rename( $part, Marchnote_Storage::backups_dir() . '/' . $name ); wp_delete_file( $meta ); self::ok( array( 'file' => $name ) ); } /* ------------------------------------------------------------------ */ /* Download */ /* ------------------------------------------------------------------ */ public static function download() { if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'marchnote-site-migration' ), 403 ); } check_admin_referer( 'marchnote_download' ); $path = Marchnote_Storage::backup_path( isset( $_GET['file'] ) ? wp_unslash( $_GET['file'] ) : '' ); // phpcs:ignore WordPress.Security if ( ! $path ) { wp_die( esc_html__( 'Backup not found.', 'marchnote-site-migration' ), 404 ); } if ( function_exists( 'set_time_limit' ) ) { set_time_limit( 0 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- streaming a large download. } self::clean_buffers(); nocache_headers(); header( 'Content-Type: application/octet-stream' ); header( 'Content-Disposition: attachment; filename="' . basename( $path ) . '"' ); header( 'Content-Length: ' . filesize( $path ) ); header( 'X-Content-Type-Options: nosniff' ); $fh = fopen( $path, 'rb' ); while ( ! feof( $fh ) ) { echo fread( $fh, Marchnote_Archive::CHUNK ); // phpcs:ignore WordPress.Security.EscapeOutput flush(); } fclose( $fh ); exit; } }