← All changes
|
jetpack_vendor/automattic/jetpack-backup/src/class-rest-controller.php
+319
-19
12.0.3
→
16.3-a.5
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. |