manager = new BackupManager( $dumper, $wpdb ); } public function register(): void { $auth = static function () { return current_user_can( 'manage_options' ); }; register_rest_route( 'shiftora/v1', '/backup/create', [ 'methods' => 'POST', 'callback' => [ $this, 'create' ], 'permission_callback' => $auth, 'args' => [ 'tables' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ], ], 'label' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], 'exclude_spam' => [ 'type' => 'boolean', 'default' => false ], 'exclude_transients' => [ 'type' => 'boolean', 'default' => false ], 'compress' => [ 'type' => 'boolean', 'default' => false ], ], ] ); register_rest_route( 'shiftora/v1', '/backup/(?P\d+)/restore', [ 'methods' => 'POST', 'callback' => [ $this, 'restore' ], 'permission_callback' => $auth, ] ); // Download streams a file, so it is an admin-post handler rather than a REST route. add_action( 'admin_post_shiftora_download_backup', [ $this, 'serve_download' ] ); } public function create( WP_REST_Request $request ): WP_REST_Response|WP_Error { $options = [ 'tables' => $request->get_param( 'tables' ) ?? [], 'label' => $request->get_param( 'label' ) ?? '', 'exclude_spam' => (bool) $request->get_param( 'exclude_spam' ), 'exclude_transients' => (bool) $request->get_param( 'exclude_transients' ), 'compress' => (bool) $request->get_param( 'compress' ), ]; // Views are excluded as in the export: a backup built from a view // definition restores to nothing. if ( ! empty( $options['tables'] ) ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- SHOW FULL TABLES lists the live schema, which no WordPress API exposes. $actual = $wpdb->get_col( "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" ); $invalid = array_diff( $options['tables'], $actual ); if ( ! empty( $invalid ) ) { return new WP_Error( 'shiftora_invalid_tables', sprintf( /* translators: %s: comma-separated list of invalid table names */ __( 'Unknown tables: %s', 'shiftora' ), implode( ', ', array_map( 'esc_html', $invalid ) ) ), [ 'status' => 400 ] ); } } try { $record = $this->manager->create_manual( $options ); } catch ( \RuntimeException $e ) { return new WP_Error( 'shiftora_backup_failed', $e->getMessage(), [ 'status' => 500 ] ); } return new WP_REST_Response( [ 'success' => true, 'backup_id' => $record->id, 'label' => PrivateFileName::display( basename( $record->file_path ) ), 'file_size' => $record->file_size, 'message' => __( 'Backup created successfully.', 'shiftora' ), ], 201 ); } public function restore( WP_REST_Request $request ): WP_REST_Response|WP_Error { $id = (int) $request->get_param( 'id' ); $record = $this->manager->find( $id ); if ( ! $record ) { return new WP_Error( 'shiftora_not_found', __( 'Backup not found.', 'shiftora' ), [ 'status' => 404 ] ); } try { // Pressed by an operator, so a backup restored before is restored again. $this->manager->restore( $record, true ); } catch ( \RuntimeException $e ) { return new WP_Error( 'shiftora_restore_failed', $e->getMessage(), [ 'status' => 500 ] ); } return new WP_REST_Response( [ 'success' => true, 'message' => __( 'Backup restored successfully.', 'shiftora' ), ], 200 ); } public function serve_download(): void { $id = isset( $_GET['id'] ) ? absint( wp_unslash( $_GET['id'] ) ) : 0; if ( ! current_user_can( 'manage_options' ) || ! $id ) { wp_die( esc_html__( 'You are not authorized to download backups.', 'shiftora' ), 403 ); } if ( ! isset( $_GET['_wpnonce'] ) || ! is_string( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'shiftora_download_backup_' . $id ) ) { wp_die( esc_html__( 'Security check failed.', 'shiftora' ), 403 ); } $record = $this->manager->find( $id ); if ( ! $record || ! file_exists( $record->file_path ) ) { wp_die( esc_html__( 'Backup file not found.', 'shiftora' ), 404 ); } $upload_dir = wp_upload_dir(); $real_path = realpath( $record->file_path ); $base_dir = realpath( (string) $upload_dir['basedir'] ); if ( false === $real_path || false === $base_dir || ! str_starts_with( $real_path, $base_dir . DIRECTORY_SEPARATOR ) ) { wp_die( esc_html__( 'Invalid backup path.', 'shiftora' ), 403 ); } $filename = PrivateFileName::display( basename( $real_path ) ); $safe_filename = sanitize_file_name( $filename ); $file_size = filesize( $real_path ); $is_gz = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ) === 'gz'; $content_type = $is_gz ? 'application/gzip' : 'application/octet-stream'; // A buffer would load the whole file into memory. if ( ob_get_level() ) { ob_end_clean(); } header( 'Content-Type: ' . $content_type ); header( "Content-Disposition: attachment; filename=\"{$safe_filename}\"; filename*=UTF-8''" . rawurlencode( $safe_filename ) ); header( 'Content-Length: ' . (string) $file_size ); header( 'Cache-Control: no-cache, no-store, must-revalidate' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); readfile( $real_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- readfile streams a verified file inside the plugin's own data folder to the browser exit; } }