PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 All 504 releases
jetpack / jetpack_vendor / automattic / jetpack-backup / src / class-rest-controller.php

class-rest-controller.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.1, at jetpack_vendor/automattic/jetpack-backup/src/class-rest-controller.php

827 lines 26.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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).
13 namespace Automattic\Jetpack\Backup\V0005;
14
15 use Automattic\Jetpack\Connection\Client;
16 use Automattic\Jetpack\Connection\Rest_Authentication;
17 use Automattic\Jetpack\Sync\Actions as Sync_Actions;
18 use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
19 use Jetpack_Options;
20 use WP_Error;
21 use WP_REST_Request;
22 use WP_REST_Server;
23 use function esc_html__;
24 use function get_comment;
25 use function get_comment_meta;
26 use function get_metadata;
27 use function get_post;
28 use function get_post_meta;
29 use function get_term;
30 use function get_term_meta;
31 use function get_user_by;
32 use function get_user_meta;
33 use function is_wp_error;
34 use function register_rest_route;
35 use function rest_authorization_required_code;
36 use function rest_ensure_response;
37 use function wp_cache_flush;
38 use function wp_remote_retrieve_response_code;
39 use function wp_using_ext_object_cache;
40
41 /**
42 * Registers the REST routes for Backup.
43 */
44 class 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 }
827