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