diff options
Diffstat (limited to 'plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php')
-rw-r--r-- | plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php | 431 |
1 files changed, 428 insertions, 3 deletions
diff --git a/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php b/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php index 70f8ed62..6af6a1ef 100644 --- a/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php +++ b/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php @@ -17,6 +17,9 @@ require_once ABSPATH . '/wp-includes/class-wp-error.php'; // Register endpoints when WP REST API is initialized. add_action( 'rest_api_init', array( 'Jetpack_Core_Json_Api_Endpoints', 'register_endpoints' ) ); +// Load API endpoints that are synced with WP.com +// Each of these is a class that will register its own routes on 'rest_api_init'. +require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/load-wpcom-endpoints.php'; /** * Class Jetpack_Core_Json_Api_Endpoints @@ -102,6 +105,13 @@ class Jetpack_Core_Json_Api_Endpoints { 'callback' => __CLASS__ . '::jetpack_connection_status', ) ); + // Test current connection status of Jetpack + register_rest_route( 'jetpack/v4', '/connection/test', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => __CLASS__ . '::jetpack_connection_test', + 'permission_callback' => __CLASS__ . '::manage_modules_permission_check', + ) ); + register_rest_route( 'jetpack/v4', '/rewind', array( 'methods' => WP_REST_Server::READABLE, 'callback' => __CLASS__ . '::get_rewind_data', @@ -174,6 +184,13 @@ class Jetpack_Core_Json_Api_Endpoints { 'permission_callback' => array( $site_endpoint , 'can_request' ), ) ); + // Get related posts of a certain site post + register_rest_route( 'jetpack/v4', '/site/posts/related', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $site_endpoint, 'get_related_posts' ), + 'permission_callback' => array( $site_endpoint , 'can_request' ), + ) ); + // Confirm that a site in identity crisis should be in staging mode register_rest_route( 'jetpack/v4', '/identity-crisis/confirm-safe-mode', array( 'methods' => WP_REST_Server::EDITABLE, @@ -378,6 +395,63 @@ class Jetpack_Core_Json_Api_Endpoints { 'callback' => array( $widget_endpoint, 'process' ), 'permission_callback' => array( $widget_endpoint, 'can_request' ), ) ); + + // Site Verify: check if the site is verified, and a get verification token if not + register_rest_route( 'jetpack/v4', '/verify-site/(?P<service>[a-z\-_]+)', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => __CLASS__ . '::is_site_verified_and_token', + 'permission_callback' => __CLASS__ . '::update_settings_permission_check', + ) ); + + register_rest_route( 'jetpack/v4', '/verify-site/(?P<service>[a-z\-_]+)/(?<keyring_id>[0-9]+)', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => __CLASS__ . '::is_site_verified_and_token', + 'permission_callback' => __CLASS__ . '::update_settings_permission_check', + ) ); + + // Site Verify: tell a service to verify the site + register_rest_route( 'jetpack/v4', '/verify-site/(?P<service>[a-z\-_]+)', array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => __CLASS__ . '::verify_site', + 'permission_callback' => __CLASS__ . '::update_settings_permission_check', + 'args' => array( + 'keyring_id' => array( + 'required' => true, + 'type' => 'integer', + 'validate_callback' => __CLASS__ . '::validate_posint', + ), + ) + ) ); + + // Get and set API keys. + // Note: permission_callback intentionally omitted from the GET method. + // Map block requires open access to API keys on the front end. + register_rest_route( + 'jetpack/v4', + '/service-api-keys/(?P<service>[a-z\-_]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => __CLASS__ . '::get_service_api_key', + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => __CLASS__ . '::update_service_api_key', + 'permission_callback' => __CLASS__ . '::edit_others_posts_check', + 'args' => array( + 'service_api_key' => array( + 'required' => true, + 'type' => 'text', + ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => __CLASS__ . '::delete_service_api_key', + 'permission_callback' => __CLASS__ . '::edit_others_posts_check', + ), + ) + ); } public static function get_plans( $request ) { @@ -461,6 +535,112 @@ class Jetpack_Core_Json_Api_Endpoints { return $result; } + + /** + * Checks if this site has been verified using a service - only 'google' supported at present - and a specfic + * keyring to use to get the token if it is not + * + * Returns 'verified' = true/false, and a token if 'verified' is false and site is ready for verification + * + * @since 6.6.0 + * + * @param WP_REST_Request $request The request sent to the WP REST API. + * + * @return array|wp-error + */ + public static function is_site_verified_and_token( $request ) { + /** + * Return an error if the site uses a Maintenance / Coming Soon plugin + * and if the plugin is configured to make the site private. + * + * We currently handle the following plugins: + * - https://github.com/mojoness/mojo-marketplace-wp-plugin (used by bluehost) + * - https://wordpress.org/plugins/mojo-under-construction + * - https://wordpress.org/plugins/under-construction-page + * - https://wordpress.org/plugins/ultimate-under-construction + * - https://wordpress.org/plugins/coming-soon + * + * You can handle this in your own plugin thanks to the `jetpack_is_under_construction_plugin` filter. + * If the filter returns true, we will consider the site as under construction. + */ + $mm_coming_soon = get_option( 'mm_coming_soon', null ); + $under_construction_activation_status = get_option( 'underConstructionActivationStatus', null ); + $ucp_options = get_option( 'ucp_options', array() ); + $uuc_settings = get_option( 'uuc_settings', array() ); + $csp4 = get_option( 'seed_csp4_settings_content', array() ); + if ( + ( Jetpack::is_plugin_active( 'mojo-marketplace-wp-plugin/mojo-marketplace.php' ) && 'true' === $mm_coming_soon ) + || Jetpack::is_plugin_active( 'mojo-under-construction/mojo-contruction.php' ) && 1 == $under_construction_activation_status // WPCS: loose comparison ok. + || ( Jetpack::is_plugin_active( 'under-construction-page/under-construction.php' ) && isset( $ucp_options['status'] ) && 1 == $ucp_options['status'] ) // WPCS: loose comparison ok. + || ( Jetpack::is_plugin_active( 'ultimate-under-construction/ultimate-under-construction.php' ) && isset( $uuc_settings['enable'] ) && 1 == $uuc_settings['enable'] ) // WPCS: loose comparison ok. + || ( Jetpack::is_plugin_active( 'coming-soon/coming-soon.php' ) && isset( $csp4['status'] ) && ( 1 == $csp4['status'] || 2 == $csp4['status'] ) ) // WPCS: loose comparison ok. + /** + * Allow plugins to mark a site as "under construction". + * + * @since 6.7.0 + * + * @param false bool Is the site under construction? Default to false. + */ + || true === apply_filters( 'jetpack_is_under_construction_plugin', false ) + ) { + return new WP_Error( 'forbidden', __( 'Site is under construction and cannot be verified', 'jetpack' ) ); + } + + Jetpack::load_xml_rpc_client(); + $xml = new Jetpack_IXR_Client( array( + 'user_id' => get_current_user_id(), + ) ); + + $args = array( + 'user_id' => get_current_user_id(), + 'service' => $request[ 'service' ], + ); + + if ( isset( $request[ 'keyring_id' ] ) ) { + $args[ 'keyring_id' ] = $request[ 'keyring_id' ]; + } + + $xml->query( 'jetpack.isSiteVerified', $args ); + + if ( $xml->isError() ) { + return new WP_Error( 'error_checking_if_site_verified_google', sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) ); + } else { + return $xml->getResponse(); + } + } + + + + public static function verify_site( $request ) { + Jetpack::load_xml_rpc_client(); + $xml = new Jetpack_IXR_Client( array( + 'user_id' => get_current_user_id(), + ) ); + + $params = $request->get_json_params(); + + $xml->query( 'jetpack.verifySite', array( + 'user_id' => get_current_user_id(), + 'service' => $request[ 'service' ], + 'keyring_id' => $params[ 'keyring_id' ], + ) + ); + + if ( $xml->isError() ) { + return new WP_Error( 'error_verifying_site_google', sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) ); + } else { + $response = $xml->getResponse(); + + if ( ! empty( $response['errors'] ) ) { + $error = new WP_Error; + $error->errors = $response['errors']; + return $error; + } + + return $response; + } + } + /** * Handles verification that a site is registered * @@ -688,6 +868,19 @@ class Jetpack_Core_Json_Api_Endpoints { } /** + * Verify that user can edit other's posts (Editors and Administrators). + * + * @return bool Whether user has the capability 'edit_others_posts'. + */ + public static function edit_others_posts_check() { + if ( current_user_can( 'edit_others_posts' ) ) { + return true; + } + + return new WP_Error( 'invalid_user_permission_edit_others_posts', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) ); + } + + /** * Contextual HTTP error code for authorization failure. * * Taken from rest_authorization_required_code() in WP-API plugin until is added to core. @@ -722,6 +915,43 @@ class Jetpack_Core_Json_Api_Endpoints { ); } + /** + * Test connection status for this Jetpack site. It uses the /jetpack-blogs/%d/test-connection wpcom endpoint. + * + * @since 6.8.0 + * + * @return array|WP_Error WP_Error returned if connection test does not succeed. + */ + public static function jetpack_connection_test() { + $response = Jetpack_Client::wpcom_json_api_request_as_blog( + sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ), + Jetpack_Client::WPCOM_JSON_API_VERSION + ); + + if ( is_wp_error( $response ) ) { + /* translators: %1$s is the error code, %2$s is the error message */ + return new WP_Error( 'connection_test_failed', sprintf( __( 'Connection test failed (#%1$s: %2$s)', 'jetpack' ), $response->get_error_code(), $response->get_error_message() ), array( 'status' => $response->get_error_code() ) ); + } + + $body = wp_remote_retrieve_body( $response ); + if ( ! $body ) { + return new WP_Error( 'connection_test_failed', __( 'Connection test failed (empty response body)', 'jetpack' ), array( 'status' => $response->get_error_code() ) ); + } + + $result = json_decode( $body ); + $is_connected = (bool) $result->connected; + $message = $result->message; + + if ( $is_connected ) { + return rest_ensure_response( array( + 'code' => 'success', + 'message' => $message, + ) ); + } else { + return new WP_Error( 'connection_test_failed', $message, array( 'status' => $response->get_error_code() ) ); + } + } + public static function rewind_data() { $site_id = Jetpack_Options::get_option( 'id' ); @@ -881,11 +1111,22 @@ class Jetpack_Core_Json_Api_Endpoints { ); } + // Update the master user in Jetpack $updated = Jetpack_Options::update_option( 'master_user', $new_owner_id ); - if ( $updated ) { + + // Notify WPCOM about the master user change + Jetpack::load_xml_rpc_client(); + $xml = new Jetpack_IXR_Client( array( + 'user_id' => get_current_user_id(), + ) ); + $xml->query( 'jetpack.switchBlogOwner', array( + 'new_blog_owner' => $new_owner_id, + ) ); + + if ( $updated && ! $xml->isError() ) { return rest_ensure_response( array( - 'code' => 'success' + 'code' => 'success', ) ); } @@ -1916,6 +2157,14 @@ class Jetpack_Core_Json_Api_Endpoints { 'validate_callback' => __CLASS__ . '::validate_boolean', 'jp_group' => 'wordads', ), + 'wordads_custom_adstxt' => array( + 'description' => esc_html__( 'Custom ads.txt entries', 'jetpack' ), + 'type' => 'string', + 'default' => '', + 'validate_callback' => __CLASS__ . '::validate_string', + 'sanitize_callback' => 'sanitize_textarea_field', + 'jp_group' => 'wordads', + ), // Google Analytics 'google_analytics_tracking_id' => array( @@ -2253,7 +2502,7 @@ class Jetpack_Core_Json_Api_Endpoints { * @return bool|WP_Error */ public static function validate_verification_service( $value = '', $request, $param ) { - if ( ! empty( $value ) && ! ( is_string( $value ) && ( preg_match( '/^[a-z0-9_-]+$/i', $value ) || preg_match( '#^<meta name="([a-z0-9_\-.:]+)?" content="([a-z0-9_-]+)?" />$#i', $value ) ) ) ) { + if ( ! empty( $value ) && ! ( is_string( $value ) && ( preg_match( '/^[a-z0-9_-]+$/i', $value ) || jetpack_verification_get_code( $value ) !== false ) ) ) { return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string or a verification tag.', 'jetpack' ), $param ) ); } return true; @@ -2831,6 +3080,182 @@ class Jetpack_Core_Json_Api_Endpoints { return array(); } + + /** + * Get third party plugin API keys. + * + * @param WP_REST_Request $request { + * Array of parameters received by request. + * + * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'. + * } + */ + public static function get_service_api_key( $request ) { + $service = self::validate_service_api_service( $request['service'] ); + if ( ! $service ) { + return self::service_api_invalid_service_response(); + } + $option = self::key_for_api_service( $service ); + $message = esc_html__( 'API key retrieved successfully.', 'jetpack' ); + return array( + 'code' => 'success', + 'service' => $service, + 'service_api_key' => Jetpack_Options::get_option( $option, '' ), + 'message' => $message, + ); + } + + /** + * Update third party plugin API keys. + * + * @param WP_REST_Request $request { + * Array of parameters received by request. + * + * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'. + * } + */ + public static function update_service_api_key( $request ) { + $service = self::validate_service_api_service( $request['service'] ); + if ( ! $service ) { + return self::service_api_invalid_service_response(); + } + $params = $request->get_json_params(); + $service_api_key = trim( $params['service_api_key'] ); + $option = self::key_for_api_service( $service ); + $validation = self::validate_service_api_key( $service_api_key, $service ); + if ( ! $validation['status'] ) { + return new WP_Error( 'invalid_key', esc_html__( 'Invalid API Key', 'jetpack' ), array( 'status' => 404 ) ); + } + $message = esc_html__( 'API key updated successfully.', 'jetpack' ); + Jetpack_Options::update_option( $option, $service_api_key ); + return array( + 'code' => 'success', + 'service' => $service, + 'service_api_key' => Jetpack_Options::get_option( $option, '' ), + 'message' => $message, + ); + } + + /** + * Delete a third party plugin API key. + * + * @param WP_REST_Request $request { + * Array of parameters received by request. + * + * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'. + * } + */ + public static function delete_service_api_key( $request ) { + $service = self::validate_service_api_service( $request['service'] ); + if ( ! $service ) { + return self::service_api_invalid_service_response(); + } + $option = self::key_for_api_service( $service ); + Jetpack_Options::delete_option( $option ); + $message = esc_html__( 'API key deleted successfully.', 'jetpack' ); + return array( + 'code' => 'success', + 'service' => $service, + 'service_api_key' => Jetpack_Options::get_option( $option, '' ), + 'message' => $message, + ); + } + + /** + * Validate the service provided in /service-api-keys/ endpoints. + * To add a service to these endpoints, add the service name to $valid_services + * and add '{service name}_api_key' to the non-compact return array in get_option_names(), + * in class-jetpack-options.php + * + * @param string $service The service the API key is for. + * @return string Returns the service name if valid, null if invalid. + */ + public static function validate_service_api_service( $service = null ) { + $valid_services = array( + 'mapbox', + ); + return in_array( $service, $valid_services, true ) ? $service : null; + } + + /** + * Error response for invalid service API key requests with an invalid service. + */ + public static function service_api_invalid_service_response() { + return new WP_Error( + 'invalid_service', + esc_html__( 'Invalid Service', 'jetpack' ), + array( 'status' => 404 ) + ); + } + + /** + * Validate API Key + * + * @param string $key The API key to be validated. + * @param string $service The service the API key is for. + */ + public static function validate_service_api_key( $key = null, $service = null ) { + $validation = false; + switch ( $service ) { + case 'mapbox': + $validation = self::validate_service_api_key_mapbox( $key ); + break; + } + return $validation; + } + + /** + * Validate Mapbox API key + * Based loosely on https://github.com/mapbox/geocoding-example/blob/master/php/MapboxTest.php + * + * @param string $key The API key to be validated. + */ + public static function validate_service_api_key_mapbox( $key ) { + $status = true; + $msg = null; + $mapbox_url = sprintf( + 'https://api.mapbox.com?%s', + $key + ); + $mapbox_response = wp_safe_remote_get( esc_url_raw( $mapbox_url ) ); + $mapbox_body = wp_remote_retrieve_body( $mapbox_response ); + if ( '{"api":"mapbox"}' !== $mapbox_body ) { + $status = false; + $msg = esc_html__( 'Can\'t connect to Mapbox', 'jetpack' ); + return array( + 'status' => $status, + 'error_message' => $msg, + ); + } + $mapbox_geocode_url = esc_url_raw( + sprintf( + 'https://api.mapbox.com/geocoding/v5/mapbox.places/%s.json?access_token=%s', + '1+broadway+new+york+ny+usa', + $key + ) + ); + $mapbox_geocode_response = wp_safe_remote_get( esc_url_raw( $mapbox_geocode_url ) ); + $mapbox_geocode_body = wp_remote_retrieve_body( $mapbox_geocode_response ); + $mapbox_geocode_json = json_decode( $mapbox_geocode_body ); + if ( isset( $mapbox_geocode_json->message ) && ! isset( $mapbox_geocode_json->query ) ) { + $status = false; + $msg = $mapbox_geocode_json->message; + } + return array( + 'status' => $status, + 'error_message' => $msg, + ); + } + + /** + * Create site option key for service + * + * @param string $service The service to create key for. + */ + private static function key_for_api_service( $service ) { + return $service . '_api_key'; + } + /** * Checks if the queried plugin is active. * |