init_hooks();
}
/**
* Initialize plugin hooks
*/
private function init_hooks() {
add_action('admin_menu', array($this, 'register_admin_menu'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
add_action('admin_init', array('WPMigPro_Activator', 'maybe_upgrade'));
add_action('admin_notices', array('WPMigPro_Activator', 'admin_notices'));
add_filter('wp_auth_check_load', array($this, 'disable_auth_check_while_restoring'), 10, 2);
add_filter('plugin_action_links_' . plugin_basename(WPMIGPRO_PLUGIN_FILE), array($this, 'plugin_action_links'));
// Backups
add_action('wp_ajax_wpmigpro_create_backup', array($this, 'ajax_create_backup'));
add_action('wp_ajax_wpmigpro_get_backup_progress', array($this, 'ajax_get_backup_progress'));
add_action('wp_ajax_wpmigpro_cancel_backup', array($this, 'ajax_cancel_backup'));
add_action('wp_ajax_wpmigpro_delete_backup', array($this, 'ajax_delete_backup'));
add_action('wp_ajax_wpmigpro_download_backup', array($this, 'ajax_download_backup'));
add_action('wp_ajax_wpmigpro_save_settings', array($this, 'ajax_save_settings'));
add_action('wpmigpro_run_backup', array($this, 'run_backup_job'), 10, 2);
// Restore: upload in chunks, then one job driven step by step from the browser
add_action('wp_ajax_wpmigpro_upload_chunk', array($this, 'ajax_upload_chunk'));
add_action('wp_ajax_wpmigpro_finalize_upload', array($this, 'ajax_finalize_upload'));
add_action('wp_ajax_wpmigpro_restore_start', array($this, 'ajax_restore_start'));
// nopriv too: the admin session is invalidated once the backup's users table is in place.
// Each call is authenticated with the secret job token instead (see WPMigPro_Importer::load()).
add_action('wp_ajax_wpmigpro_restore_step', array($this, 'ajax_restore_step'));
add_action('wp_ajax_nopriv_wpmigpro_restore_step', array($this, 'ajax_restore_step'));
add_action('wp_ajax_wpmigpro_download_import_log', array($this, 'ajax_download_import_log'));
// Scheduled events
add_action('wpmigpro_auto_backup', array($this, 'perform_auto_backup'));
add_action('wpmigpro_cleanup_temp', array($this, 'cleanup_temp_files'));
if (!wp_next_scheduled('wpmigpro_cleanup_temp')) {
wp_schedule_event(time(), 'daily', 'wpmigpro_cleanup_temp');
}
}
/**
* Restoring replaces the users table, which invalidates the current session
* mid-process. WordPress' "session expired" login dialog would then cover the
* restore progress, so it is not loaded on the screens that run a restore.
*/
public function disable_auth_check_while_restoring($show, $screen) {
if ($screen && (strpos($screen->id, 'wpmigpro-imports') !== false || strpos($screen->id, 'wpmigpro-backups') !== false)) {
return false;
}
return $show;
}
/* ------------------------------------------------------------------
* Settings
* ------------------------------------------------------------------ */
public static function default_settings() {
return array(
'auto_backup' => false,
'backup_time' => '02:00',
'keep_backups' => 10,
'include_database' => true,
'include_plugins' => true,
'include_themes' => true,
'include_uploads' => true,
'compression_level' => 6,
);
}
public static function get_settings() {
$saved = get_option('wpmigpro_settings', array());
return array_merge(self::default_settings(), is_array($saved) ? $saved : array());
}
/**
* (Re)schedule the daily automatic backup at the configured local time.
*/
public static function schedule_auto_backup() {
wp_clear_scheduled_hook('wpmigpro_auto_backup');
$settings = self::get_settings();
if (!$settings['auto_backup']) {
return;
}
list($hour, $minute) = array_map('intval', explode(':', $settings['backup_time']));
$next = new DateTime('now', wp_timezone());
$next->setTime($hour, $minute, 0);
if ($next->getTimestamp() <= time()) {
$next->modify('+1 day');
}
wp_schedule_event($next->getTimestamp(), 'daily', 'wpmigpro_auto_backup');
}
/* ------------------------------------------------------------------
* Admin pages
* ------------------------------------------------------------------ */
public function register_admin_menu() {
add_menu_page(
__('EnMigrate Pro', 'enmigrate-pro'),
__('EnMigrate Pro', 'enmigrate-pro'),
'manage_options',
'wpmigpro',
array($this, 'display_dashboard'),
'dashicons-backup'
);
add_submenu_page('wpmigpro', __('Dashboard', 'enmigrate-pro'), __('Dashboard', 'enmigrate-pro'), 'manage_options', 'wpmigpro', array($this, 'display_dashboard'));
add_submenu_page('wpmigpro', __('Create Backup', 'enmigrate-pro'), __('Create Backup', 'enmigrate-pro'), 'manage_options', 'wpmigpro-create', array($this, 'display_create_backup'));
add_submenu_page('wpmigpro', __('Backups', 'enmigrate-pro'), __('Backups', 'enmigrate-pro'), 'manage_options', 'wpmigpro-backups', array($this, 'display_backups'));
add_submenu_page('wpmigpro', __('Restore', 'enmigrate-pro'), __('Restore', 'enmigrate-pro'), 'manage_options', 'wpmigpro-imports', array($this, 'display_imports'));
add_submenu_page('wpmigpro', __('Settings', 'enmigrate-pro'), __('Settings', 'enmigrate-pro'), 'manage_options', 'wpmigpro-settings', array($this, 'display_settings'));
add_submenu_page('wpmigpro', __('Help', 'enmigrate-pro'), __('Help', 'enmigrate-pro'), 'manage_options', 'wpmigpro-help', array($this, 'display_help'));
// Without the Enwebs Suite Pro add-on, its screens show what they offer (the add-on adds the real ones).
if (!defined('ENWEBS_PRO_VERSION')) {
$position = 3;
foreach (self::pro_screens() as $slug => $screen) {
$at = $slug === 'wpmigpro-enwebs-pro' ? null : $position++;
add_submenu_page('wpmigpro', $screen['title'], $screen['menu'] . self::crown(), 'manage_options', $slug, array($this, 'display_pro_teaser'), $at);
}
}
}
/**
* Gold crown that marks Enwebs Suite Pro screens in the menu.
*/
/**
* Tabs inside the header with every screen of the plugin's menu (also the Pro ones).
*/
public static function nav() {
global $submenu;
if (empty($submenu['wpmigpro'])) {
return;
}
$icons = array('wpmigpro' => 'bi-speedometer2', 'wpmigpro-create' => 'bi-cloud-arrow-down', 'wpmigpro-backups' => 'bi-cloud', 'wpmigpro-imports' => 'bi-upload', 'wpmigpro-settings' => 'bi-gear', 'wpmigpro-destinations' => 'bi-cloud-arrow-up', 'wpmigpro-schedules' => 'bi-calendar2-week', 'wpmigpro-pro' => 'bi-sliders', 'wpmigpro-staging' => 'bi-layers', 'wpmigpro-enwebs-pro' => 'bi-gem', 'wpmigpro-help' => 'bi-question-circle');
$current = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- menu highlight only.
echo '';
}
/**
* Asset version: plugin version plus the file's modification time, so browsers
* load the new file after an update.
*/
public static function asset_ver($relative) {
$file = WPMIGPRO_PLUGIN_DIR . $relative;
return WPMIGPRO_VERSION . (file_exists($file) ? '.' . filemtime($file) : '');
}
public static function crown() {
return ' ';
}
/**
* Screens of the Enwebs Suite Pro add-on (a separate, paid plugin).
*/
public static function pro_screens() {
return array(
'wpmigpro-destinations' => array(
'menu' => __('Destinations', 'enmigrate-pro'),
'title' => __('Backups to the cloud and your own servers', 'enmigrate-pro'),
'icon' => 'bi-cloud-arrow-up',
'points' => array(
__('Google Drive, Dropbox, OneDrive, Box and pCloud', 'enmigrate-pro'),
__('Amazon S3, Wasabi, Backblaze, Cloudflare R2 and 9 more', 'enmigrate-pro'),
__('Your own FTP, FTPS, SFTP, WebDAV and Nextcloud servers', 'enmigrate-pro'),
__('Restore any backup straight from the cloud', 'enmigrate-pro'),
),
'tiles' => array('Google Drive', 'Dropbox', 'OneDrive', 'Amazon S3', 'Wasabi', 'Backblaze B2', 'Cloudflare R2', 'DigitalOcean', 'FTP / SFTP', 'WebDAV', 'Nextcloud', 'pCloud'),
),
'wpmigpro-schedules' => array(
'menu' => __('Schedules', 'enmigrate-pro'),
'title' => __('Unlimited backup schedules', 'enmigrate-pro'),
'icon' => 'bi-calendar2-week',
'points' => array(
__('Every 6 or 12 hours, daily, weekly or monthly', 'enmigrate-pro'),
__('Its own content and destinations for each schedule', 'enmigrate-pro'),
__('Retention per destination: keep only the latest backups', 'enmigrate-pro'),
),
'tiles' => array(__('Database every 6 hours', 'enmigrate-pro'), __('Full backup weekly', 'enmigrate-pro'), __('Media monthly', 'enmigrate-pro'), __('Nightly to S3', 'enmigrate-pro')),
),
'wpmigpro-staging' => array(
'menu' => __('Staging', 'enmigrate-pro'),
'title' => __('Try changes on a private copy of your site', 'enmigrate-pro'),
'icon' => 'bi-layers',
'points' => array(
__('A copy of the whole site in one click, in a private folder', 'enmigrate-pro'),
__('Only administrators can see it; search engines and emails stay out', 'enmigrate-pro'),
__('Push the changes to the live site when they are ready', 'enmigrate-pro'),
__('A backup of the live site is made before every push', 'enmigrate-pro'),
),
'tiles' => array(__('Copy', 'enmigrate-pro'), __('Test', 'enmigrate-pro'), __('Push', 'enmigrate-pro'), __('Safety backup', 'enmigrate-pro')),
),
'wpmigpro-pro' => array(
'menu' => __('Pro settings', 'enmigrate-pro'),
'title' => __('Encryption, exclusions and alerts', 'enmigrate-pro'),
'icon' => 'bi-sliders',
'points' => array(
__('AES-256 encrypted backups', 'enmigrate-pro'),
__('Leave out folders, file types, big files and tables', 'enmigrate-pro'),
__('Alerts by email, Slack, Discord and Telegram', 'enmigrate-pro'),
__('Database backup before every update', 'enmigrate-pro'),
),
'tiles' => array(__('Encryption', 'enmigrate-pro'), __('Exclusions', 'enmigrate-pro'), __('Notifications', 'enmigrate-pro'), __('Safety backups', 'enmigrate-pro')),
),
'wpmigpro-enwebs-pro' => array(
'menu' => 'Enwebs Suite Pro',
'title' => __('One license, every Pro feature', 'enmigrate-pro'),
'icon' => 'bi-gem',
'points' => array(
__('EnMigrate: backups to the cloud, schedules and encryption', 'enmigrate-pro'),
__('EnLogin Variants: two-step verification, modals and 10 designs', 'enmigrate-pro'),
__('EnAdmin UI Theme: 13 premium admin templates and tools', 'enmigrate-pro'),
__('Automatic updates and priority support', 'enmigrate-pro'),
),
'tiles' => array('EnMigrate Pro', 'EnLogin Variants', 'EnAdmin UI Theme', __('Updates', 'enmigrate-pro')),
),
);
}
public function display_pro_teaser() {
$screens = self::pro_screens();
$page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- menu routing.
$wpmigpro_screen = isset($screens[$page]) ? $screens[$page] : reset($screens);
include WPMIGPRO_PLUGIN_DIR . 'views/pro.php';
}
/**
* "Settings | Help" links in the plugin's row on the Plugins screen.
*/
public function plugin_action_links($links) {
array_unshift(
$links,
'' . esc_html__('Settings', 'enmigrate-pro') . '',
'' . esc_html__('Help', 'enmigrate-pro') . ''
);
$links[] = '' . esc_html__('Docs', 'enmigrate-pro') . '';
return $links;
}
public function display_help() {
include WPMIGPRO_PLUGIN_DIR . 'views/help.php';
}
public function display_dashboard() {
include WPMIGPRO_PLUGIN_DIR . 'views/dashboard.php';
}
public function display_create_backup() {
include WPMIGPRO_PLUGIN_DIR . 'views/create-backup.php';
}
public function display_backups() {
include WPMIGPRO_PLUGIN_DIR . 'views/backups.php';
}
public function display_imports() {
include WPMIGPRO_PLUGIN_DIR . 'views/imports.php';
}
public function display_settings() {
include WPMIGPRO_PLUGIN_DIR . 'views/settings.php';
}
public static function status_label($status) {
$labels = array(
'completed' => __('Completed', 'enmigrate-pro'),
'processing' => __('Processing', 'enmigrate-pro'),
'failed' => __('Failed', 'enmigrate-pro'),
'pending' => __('Pending', 'enmigrate-pro'),
);
return isset($labels[$status]) ? $labels[$status] : ucfirst((string) $status);
}
public static function type_label($type) {
$labels = array(
'full' => __('Full backup', 'enmigrate-pro'),
'database' => __('Database only', 'enmigrate-pro'),
'files' => __('Files only', 'enmigrate-pro'),
'imported' => __('Uploaded', 'enmigrate-pro'),
);
return isset($labels[$type]) ? $labels[$type] : ucfirst((string) $type);
}
/**
* Shared page footer.
*/
public static function render_footer() {
include WPMIGPRO_PLUGIN_DIR . 'views/footer.php';
}
public function enqueue_admin_assets($hook) {
if (strpos($hook, 'wpmigpro') === false) {
return;
}
wp_enqueue_style('wpmigpro-bootstrap-icons', WPMIGPRO_PLUGIN_URL . 'assets/vendor/bootstrap-icons/bootstrap-icons.min.css', array(), '1.11.3');
wp_enqueue_style('wpmigpro-style', WPMIGPRO_PLUGIN_URL . 'assets/css/style.css', array('wpmigpro-bootstrap-icons'), self::asset_ver('assets/css/style.css'));
wp_enqueue_script('wpmigpro-admin', WPMIGPRO_PLUGIN_URL . 'assets/js/admin.js', array('jquery'), self::asset_ver('assets/js/admin.js'), true);
wp_enqueue_script('wpmigpro-restore', WPMIGPRO_PLUGIN_URL . 'assets/js/restore.js', array('jquery', 'wpmigpro-admin'), self::asset_ver('assets/js/restore.js'), true);
// Upload chunk size: as big as the server allows, capped at 8MB, leaving room for the rest of the POST.
$chunk_size = (int) min(8 * MB_IN_BYTES, max(512 * KB_IN_BYTES, wp_max_upload_size() - 256 * KB_IN_BYTES));
wp_localize_script('wpmigpro-admin', 'wpmigproAjax', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wpmigpro_nonce'),
'chunkSize' => $chunk_size,
'backupsUrl' => admin_url('admin.php?page=wpmigpro-backups'),
'loginUrl' => wp_login_url(admin_url('admin.php?page=wpmigpro')),
'logUrl' => add_query_arg(array('action' => 'wpmigpro_download_import_log', 'nonce' => wp_create_nonce('wpmigpro_nonce')), admin_url('admin-ajax.php')),
'i18n' => self::js_strings(),
));
}
private static function js_strings() {
return array(
'ok' => __('OK', 'enmigrate-pro'),
'cancel' => __('Cancel', 'enmigrate-pro'),
'close' => __('Close', 'enmigrate-pro'),
'error' => __('Error', 'enmigrate-pro'),
'serverError' => __('Server error. Please try again.', 'enmigrate-pro'),
'createTitle' => __('Create a backup now?', 'enmigrate-pro'),
'createConfirm' => __('Create backup', 'enmigrate-pro'),
'creating' => __('Creating backup...', 'enmigrate-pro'),
'backupDone' => __('Backup completed!', 'enmigrate-pro'),
'backupFailed' => __('Backup failed', 'enmigrate-pro'),
'cancelTitle' => __('Cancel the backup?', 'enmigrate-pro'),
'cancelConfirm' => __('Yes, cancel', 'enmigrate-pro'),
'cancelled' => __('The backup was cancelled.', 'enmigrate-pro'),
'restoreTitle' => __('Restore this backup?', 'enmigrate-pro'),
'restoreText' => __('All current content (database, plugins, themes and media) will be replaced by the backup. When it finishes you will log in with a user of the restored site.', 'enmigrate-pro'),
'restoreConfirm' => __('Yes, restore', 'enmigrate-pro'),
'deleteTitle' => __('Delete this backup?', 'enmigrate-pro'),
'deleteText' => __('The backup file will be permanently deleted.', 'enmigrate-pro'),
'deleteConfirm' => __('Yes, delete', 'enmigrate-pro'),
'invalidFile' => __('Please select a .wpb or .zip backup file.', 'enmigrate-pro'),
'noFile' => __('Please select a backup file first.', 'enmigrate-pro'),
'settingsSaved' => __('Settings saved.', 'enmigrate-pro'),
'settingsError' => __('The settings could not be saved.', 'enmigrate-pro'),
// Restore modal
'restoringSite' => __('Restoring site', 'enmigrate-pro'),
'dontClose' => __('Do not close this tab until it finishes.', 'enmigrate-pro'),
'preparing' => __('Preparing...', 'enmigrate-pro'),
'uploading' => __('Uploading file', 'enmigrate-pro'),
/* translators: %s: file name and size */
'fileLabel' => __('File: %s', 'enmigrate-pro'),
/* translators: 1: number of parts, 2: part size in MB */
'uploadStart' => __('Uploading file to the server (%1$s parts of %2$s MB)', 'enmigrate-pro'),
/* translators: %s: percentage */
'uploaded' => __('Uploaded %s%%', 'enmigrate-pro'),
/* translators: 1: part number, 2: reason */
'uploadRetry' => __('Upload of part %1$s failed (%2$s), retrying...', 'enmigrate-pro'),
/* translators: 1: part number, 2: total parts, 3: reason */
'uploadFailed' => __('The upload failed at part %1$s of %2$s: %3$s', 'enmigrate-pro'),
'verifying' => __('Upload complete, verifying file...', 'enmigrate-pro'),
'finalizeFailed' => __('Could not finalize the upload', 'enmigrate-pro'),
/* translators: %s: backup number */
'startingBackup' => __('Starting restore of backup #%s...', 'enmigrate-pro'),
'startFailed' => __('Could not start the restore', 'enmigrate-pro'),
'invalidResponse' => __('Invalid server response', 'enmigrate-pro'),
'timeout' => __('timed out', 'enmigrate-pro'),
/* translators: 1: reason, 2: attempt number, 3: max attempts */
'noResponse' => __('The server did not respond (%1$s), retrying %2$s/%3$s...', 'enmigrate-pro'),
/* translators: %s: reason */
'stoppedResponding' => __('The server stopped responding (%s).', 'enmigrate-pro'),
'restoreDone' => __('Restore completed!', 'enmigrate-pro'),
'restoreDoneText' => __('Log in with the username and password of the restored site.', 'enmigrate-pro'),
'goToLogin' => __('Go to login', 'enmigrate-pro'),
'done' => __('Done.', 'enmigrate-pro'),
'restoreStopped' => __('The restore stopped', 'enmigrate-pro'),
'downloadLog' => __('Download log', 'enmigrate-pro'),
'leaveWarning' => __('A restore is in progress.', 'enmigrate-pro'),
);
}
/* ------------------------------------------------------------------
* AJAX: backups
* ------------------------------------------------------------------ */
/**
* Stop the request unless the current user may manage backups.
*/
private function require_admin() {
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Insufficient permissions', 'enmigrate-pro')), 403);
}
}
public function ajax_create_backup() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
$this->require_admin();
$type = isset($_POST['backup_type']) ? sanitize_key(wp_unslash($_POST['backup_type'])) : 'full';
$label = isset($_POST['backup_name']) ? sanitize_file_name(wp_unslash($_POST['backup_name'])) : '';
$overrides = array('label' => $label);
if ($type === 'database') {
$overrides += array('include_plugins' => false, 'include_themes' => false, 'include_uploads' => false, 'include_database' => true);
} elseif ($type === 'files') {
$overrides += array('include_database' => false);
}
// Create the record now so the browser polls this exact backup (not an older one).
$exporter = new WPMigPro_Exporter();
$backup_id = $exporter->create_backup_record(($label ? $label : 'backup') . '-' . gmdate('Y-m-d-H-i-s'), $type === 'database' || $type === 'files' ? $type : 'full');
update_option('wpmigpro_backup_log', array('[' . wp_date('H:i:s') . '] ' . __('Backup queued, starting...', 'enmigrate-pro')), false);
update_option('wpmigpro_backup_step', __('Starting', 'enmigrate-pro'), false);
update_option('wpmigpro_backup_details', '', false);
// Run in the background via WP-Cron, triggered right away (non-blocking).
wp_schedule_single_event(time(), 'wpmigpro_run_backup', array($backup_id, $overrides));
spawn_cron();
wp_send_json_success(array('backup_id' => $backup_id));
}
/**
* Run backup job (hooked to wpmigpro_run_backup)
*/
public function run_backup_job($backup_id = 0, $overrides = array()) {
$exporter = new WPMigPro_Exporter();
$exporter->create_backup($backup_id, $overrides);
}
public function perform_auto_backup() {
if (!self::get_settings()['auto_backup']) {
return;
}
$exporter = new WPMigPro_Exporter();
$exporter->create_backup(0, array('label' => 'auto', 'trigger' => 'auto'));
}
public function ajax_get_backup_progress() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
$this->require_admin();
$backup_id = isset($_POST['backup_id']) ? absint($_POST['backup_id']) : 0;
$from = isset($_POST['log_from']) ? absint($_POST['log_from']) : 0;
$manager = new WPMigPro_Backup_Manager();
$backup = $manager->get_backup($backup_id);
if (!$backup) {
wp_send_json_success(array('status' => 'idle', 'progress' => 0, 'step' => '', 'details' => '', 'log' => array(), 'log_total' => 0));
}
$log = (array) get_option('wpmigpro_backup_log', array());
wp_send_json_success(array(
'status' => $backup->status,
'progress' => intval($backup->progress),
'step' => get_option('wpmigpro_backup_step', ''),
'details' => get_option('wpmigpro_backup_details', ''),
'log' => array_slice($log, $from),
'log_total' => count($log),
));
}
public function ajax_cancel_backup() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
$this->require_admin();
global $wpdb;
wp_clear_scheduled_hook('wpmigpro_run_backup');
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- plugin's own table.
$wpdb->update($wpdb->prefix . 'wpmigpro_backups', array('status' => 'failed', 'progress' => 0), array('status' => 'processing'));
wp_send_json_success();
}
public function ajax_delete_backup() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
$this->require_admin();
$manager = new WPMigPro_Backup_Manager();
$result = $manager->delete_backup(isset($_POST['backup_id']) ? absint($_POST['backup_id']) : 0);
if ($result['success']) {
wp_send_json_success($result);
}
wp_send_json_error($result);
}
/**
* Stream a backup file to an administrator. Backups are never served directly by
* the web server: the folder is locked and they contain the whole database.
*/
public function ajax_download_backup() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Insufficient permissions', 'enmigrate-pro'), 403);
}
$manager = new WPMigPro_Backup_Manager();
$backup = $manager->get_backup(isset($_GET['backup_id']) ? absint($_GET['backup_id']) : 0);
if (!$backup || !is_file($backup->file_path) || !$manager->is_inside_backups_dir($backup->file_path)) {
wp_die(esc_html__('Backup file not found', 'enmigrate-pro'), 404);
}
WPMigPro_Exporter::raise_limits();
while (ob_get_level()) {
ob_end_clean();
}
nocache_headers();
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . sanitize_file_name(basename($backup->file_path)) . '"');
header('Content-Length: ' . filesize($backup->file_path));
WPMigPro_FS::output($backup->file_path);
exit;
}
public function ajax_save_settings() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
$this->require_admin();
$time = isset($_POST['backup_time']) ? sanitize_text_field(wp_unslash($_POST['backup_time'])) : '02:00';
if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $time)) {
$time = '02:00';
}
$settings = array(
'auto_backup' => !empty($_POST['auto_backup']),
'backup_time' => $time,
'keep_backups' => isset($_POST['keep_backups']) ? max(1, min(100, absint($_POST['keep_backups']))) : 10,
'include_database' => !empty($_POST['include_database']),
'include_plugins' => !empty($_POST['include_plugins']),
'include_themes' => !empty($_POST['include_themes']),
'include_uploads' => !empty($_POST['include_uploads']),
'compression_level' => isset($_POST['compression_level']) ? max(1, min(9, absint($_POST['compression_level']))) : 6,
);
update_option('wpmigpro_settings', $settings, false);
self::schedule_auto_backup();
wp_send_json_success(array('message' => __('Settings saved.', 'enmigrate-pro')));
}
/* ------------------------------------------------------------------
* AJAX: upload + restore
* ------------------------------------------------------------------ */
/**
* Receive one chunk of a backup being uploaded. Each chunk is written at its offset
* straight into the final file, so there is no slow "assemble" phase at the end.
* The file is a backup archive (not a media item), so wp_handle_upload() doesn't apply;
* it is stored as .part in the protected temp folder and validated as a ZIP when finalized.
*/
public function ajax_upload_chunk() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
$this->require_admin();
$upload_id = isset($_POST['upload_id']) ? preg_replace('/[^a-zA-Z0-9]/', '', sanitize_text_field(wp_unslash($_POST['upload_id']))) : '';
$chunk_index = isset($_POST['chunk_index']) ? absint($_POST['chunk_index']) : 0;
$chunk_size = isset($_POST['chunk_size']) ? absint($_POST['chunk_size']) : 0;
$tmp_name = isset($_FILES['chunk']['tmp_name']) ? sanitize_text_field($_FILES['chunk']['tmp_name']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- server-generated temp path, checked with is_uploaded_file().
$error = isset($_FILES['chunk']['error']) ? absint($_FILES['chunk']['error']) : UPLOAD_ERR_NO_FILE;
if ($upload_id === '' || $chunk_size <= 0 || $error !== UPLOAD_ERR_OK || !is_uploaded_file($tmp_name)) {
WPMigPro_Logger::log("Chunk upload error: code $error for upload $upload_id chunk $chunk_index", 'error');
wp_send_json_error(array('message' => __('Chunk upload failed', 'enmigrate-pro')));
}
$dir = WPMIGPRO_TEMP_DIR . '/uploads';
if (!is_dir($dir)) {
WPMigPro_FS::protect_dir($dir);
}
$out = WPMigPro_FS::open($dir . '/' . $upload_id . '.part', 'c+b');
$in = WPMigPro_FS::open($tmp_name, 'rb');
if (!$out || !$in) {
wp_send_json_error(array('message' => __('Failed to save chunk', 'enmigrate-pro')));
}
fseek($out, $chunk_index * $chunk_size);
WPMigPro_FS::copy_stream($in, $out);
WPMigPro_FS::close($in);
WPMigPro_FS::close($out);
WPMigPro_FS::delete($tmp_name);
wp_send_json_success(array('chunk_index' => $chunk_index));
}
/**
* All chunks sent: move the file to the backups folder, register it and start the
* restore job right away (no second confirmation step).
*/
public function ajax_finalize_upload() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
$this->require_admin();
$upload_id = isset($_POST['upload_id']) ? preg_replace('/[^a-zA-Z0-9]/', '', sanitize_text_field(wp_unslash($_POST['upload_id']))) : '';
$total_size = isset($_POST['total_size']) ? (float) sanitize_text_field(wp_unslash($_POST['total_size'])) : 0;
$original_name = isset($_POST['file_name']) ? sanitize_file_name(wp_unslash($_POST['file_name'])) : '';
$file_ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION));
if ($upload_id === '' || !in_array($file_ext, array('wpb', 'zip'), true)) {
wp_send_json_error(array('message' => __('Invalid file format. Only .wpb and .zip files are allowed.', 'enmigrate-pro')));
}
$part = WPMIGPRO_TEMP_DIR . '/uploads/' . $upload_id . '.part';
clearstatcache();
if (!is_file($part) || (float) filesize($part) !== $total_size) {
wp_send_json_error(array('message' => __('Upload incomplete. Please try again.', 'enmigrate-pro')));
}
WPMigPro_FS::protect_dir(WPMIGPRO_BACKUPS_DIR);
$base = sanitize_file_name(pathinfo($original_name, PATHINFO_FILENAME));
$filename = 'imported-' . gmdate('Y-m-d-H-i-s') . '-' . $base . '-' . strtolower(wp_generate_password(6, false, false)) . '.wpb';
$dest = WPMIGPRO_BACKUPS_DIR . '/' . $filename;
if (!WPMigPro_FS::move($part, $dest)) {
wp_send_json_error(array('message' => __('Failed to save backup file', 'enmigrate-pro')));
}
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- plugin's own table.
$wpdb->insert($wpdb->prefix . 'wpmigpro_backups', array(
'backup_name' => $base,
'backup_type' => 'imported',
'file_path' => $dest,
'file_size' => filesize($dest),
'status' => 'completed',
'progress' => 100,
'created_at' => current_time('mysql'),
));
$backup_id = $wpdb->insert_id;
$job = WPMigPro_Importer::create_job($dest, $backup_id);
if (is_wp_error($job)) {
wp_send_json_error(array('message' => $job->get_error_message()));
}
wp_send_json_success($job);
}
/**
* Start restoring a backup that is already on the server.
*/
public function ajax_restore_start() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
$this->require_admin();
$manager = new WPMigPro_Backup_Manager();
$backup = $manager->get_backup(isset($_POST['backup_id']) ? absint($_POST['backup_id']) : 0);
if (!$backup || !is_file($backup->file_path) || !$manager->is_inside_backups_dir($backup->file_path)) {
wp_send_json_error(array('message' => __('Backup file not found', 'enmigrate-pro')));
}
$job = WPMigPro_Importer::create_job($backup->file_path, $backup->id);
if (is_wp_error($job)) {
wp_send_json_error(array('message' => $job->get_error_message()));
}
wp_send_json_success($job);
}
/**
* Run the next slice of a restore job.
*
* No nonce/capability check on purpose: halfway through a restore the users table and
* the salts may belong to another site, so the admin's cookie and nonce stop being valid.
* Each call is authenticated by the 40-character secret token returned only to the
* administrator who started the job (stored hashed, compared with hash_equals()).
*/
public function ajax_restore_step() {
// phpcs:disable WordPress.Security.NonceVerification.Missing -- token authentication, see above.
$job_id = isset($_POST['job_id']) ? sanitize_text_field(wp_unslash($_POST['job_id'])) : '';
$token = isset($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : '';
$log_from = isset($_POST['log_from']) ? absint($_POST['log_from']) : 0;
// phpcs:enable WordPress.Security.NonceVerification.Missing
$job = WPMigPro_Importer::load($job_id, $token);
if (is_wp_error($job)) {
wp_send_json_error(array('message' => $job->get_error_message()), 403);
}
nocache_headers();
wp_send_json_success($job->run($log_from));
}
public function ajax_download_import_log() {
check_ajax_referer('wpmigpro_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Insufficient permissions', 'enmigrate-pro'), 403);
}
$file = WPMigPro_Logger::latest_file();
if (!$file || !is_readable($file)) {
wp_die(esc_html__('Log file not found', 'enmigrate-pro'), 404);
}
nocache_headers();
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="' . sanitize_file_name(basename($file)) . '"');
header('Content-Length: ' . filesize($file));
WPMigPro_FS::output($file);
exit;
}
/* ------------------------------------------------------------------
* Maintenance
* ------------------------------------------------------------------ */
public function cleanup_temp_files() {
$now = time();
foreach ((array) glob(WPMIGPRO_TEMP_DIR . '/*.sql') as $file) {
if ($now - filemtime($file) > DAY_IN_SECONDS) {
WPMigPro_FS::delete($file);
}
}
foreach ((array) glob(WPMIGPRO_TEMP_DIR . '/uploads/*.part') as $file) {
if ($now - filemtime($file) > DAY_IN_SECONDS) {
WPMigPro_FS::delete($file);
}
}
WPMigPro_Importer::cleanup_old_jobs();
WPMigPro_Logger::clear_old_logs(30);
}
}