__('Your session expired. Reload the page and try again.', 'shiftbox')), 403); } if (! current_user_can(self::capability())) { wp_send_json_error(array('message' => __('You are not allowed to do this.', 'shiftbox')), 403); } } /** * Reads a POST string. * * @param string $key Key. * @return string */ private static function post($key) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in guard(). return isset($_POST[$key]) ? sanitize_text_field(wp_unslash($_POST[$key])) : ''; } /** * Starts a backup. * * @return void */ public static function export_start() { self::guard(); $opts = array(); foreach (array('no_media', 'no_themes', 'no_plugins', 'no_database', 'no_inactive_plugins', 'no_inactive_themes', 'no_revisions', 'no_spam') as $key) { $opts[$key] = '1' === self::post($key); } $opts['tag'] = 'pre-restore' === self::post('tag') ? 'pre-restore' : ''; if ($opts['no_database'] && $opts['no_media'] && $opts['no_themes'] && $opts['no_plugins']) { wp_send_json_error(array('message' => __('Nothing to back up. Tick at least one of Database, Media library, Themes or Plugins.', 'shiftbox'))); } $job = Shiftbox_Runner::start_export($opts); if (is_wp_error($job)) { wp_send_json_error(array('message' => $job->get_error_message())); } wp_send_json_success(Shiftbox_Runner::status($job)); } /** * Advances a job. * * @return void */ public static function tick() { self::guard(); $status = Shiftbox_Runner::tick(self::post('job')); if (! empty($status['error'])) { wp_send_json_error(array_merge($status, array('message' => $status['error']))); } wp_send_json_success($status); } /** * Cancels a job. * * @return void */ public static function cancel() { self::guard(); $id = self::post('job'); if ('' === $id) { $active = Shiftbox_Job::active(true); $id = $active ? $active['id'] : ''; } if (! Shiftbox_Runner::cancel($id)) { wp_send_json_error(array('message' => __('This job cannot be cancelled now.', 'shiftbox'))); } wp_send_json_success(); } /** * Opens or resumes an upload. * * @return void */ public static function upload_init() { self::guard(); Shiftbox_Storage::ensure(); $name = sanitize_file_name(self::post('name')); $size = (float) self::post('size'); $fp = self::post('fp'); $wpress = Shiftbox_Wpress::is_name($name); if ('.shiftbox' !== strtolower(substr($name, -9)) && ! $wpress) { wp_send_json_error(array('message' => __('Choose a .shiftbox or .wpress backup file.', 'shiftbox'))); } if ($size <= 0) { wp_send_json_error(array('message' => __('The file is empty.', 'shiftbox'))); } $limit = (int) Shiftbox_Settings::get('max_import'); if ($limit > 0 && $size > $limit) { /* translators: 1: file size, 2: limit. */ wp_send_json_error(array('message' => sprintf(__('This file is %1$s. The import limit set in Settings is %2$s.', 'shiftbox'), size_format($size), Shiftbox_Settings::human($limit)))); } $free = Shiftbox_Storage::free_space(); if ($free >= 0 && $free < $size * 2.05) { /* translators: 1: needed, 2: free. */ wp_send_json_error(array('message' => sprintf(__('Not enough disk space: uploading and restoring needs about %1$s, but %2$s is free.', 'shiftbox'), size_format($size * 2.05), size_format($free)))); } $id = md5(get_current_user_id() . '|' . $name . '|' . $size . '|' . $fp); $part = Shiftbox_Storage::uploads_dir() . '/' . $id . '.part'; $final = Shiftbox_Storage::uploads_dir() . '/' . $id . ($wpress ? Shiftbox_Wpress::EXT : Shiftbox_Storage::EXT); if (is_file($final) && (float) filesize($final) === $size) { $manifest = Shiftbox_Importer::inspect($final); if (! is_wp_error($manifest)) { wp_send_json_success( array( 'upload' => $id, 'offset' => $size, 'chunk' => Shiftbox_Settings::upload_chunk(), 'complete' => true, 'source' => 'upload:' . $id, 'summary' => self::summary($manifest), ) ); } } file_put_contents( Shiftbox_Storage::uploads_dir() . '/' . $id . '.json', wp_json_encode( array( 'name' => $name, 'size' => $size, 'wpress' => $wpress, ) ) ); clearstatcache(); wp_send_json_success( array( 'upload' => $id, 'offset' => is_file($part) ? (float) filesize($part) : 0.0, 'chunk' => Shiftbox_Settings::upload_chunk(), ) ); } /** * Appends one chunk. The client sends the offset it believes is next; * a mismatch returns the real offset so the client can resume exactly. * * @return void */ public static function upload_chunk() { self::guard(); $id = self::post('upload'); if (! preg_match('/^[a-f0-9]{32}$/', $id)) { wp_send_json_error(array('message' => __('Unknown upload.', 'shiftbox'))); } $dir = Shiftbox_Storage::uploads_dir(); $meta_file = $dir . '/' . $id . '.json'; $meta = is_file($meta_file) ? wp_json_file_decode($meta_file, array('associative' => true)) : null; if (! is_array($meta)) { wp_send_json_error(array('message' => __('Upload expired. Choose the file again.', 'shiftbox'))); } $part = $dir . '/' . $id . '.part'; $offset = (float) self::post('offset'); clearstatcache(); $have = is_file($part) ? (float) filesize($part) : 0.0; if (abs($offset - $have) > 0.5) { wp_send_json_success( array( 'offset' => $have, 'resync' => true, ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified in guard(); binary file data. $file = isset($_FILES['chunk']) ? $_FILES['chunk'] : null; if (! $file || ! empty($file['error']) || ! is_uploaded_file($file['tmp_name'])) { wp_send_json_error( array( 'message' => __('The server rejected a piece of the upload. Retrying with smaller pieces.', 'shiftbox'), 'shrink' => true, ) ); } $len = (float) filesize($file['tmp_name']); if ($have + $len > (float) $meta['size']) { wp_send_json_error(array('message' => __('The upload is larger than announced. Choose the file again.', 'shiftbox'))); } $in = fopen($file['tmp_name'], 'rb'); $out = fopen($part, 'ab'); if (! $in || ! $out) { wp_send_json_error(array('message' => __('Could not save the upload. Check that the uploads folder is writable.', 'shiftbox'))); } $wpress = ! empty($meta['wpress']); if (0.0 === $have) { $head = (string) fread($in, $wpress ? Shiftbox_Wpress::HEADER : strlen(Shiftbox_Archive::MAGIC)); $valid = $wpress ? Shiftbox_Wpress::looks_valid($head) : Shiftbox_Archive::MAGIC === $head; if (! $valid) { fclose($in); fclose($out); @unlink($part); wp_send_json_error(array('message' => $wpress ? __('This .wpress file is damaged or not a backup.', 'shiftbox') : __('This is not a Shiftbox backup file.', 'shiftbox'))); } rewind($in); } $copied = stream_copy_to_stream($in, $out); fclose($in); fclose($out); if ((float) $copied !== $len) { ftruncate(fopen($part, 'r+b'), (int) $have); wp_send_json_error(array('message' => __('Saving the upload failed. The disk may be full.', 'shiftbox'))); } $now = $have + $len; if ($now < (float) $meta['size']) { wp_send_json_success(array('offset' => $now)); } $final = $dir . '/' . $id . ($wpress ? Shiftbox_Wpress::EXT : Shiftbox_Storage::EXT); @rename($part, $final); $manifest = Shiftbox_Importer::inspect($final); if (is_wp_error($manifest)) { @unlink($final); wp_send_json_error(array('message' => $manifest->get_error_message())); } wp_send_json_success( array( 'offset' => $now, 'complete' => true, 'source' => 'upload:' . $id, 'summary' => self::summary($manifest), ) ); } /** * Short manifest summary for the confirmation screen. * * @param array $m Manifest. * @return array */ private static function summary(array $m) { return array( 'home' => isset($m['home']) ? esc_url_raw($m['home']) : '', 'created' => isset($m['created']) ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), (int) $m['created']) : '', 'wp' => isset($m['wp']) ? sanitize_text_field($m['wp']) : '', 'size' => size_format(isset($m['archive_size']) ? $m['archive_size'] : 0), 'files' => ! empty($m['files']) ? number_format_i18n((int) $m['files']) : '', 'source' => (isset($m['format']) && 'wpress' === $m['format']) ? __('All-in-One WP Migration (.wpress)', 'shiftbox') : 'Shiftbox', 'database' => ! empty($m['database']), ); } /** * Resolves a source token to a file path. * * @param string $source upload: or backup:. * @param bool $delete Out: whether the file is temporary. * @return string|false */ private static function resolve($source, &$delete) { $delete = false; if (0 === strpos($source, 'upload:')) { $id = substr($source, 7); if (preg_match('/^[a-f0-9]{32}$/', $id)) { foreach (array(Shiftbox_Storage::EXT, Shiftbox_Wpress::EXT) as $ext) { $path = Shiftbox_Storage::uploads_dir() . '/' . $id . $ext; if (is_file($path)) { $delete = true; return $path; } } } return false; } if (0 === strpos($source, 'ai1wm:')) { return Shiftbox_Storage::ai1wm_path(substr($source, 6)); } if (0 === strpos($source, 'backup:')) { return Shiftbox_Storage::backup_path(substr($source, 7)); } return false; } /** * Summary of a stored backup before restoring it. * * @return void */ public static function inspect() { self::guard(); $delete = false; $source = 'ai1wm' === self::post('from') ? 'ai1wm:' . self::post('name') : 'backup:' . self::post('name'); $path = self::resolve($source, $delete); if (! $path) { wp_send_json_error(array('message' => __('Backup not found.', 'shiftbox'))); } $manifest = Shiftbox_Importer::inspect($path); if (is_wp_error($manifest)) { wp_send_json_error(array('message' => $manifest->get_error_message())); } wp_send_json_success( array( 'source' => $source, 'summary' => self::summary($manifest), ) ); } /** * Starts a restore. * * @return void */ public static function import_start() { self::guard(); $delete = false; $path = self::resolve(self::post('source'), $delete); if (! $path) { wp_send_json_error(array('message' => __('Backup not found. Upload it again.', 'shiftbox'))); } $job = Shiftbox_Runner::start_import($path, $delete); if (is_wp_error($job)) { wp_send_json_error(array('message' => $job->get_error_message())); } wp_send_json_success(Shiftbox_Runner::status($job)); } /** * Deletes a stored backup. * * @return void */ public static function delete() { self::guard(); $path = Shiftbox_Storage::backup_path(self::post('name')); if (! $path || ! @unlink($path)) { wp_send_json_error(array('message' => __('The backup could not be deleted.', 'shiftbox'))); } wp_send_json_success(); } /** * Lists stored backups. * * @return void */ public static function list_backups() { self::guard(); wp_send_json_success(array('html' => Shiftbox_Admin::backups_rows())); } /** * Streams a backup with HTTP Range support, so downloads can resume. * * @return void */ public static function download() { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : ''; if (! wp_verify_nonce($nonce, 'shiftbox_download') || ! current_user_can(self::capability())) { wp_die(esc_html__('This download link has expired. Reload the Shiftbox page and try again.', 'shiftbox'), 403); } $name = isset($_GET['file']) ? sanitize_file_name(wp_unslash($_GET['file'])) : ''; $path = Shiftbox_Storage::backup_path($name); if (! $path) { wp_die(esc_html__('Backup not found.', 'shiftbox'), 404); } $size = (float) filesize($path); $start = 0; $end = $size - 1; $range = isset($_SERVER['HTTP_RANGE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_RANGE'])) : ''; // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ($range && preg_match('/^bytes=(\d*)-(\d*)$/', $range, $m)) { if ('' === $m[1] && '' !== $m[2]) { $start = max(0, $size - (float) $m[2]); } else { $start = (float) $m[1]; if ('' !== $m[2]) { $end = min($end, (float) $m[2]); } } if ($start > $end || $start >= $size) { status_header(416); header('Content-Range: bytes */' . $size); exit; } status_header(206); header(sprintf('Content-Range: bytes %.0f-%.0f/%.0f', $start, $end, $size)); } while (ob_get_level()) { ob_end_clean(); } if (function_exists('set_time_limit')) { @set_time_limit(0); } nocache_headers(); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . basename($path) . '"'); header('Accept-Ranges: bytes'); header(sprintf('Content-Length: %.0f', $end - $start + 1)); header('X-Content-Type-Options: nosniff'); $fh = fopen($path, 'rb'); fseek($fh, (int) $start); $left = $end - $start + 1; $cap = (float) Shiftbox_Settings::get('speed_cap') * 1048576; $chunk = 1048576; $started = microtime(true); $sent = 0; while ($left > 0 && ! feof($fh) && ! connection_aborted()) { $data = fread($fh, (int) min($chunk, $left)); echo $data; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary file stream. flush(); $left -= strlen($data); $sent += strlen($data); if ($cap > 0) { $ahead = ($sent / $cap) - (microtime(true) - $started); if ($ahead > 0) { usleep((int) ($ahead * 1000000)); } } } fclose($fh); exit; } }