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 '
';
}
$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 '
';
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 '
';
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()));
}
?>
';
}
// 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') . '
';
foreach (array_slice($differences, 0, 12) as $line) {
echo '
' . esc_html($line) . '
';
}
echo '
';
}
?>
restore_service->unrecorded_point_tables();
if (! empty($unrecorded)) {
?>
__('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;
}
}