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); } }