' . 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 .= '
' . esc_html( $caption ) . '
';
}
$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'] ) . '
'
. ''
. '- Export the database and everything in wp-content into one
.mwp file, with options to leave out media, plugins, themes, spam comments, revisions and more. '
. '- Import archives of any size. Uploads are sent in small chunks, so
upload_max_filesize and post_max_size do not limit you, and an interrupted upload resumes. '
. '- Automatic find & replace of the old URL and server paths, including inside serialized data and page-builder (JSON) content.
'
. '- Safe database restore: tables load under a temporary name and are swapped in at the very end, so a failed import leaves the live site untouched.
'
. '- Backups tab to download, restore or delete archives.
'
. '- WP-CLI:
wp marchnote export, wp marchnote import <file>, wp marchnote list. '
. '
',
'installation' => ''
. '- Go to Plugins → Add New → Upload Plugin and upload
marchnote-site-migration.zip. '
. '- Activate it, then open Marchnote in the admin menu.
'
. '- On the old site click Export to file; on the new site drop the file on the Import tab.
'
. '- After importing a database, log in with an account from the imported site.
'
. '
',
'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
- Renamed to Marchnote Site Migration.
- All database queries now go through $wpdb and prepare().
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;
}
}