PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-a.5 16.3-a.7 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 All 506 releases
← All changes | jetpack_vendor/automattic/jetpack-backup/src/class-rest-controller.php +319 -19 12.3.2 → 16.3-a.7 View file →
@@ -5,15 +5,39 @@
5 5 *
6 6 * @package automattic/jetpack-backup
7 7 */
8 8
9 -namespace Automattic\Jetpack\Backup;
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;
10 14
15 +use Automattic\Jetpack\Connection\Client;
11 16 use Automattic\Jetpack\Connection\Rest_Authentication;
12 17 use Automattic\Jetpack\Sync\Actions as Sync_Actions;
18 +use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
19 +use Jetpack_Options;
13 20 use WP_Error;
14 21 use WP_REST_Request;
15 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;
16 40
17 41 /**
18 42 * Registers the REST routes for Backup.
19 43 */
@@ -175,8 +199,52 @@
175 199 'callback' => __CLASS__ . '::fetch_user_backup',
176 200 'permission_callback' => __CLASS__ . '::backup_permissions_callback',
177 201 )
178 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 + );
179 247 }
180 248
181 249 /**
182 250 * The Backup endpoints should only be available via site-level authentication.
@@ -206,14 +274,16 @@
206 274 * @access public
207 275 * @static
208 276 *
209 277 * @param WP_REST_Request $request The request sent to the WP REST API.
210 - * @return array|WP_Error Returns the result of Helper Script installation. Returns one of:
211 - * - WP_Error on failure, or
212 - * - An array with installation info on success:
213 - * 'path' (string) The sinstallation path.
214 - * 'url' (string) The access url.
215 - * 'abspath' (string) The abspath.
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.
216 286 */
217 287 public static function install_backup_helper_script( $request ) {
218 288 $helper_script = $request->get_param( 'helper' );
219 289
@@ -225,13 +295,8 @@
225 295
226 296 $installation_info = Helper_Script_Manager::install_helper_script( $helper_script );
227 297 Helper_Script_Manager::cleanup_expired_helper_scripts();
228 298
229 - // Include ABSPATH with successful result.
230 - if ( ! is_wp_error( $installation_info ) ) {
231 - $installation_info['abspath'] = ABSPATH;
232 - }
233 -
234 299 return rest_ensure_response( $installation_info );
235 300 }
236 301
237 302 /**
@@ -240,21 +305,22 @@
240 305 * @access public
241 306 * @static
242 307 *
243 308 * @param WP_REST_Request $request The request sent to the WP REST API.
244 - * @return array An array with 'success' key indicating the result of the delete operation.
309 + *
310 + * @return array|WP_Error An array with 'success' key, or an instance of WP_Error on failure.
245 311 */
246 312 public static function delete_backup_helper_script( $request ) {
247 313 $path_to_helper_script = $request->get_param( 'path' );
248 314
249 - $deleted = Helper_Script_Manager::delete_helper_script( $path_to_helper_script );
315 + $delete_result = Helper_Script_Manager::delete_helper_script( $path_to_helper_script );
250 316 Helper_Script_Manager::cleanup_expired_helper_scripts();
251 317
252 - return rest_ensure_response(
253 - array(
254 - 'success' => $deleted,
255 - )
256 - );
318 + if ( is_wp_error( $delete_result ) ) {
319 + return $delete_result;
320 + }
321 +
322 + return rest_ensure_response( array( 'success' => true ) );
257 323 }
258 324
259 325 /**
260 326 * Fetch a backup of a database object, along with all of its metadata.
@@ -262,8 +328,9 @@
262 328 * @access public
263 329 * @static
264 330 *
265 331 * @param WP_REST_Request $request The request sent to the WP REST API.
332 + *
266 333 * @return array
267 334 */
268 335 public static function fetch_database_object_backup( $request ) {
269 336 global $wpdb;
@@ -319,8 +386,9 @@
319 386 * @access public
320 387 * @static
321 388 *
322 389 * @param WP_REST_Request $request The request sent to the WP REST API.
390 + *
323 391 * @return array
324 392 */
325 393 public static function fetch_options_backup( $request ) {
326 394 // Disable Sync as this is a read-only operation and triggered by sync activity.
@@ -338,8 +406,9 @@
338 406 * @access public
339 407 * @static
340 408 *
341 409 * @param WP_REST_Request $request The request sent to the WP REST API.
410 + *
342 411 * @return array
343 412 */
344 413 public static function fetch_comment_backup( $request ) {
345 414 // Disable Sync as this is a read-only operation and triggered by sync activity.
@@ -386,8 +455,9 @@
386 455 * @access public
387 456 * @static
388 457 *
389 458 * @param WP_REST_Request $request The request sent to the WP REST API.
459 + *
390 460 * @return array
391 461 */
392 462 public static function fetch_post_backup( $request ) {
393 463 global $wpdb;
@@ -423,8 +493,9 @@
423 493 * @access public
424 494 * @static
425 495 *
426 496 * @param WP_REST_Request $request The request sent to the WP REST API.
497 + *
427 498 * @return array
428 499 */
429 500 public static function fetch_term_backup( $request ) {
430 501 // Disable Sync as this is a read-only operation and triggered by sync activity.
@@ -507,8 +578,237 @@
507 578 'table' => 'wc_webhooks',
508 579 'id_field' => 'webhook_id',
509 580 ),
510 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 ) );
511 811 }
512 812
513 813 /**
514 814 * Fetch option row by option name.