Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.48% covered (warning)
79.48%
306 / 385
33.33% covered (danger)
33.33%
5 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Controller
79.48% covered (warning)
79.48%
306 / 385
33.33% covered (danger)
33.33%
5 / 15
71.60
0.00% covered (danger)
0.00%
0 / 1
 register_rest_routes
96.13% covered (success)
96.13%
149 / 155
0.00% covered (danger)
0.00%
0 / 1
5
 backup_permissions_callback
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 install_backup_helper_script
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 delete_backup_helper_script
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 fetch_database_object_backup
51.85% covered (warning)
51.85%
14 / 27
0.00% covered (danger)
0.00%
0 / 1
5.79
 fetch_options_backup
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 fetch_comment_backup
17.86% covered (danger)
17.86%
5 / 28
0.00% covered (danger)
0.00%
0 / 1
7.99
 fetch_post_backup
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
2.00
 fetch_term_backup
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 fetch_user_backup
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 get_allowed_object_types
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
1
 get_site_backup_undo_event
89.74% covered (warning)
89.74%
35 / 39
0.00% covered (danger)
0.00%
0 / 1
11.13
 fetch_wc_orders_backup
33.33% covered (danger)
33.33%
8 / 24
0.00% covered (danger)
0.00%
0 / 1
21.52
 get_site_backup_preflight
78.26% covered (warning)
78.26%
18 / 23
0.00% covered (danger)
0.00%
0 / 1
5.26
 get_option_row
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * The Backup Rest Controller class.
4 * Registers the REST routes for Backup.
5 *
6 * @package automattic/jetpack-backup
7 */
8
9// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
10// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
11// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
12// are installed, or in some other cases).
13namespace Automattic\Jetpack\Backup\V0005;
14
15use Automattic\Jetpack\Connection\Client;
16use Automattic\Jetpack\Connection\Rest_Authentication;
17use Automattic\Jetpack\Sync\Actions as Sync_Actions;
18use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
19use Jetpack_Options;
20use WP_Error;
21use WP_REST_Request;
22use WP_REST_Server;
23use function esc_html__;
24use function get_comment;
25use function get_comment_meta;
26use function get_metadata;
27use function get_post;
28use function get_post_meta;
29use function get_term;
30use function get_term_meta;
31use function get_user_by;
32use function get_user_meta;
33use function is_wp_error;
34use function register_rest_route;
35use function rest_authorization_required_code;
36use function rest_ensure_response;
37use function wp_remote_retrieve_response_code;
38
39/**
40 * Registers the REST routes for Backup.
41 */
42class REST_Controller {
43    /**
44     * Registers the REST routes for Backup.
45     *
46     * @access public
47     * @static
48     */
49    public static function register_rest_routes() {
50        // Install a Helper Script to assist Jetpack Backup fetch data.
51        register_rest_route(
52            'jetpack/v4',
53            '/backup-helper-script',
54            array(
55                'methods'             => WP_REST_Server::CREATABLE,
56                'callback'            => __CLASS__ . '::install_backup_helper_script',
57                'permission_callback' => __CLASS__ . '::backup_permissions_callback',
58                'args'                => array(
59                    'helper' => array(
60                        'description' => __( 'base64 encoded Backup Helper Script body.', 'jetpack-backup-pkg' ),
61                        'type'        => 'string',
62                        'required'    => true,
63                    ),
64                ),
65            )
66        );
67
68        // Delete a Backup Helper Script.
69        register_rest_route(
70            'jetpack/v4',
71            '/backup-helper-script',
72            array(
73                'methods'             => WP_REST_Server::DELETABLE,
74                'callback'            => __CLASS__ . '::delete_backup_helper_script',
75                'permission_callback' => __CLASS__ . '::backup_permissions_callback',
76                'args'                => array(
77                    'path' => array(
78                        'description' => __( 'Path to Backup Helper Script', 'jetpack-backup-pkg' ),
79                        'type'        => 'string',
80                        'required'    => true,
81                    ),
82                ),
83            )
84        );
85
86        // Fetch a backup of a database object, along with all of its metadata.
87        register_rest_route(
88            'jetpack/v4',
89            '/database-object/backup',
90            array(
91                'methods'             => WP_REST_Server::READABLE,
92                'callback'            => __CLASS__ . '::fetch_database_object_backup',
93                'permission_callback' => __CLASS__ . '::backup_permissions_callback',
94                'args'                => array(
95                    'object_type' => array(
96                        'description'       => __( 'Type of object to fetch from the database', 'jetpack-backup-pkg' ),
97                        'required'          => true,
98                        'validate_callback' => function ( $value ) {
99                            if ( ! is_string( $value ) ) {
100                                return new WP_Error(
101                                    'rest_invalid_param',
102                                    __( 'The object_type argument must be a non-empty string.', 'jetpack-backup-pkg' ),
103                                    array( 'status' => 400 )
104                                );
105                            }
106
107                            $allowed_object_types = array_keys( self::get_allowed_object_types() );
108
109                            if ( ! in_array( $value, $allowed_object_types, true ) ) {
110                                return new WP_Error(
111                                    'rest_invalid_param',
112                                    sprintf(
113                                        /* translators: %s: comma-separated list of allowed object types */
114                                        __( 'The object_type argument should be one of %s', 'jetpack-backup-pkg' ),
115                                        implode( ', ', $allowed_object_types )
116                                    ),
117                                    array( 'status' => 400 )
118                                );
119                            }
120
121                            return true;
122                        },
123                    ),
124                    'object_id'   => array(
125                        'description' => __( 'ID of the database object to fetch', 'jetpack-backup-pkg' ),
126                        'type'        => 'integer',
127                        'required'    => true,
128                    ),
129                ),
130            )
131        );
132
133        // Fetch a backup of an option.
134        register_rest_route(
135            'jetpack/v4',
136            '/options/backup',
137            array(
138                'methods'             => WP_REST_Server::READABLE,
139                'callback'            => __CLASS__ . '::fetch_options_backup',
140                'permission_callback' => __CLASS__ . '::backup_permissions_callback',
141                'args'                => array(
142                    'name' => array(
143                        'description'       => __( 'One or more option names to include in the backup', 'jetpack-backup-pkg' ),
144                        'validate_callback' => function ( $value ) {
145                            $is_valid = is_array( $value ) || is_string( $value );
146                            if ( ! $is_valid ) {
147                                return new WP_Error( 'rest_invalid_param', __( 'The name argument should be an option name or an array of option names', 'jetpack-backup-pkg' ), array( 'status' => 400 ) );
148                            }
149
150                            return true;
151                        },
152                        'required'          => true,
153                    ),
154                ),
155            )
156        );
157
158        // Fetch a backup of a comment, along with all of its metadata.
159        register_rest_route(
160            'jetpack/v4',
161            '/comments/(?P<id>\d+)/backup',
162            array(
163                'methods'             => WP_REST_Server::READABLE,
164                'callback'            => __CLASS__ . '::fetch_comment_backup',
165                'permission_callback' => __CLASS__ . '::backup_permissions_callback',
166            )
167        );
168
169        // Fetch a backup of a post, along with all of its metadata.
170        register_rest_route(
171            'jetpack/v4',
172            '/posts/(?P<id>\d+)/backup',
173            array(
174                'methods'             => WP_REST_Server::READABLE,
175                'callback'            => __CLASS__ . '::fetch_post_backup',
176                'permission_callback' => __CLASS__ . '::backup_permissions_callback',
177            )
178        );
179
180        // Fetch a backup of a term, along with all of its metadata.
181        register_rest_route(
182            'jetpack/v4',
183            '/terms/(?P<id>\d+)/backup',
184            array(
185                'methods'             => WP_REST_Server::READABLE,
186                'callback'            => __CLASS__ . '::fetch_term_backup',
187                'permission_callback' => __CLASS__ . '::backup_permissions_callback',
188            )
189        );
190
191        // Fetch a backup of a user, along with all of its metadata.
192        register_rest_route(
193            'jetpack/v4',
194            '/users/(?P<id>\d+)/backup',
195            array(
196                'methods'             => WP_REST_Server::READABLE,
197                'callback'            => __CLASS__ . '::fetch_user_backup',
198                'permission_callback' => __CLASS__ . '::backup_permissions_callback',
199            )
200        );
201
202        // Get backup undo event
203        register_rest_route(
204            'jetpack/v4',
205            '/site/backup/undo-event',
206            array(
207                'methods'             => WP_REST_Server::READABLE,
208                'callback'            => __CLASS__ . '::get_site_backup_undo_event',
209                'permission_callback' => __NAMESPACE__ . '\Jetpack_Backup::backups_permissions_callback',
210            )
211        );
212
213        // Fetch a backup of a wc_order along with all of its data.
214        register_rest_route(
215            'jetpack/v4',
216            '/orders/(?P<id>\d+)/backup',
217            array(
218                'methods'             => WP_REST_Server::READABLE,
219                'callback'            => __CLASS__ . '::fetch_wc_orders_backup',
220                'permission_callback' => __CLASS__ . '::backup_permissions_callback',
221            )
222        );
223
224        // Fetch backup preflight status
225        register_rest_route(
226            'jetpack/v4',
227            '/site/backup/preflight',
228            array(
229                'methods'             => WP_REST_Server::READABLE,
230                'callback'            => __CLASS__ . '::get_site_backup_preflight',
231                'permission_callback' => __NAMESPACE__ . '\Jetpack_Backup::backups_permissions_callback',
232            )
233        );
234    }
235
236    /**
237     * The Backup endpoints should only be available via site-level authentication.
238     * This means that the corresponding endpoints can only be accessible from WPCOM.
239     *
240     * @access public
241     * @static
242     *
243     * @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise.
244     */
245    public static function backup_permissions_callback() {
246        if ( Rest_Authentication::is_signed_with_blog_token() ) {
247            return true;
248        }
249
250        $error_msg = esc_html__(
251            'You are not allowed to perform this action.',
252            'jetpack-backup-pkg'
253        );
254
255        return new WP_Error( 'rest_forbidden', $error_msg, array( 'status' => rest_authorization_required_code() ) );
256    }
257
258    /**
259     * Install the Backup Helper Script.
260     *
261     * @access public
262     * @static
263     *
264     * @param WP_REST_Request $request The request sent to the WP REST API.
265     *
266     * @return array|WP_Error Array with installation info on success:
267     *
268     *   'path'    (string) Helper script installation path on the filesystem.
269     *   'url'     (string) URL to the helper script.
270     *   'abspath' (string) WordPress root.
271     *
272     *   or an instance of WP_Error on failure.
273     */
274    public static function install_backup_helper_script( $request ) {
275        $helper_script = $request->get_param( 'helper' );
276
277        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
278        $helper_script = base64_decode( $helper_script );
279        if ( ! $helper_script ) {
280            return new WP_Error( 'invalid_args', __( 'Helper script body must be base64 encoded', 'jetpack-backup-pkg' ), 400 );
281        }
282
283        $installation_info = Helper_Script_Manager::install_helper_script( $helper_script );
284        Helper_Script_Manager::cleanup_expired_helper_scripts();
285
286        return rest_ensure_response( $installation_info );
287    }
288
289    /**
290     * Delete a Backup Helper Script.
291     *
292     * @access public
293     * @static
294     *
295     * @param WP_REST_Request $request The request sent to the WP REST API.
296     *
297     * @return array|WP_Error An array with 'success' key, or an instance of WP_Error on failure.
298     */
299    public static function delete_backup_helper_script( $request ) {
300        $path_to_helper_script = $request->get_param( 'path' );
301
302        $delete_result = Helper_Script_Manager::delete_helper_script( $path_to_helper_script );
303        Helper_Script_Manager::cleanup_expired_helper_scripts();
304
305        if ( is_wp_error( $delete_result ) ) {
306            return $delete_result;
307        }
308
309        return rest_ensure_response( array( 'success' => true ) );
310    }
311
312    /**
313     * Fetch a backup of a database object, along with all of its metadata.
314     *
315     * @access public
316     * @static
317     *
318     * @param WP_REST_Request $request The request sent to the WP REST API.
319     *
320     * @return array
321     */
322    public static function fetch_database_object_backup( $request ) {
323        global $wpdb;
324
325        // Disable Sync as this is a read-only operation and triggered by sync activity.
326        Sync_Actions::mark_sync_read_only();
327
328        $allowed_object_types = self::get_allowed_object_types();
329        // Safe to do this as we have already validated the object_type key exists in self::get_allowed_object_types().
330        $object_type = $allowed_object_types[ $request->get_param( 'object_type' ) ];
331        $object_id   = $request->get_param( 'object_id' );
332        $table       = $wpdb->prefix . $object_type['table'];
333        $id_field    = $object_type['id_field'];
334
335        // Fetch the requested object.
336        $object = $wpdb->get_row(
337            $wpdb->prepare(
338                'SELECT * FROM `' . $table . '` WHERE `' . $id_field . '` = %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
339                $object_id
340            )
341        );
342
343        if ( empty( $object ) ) {
344            return new WP_Error( 'object_not_found', __( 'Object not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
345        }
346
347        $result = array( 'object' => $object );
348
349        // Fetch associated metadata (if this object type has any).
350        if ( ! empty( $object_type['meta_type'] ) ) {
351            $result['meta'] = get_metadata( $object_type['meta_type'], $object_id );
352        }
353
354        // If there is a child linked table (eg: woocommerce_tax_rate_locations), fetch linked records.
355        if ( ! empty( $object_type['child_table'] ) ) {
356            $child_table    = $wpdb->prefix . $object_type['child_table'];
357            $child_id_field = $object_type['child_id_field'];
358
359            $result['children'] = $wpdb->get_results(
360                $wpdb->prepare(
361                    'SELECT * FROM `' . $child_table . '` where `' . $child_id_field . '` = %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
362                    $object_id
363                )
364            );
365        }
366
367        return $result;
368    }
369
370    /**
371     * Fetch a backup of an option.
372     *
373     * @access public
374     * @static
375     *
376     * @param WP_REST_Request $request The request sent to the WP REST API.
377     *
378     * @return array
379     */
380    public static function fetch_options_backup( $request ) {
381        // Disable Sync as this is a read-only operation and triggered by sync activity.
382        Sync_Actions::mark_sync_read_only();
383
384        $option_names = (array) $request->get_param( 'name' );
385
386        $options = array_map( self::class . '::get_option_row', $option_names );
387        return array( 'options' => $options );
388    }
389
390    /**
391     * Fetch a backup of a comment, along with all of its metadata.
392     *
393     * @access public
394     * @static
395     *
396     * @param WP_REST_Request $request The request sent to the WP REST API.
397     *
398     * @return array
399     */
400    public static function fetch_comment_backup( $request ) {
401        // Disable Sync as this is a read-only operation and triggered by sync activity.
402        Sync_Actions::mark_sync_read_only();
403
404        $comment_id = $request['id'];
405        $comment    = get_comment( $comment_id );
406
407        if ( empty( $comment ) ) {
408            return new WP_Error( 'comment_not_found', __( 'Comment not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
409        }
410
411        $allowed_keys = array(
412            'comment_ID',
413            'comment_post_ID',
414            'comment_author',
415            'comment_author_email',
416            'comment_author_url',
417            'comment_author_IP',
418            'comment_date',
419            'comment_date_gmt',
420            'comment_content',
421            'comment_karma',
422            'comment_approved',
423            'comment_agent',
424            'comment_type',
425            'comment_parent',
426            'user_id',
427        );
428
429        $comment = array_intersect_key( $comment->to_array(), array_flip( $allowed_keys ) );
430
431        $comment_meta = get_comment_meta( $comment['comment_ID'] );
432
433        return array(
434            'comment' => $comment,
435            'meta'    => is_array( $comment_meta ) ? $comment_meta : array(),
436        );
437    }
438
439    /**
440     * Fetch a backup of a post, along with all of its metadata.
441     *
442     * @access public
443     * @static
444     *
445     * @param WP_REST_Request $request The request sent to the WP REST API.
446     *
447     * @return array
448     */
449    public static function fetch_post_backup( $request ) {
450        global $wpdb;
451
452        // Disable Sync as this is a read-only operation and triggered by sync activity.
453        Sync_Actions::mark_sync_read_only();
454
455        $post_id = $request['id'];
456        $post    = get_post( $post_id );
457
458        if ( empty( $post ) ) {
459            return new WP_Error( 'post_not_found', __( 'Post not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
460        }
461
462        // Fetch terms associated with this post object.
463        $terms = $wpdb->get_results(
464            $wpdb->prepare(
465                "SELECT term_taxonomy_id, term_order FROM {$wpdb->term_relationships} WHERE object_id = %d;",
466                $post->ID
467            )
468        );
469
470        return array(
471            'post'  => (array) $post,
472            'meta'  => get_post_meta( $post->ID ),
473            'terms' => (array) $terms,
474        );
475    }
476
477    /**
478     * Fetch a backup of a term, along with all of its metadata.
479     *
480     * @access public
481     * @static
482     *
483     * @param WP_REST_Request $request The request sent to the WP REST API.
484     *
485     * @return array
486     */
487    public static function fetch_term_backup( $request ) {
488        // Disable Sync as this is a read-only operation and triggered by sync activity.
489        Sync_Actions::mark_sync_read_only();
490
491        $term_id = $request['id'];
492        $term    = get_term( $term_id );
493
494        if ( empty( $term ) ) {
495            return new WP_Error( 'term_not_found', __( 'Term not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
496        }
497
498        return array(
499            'term' => (array) $term,
500            'meta' => get_term_meta( $term_id ),
501        );
502    }
503
504    /**
505     * Fetch a backup of a user, along with all of its metadata.
506     *
507     * @access public
508     * @static
509     *
510     * @param WP_REST_Request $request The request sent to the WP REST API.
511     * @return array
512     */
513    public static function fetch_user_backup( $request ) {
514        // Disable Sync as this is a read-only operation and triggered by sync activity.
515        Sync_Actions::mark_sync_read_only();
516
517        $user_id = $request['id'];
518        $user    = get_user_by( 'id', $user_id );
519
520        if ( empty( $user ) ) {
521            return new WP_Error( 'user_not_found', __( 'User not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
522        }
523
524        return array(
525            'user' => $user->to_array(),
526            'meta' => get_user_meta( $user->ID ),
527        );
528    }
529
530    /**
531     * Get allowed object types for the '/database-object/backup' endpoint.
532     *
533     * @access private
534     * @static
535     *
536     * @return array
537     */
538    private static function get_allowed_object_types() {
539        return array(
540            'woocommerce_attribute'                       => array(
541                'table'    => 'woocommerce_attribute_taxonomies',
542                'id_field' => 'attribute_id',
543            ),
544            'woocommerce_downloadable_product_permission' => array(
545                'table'    => 'woocommerce_downloadable_product_permissions',
546                'id_field' => 'permission_id',
547            ),
548            'woocommerce_order_item'                      => array(
549                'table'     => 'woocommerce_order_items',
550                'id_field'  => 'order_item_id',
551                'meta_type' => 'order_item',
552            ),
553            'woocommerce_payment_token'                   => array(
554                'table'     => 'woocommerce_payment_tokens',
555                'id_field'  => 'token_id',
556                'meta_type' => 'payment_token',
557            ),
558            'woocommerce_tax_rate'                        => array(
559                'table'          => 'woocommerce_tax_rates',
560                'id_field'       => 'tax_rate_id',
561                'child_table'    => 'woocommerce_tax_rate_locations',
562                'child_id_field' => 'tax_rate_id',
563            ),
564            'woocommerce_webhook'                         => array(
565                'table'    => 'wc_webhooks',
566                'id_field' => 'webhook_id',
567            ),
568        );
569    }
570
571    /**
572     * This will fetch the last rewindable event from the Activity Log and
573     * the last rewind_id prior to that.
574     */
575    public static function get_site_backup_undo_event() {
576        $blog_id = Jetpack_Options::get_option( 'id' );
577
578        $response = Client::wpcom_json_api_request_as_user(
579            '/sites/' . $blog_id . '/activity?force=wpcom',
580            'v2',
581            array(),
582            null,
583            'wpcom'
584        );
585
586        // Cast: `wp_remote_retrieve_response_code()` hands back whatever the
587        // transport put there, and a numeric-string `'200'` fails this
588        // strict comparison — so a perfectly good answer is discarded and
589        // the route reports that the site has no rewindable event to undo.
590        if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
591            return null;
592        }
593
594        $body = json_decode( $response['body'], true );
595
596        if ( ! isset( $body['current'] ) ) {
597            return null;
598        }
599
600        if ( ! isset( $body['current']['orderedItems'] ) ) {
601            return null;
602        }
603
604        // Preparing the response structure
605        $undo_event = array(
606            'last_rewindable_event' => null,
607            'undo_backup_id'        => null,
608        );
609
610        // List of events that will not be considered to be undo.
611        // Basically we should not `undo` a full backup event, but we could
612        // use them to undo any other action like plugin updates.
613        $last_event_exceptions = array(
614            'rewind__backup_only_complete_full',
615            'rewind__backup_only_complete_initial',
616            'rewind__backup_only_complete',
617            'rewind__backup_complete_full',
618            'rewind__backup_complete_initial',
619            'rewind__backup_complete',
620        );
621
622        // Looping through the events to find the last rewindable event and the last backup_id.
623        // The idea is to find the last rewindable event and then the last rewind_id before that.
624        $found_last_event = false;
625        foreach ( $body['current']['orderedItems'] as $event ) {
626            if ( $event['is_rewindable'] ) {
627                if ( ! $found_last_event && ! in_array( $event['name'], $last_event_exceptions, true ) ) {
628                    $undo_event['last_rewindable_event'] = $event;
629                    $found_last_event                    = true;
630                } elseif ( $found_last_event ) {
631                    $undo_event['undo_backup_id'] = $event['rewind_id'];
632                    break;
633                }
634            }
635        }
636
637        // Ensure that we have a rewindable event and a backup_id to undo.
638        if ( $undo_event['last_rewindable_event'] === null || $undo_event['undo_backup_id'] === null ) {
639            return null;
640        }
641
642        return rest_ensure_response( $undo_event );
643    }
644
645    /**
646     * Fetch a backup of a order, along with all of its data.
647     *
648     * @access public
649     * @static
650     *
651     * @param WP_REST_Request $request The request sent to the WP REST API.
652     *
653     * @return array
654     */
655    public static function fetch_wc_orders_backup( $request ) {
656        global $wpdb;
657
658        // Disable Sync as this is a read-only operation and triggered by sync activity.
659        Sync_Actions::mark_sync_read_only();
660
661        $order_id = $request['id'];
662
663        $order                  = array();
664        $order_addresses        = array();
665        $order_operational_data = array();
666        $order_meta             = array();
667
668        if ( ! class_exists( OrdersTableDataStore::class ) ) {
669            return new WP_Error( 'order_not_allowed', __( 'Not allowed to get the order with current configuration', 'jetpack-backup-pkg' ), array( 'status' => 403 ) );
670        }
671
672        if ( method_exists( OrdersTableDataStore::class, 'get_orders_table_name' ) ) {
673            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
674            $order = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_orders_table_name() . '` WHERE id = %s', $order_id ) );
675        }
676
677        if ( empty( $order ) ) {
678            // No order in HPOS
679            return new WP_Error( 'order_not_found', __( 'Order not found ', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
680        }
681
682        if ( method_exists( OrdersTableDataStore::class, 'get_addresses_table_name' ) ) {
683            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
684            $order_addresses = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_addresses_table_name() . '` WHERE order_id = %s', $order_id ) );
685        }
686
687        if ( method_exists( OrdersTableDataStore::class, 'get_operational_data_table_name' ) ) {
688            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
689            $order_operational_data = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_operational_data_table_name() . '` WHERE order_id = %s', $order_id ) );
690        }
691
692        if ( method_exists( OrdersTableDataStore::class, 'get_meta_table_name' ) ) {
693            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
694            $order_meta = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_meta_table_name() . '` WHERE order_id = %s', $order_id ) );
695        }
696
697        return array(
698            'order'                  => (array) $order,
699            'order_addresses'        => (array) $order_addresses,
700            'order_operational_data' => (array) $order_operational_data,
701            'order_meta'             => (array) $order_meta,
702        );
703    }
704
705    /**
706     * Fetch backup preflight status
707     *
708     * The `array` this used to advertise was never a shape it could return;
709     * both branches below hand back an object. Corrected because Phan reads
710     * it, and a caller that believed it would be calling array offsets on a
711     * `WP_REST_Response`.
712     *
713     * @return \WP_REST_Response|WP_Error The preflight payload, or a WP_Error if WordPress.com refused or could not be reached.
714     */
715    public static function get_site_backup_preflight() {
716        $blog_id = Jetpack_Options::get_option( 'id' );
717
718        $response = Client::wpcom_json_api_request_as_user(
719            '/sites/' . $blog_id . '/rewind/preflight?force=wpcom',
720            'v2',
721            array(),
722            null,
723            'wpcom'
724        );
725
726        if ( is_wp_error( $response ) ) {
727            return new WP_Error(
728                'wp_error_fetch_preflight',
729                $response->get_error_message(),
730                array( 'status' => 500 )
731            );
732        }
733
734        // Cast and then clamp, and this route needs both more than any
735        // other in the package. `wp_remote_retrieve_response_code()` hands
736        // back whatever the transport put there, so an uncast `'200'` fails
737        // the comparison below — and this is the one place that then
738        // forwards the status it just read straight into `data.status`.
739        // WordPress runs that through `absint()`, so the error envelope is
740        // served as HTTP 200: `apiFetch` resolves, nothing throws, and a
741        // failure arrives at the caller looking like a successful preflight.
742        //
743        // The clamp covers what the cast cannot. `(int)` is total, so an
744        // absent or unparseable code becomes `0` and `'2 Bad'` becomes `2`,
745        // and neither is a status `status_header()` can emit. The same
746        // reasoning, written out at length, is on
747        // `REST\Rest_Controller::upstream_error()`; it is open-coded here
748        // rather than borrowed because that helper also attaches
749        // WordPress.com's own reason under a `wpcom` key, which would change
750        // this route's response shape for callers we do not control.
751        $response_code = (int) wp_remote_retrieve_response_code( $response );
752        if ( 200 !== $response_code ) {
753            return new WP_Error(
754                'http_error_fetch_preflight',
755                wp_remote_retrieve_response_message( $response ),
756                array( 'status' => $response_code >= 400 && $response_code <= 599 ? $response_code : 500 )
757            );
758        }
759
760        $body = json_decode( $response['body'], true );
761        return rest_ensure_response( $body );
762    }
763
764    /**
765     * Fetch option row by option name.
766     *
767     * @access private
768     * @static
769     *
770     * @param string $name The option name.
771     * @return object|null Database query result as object format specified or null on failure.
772     */
773    private static function get_option_row( $name ) {
774        global $wpdb;
775        return $wpdb->get_row( $wpdb->prepare( "select * from `{$wpdb->options}` where option_name = %s", $name ) );
776    }
777}