Edit file File name : class-rest-controller.php Content :<?php /** * The Backup Rest Controller class. * Registers the REST routes for Backup. * * @package automattic/jetpack-backup */ // After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in // order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide // to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin // are installed, or in some other cases). namespace Automattic\Jetpack\Backup\V0004; use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Connection\Rest_Authentication; use Automattic\Jetpack\Sync\Actions as Sync_Actions; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; use Jetpack_Options; use WP_Error; use WP_REST_Request; use WP_REST_Server; // phpcs:ignore WordPress.Utils.I18nTextDomainFixer.MissingArgs use function esc_html__; use function get_comment; use function get_comment_meta; use function get_metadata; use function get_post; use function get_post_meta; use function get_term; use function get_term_meta; use function get_user_by; use function get_user_meta; use function is_wp_error; use function register_rest_route; use function rest_authorization_required_code; use function rest_ensure_response; use function wp_remote_retrieve_response_code; /** * Registers the REST routes for Backup. */ class REST_Controller { /** * Registers the REST routes for Backup. * * @access public * @static */ public static function register_rest_routes() { // Install a Helper Script to assist Jetpack Backup fetch data. register_rest_route( 'jetpack/v4', '/backup-helper-script', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => __CLASS__ . '::install_backup_helper_script', 'permission_callback' => __CLASS__ . '::backup_permissions_callback', 'args' => array( 'helper' => array( 'description' => __( 'base64 encoded Backup Helper Script body.', 'jetpack-backup-pkg' ), 'type' => 'string', 'required' => true, ), ), ) ); // Delete a Backup Helper Script. register_rest_route( 'jetpack/v4', '/backup-helper-script', array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => __CLASS__ . '::delete_backup_helper_script', 'permission_callback' => __CLASS__ . '::backup_permissions_callback', 'args' => array( 'path' => array( 'description' => __( 'Path to Backup Helper Script', 'jetpack-backup-pkg' ), 'type' => 'string', 'required' => true, ), ), ) ); // Fetch a backup of a database object, along with all of its metadata. register_rest_route( 'jetpack/v4', '/database-object/backup', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::fetch_database_object_backup', 'permission_callback' => __CLASS__ . '::backup_permissions_callback', 'args' => array( 'object_type' => array( 'description' => __( 'Type of object to fetch from the database', 'jetpack-backup-pkg' ), 'required' => true, 'validate_callback' => function ( $value ) { if ( ! is_string( $value ) ) { return new WP_Error( 'rest_invalid_param', __( 'The object_type argument must be a non-empty string.', 'jetpack-backup-pkg' ), array( 'status' => 400 ) ); } $allowed_object_types = array_keys( self::get_allowed_object_types() ); if ( ! in_array( $value, $allowed_object_types, true ) ) { return new WP_Error( 'rest_invalid_param', sprintf( /* translators: %s: comma-separated list of allowed object types */ __( 'The object_type argument should be one of %s', 'jetpack-backup-pkg' ), implode( ', ', $allowed_object_types ) ), array( 'status' => 400 ) ); } return true; }, ), 'object_id' => array( 'description' => __( 'ID of the database object to fetch', 'jetpack-backup-pkg' ), 'type' => 'integer', 'required' => true, ), ), ) ); // Fetch a backup of an option. register_rest_route( 'jetpack/v4', '/options/backup', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::fetch_options_backup', 'permission_callback' => __CLASS__ . '::backup_permissions_callback', 'args' => array( 'name' => array( 'description' => __( 'One or more option names to include in the backup', 'jetpack-backup-pkg' ), 'validate_callback' => function ( $value ) { $is_valid = is_array( $value ) || is_string( $value ); if ( ! $is_valid ) { 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 ) ); } return true; }, 'required' => true, ), ), ) ); // Fetch a backup of a comment, along with all of its metadata. register_rest_route( 'jetpack/v4', '/comments/(?P<id>\d+)/backup', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::fetch_comment_backup', 'permission_callback' => __CLASS__ . '::backup_permissions_callback', ) ); // Fetch a backup of a post, along with all of its metadata. register_rest_route( 'jetpack/v4', '/posts/(?P<id>\d+)/backup', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::fetch_post_backup', 'permission_callback' => __CLASS__ . '::backup_permissions_callback', ) ); // Fetch a backup of a term, along with all of its metadata. register_rest_route( 'jetpack/v4', '/terms/(?P<id>\d+)/backup', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::fetch_term_backup', 'permission_callback' => __CLASS__ . '::backup_permissions_callback', ) ); // Fetch a backup of a user, along with all of its metadata. register_rest_route( 'jetpack/v4', '/users/(?P<id>\d+)/backup', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::fetch_user_backup', 'permission_callback' => __CLASS__ . '::backup_permissions_callback', ) ); // Get backup undo event register_rest_route( 'jetpack/v4', '/site/backup/undo-event', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::get_site_backup_undo_event', 'permission_callback' => __NAMESPACE__ . '\Jetpack_Backup::backups_permissions_callback', ) ); // Fetch a backup of a wc_order along with all of its data. register_rest_route( 'jetpack/v4', '/orders/(?P<id>\d+)/backup', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::fetch_wc_orders_backup', 'permission_callback' => __CLASS__ . '::backup_permissions_callback', ) ); // Fetch backup preflight status register_rest_route( 'jetpack/v4', '/site/backup/preflight', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::get_site_backup_preflight', 'permission_callback' => __NAMESPACE__ . '\Jetpack_Backup::backups_permissions_callback', ) ); } /** * The Backup endpoints should only be available via site-level authentication. * This means that the corresponding endpoints can only be accessible from WPCOM. * * @access public * @static * * @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise. */ public static function backup_permissions_callback() { if ( Rest_Authentication::is_signed_with_blog_token() ) { return true; } $error_msg = esc_html__( 'You are not allowed to perform this action.', 'jetpack-backup-pkg' ); return new WP_Error( 'rest_forbidden', $error_msg, array( 'status' => rest_authorization_required_code() ) ); } /** * Install the Backup Helper Script. * * @access public * @static * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return array|WP_Error Array with installation info on success: * * 'path' (string) Helper script installation path on the filesystem. * 'url' (string) URL to the helper script. * 'abspath' (string) WordPress root. * * or an instance of WP_Error on failure. */ public static function install_backup_helper_script( $request ) { $helper_script = $request->get_param( 'helper' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $helper_script = base64_decode( $helper_script ); if ( ! $helper_script ) { return new WP_Error( 'invalid_args', __( 'Helper script body must be base64 encoded', 'jetpack-backup-pkg' ), 400 ); } $installation_info = Helper_Script_Manager::install_helper_script( $helper_script ); Helper_Script_Manager::cleanup_expired_helper_scripts(); return rest_ensure_response( $installation_info ); } /** * Delete a Backup Helper Script. * * @access public * @static * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return array|WP_Error An array with 'success' key, or an instance of WP_Error on failure. */ public static function delete_backup_helper_script( $request ) { $path_to_helper_script = $request->get_param( 'path' ); $delete_result = Helper_Script_Manager::delete_helper_script( $path_to_helper_script ); Helper_Script_Manager::cleanup_expired_helper_scripts(); if ( is_wp_error( $delete_result ) ) { return $delete_result; } return rest_ensure_response( array( 'success' => true ) ); } /** * Fetch a backup of a database object, along with all of its metadata. * * @access public * @static * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return array */ public static function fetch_database_object_backup( $request ) { global $wpdb; // Disable Sync as this is a read-only operation and triggered by sync activity. Sync_Actions::mark_sync_read_only(); $allowed_object_types = self::get_allowed_object_types(); // Safe to do this as we have already validated the object_type key exists in self::get_allowed_object_types(). $object_type = $allowed_object_types[ $request->get_param( 'object_type' ) ]; $object_id = $request->get_param( 'object_id' ); $table = $wpdb->prefix . $object_type['table']; $id_field = $object_type['id_field']; // Fetch the requested object. $object = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM `' . $table . '` WHERE `' . $id_field . '` = %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $object_id ) ); if ( empty( $object ) ) { return new WP_Error( 'object_not_found', __( 'Object not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) ); } $result = array( 'object' => $object ); // Fetch associated metadata (if this object type has any). if ( ! empty( $object_type['meta_type'] ) ) { $result['meta'] = get_metadata( $object_type['meta_type'], $object_id ); } // If there is a child linked table (eg: woocommerce_tax_rate_locations), fetch linked records. if ( ! empty( $object_type['child_table'] ) ) { $child_table = $wpdb->prefix . $object_type['child_table']; $child_id_field = $object_type['child_id_field']; $result['children'] = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . $child_table . '` where `' . $child_id_field . '` = %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $object_id ) ); } return $result; } /** * Fetch a backup of an option. * * @access public * @static * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return array */ public static function fetch_options_backup( $request ) { // Disable Sync as this is a read-only operation and triggered by sync activity. Sync_Actions::mark_sync_read_only(); $option_names = (array) $request->get_param( 'name' ); $options = array_map( self::class . '::get_option_row', $option_names ); return array( 'options' => $options ); } /** * Fetch a backup of a comment, along with all of its metadata. * * @access public * @static * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return array */ public static function fetch_comment_backup( $request ) { // Disable Sync as this is a read-only operation and triggered by sync activity. Sync_Actions::mark_sync_read_only(); $comment_id = $request['id']; $comment = get_comment( $comment_id ); if ( empty( $comment ) ) { return new WP_Error( 'comment_not_found', __( 'Comment not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) ); } $allowed_keys = array( 'comment_ID', 'comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_author_IP', 'comment_date', 'comment_date_gmt', 'comment_content', 'comment_karma', 'comment_approved', 'comment_agent', 'comment_type', 'comment_parent', 'user_id', ); $comment = array_intersect_key( $comment->to_array(), array_flip( $allowed_keys ) ); $comment_meta = get_comment_meta( $comment['comment_ID'] ); return array( 'comment' => $comment, 'meta' => is_array( $comment_meta ) ? $comment_meta : array(), ); } /** * Fetch a backup of a post, along with all of its metadata. * * @access public * @static * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return array */ public static function fetch_post_backup( $request ) { global $wpdb; // Disable Sync as this is a read-only operation and triggered by sync activity. Sync_Actions::mark_sync_read_only(); $post_id = $request['id']; $post = get_post( $post_id ); if ( empty( $post ) ) { return new WP_Error( 'post_not_found', __( 'Post not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) ); } // Fetch terms associated with this post object. $terms = $wpdb->get_results( $wpdb->prepare( "SELECT term_taxonomy_id, term_order FROM {$wpdb->term_relationships} WHERE object_id = %d;", $post->ID ) ); return array( 'post' => (array) $post, 'meta' => get_post_meta( $post->ID ), 'terms' => (array) $terms, ); } /** * Fetch a backup of a term, along with all of its metadata. * * @access public * @static * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return array */ public static function fetch_term_backup( $request ) { // Disable Sync as this is a read-only operation and triggered by sync activity. Sync_Actions::mark_sync_read_only(); $term_id = $request['id']; $term = get_term( $term_id ); if ( empty( $term ) ) { return new WP_Error( 'term_not_found', __( 'Term not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) ); } return array( 'term' => (array) $term, 'meta' => get_term_meta( $term_id ), ); } /** * Fetch a backup of a user, along with all of its metadata. * * @access public * @static * * @param WP_REST_Request $request The request sent to the WP REST API. * @return array */ public static function fetch_user_backup( $request ) { // Disable Sync as this is a read-only operation and triggered by sync activity. Sync_Actions::mark_sync_read_only(); $user_id = $request['id']; $user = get_user_by( 'id', $user_id ); if ( empty( $user ) ) { return new WP_Error( 'user_not_found', __( 'User not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) ); } return array( 'user' => $user->to_array(), 'meta' => get_user_meta( $user->ID ), ); } /** * Get allowed object types for the '/database-object/backup' endpoint. * * @access private * @static * * @return array */ private static function get_allowed_object_types() { return array( 'woocommerce_attribute' => array( 'table' => 'woocommerce_attribute_taxonomies', 'id_field' => 'attribute_id', ), 'woocommerce_downloadable_product_permission' => array( 'table' => 'woocommerce_downloadable_product_permissions', 'id_field' => 'permission_id', ), 'woocommerce_order_item' => array( 'table' => 'woocommerce_order_items', 'id_field' => 'order_item_id', 'meta_type' => 'order_item', ), 'woocommerce_payment_token' => array( 'table' => 'woocommerce_payment_tokens', 'id_field' => 'token_id', 'meta_type' => 'payment_token', ), 'woocommerce_tax_rate' => array( 'table' => 'woocommerce_tax_rates', 'id_field' => 'tax_rate_id', 'child_table' => 'woocommerce_tax_rate_locations', 'child_id_field' => 'tax_rate_id', ), 'woocommerce_webhook' => array( 'table' => 'wc_webhooks', 'id_field' => 'webhook_id', ), ); } /** * This will fetch the last rewindable event from the Activity Log and * the last rewind_id prior to that. */ public static function get_site_backup_undo_event() { $blog_id = Jetpack_Options::get_option( 'id' ); $response = Client::wpcom_json_api_request_as_user( '/sites/' . $blog_id . '/activity?force=wpcom', 'v2', array(), null, 'wpcom' ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return null; } $body = json_decode( $response['body'], true ); if ( ! isset( $body['current'] ) ) { return null; } if ( ! isset( $body['current']['orderedItems'] ) ) { return null; } // Preparing the response structure $undo_event = array( 'last_rewindable_event' => null, 'undo_backup_id' => null, ); // List of events that will not be considered to be undo. // Basically we should not `undo` a full backup event, but we could // use them to undo any other action like plugin updates. $last_event_exceptions = array( 'rewind__backup_only_complete_full', 'rewind__backup_only_complete_initial', 'rewind__backup_only_complete', 'rewind__backup_complete_full', 'rewind__backup_complete_initial', 'rewind__backup_complete', ); // Looping through the events to find the last rewindable event and the last backup_id. // The idea is to find the last rewindable event and then the last rewind_id before that. $found_last_event = false; foreach ( $body['current']['orderedItems'] as $event ) { if ( $event['is_rewindable'] ) { if ( ! $found_last_event && ! in_array( $event['name'], $last_event_exceptions, true ) ) { $undo_event['last_rewindable_event'] = $event; $found_last_event = true; } elseif ( $found_last_event ) { $undo_event['undo_backup_id'] = $event['rewind_id']; break; } } } // Ensure that we have a rewindable event and a backup_id to undo. if ( $undo_event['last_rewindable_event'] === null || $undo_event['undo_backup_id'] === null ) { return null; } return rest_ensure_response( $undo_event ); } /** * Fetch a backup of a order, along with all of its data. * * @access public * @static * * @param WP_REST_Request $request The request sent to the WP REST API. * * @return array */ public static function fetch_wc_orders_backup( $request ) { global $wpdb; // Disable Sync as this is a read-only operation and triggered by sync activity. Sync_Actions::mark_sync_read_only(); $order_id = $request['id']; $order = array(); $order_addresses = array(); $order_operational_data = array(); $order_meta = array(); if ( ! class_exists( OrdersTableDataStore::class ) ) { return new WP_Error( 'order_not_allowed', __( 'Not allowed to get the order with current configuration', 'jetpack-backup-pkg' ), array( 'status' => 403 ) ); } if ( method_exists( OrdersTableDataStore::class, 'get_orders_table_name' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared $order = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_orders_table_name() . '` WHERE id = %s', $order_id ) ); } if ( empty( $order ) ) { // No order in HPOS return new WP_Error( 'order_not_found', __( 'Order not found ', 'jetpack-backup-pkg' ), array( 'status' => 404 ) ); } if ( method_exists( OrdersTableDataStore::class, 'get_addresses_table_name' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared $order_addresses = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_addresses_table_name() . '` WHERE order_id = %s', $order_id ) ); } if ( method_exists( OrdersTableDataStore::class, 'get_operational_data_table_name' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared $order_operational_data = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_operational_data_table_name() . '` WHERE order_id = %s', $order_id ) ); } if ( method_exists( OrdersTableDataStore::class, 'get_meta_table_name' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared $order_meta = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_meta_table_name() . '` WHERE order_id = %s', $order_id ) ); } return array( 'order' => (array) $order, 'order_addresses' => (array) $order_addresses, 'order_operational_data' => (array) $order_operational_data, 'order_meta' => (array) $order_meta, ); } /** * Fetch backup preflight status * * @return array */ public static function get_site_backup_preflight() { $blog_id = Jetpack_Options::get_option( 'id' ); $response = Client::wpcom_json_api_request_as_user( '/sites/' . $blog_id . '/rewind/preflight?force=wpcom', 'v2', array(), null, 'wpcom' ); if ( is_wp_error( $response ) ) { return new WP_Error( 'wp_error_fetch_preflight', $response->get_error_message(), array( 'status' => 500 ) ); } $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $response_code ) { return new WP_Error( 'http_error_fetch_preflight', wp_remote_retrieve_response_message( $response ), array( 'status' => $response_code ) ); } $body = json_decode( $response['body'], true ); return rest_ensure_response( $body ); } /** * Fetch option row by option name. * * @access private * @static * * @param string $name The option name. * @return object|null Database query result as object format specified or null on failure. */ private static function get_option_row( $name ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "select * from `{$wpdb->options}` where option_name = %s", $name ) ); } } Save