← All changes
|
jetpack_vendor/automattic/jetpack-backup/src/class-rest-controller.php
+197
-3
13.0.2
→
16.3-a.7
View file →
| @@ -9,18 +9,18 @@ | ||
| 9 | 9 | // After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in |
| 10 | 10 | // order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide |
| 11 | 11 | // to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin |
| 12 | 12 | // are installed, or in some other cases). |
| 13 | -namespace Automattic\Jetpack\Backup\V0001; | |
| 13 | +namespace Automattic\Jetpack\Backup\V0005; | |
| 14 | 14 | |
| 15 | 15 | use Automattic\Jetpack\Connection\Client; |
| 16 | 16 | use Automattic\Jetpack\Connection\Rest_Authentication; |
| 17 | 17 | use Automattic\Jetpack\Sync\Actions as Sync_Actions; |
| 18 | +use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; | |
| 18 | 19 | use Jetpack_Options; |
| 19 | 20 | use WP_Error; |
| 20 | 21 | use WP_REST_Request; |
| 21 | 22 | use WP_REST_Server; |
| 22 | -// phpcs:ignore WordPress.Utils.I18nTextDomainFixer.MissingArgs | |
| 23 | 23 | use function esc_html__; |
| 24 | 24 | use function get_comment; |
| 25 | 25 | use function get_comment_meta; |
| 26 | 26 | use function get_metadata; |
| @@ -33,9 +33,11 @@ | ||
| 33 | 33 | use function is_wp_error; |
| 34 | 34 | use function register_rest_route; |
| 35 | 35 | use function rest_authorization_required_code; |
| 36 | 36 | use function rest_ensure_response; |
| 37 | +use function wp_cache_flush; | |
| 37 | 38 | use function wp_remote_retrieve_response_code; |
| 39 | +use function wp_using_ext_object_cache; | |
| 38 | 40 | |
| 39 | 41 | /** |
| 40 | 42 | * Registers the REST routes for Backup. |
| 41 | 43 | */ |
| @@ -208,8 +210,41 @@ | ||
| 208 | 210 | 'callback' => __CLASS__ . '::get_site_backup_undo_event', |
| 209 | 211 | 'permission_callback' => __NAMESPACE__ . '\Jetpack_Backup::backups_permissions_callback', |
| 210 | 212 | ) |
| 211 | 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 | + ); | |
| 212 | 247 | } |
| 213 | 248 | |
| 214 | 249 | /** |
| 215 | 250 | * The Backup endpoints should only be available via site-level authentication. |
| @@ -560,9 +595,13 @@ | ||
| 560 | 595 | null, |
| 561 | 596 | 'wpcom' |
| 562 | 597 | ); |
| 563 | 598 | |
| 564 | - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { | |
| 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 ) ) { | |
| 565 | 604 | return null; |
| 566 | 605 | } |
| 567 | 606 | |
| 568 | 607 | $body = json_decode( $response['body'], true ); |
| @@ -613,8 +652,163 @@ | ||
| 613 | 652 | return null; |
| 614 | 653 | } |
| 615 | 654 | |
| 616 | 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 ) ); | |
| 617 | 811 | } |
| 618 | 812 | |
| 619 | 813 | /** |
| 620 | 814 | * Fetch option row by option name. |