backup_service = $backup_service; $this->restore_service = $restore_service; $this->job_service = $job_service; } public function register_menu(): void { $this->page_hook = (string) add_menu_page( 'KiraBackup', 'KiraBackup', 'manage_options', 'kirabackup', array($this, 'render_page'), 'dashicons-backup', 80 ); } // ------------------------------------------------------------------------- // Page render // ------------------------------------------------------------------------- public function render_page(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to access this page.', 'kirabackup')); } $current_tab = sanitize_key(wp_unslash($_GET['tab'] ?? 'backup')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only picks the tab to show; nothing is changed. if (! in_array($current_tab, array('backup', 'restore', 'list', 'settings'), true)) { $current_tab = 'backup'; } // When a preview token is present and no tab is specified, jump straight to restore. if (isset($_GET['wpcbr_preview_token']) && '' === sanitize_key(wp_unslash($_GET['tab'] ?? ''))) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only picks the tab to show; nothing is changed. $current_tab = 'restore'; } $tab_url = function (string $tab): string { return add_query_arg(array('page' => 'kirabackup', 'tab' => $tab), admin_url('admin.php')); }; ?>

render_notices(); ?> render_rollback_box(); ?>

render_tab_backup(); ?> render_tab_restore(); ?> render_tab_settings(); ?> render_tab_list(); ?>
render_import_overlay(); ?>

$target) : ?>

get_import_preview(); $has_preview = ! empty($preview); $targets = WPCBR_Migration_Helper::get_export_targets(); $preview_defaults = array(); if ($has_preview && ! empty($preview['available_targets'])) { $preview_defaults = array_flip( array_values( array_intersect( WPCBR_Migration_Helper::get_default_selection(), (array) $preview['available_targets'] ) ) ); } ?>

→
2

upload_chunk_bytes())) ); ?>

render_server_packages(); ?>
 

'wp-admin', 'core-includes' => 'wp-includes', 'root-files' => __('the files in the WordPress folder (.htaccess and others)', 'kirabackup'), 'wp-config' => 'wp-config.php', ); $ignored = array_intersect_key($ignored_names, array_flip((array) $preview['available_targets'])); ?>

$target) : ?>

0; } ); $limit = empty($limits) ? 16 * MB_IN_BYTES : (int) min($limits); return (int) max(256 * KB_IN_BYTES, min(16 * MB_IN_BYTES, (int) floor($limit * 0.8) - 64 * KB_IN_BYTES)); } /** Restore without uploading: ZIPs placed in the imports folder (FTP / file manager). */ private function render_server_packages(): void { $packages = $this->restore_service->list_server_packages(); $folder = WPCBR_Recovery::relative($this->restore_service->get_imports_dir()); ?>

', 'kirabackup'), '' . esc_html($folder) . '' ); ?>

backup_service->get_backup_dir()); $in_web_root = 0 === strpos($backup_dir, wp_normalize_path(WPCBR_Migration_Helper::site_root())); $file_mods_ok = ! (defined('DISALLOW_FILE_MODS') && DISALLOW_FILE_MODS); ?>

restore_service->list_server_packages(), 'name'); if (! empty($own_zips)) { /* translators: %s: list of ZIP file names */ echo '

' . esc_html(sprintf(__('ZIP files you put into the imports folder yourself are always kept (delete them yourself when you no longer need them): %s.', 'kirabackup'), implode(', ', array_slice($own_zips, 0, 10)) . (count($own_zips) > 10 ? ' …' : ''))) . '

'; } submit_button(__('Save', 'kirabackup'), 'secondary'); ?>

' . esc_html(size_format(wp_max_upload_size())) . '' ); ?>

backup_service->get_backup_dir(); $archives = $this->backup_service->list_backups(); ?>

  • ()
page_hook || $hook_suffix !== $this->page_hook) { return; } wp_enqueue_style('wpcbr-admin', WPCBR_PLUGIN_URL . 'assets/css/admin.css', array(), WPCBR_VERSION); wp_enqueue_script('wpcbr-admin', WPCBR_PLUGIN_URL . 'assets/js/admin.js', array(), WPCBR_VERSION, true); $data = array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('wpcbr_job'), 'chunkBytes' => $this->upload_chunk_bytes(), 'i18n' => array( /* translators: 1: file size, 2: slice size, 3: number of slices */ 'uploadPlan' => __('%1$s — sent in %3$s slice(s) of up to %2$s.', 'kirabackup'), /* translators: 1: bytes sent so far, 2: file size */ 'uploading' => __('Uploading… %1$s of %2$s', 'kirabackup'), /* translators: %s: bytes already on the server */ 'resuming' => __('Continuing the earlier upload (%s already on the server).', 'kirabackup'), /* translators: %s: new slice size */ 'smallerSlices' => __('The server refused a slice; sending smaller slices of %s.', 'kirabackup'), /* translators: 1: attempt number, 2: maximum attempts */ 'retrying' => __('Connection problem — trying again (%1$d/%2$d)…', 'kirabackup'), 'assembling' => __('Reassembling and inspecting…', 'kirabackup'), 'uploadFailed' => __('Upload failed:', 'kirabackup'), 'uploadResumeHint' => __('Select the same file again to continue from where it stopped.', 'kirabackup'), 'chunkTooSmall' => __('The server refused even the smallest slice (a request-size limit or a firewall). Put the ZIP in the imports folder by FTP and choose it below instead.', 'kirabackup'), 'sessionExpired' => __('Your login session has expired. Reload the page and select the file again; the upload continues where it stopped.', 'kirabackup'), 'networkError' => __('The server could not be reached.', 'kirabackup'), 'importFailedPrefix' => __('Import failed:', 'kirabackup'), 'importDone' => __('Import complete. Reloading…', 'kirabackup'), 'importFailedHeading' => __('Import failed', 'kirabackup'), 'importKeptZip' => __('Your site was not changed and the uploaded ZIP is kept: you can start the restore again.', 'kirabackup'), 'backupDone' => __('Backup complete.', 'kirabackup'), 'backupCreated' => __('Created:', 'kirabackup'), /* translators: %d: number of files */ 'backupSkipped' => __('%d file(s) could not be read and were left out (listed in skipped-files.txt inside the ZIP):', 'kirabackup'), 'errorPrefix' => __('Error:', 'kirabackup'), 'selectTarget' => __('Select at least one export target.', 'kirabackup'), 'stalled' => __('The server has not reported progress for a while. It may still be working, or the request was stopped.', 'kirabackup'), 'noResponse' => __('The server is not responding.', 'kirabackup'), /* translators: 1: "backup" or "restore", 2: step, 3: percent, 4: seconds */ 'openJob' => __('A %1$s from earlier is still open: %2$s (%3$d%%, last activity %4$d s ago).', 'kirabackup'), /* translators: 1: "backup" or "restore", 2: error message */ 'failedJob' => __('The last %1$s failed: %2$s', 'kirabackup'), 'finishingJob' => __('A restore is finishing. Continue to let it complete.', 'kirabackup'), 'jobBackup' => __('backup', 'kirabackup'), 'jobRestore' => __('restore', 'kirabackup'), 'cancelled' => __('Cancelled and cleaned up.', 'kirabackup'), 'confirmCancel' => __('Cancel this job and remove its temporary files? The site is not changed.', 'kirabackup'), ), ); wp_add_inline_script('wpcbr-admin', 'var wpcbrAdmin = ' . wp_json_encode($data) . ';', 'before'); } /** * Full-screen overlay shown while a chunked import runs (styles in assets/css/admin.css). */ private function render_import_overlay(): void { ?>

job_service->start('export', $selection, array('split_size' => 0)); if (is_wp_error($job)) { $this->redirect_with_notice('error', $job->get_error_message(), 'backup'); } $job = $this->job_service->run_to_completion($job); if ('done' !== $job['phase']) { $this->redirect_with_notice('error', (string) $job['error'], 'backup'); } $this->redirect_with_notice( 'success', sprintf( /* translators: %s: archive file name */ __('Backup created: %s', 'kirabackup'), (string) ($job['result']['file'] ?? '') ), 'list' ); } public function handle_fix_upload_limit(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_fix_upload_limit'); if (defined('DISALLOW_FILE_MODS') && DISALLOW_FILE_MODS) { $this->redirect_with_notice('error', __('This site sets DISALLOW_FILE_MODS, so KiraBackup does not edit server configuration files.', 'kirabackup'), 'settings'); } $allowed = array_keys(WPCBR_Migration_Helper::get_allowed_upload_sizes()); $target_size = sanitize_text_field(wp_unslash($_POST['wpcbr_target_size'] ?? '512M')); if (! in_array($target_size, $allowed, true)) { $this->redirect_with_notice('error', __('Invalid size value.', 'kirabackup'), 'settings'); } $result = WPCBR_Migration_Helper::try_raise_upload_limit($target_size); $written = array(); if ($result['htaccess']) { $written[] = '.htaccess'; } if ($result['user_ini']) { $written[] = '.user.ini'; } if (empty($written)) { $this->redirect_with_notice( 'error', __('Could not write to any configuration file. Please update your server configuration manually or contact your hosting provider.', 'kirabackup'), 'settings' ); } $this->redirect_with_notice( 'success', sprintf( /* translators: 1: target size (e.g. 512M), 2: file names written */ __('Upload limit set to %1$s in %2$s. Reload this page to verify the new limit.', 'kirabackup'), $target_size, implode(__(' and ', 'kirabackup'), $written) ), 'settings' ); } public function handle_save_settings(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_save_settings'); update_option(self::UNINSTALL_OPTION, ! empty($_POST['wpcbr_delete_on_uninstall']), false); $this->redirect_with_notice('success', __('Settings saved.', 'kirabackup'), 'settings'); } public function handle_delete_request(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } $file_name = sanitize_file_name(wp_unslash($_POST['file'] ?? '')); check_admin_referer('wpcbr_delete_backup_' . $file_name); $result = $this->backup_service->delete_backup($file_name); if (is_wp_error($result)) { $this->redirect_with_notice('error', $result->get_error_message(), 'list'); } $this->redirect_with_notice('success', __('Backup deleted.', 'kirabackup'), 'list'); } public function handle_download_request(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } $file_name = sanitize_file_name(wp_unslash($_GET['file'] ?? '')); check_admin_referer('wpcbr_download_backup_' . $file_name); $file_path = $this->backup_service->get_backup_file_path($file_name); if (is_wp_error($file_path)) { wp_die(esc_html($file_path->get_error_message())); } while (ob_get_level()) { ob_end_clean(); } if (headers_sent()) { wp_die(esc_html__('Cannot send file download, headers already sent.', 'kirabackup')); } nocache_headers(); header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . $file_name . '"'); header('Content-Length: ' . filesize($file_path)); header('X-Content-Type-Options: nosniff'); readfile($file_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- sends the ZIP as it is read: WP_Filesystem would load the whole file into memory. exit; } public function handle_inspect_request(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_inspect_backup'); if (empty($_FILES['wpcbr_backup_zip']['tmp_name'])) { $this->redirect_with_notice('error', __('No backup ZIP was uploaded.', 'kirabackup'), 'restore'); } $result = $this->restore_service->stage_and_inspect_upload($_FILES['wpcbr_backup_zip']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- the uploaded file array; stage_and_inspect_upload() moves it with wp_handle_upload(). if (is_wp_error($result)) { $this->redirect_with_notice('error', $result->get_error_message(), 'restore'); } $this->redirect_with_notice( 'success', __('Package inspected. Review the details before importing.', 'kirabackup'), 'restore', add_query_arg('wpcbr_preview_token', $result['token'], admin_url('admin.php')) ); } public function handle_restore_request(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_restore_backup'); $token = sanitize_key(wp_unslash($_POST['wpcbr_preview_token'] ?? '')); if ('' === $token) { $this->redirect_with_notice('error', __('Import preview token is missing. Inspect the package again.', 'kirabackup'), 'restore'); } // Capture admin URL before restore — DB import may change siteurl in the database, // which would cause admin_url() to return a different host after the restore. $base_url = admin_url('admin.php'); $selection = WPCBR_Migration_Helper::sanitize_restore_selection( array_map('sanitize_key', (array) wp_unslash($_POST['wpcbr_restore_targets'] ?? array())) ); $options = array( 'token' => $token, 'selection' => $selection, 'replace_urls' => ! empty($_POST['wpcbr_replace_urls']), 'target_site_url' => esc_url_raw(wp_unslash($_POST['wpcbr_target_site_url'] ?? site_url())), 'target_home_url' => esc_url_raw(wp_unslash($_POST['wpcbr_target_home_url'] ?? home_url())), 'recovery_key' => sanitize_key(wp_unslash($_POST['wpcbr_recovery_key'] ?? '')), ); // Without JavaScript the same job runs to the end inside this one request. $job = $this->job_service->start('import', $selection, $options); if (is_wp_error($job)) { $this->redirect_with_notice('error', $job->get_error_message(), 'restore', $base_url); } $job = $this->job_service->run_to_completion($job); if ('done' !== $job['phase']) { $this->redirect_with_notice('error', (string) $job['error'], 'restore', $base_url); } $this->redirect_with_notice('success', __('Backup restored successfully.', 'kirabackup'), 'restore', $base_url); } public function handle_rollback_request(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_rollback_restore'); // admin_url() may change after the rollback swaps the options table back. $base_url = admin_url('admin.php'); if (WPCBR_Recovery::exists()) { $result = WPCBR_Recovery::rollback(); if (is_wp_error($result)) { $this->redirect_with_notice('error', $result->get_error_message(), 'restore', $base_url); } // Names what was added after the restore and went with it (a plugin installed again…), // and what was found in the kept copy but kept aside (7章 E). $left = trim(WPCBR_Recovery::leftover_text() . ' ' . WPCBR_Recovery::told_text()); // And what the kept copy held besides (QA 2026-10-09 L1). $this->redirect_with_notice('success', WPCBR_Recovery::notice_text(implode("\n", array_merge(array('rolled_back_now'), WPCBR_Recovery::items()))) . ('' === $left ? '' : ' ' . $left), 'restore', $base_url); } $result = $this->restore_service->rollback_last_restore(); if (is_wp_error($result)) { $this->redirect_with_notice('error', $result->get_error_message(), 'restore', $base_url); } $this->redirect_with_notice('success', __('Database rolled back to the pre-import state.', 'kirabackup'), 'restore', $base_url); } public function handle_confirm_restore_request(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_confirm_restore'); $result = WPCBR_Recovery::confirm(WPCBR_Recovery::CONFIRM_BUDGET); if ('done' === $result || 'none' === $result) { $this->redirect_with_notice('success', __('Kept the restored site; the copy from before the restore was deleted.', 'kirabackup'), 'restore'); } // pending: deleting goes on in the background; incomplete / foreign / locked: explained. $this->redirect_with_notice('pending' === $result ? 'success' : 'error', WPCBR_Recovery::result_text($result), 'restore'); } public function handle_discard_rollback_request(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_discard_rollback'); $what = isset($_POST['wpcbr_what']) && 'unrecorded' === sanitize_key(wp_unslash($_POST['wpcbr_what'])) ? 'unrecorded' : 'all'; $result = $this->restore_service->discard_rollback_point($what); if (is_wp_error($result)) { $this->redirect_with_notice('error', $result->get_error_message(), 'restore'); } $this->redirect_with_notice('success', __('Restore point discarded.', 'kirabackup'), 'restore'); } /** "Clean up as this site's": imported tables left behind that a person ticked (QA 2026-10-09 L1). */ public function handle_discard_imported_request(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_discard_imported'); // Only names offered on the screen are acted on (discard_imported() checks them again). $names = isset($_POST['wpcbr_tables']) && is_array($_POST['wpcbr_tables']) ? array_map('sanitize_text_field', wp_unslash($_POST['wpcbr_tables'])) : array(); if (empty($names)) { $this->redirect_with_notice('error', __('No table was ticked. Nothing was deleted.', 'kirabackup'), 'restore'); } $result = $this->restore_service->discard_imported($names); if (is_wp_error($result)) { $this->redirect_with_notice('error', $result->get_error_message(), 'restore'); } $this->redirect_with_notice('success', __('The imported tables left behind were cleaned up.', 'kirabackup'), 'restore'); } // ------------------------------------------------------------------------- // AJAX endpoints (chunked export/import jobs) // ------------------------------------------------------------------------- /** * Capability and service checks shared by the AJAX endpoints. Each endpoint checks its * nonce itself, first, before reading any request data. */ private function ajax_guard(): void { if (! current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'kirabackup')), 403); } if (null === $this->job_service) { wp_send_json_error(array('message' => __('Background jobs are unavailable.', 'kirabackup'))); } } public function handle_export_start(): void { check_ajax_referer('wpcbr_job'); $this->ajax_guard(); $selection = WPCBR_Migration_Helper::sanitize_selection( array_map('sanitize_key', (array) wp_unslash($_POST['targets'] ?? array())) ); // Backups are always kept as one portable ZIP. Import handles server upload // limits by slicing that ZIP in the browser when necessary. $job = $this->job_service->start('export', $selection, array('split_size' => 0)); if (is_wp_error($job)) { wp_send_json_error(array('message' => $job->get_error_message())); } wp_send_json_success($this->job_service->to_status($job)); } public function handle_export_tick(): void { check_ajax_referer('wpcbr_job'); $this->ajax_guard(); $this->run_job_tick('export'); } public function handle_import_start(): void { check_ajax_referer('wpcbr_job'); $this->ajax_guard(); $token = sanitize_key(wp_unslash($_POST['token'] ?? '')); if ('' === $token) { wp_send_json_error(array('message' => __('Import preview token is missing. Inspect the package again.', 'kirabackup'))); } $selection = WPCBR_Migration_Helper::sanitize_restore_selection( array_map('sanitize_key', (array) wp_unslash($_POST['targets'] ?? array())) ); $options = array( 'token' => $token, 'selection' => $selection, 'replace_urls' => ! empty($_POST['replace_urls']), 'target_site_url' => esc_url_raw(wp_unslash($_POST['target_site_url'] ?? site_url())), 'target_home_url' => esc_url_raw(wp_unslash($_POST['target_home_url'] ?? home_url())), 'recovery_key' => sanitize_key(wp_unslash($_POST['recovery_key'] ?? '')), ); $job = $this->job_service->start('import', $selection, $options); if (is_wp_error($job)) { wp_send_json_error(array('message' => $job->get_error_message())); } wp_send_json_success($this->job_service->to_status($job)); } public function handle_import_tick(): void { check_ajax_referer('wpcbr_job'); $this->ajax_guard(); $this->run_job_tick('import'); } /** Advance the active job by one tick and report its status. */ private function run_job_tick(string $type): void { $job = $this->job_service->get_active_job(); if (null === $job || $type !== (string) ($job['type'] ?? '')) { wp_send_json_error(array('message' => __('No job is running. It may have been cancelled or finished in another tab.', 'kirabackup'), 'code' => 'no_job')); } // A job that already failed (e.g. PHP stopped during the last tick) is reported, not re-run. if ('error' !== $job['phase']) { $job = 'export' === $type ? $this->job_service->run_export_tick($job) : $this->job_service->run_import_tick($job); } $status = $this->job_service->to_status($job); if (! empty($status['complete']) || ! empty($status['failed'])) { $this->job_service->clear((string) ($job['id'] ?? '')); } wp_send_json_success($status); } /** * Store one slice of the backup ZIP. Slices are addressed by their byte offset, so * the browser can resend a slice in a smaller size, or resume after a lost connection, * from exactly where the server's copy ends. */ public function handle_upload_part(): void { check_ajax_referer('wpcbr_job'); $this->ajax_guard(); $session = sanitize_key(wp_unslash($_POST['session'] ?? '')); $offset = isset($_POST['offset']) ? (int) $_POST['offset'] : -1; $total_size = isset($_POST['total_size']) ? (int) $_POST['total_size'] : 0; $file = isset($_FILES['part']) && is_array($_FILES['part']) ? $_FILES['part'] : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- an uploaded file array; moved with wp_handle_upload(). if (isset($file['error']) && in_array((int) $file['error'], array(UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE), true)) { wp_send_json_error(array('code' => 'too_large', 'message' => __('This slice is larger than the server accepts in one request.', 'kirabackup')), 413); } if (empty($file['tmp_name'])) { wp_send_json_error(array('code' => 'retry', 'message' => __('The slice did not arrive completely.', 'kirabackup')), 400); } $result = $this->restore_service->store_uploaded_part($session, $file, $offset, $total_size); if (is_wp_error($result)) { $code = 'wpcbr_upload_size' === $result->get_error_code() ? 'too_large' : 'failed'; wp_send_json_error(array('code' => $code, 'message' => $result->get_error_message()), 'too_large' === $code ? 413 : 400); } wp_send_json_success(array('received' => (int) $result)); } /** How many bytes of this upload the server already holds (to resume after a failure). */ public function handle_upload_status(): void { check_ajax_referer('wpcbr_job'); $this->ajax_guard(); $session = sanitize_key(wp_unslash($_POST['session'] ?? '')); $total_size = isset($_POST['total_size']) ? (int) $_POST['total_size'] : 0; wp_send_json_success(array('received' => $this->restore_service->get_uploaded_bytes($session, $total_size))); } public function handle_assemble_parts(): void { check_ajax_referer('wpcbr_job'); $this->ajax_guard(); $session = sanitize_key(wp_unslash($_POST['session'] ?? '')); $total_size = isset($_POST['total_size']) ? (int) $_POST['total_size'] : 0; $original_name = sanitize_file_name(wp_unslash($_POST['original_name'] ?? 'backup.zip')); $result = $this->restore_service->assemble_parts($session, $total_size, $original_name); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } $url = add_query_arg( array('page' => 'kirabackup', 'tab' => 'restore', 'wpcbr_preview_token' => $result['token']), admin_url('admin.php') ); wp_send_json_success(array('token' => $result['token'], 'redirect' => $url)); } public function handle_job_status(): void { check_ajax_referer('wpcbr_job'); $this->ajax_guard(); $job = $this->job_service->get_active_job(); if (null === $job) { wp_send_json_success(array('idle' => true)); } wp_send_json_success($this->job_service->to_status($job)); } public function handle_job_cancel(): void { check_ajax_referer('wpcbr_job'); $this->ajax_guard(); $result = $this->job_service->cancel(); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } wp_send_json_success(array('cancelled' => true)); } /** Use a ZIP that was placed in the imports folder by FTP (no browser upload). */ public function handle_stage_server_file(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_stage_server_file'); $name = sanitize_file_name(wp_unslash($_POST['wpcbr_server_file'] ?? '')); $path = $this->restore_service->get_server_package_path($name); $result = is_wp_error($path) ? $path : $this->restore_service->stage_local_file($path, true); if (is_wp_error($result)) { $this->redirect_with_notice('error', $result->get_error_message(), 'restore'); } $this->redirect_with_notice( 'success', __('Package inspected. Review the details before importing.', 'kirabackup'), 'restore', add_query_arg('wpcbr_preview_token', $result['token'], admin_url('admin.php')) ); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private function get_import_preview(): array { $token = sanitize_key(wp_unslash($_GET['wpcbr_preview_token'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only shows the inspected package; the import itself checks its nonce. if ('' === $token) { return array(); } $preview = $this->restore_service->get_staged_preview($token); if (is_wp_error($preview)) { return array(); } return $preview; } private function render_rollback_box(): void { // What the recovery watcher did on its own (a stopped restore put back or finished). $code = (string) get_option(WPCBR_Recovery::NOTICE_OPTION, ''); if ('' !== $code) { delete_option(WPCBR_Recovery::NOTICE_OPTION); echo '

' . esc_html(WPCBR_Recovery::notice_text($code)) . '

'; } $state = WPCBR_Recovery::read_state(); if ('unreadable' === $state['read']) { // Nothing is done with a record that cannot be read (v3 4.4): no buttons, the way by hand. echo '

' . esc_html(WPCBR_Recovery::unreadable_text($state['why'])) . '

'; return; } $journal = $state['journal']; if (null !== $journal && ! WPCBR_Recovery::here($journal)) { $this->render_foreign_box($journal); return; } if (null === $journal) { $orphan = WPCBR_Recovery::orphan_record(); if (null !== $orphan) { $this->render_orphan_box($orphan); return; } } if (null !== $journal && in_array($journal['state'], array('confirming', 'requested_keep'), true)) { // "Everything is fine" was chosen: this screen carries the check and the deletion on too. $result = WPCBR_Recovery::confirm(5, 'auto'); // Carried on as it was asked (by a person or by the 7 days). $journal = WPCBR_Recovery::read(); if (null !== $journal && in_array($journal['state'], array('confirming', 'requested_keep'), true)) { echo '

' . esc_html(WPCBR_Recovery::result_text('sweep_blocked' === $result ? 'sweep_blocked' : 'pending', WPCBR_Recovery::items())) . '

'; return; } } if (null === $journal && WPCBR_Recovery::legacy_pending()) { // A roll-back of a 1.1 restore point that stopped: carried on from here (7章 H). $result = $this->restore_service->legacy_continue(); if (is_wp_error($result)) { echo '

' . esc_html($result->get_error_message()) . '

'; return; } } if (null !== $journal && in_array($journal['state'], array('requested_rollback', 'rolling_back', 'rolled_back', 'rollback_incomplete'), true)) { if (! empty($journal['blocked']['items'])) { $text = WPCBR_Recovery::result_text('blocked', (array) $journal['blocked']['items']); } elseif ('requested_rollback' === $journal['state']) { $text = WPCBR_Recovery::result_text('checking'); } else { $text = in_array($journal['state'], array('rolling_back', 'rolled_back'), true) ? __('Putting the site back as it was before the restore did not finish. KiraBackup carries it on automatically within a few minutes, or right now with the button.', 'kirabackup') : WPCBR_Recovery::result_text('partial', (array) ($journal['incomplete']['items'] ?? array())); } ?>

' . esc_html(WPCBR_Recovery::result_text('blocked', (array) $journal['blocked']['items'])) . '

'; } if (! empty($journal['confirm_blocked']['items'])) { echo '

' . esc_html(WPCBR_Recovery::result_text('incomplete', (array) $journal['confirm_blocked']['items'])) . '

'; } // Kept beyond the 7 days until a person decides (D1): why, and what it takes. $reasons = WPCBR_Recovery::hold_reasons($journal); if (! empty($reasons)) { $free = function_exists('disk_free_space') ? @disk_free_space(WPCBR_Recovery_Gate::wp_dir('content')) : false; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- may be disabled by the host; then it is not shown. echo '

' . esc_html( sprintf( /* translators: 1: list of reasons, 2: size of the kept copy, 3: free disk space */ __('KiraBackup will not delete the copy of the site from before the restore on its own, not even after 7 days, because a person has to decide: %1$s. Choose "Everything is fine" or "Roll back". The copy takes about %2$s (free on the disk: %3$s).', 'kirabackup'), $this->hold_text($reasons), size_format(WPCBR_Recovery::kept_size($journal)), false !== $free ? size_format((int) $free) : __('unknown', 'kirabackup') ) ) . '

'; } $skipped = array(); foreach ((array) $journal['steps'] as $step) { $skipped = array_merge($skipped, (array) ($step['skipped'] ?? array())); } if (! empty($skipped)) { /* translators: %s: list of database tables */ echo '

' . esc_html(sprintf(__('Not restored: the backup also held tables that are not this site\'s (another WordPress in the same database, or tables without this site\'s table prefix). They were left as they are: %s.', 'kirabackup'), implode(', ', array_slice($skipped, 0, 20)) . (count($skipped) > 20 ? ' …' : ''))) . '

'; } ?>

' . esc_html__('Compared with the site before the restore (check that this is what you expect before choosing "Everything is fine"):', 'kirabackup') . '

'; } ?>

restore_service->unrecorded_point_tables(); if (! empty($unrecorded)) { ?>

restore_service->unrecorded_text($unrecorded)); ?>

restore_service->other_imported_tables(); if (! empty($others)) { $split = $this->restore_service->other_imported_split(); ?>

restore_service->other_imported_text($others)); ?>

restore_service->get_rollback_point(); if (empty($point) || WPCBR_Recovery::is_v3_record($point)) { return; } $created = isset($point['created_gmt']) ? (string) $point['created_gmt'] : ''; ?>

__('your "Roll back" was refused (the kept copy is incomplete)', 'kirabackup'), 'asked' => __('a "Roll back" was asked for and is not finished', 'kirabackup'), 'unverifiable' => __('the restore record or its list of files cannot be checked', 'kirabackup'), 'restored' => __('the restored site lacks much of what the backup held', 'kirabackup'), 'smaller' => __('the restored site has much less than the site before the restore', 'kirabackup'), ); $parts = array(); foreach ($reasons as $reason => $named) { $parts[] = ($texts[ $reason ] ?? $reason) . (empty($named) ? '' : ' (' . implode('; ', array_slice((array) $named, 0, 3)) . ')'); } return implode('; ', $parts); } /** A restore record of another place (v3 4.3): nothing is done; a copy can set it aside (行15). */ private function render_foreign_box(array $journal): void { $verdict = WPCBR_Recovery::identity($journal)['verdict']; ?>

redirect_with_notice('success', __('Deleted the database copy of the site from before the earlier restore. The current site stays as it is.', 'kirabackup'), 'restore'); } if ('incomplete' === $result) { /* translators: %s: list of tables */ $this->redirect_with_notice('error', sprintf(__('The current site is missing %s, so the database copy was not deleted (it may be needed).', 'kirabackup'), implode(', ', WPCBR_Recovery::items())), 'restore'); } $this->redirect_with_notice('error', WPCBR_Recovery::result_text($result), 'restore'); } public function handle_mark_copy_request(): void { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'kirabackup')); } check_admin_referer('wpcbr_mark_copy'); $result = WPCBR_Recovery::mark_as_duplicate(); if ('done' === $result) { $this->redirect_with_notice('success', __('The copied restore record was set aside. Its kept files and tables were not touched; delete them yourself if this copy does not need them.', 'kirabackup'), 'restore'); } $this->redirect_with_notice('error', __('The restore record is not surely another site\'s, so it was not set aside. Nothing was changed.', 'kirabackup'), 'restore'); } private function render_notices(): void { $key = 'wpcbr_notice_' . get_current_user_id(); $notice = get_transient($key); if (! is_array($notice) || empty($notice['message'])) { return; } delete_transient($key); $type = sanitize_key((string) ($notice['type'] ?? 'info')); $message = (string) $notice['message']; $class = 'notice notice-info'; if ('success' === $type) { $class = 'notice notice-success'; } elseif ('error' === $type) { $class = 'notice notice-error'; } printf('

%2$s

', esc_attr($class), esc_html($message)); } private function redirect_with_notice(string $type, string $message, string $tab = '', string $base_url = ''): void { // Store the notice in a per-user transient instead of the query string so it // cannot be spoofed by crafting a URL, and so messages never appear in the URL. set_transient( 'wpcbr_notice_' . get_current_user_id(), array('type' => $type, 'message' => $message), MINUTE_IN_SECONDS ); $args = array( 'page' => 'kirabackup', ); if ('' !== $tab) { $args['tab'] = $tab; } // Fall back to admin_url() only when no pre-captured URL is supplied. // After a DB restore, admin_url() may return the restored site's URL. $url = add_query_arg($args, $base_url ?: admin_url('admin.php')); wp_safe_redirect($url); exit; } }