-
Notifications
You must be signed in to change notification settings - Fork 800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Connection: Add REST support for jsonAPI endpoints #39432
base: trunk
Are you sure you want to change the base?
Changes from all commits
3a60d13
9b85565
36368f0
61b021e
8bfbadb
6357e97
9b5e5ae
20974a0
52fe990
549e826
a3cc273
0d757d3
4aaa656
40eec5f
b4836df
ab97174
9ba813f
bb92f7a
3db7335
9b3202f
2e1ae27
8abca0d
e97dd90
79fdce8
22e01d7
01a8fbc
a0ec6ce
541cbe3
8013673
b2b467a
b9faed5
eb68e2c
d2a9835
bbb226a
debee21
95e1279
2b2ff8c
a4806b7
9d6f78a
9739eaa
4850259
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Significance: minor | ||
Type: added | ||
|
||
Add the 'is_signed_with_user_token()' method for REST authentication. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Significance: minor | ||
Type: other | ||
|
||
Add REST support for jsonAPI endpoints. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,9 @@ | |
*/ | ||
|
||
use Automattic\Jetpack\Connection\Client; | ||
use Automattic\Jetpack\Connection\Manager; | ||
use Automattic\Jetpack\Connection\Rest_Authentication; | ||
use Automattic\Jetpack\Connection\Tokens; | ||
use Automattic\Jetpack\Status; | ||
|
||
require_once __DIR__ . '/json-api-config.php'; | ||
|
@@ -124,6 +127,20 @@ abstract class WPCOM_JSON_API_Endpoint { | |
*/ | ||
public $path_labels = array(); | ||
|
||
/** | ||
* The REST endpoint if available. | ||
* | ||
* @var string | ||
*/ | ||
public $rest_route; | ||
|
||
/** | ||
* Jetpack Version in which REST support was introduced. | ||
* | ||
* @var string | ||
*/ | ||
public $rest_min_jp_version; | ||
|
||
/** | ||
* Accepted query parameters | ||
* | ||
|
@@ -277,6 +294,11 @@ abstract class WPCOM_JSON_API_Endpoint { | |
*/ | ||
public $allow_fallback_to_jetpack_blog_token = false; | ||
|
||
/** | ||
* REST namespace. | ||
*/ | ||
const REST_NAMESPACE = 'jetpack/rest'; | ||
|
||
/** | ||
* Constructor. | ||
* | ||
|
@@ -300,6 +322,8 @@ public function __construct( $args ) { | |
'new_version' => WPCOM_JSON_API__CURRENT_VERSION, | ||
'jp_disabled' => false, | ||
'path_labels' => array(), | ||
'rest_route' => null, | ||
'rest_min_jp_version' => null, | ||
'request_format' => array(), | ||
'response_format' => array(), | ||
'query_parameters' => array(), | ||
|
@@ -339,6 +363,9 @@ public function __construct( $args ) { | |
$this->deprecated = $args['deprecated']; | ||
$this->new_version = $args['new_version']; | ||
|
||
$this->rest_route = $args['rest_route']; | ||
$this->rest_min_jp_version = $args['rest_min_jp_version']; | ||
|
||
// Ensure max version is not less than min version. | ||
if ( version_compare( $this->min_version, $this->max_version, '>' ) ) { | ||
$this->max_version = $this->min_version; | ||
|
@@ -387,6 +414,10 @@ public function __construct( $args ) { | |
$this->example_response = $args['example_response']; | ||
|
||
$this->api->add( $this ); | ||
|
||
if ( ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) && $this->rest_route && ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) ) { | ||
$this->create_rest_route_for_endpoint(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should never happen on WPCOM :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, it feels like this logic would be better suited to be part of the api, aka There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I added the "not WPCOM" condition: 9b3202f
The REST endpoint belongs to the endpoint object, so that seems logical to me to let the endpoint object initialize it. |
||
} | ||
} | ||
|
||
/** | ||
|
@@ -2618,6 +2649,173 @@ public function get_amp_cache_origins( $siteurl ) { | |
); | ||
} | ||
|
||
/** | ||
* Register a REST route for this jsonAPI endpoint. | ||
* | ||
* @return void | ||
* @throws Exception The exception if something goes wrong. | ||
*/ | ||
public function create_rest_route_for_endpoint() { | ||
register_rest_route( | ||
static::REST_NAMESPACE, | ||
$this->build_rest_route(), | ||
array( | ||
'methods' => $this->method, | ||
'callback' => array( $this, 'rest_callback' ), | ||
'permission_callback' => array( $this, 'rest_permission_callback' ), | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* Handle the rest call. | ||
* | ||
* @param WP_REST_Request $request The request object. | ||
* | ||
* @return mixed|WP_Error | ||
*/ | ||
public function rest_callback( WP_REST_Request $request ) { | ||
// phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed -- Making sure random warnings don't break JSON. | ||
ini_set( 'display_errors', false ); | ||
|
||
$blog_id = Jetpack_Options::get_option( 'id' ); | ||
|
||
$this->api->initialize(); | ||
$this->api->endpoint = $this; | ||
|
||
$locale = $request->get_param( 'language' ); | ||
if ( $locale ) { | ||
$this->api->init_locale( $locale ); | ||
} | ||
|
||
if ( $this->in_testing && ! WPCOM_JSON_API__DEBUG ) { | ||
return new WP_Error( 'endpoint_not_available' ); | ||
} | ||
|
||
$token_data = ( new Manager() )->verify_xml_rpc_signature(); | ||
if ( ! $token_data || empty( $token_data['token_key'] ) || ! array_key_exists( 'user_id', $token_data ) ) { | ||
return new WP_Error( 'response_signature_error' ); | ||
} | ||
|
||
$token = ( new Tokens() )->get_access_token( $token_data['user_id'], $token_data['token_key'] ); | ||
if ( is_wp_error( $token ) ) { | ||
return $token; | ||
} | ||
if ( ! $token ) { | ||
return new WP_Error( 'response_signature_error' ); | ||
} | ||
|
||
/** This action is documented in class.json-api.php */ | ||
do_action( 'wpcom_json_api_output', $this->stat ); | ||
|
||
$response = call_user_func_array( | ||
array( $this, 'callback' ), | ||
array_values( array( $this->path, $blog_id ) + $request->get_url_params() ) | ||
); | ||
|
||
if ( ! $response && ! is_array( $response ) ) { | ||
// Dealing with empty non-array response. Phan is wrong about it being an "impossible condition". | ||
$response = new WP_Error( 'empty_response', 'Endpoint response is empty', 500 ); | ||
} | ||
|
||
$status_code = 200; | ||
|
||
if ( is_wp_error( $response ) ) { | ||
$status_code = 500; | ||
|
||
if ( $response->get_error_data() && is_scalar( $response->get_error_data() ) | ||
&& (string) (int) $response->get_error_data() === (string) $response->get_error_data() | ||
) { | ||
$status_code = (int) $response->get_error_data(); | ||
} | ||
|
||
$response = WPCOM_JSON_API::serializable_error( $response ); | ||
} | ||
|
||
if ( $request->get_param( 'http_envelope' ) ) { | ||
$response = wp_json_encode( WPCOM_JSON_API::wrap_http_envelope( $status_code, $response, 'application/json' ) ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we need to json encode the response independently of the |
||
} | ||
|
||
$nonce = wp_generate_password( 10, false ); | ||
$hmac = hash_hmac( 'sha1', $nonce . $response, $token->secret ); | ||
|
||
return array( | ||
$response, | ||
(string) $nonce, | ||
(string) $hmac, | ||
); | ||
} | ||
|
||
/** | ||
* The REST endpoint should only be available for requests signed with a valid blog or user token. | ||
* Declaring it "final" so individual endpoints couldn't remove this requirement. | ||
* | ||
* If you need to add custom permissions to individual endpoints, you can override method `rest_permission_callback_custom()`. | ||
* | ||
* @see self::rest_permission_callback_custom() | ||
* | ||
* @return true|WP_Error | ||
*/ | ||
final public function rest_permission_callback() { | ||
$manager = new Manager( 'jetpack' ); | ||
if ( ! $manager->is_connected() ) { | ||
return new WP_Error( 'site_not_connected' ); | ||
} | ||
|
||
if ( ( $this->allow_jetpack_site_auth && Rest_Authentication::is_signed_with_blog_token() ) || ( get_current_user_id() && Rest_Authentication::is_signed_with_user_token() ) ) { | ||
$custom_permission_result = $this->rest_permission_callback_custom(); | ||
|
||
// Successful custom permission check. | ||
if ( $custom_permission_result === true ) { | ||
return true; | ||
} | ||
|
||
// Custom permission check errored, returning the error. | ||
if ( is_wp_error( $custom_permission_result ) ) { | ||
return $custom_permission_result; | ||
} | ||
|
||
// Custom permission check failed, but didn't return a specific error. Proceed to returning the generic error. | ||
} | ||
|
||
$message = esc_html__( | ||
'You do not have the correct user permissions to perform this action. Please contact your site admin if you think this is a mistake.', | ||
'jetpack' | ||
); | ||
return new WP_Error( 'rest_api_invalid_permission', $message, array( 'status' => rest_authorization_required_code() ) ); | ||
} | ||
|
||
/** | ||
* You can override this method in individual endpoints to add custom permission checks. | ||
* This will run on top of `rest_permission_callback()`. | ||
* | ||
* @see self::rest_permission_callback() | ||
* | ||
* @return true|WP_Error | ||
*/ | ||
public function rest_permission_callback_custom() { | ||
fgiannar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return true; | ||
} | ||
|
||
/** | ||
* Build the REST endpoint URL. | ||
* | ||
* @return string | ||
*/ | ||
public function build_rest_route() { | ||
$version_prefix = $this->max_version ? 'v' . $this->max_version : ''; | ||
return $version_prefix . $this->rest_route; | ||
fgiannar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Get Jetpack Version where support for the endpoint was introduced. | ||
* | ||
* @return string | ||
*/ | ||
public function get_rest_min_jp_version() { | ||
return $this->rest_min_jp_version; | ||
} | ||
|
||
/** | ||
* Return endpoint response | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking that maybe it would make sense to shuffle things a bit so that we make this method consistent with json_api here.