Skip to content

Commit

Permalink
Jetpack core api: Refactor and reuse trait for proxying requests to W…
Browse files Browse the repository at this point in the history
…PCOM (#40245)

* WPCOM_REST_API_Proxy_Request_Trait: Refactor trait and use in multiple endpoints
  • Loading branch information
fgiannar authored Nov 25, 2024
1 parent 0a13b79 commit bcf6634
Show file tree
Hide file tree
Showing 7 changed files with 1,137 additions and 171 deletions.
10 changes: 5 additions & 5 deletions projects/plugins/jetpack/.phan/baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
// PhanRedundantCondition : 70+ occurrences
// PhanDeprecatedFunction : 65+ occurrences
// PhanPossiblyUndeclaredVariable : 60+ occurrences
// PhanTypeArraySuspiciousNullable : 60+ occurrences
// PhanRedefineFunction : 55+ occurrences
// PhanTypeArraySuspiciousNullable : 50+ occurrences
// PhanTypeMismatchArgumentNullable : 50+ occurrences
// PhanParamTooMany : 40+ occurrences
// PhanPluginDuplicateAdjacentStatement : 40+ occurrences
// PhanTypeExpectedObjectPropAccess : 40+ occurrences
// PhanTypeMismatchArgumentInternal : 40+ occurrences
// PhanUndeclaredProperty : 35+ occurrences
// PhanUndeclaredProperty : 40+ occurrences
// PhanParamSignatureMismatch : 25+ occurrences
// PhanTypeMismatchDefault : 25+ occurrences
// PhanTypeMismatchPropertyProbablyReal : 25+ occurrences
Expand Down Expand Up @@ -58,12 +58,12 @@
// PhanDeprecatedClass : 5 occurrences
// PhanNonClassMethodCall : 5 occurrences
// PhanTypeMismatchDimAssignment : 5 occurrences
// PhanTypeSuspiciousStringExpression : 5 occurrences
// PhanAccessMethodInternal : 4 occurrences
// PhanImpossibleCondition : 4 occurrences
// PhanTypeInvalidLeftOperandOfAdd : 4 occurrences
// PhanTypeInvalidLeftOperandOfBitwiseOp : 4 occurrences
// PhanTypeInvalidRightOperandOfBitwiseOp : 4 occurrences
// PhanTypeSuspiciousStringExpression : 4 occurrences
// PhanUndeclaredFunctionInCallable : 4 occurrences
// PhanDeprecatedFunctionInternal : 3 occurrences
// PhanDeprecatedTrait : 3 occurrences
Expand Down Expand Up @@ -134,9 +134,9 @@
'_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-resolve-redirect.php' => ['PhanPluginMixedKeyNoKey'],
'_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-send-email-preview.php' => ['PhanTypeMismatchReturn'],
'_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-template-loader.php' => ['PhanTypeMismatchReturnProbablyReal'],
'_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanPluginMixedKeyNoKey', 'PhanTypeInvalidLeftOperandOfAdd', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchReturnProbablyReal'],
'_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php' => ['PhanPluginMixedKeyNoKey', 'PhanTypeInvalidLeftOperandOfAdd', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchReturnProbablyReal'],
'_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php' => ['PhanPluginMixedKeyNoKey'],
'_inc/lib/core-api/wpcom-endpoints/memberships.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanPluginUnreachableCode', 'PhanTypeArraySuspicious', 'PhanTypeArraySuspiciousNullable', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanTypeSuspiciousStringExpression'],
'_inc/lib/core-api/wpcom-endpoints/memberships.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanPluginUnreachableCode', 'PhanTypeArraySuspicious'],
'_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php' => ['PhanPluginMixedKeyNoKey', 'PhanTypeMismatchArgument'],
'_inc/lib/core-api/wpcom-endpoints/publicize-connections.php' => ['PhanParamSignatureMismatch', 'PhanPluginMixedKeyNoKey', 'PhanTypeMismatchArgument'],
'_inc/lib/core-api/wpcom-endpoints/publicize-services.php' => ['PhanParamSignatureMismatch', 'PhanPluginMixedKeyNoKey', 'PhanTypeMismatchArgument'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
* @package automattic/jetpack
*/

use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager;
// Ensure WPCOM_REST_API_Proxy_Request_Trait is present.
require_once __DIR__ . '/trait-wpcom-rest-api-proxy-request-trait.php';

/**
* REST API endpoint wpcom/v3/sites/%s/blogging-prompts.
*/
class WPCOM_REST_API_V3_Endpoint_Blogging_Prompts extends WP_REST_Posts_Controller {

use WPCOM_REST_API_Proxy_Request_Trait;

const TEMPLATE_BLOG_ID = 205876834;

/**
Expand Down Expand Up @@ -42,7 +45,9 @@ class WPCOM_REST_API_V3_Endpoint_Blogging_Prompts extends WP_REST_Posts_Controll
*/
public function __construct() {
$this->post_type = 'post';
$this->namespace = 'wpcom/v3';
$this->base_api_path = 'wpcom';
$this->version = 'v3';
$this->namespace = $this->base_api_path . '/' . $this->version;
$this->rest_base = 'blogging-prompts';
$this->wpcom_is_wpcom_only_endpoint = true;
$this->wpcom_is_site_specific_endpoint = true;
Expand Down Expand Up @@ -99,7 +104,7 @@ public function register_routes() {
*/
public function get_items( $request ) {
if ( ! $this->is_wpcom ) {
return $this->proxy_request_to_wpcom( $request );
return $this->proxy_request_to_wpcom( $request, '', 'user', true );
}

if ( $request->get_param( 'force_year' ) ) {
Expand All @@ -125,7 +130,7 @@ public function get_items( $request ) {
*/
public function get_item( $request ) {
if ( ! $this->is_wpcom ) {
return $this->proxy_request_to_wpcom( $request, $request->get_param( 'id' ) );
return $this->proxy_request_to_wpcom( $request, $request->get_param( 'id' ), 'user', true );
}

if ( $request->get_param( 'force_year' ) ) {
Expand Down Expand Up @@ -524,39 +529,6 @@ public function permissions_check() {
);
}

/**
* Proxy request to wpcom servers for the site and user.
*
* @param WP_Rest_Request $request Request to proxy.
* @param string $path Path to append to the rest base.
* @return mixed|WP_Error Response from wpcom servers or an error.
*/
public function proxy_request_to_wpcom( $request, $path = '' ) {
$blog_id = \Jetpack_Options::get_option( 'id' );
$path = '/sites/' . rawurldecode( $blog_id ) . '/' . rawurldecode( $this->rest_base ) . ( $path ? '/' . rawurldecode( $path ) : '' );
$api_url = add_query_arg( $request->get_query_params(), $path );

// Prefer request as user, if possible. Fall back to blog request to show prompt data for unconnected users.
$response = ( new Manager() )->is_user_connected()
? Client::wpcom_json_api_request_as_user( $api_url, '3', array(), null, 'wpcom' )
: Client::wpcom_json_api_request_as_blog( $api_url, 'v3', array(), null, 'wpcom' );

if ( is_wp_error( $response ) ) {
return $response;
}

$response_status = wp_remote_retrieve_response_code( $response );
$response_body = json_decode( wp_remote_retrieve_body( $response ), true );

if ( $response_status >= 400 ) {
$code = isset( $response_body['code'] ) ? $response_body['code'] : 'unknown_error';
$message = isset( $response_body['message'] ) ? $response_body['message'] : __( 'An unknown error occurred.', 'jetpack' );
return new WP_Error( $code, $message, array( 'status' => $response_status ) );
}

return $response_body;
}

/**
* Creates a sample of users who have answered a blogging prompt.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@
* @since 7.3.0
*/

use Automattic\Jetpack\Connection\Client;
require_once __DIR__ . '/trait-wpcom-rest-api-proxy-request-trait.php';

/**
* Class WPCOM_REST_API_V2_Endpoint_Memberships
* This introduces V2 endpoints.
*/
class WPCOM_REST_API_V2_Endpoint_Memberships extends WP_REST_Controller {

use WPCOM_REST_API_Proxy_Request_Trait;

/**
* WPCOM_REST_API_V2_Endpoint_Memberships constructor.
*/
public function __construct() {
$this->namespace = 'wpcom/v2';
$this->base_api_path = 'wpcom';
$this->version = 'v2';
$this->namespace = $this->base_api_path . '/' . $this->version;
$this->rest_base = 'memberships';
$this->wpcom_is_wpcom_only_endpoint = true;
$this->wpcom_is_site_specific_endpoint = true;
Expand Down Expand Up @@ -86,7 +90,7 @@ public function register_routes() {
'required' => true,
),
'price' => array(
'type' => 'float',
'type' => 'number',
'required' => true,
),
'currency' => array(
Expand Down Expand Up @@ -120,6 +124,20 @@ public function register_routes() {
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_products' ),
'permission_callback' => array( $this, 'can_modify_products_permission_check' ),
'args' => array(
'currency' => array(
'type' => 'string',
'required' => true,
),
'type' => array(
'type' => 'string',
'required' => true,
),
'is_editable' => array(
'type' => 'boolean',
'required' => false,
),
),
),
array(
'methods' => WP_REST_Server::READABLE,
Expand All @@ -142,7 +160,7 @@ public function register_routes() {
'required' => true,
),
'price' => array(
'type' => 'float',
'type' => 'number',
'required' => true,
),
'currency' => array(
Expand Down Expand Up @@ -221,37 +239,7 @@ public function create_products( $request ) {
}
return $result;
} else {
$payload = array(
'type' => $request['type'],
'currency' => $request['currency'],
);

// If we pass directly is_editable as null, it would break API argument validation.
if ( null !== $is_editable ) {
$payload['is_editable'] = $is_editable;
}

$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
"/sites/$blog_id/{$this->rest_base}/products",
'v2',
array(
'method' => 'POST',
),
$payload
);
if ( is_wp_error( $response ) ) {
if ( $response->get_error_code() === 'missing_token' ) {
return new WP_Error( 'missing_token', __( 'Please connect your user account to WordPress.com', 'jetpack' ), array( 'status' => 404 ) );
}
return new WP_Error( 'wpcom_connection_error', __( 'Could not connect to WordPress.com', 'jetpack' ), array( 'status' => 404 ) );
}
$data = isset( $response['body'] ) ? json_decode( $response['body'], true ) : null;
// If endpoint returned error, we have to detect it.
if ( 200 !== $response['response']['code'] && $data['code'] ) {
return new WP_Error( $data['code'], $data['message'] ? $data['message'] : '', array( 'status' => 401 ) );
}
return $data;
return $this->proxy_request_to_wpcom_as_user( $request, 'products' );
}

return $request;
Expand All @@ -265,7 +253,6 @@ public function create_products( $request ) {
* @return WP_Error|array ['products']
*/
public function list_products( WP_REST_Request $request ) {
$query = null;
$is_editable = isset( $request['is_editable'] ) ? (bool) $request['is_editable'] : null;
$type = isset( $request['type'] ) ? $request['type'] : null;

Expand All @@ -278,17 +265,8 @@ public function list_products( WP_REST_Request $request ) {
return array( 'error' => $e->getMessage() );
}
} else {
$query_parts = array();
if ( $type !== null ) {
$query_parts[] = 'type=' . $type;
}
if ( $is_editable !== null ) {
$query_parts[] = 'is_editable=' . $is_editable;
}
if ( ! empty( $query_parts ) ) {
$query = '?' . implode( '&', $query_parts );
}
return $this->proxy_request_to_wpcom( "products$query", 'GET' );

return $this->proxy_request_to_wpcom_as_user( $request, 'products' );
}
}

Expand All @@ -310,7 +288,7 @@ public function create_product( WP_REST_Request $request ) {
return array( 'error' => $e->getMessage() );
}
} else {
return $this->proxy_request_to_wpcom( 'product', 'POST', $payload );
return $this->proxy_request_to_wpcom_as_user( $request, 'product' );
}
}

Expand All @@ -333,7 +311,7 @@ public function update_product( \WP_REST_Request $request ) {
return array( 'error' => $e->getMessage() );
}
} else {
return $this->proxy_request_to_wpcom( "product/$product_id", 'POST', $payload );
return $this->proxy_request_to_wpcom_as_user( $request, "product/$product_id" );
}
}

Expand All @@ -356,11 +334,7 @@ public function delete_product( \WP_REST_Request $request ) {
return array( 'error' => $e->getMessage() );
}
} else {
return $this->proxy_request_to_wpcom(
"product/$product_id",
'DELETE',
array( 'cancel_subscriptions' => $cancel_subscriptions )
);
return $this->proxy_request_to_wpcom_as_user( $request, "product/$product_id" );
}
}

Expand Down Expand Up @@ -411,79 +385,10 @@ public function get_status( \WP_REST_Request $request ) {

return (array) $membership_settings;
} else {
$payload = array(
'type' => $request['type'],
'source' => $source,
);

// If we pass directly is_editable as null, it would break API argument validation.
// This also needs to be converted to int because boolean false is ignored by add_query_arg.
if ( null !== $is_editable ) {
$payload['is_editable'] = (int) $is_editable;
}

$blog_id = Jetpack_Options::get_option( 'id' );
$path = "/sites/$blog_id/{$this->rest_base}/status";
if ( $product_type ) {
$path = add_query_arg(
$payload,
$path
);
}
$response = Client::wpcom_json_api_request_as_user( $path, 'v2' );
if ( is_wp_error( $response ) ) {
if ( $response->get_error_code() === 'missing_token' ) {
return new WP_Error( 'missing_token', __( 'Please connect your user account to WordPress.com', 'jetpack' ), array( 'status' => 404 ) );
}
return new WP_Error( 'wpcom_connection_error', __( 'Could not connect to WordPress.com', 'jetpack' ), array( 'status' => 404 ) );
}
$data = isset( $response['body'] ) ? json_decode( $response['body'], true ) : null;
if ( 200 !== $response['response']['code'] && $data['code'] && $data['message'] ) {
return new WP_Error( $data['code'], $data['message'], array( 'status' => 401 ) );
}
return $data;
return $this->proxy_request_to_wpcom_as_user( $request, 'status' );
}
}

/**
* Proxy a request to WPCOM, look for errors and return a response or a WP_Error.
*
* @param string $uri Whatever would go at the end of the url after /sites/$blog_id/$this->rest_base/. This is usually `product`, `products`, or `product/$product_id`.
* @param string $method The HTTP method being used.
* @param array|null $payload An optional payload to be sent with the request.
* @return string The response from WPCOM
*/
private function proxy_request_to_wpcom( $uri, $method, $payload = null ) {
// get blog id
$blog_id = Jetpack_Options::get_option( 'id' );

// proxy request to wpcom
$response = Client::wpcom_json_api_request_as_user(
"/sites/$blog_id/{$this->rest_base}/$uri",
'v2',
array(
'method' => strtoupper( $method ),
),
$payload
);
if ( is_wp_error( $response ) ) {
if ( $response->get_error_code() === 'missing_token' ) {
return new WP_Error( 'missing_token', __( 'Please connect your user account to WordPress.com', 'jetpack' ), array( 'status' => 404 ) );
}
return new WP_Error( 'wpcom_connection_error', __( 'Could not connect to WordPress.com', 'jetpack' ), array( 'status' => 404 ) );
}

// decode response
$data = isset( $response['body'] ) ? json_decode( $response['body'], true ) : null;
// If endpoint returned error, we have to detect it.
if ( 200 !== $response['response']['code'] && $data['code'] && $data['message'] ) {
return new WP_Error( $data['code'], $data['message'], array( 'status' => 401 ) );
}

// return response
return $data;
}

/**
* This function throws an exception if it is run outside of wpcom.
*
Expand Down
Loading

0 comments on commit bcf6634

Please sign in to comment.