Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.65% |
83 / 85 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
| PayPal_Platform_Client | |
100.00% |
83 / 83 |
|
100.00% |
4 / 4 |
20 | |
100.00% |
1 / 1 |
| request | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
5 | |||
| get_merchant_integration | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| call_wpcom | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
6 | |||
| wpcom_error | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * PayPal calls made through WordPress.com on a referred seller's behalf. |
| 4 | * |
| 5 | * A seller onboarded through Partner Referrals (THIRD_PARTY) holds no API |
| 6 | * credentials on the site. Automattic's platform credentials live on |
| 7 | * WordPress.com, so every PayPal call for that seller is made there, signed |
| 8 | * with a PayPal-Auth-Assertion naming the seller. |
| 9 | * |
| 10 | * @package automattic/jetpack-paypal-payments |
| 11 | * @since 0.12.0 |
| 12 | */ |
| 13 | |
| 14 | namespace Automattic\Jetpack\PaypalPayments; |
| 15 | |
| 16 | use Automattic\Jetpack\Connection\Client; |
| 17 | |
| 18 | if ( ! defined( 'ABSPATH' ) ) { |
| 19 | exit; |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * Class PayPal_Platform_Client |
| 24 | */ |
| 25 | class PayPal_Platform_Client { |
| 26 | |
| 27 | /** |
| 28 | * WordPress.com route that forwards one Payment Links & Buttons call. |
| 29 | * |
| 30 | * @var string |
| 31 | */ |
| 32 | const WPCOM_REQUEST_ROUTE = '/paypal/platform/request'; |
| 33 | |
| 34 | /** |
| 35 | * WordPress.com route that reads a referred seller's integration status. |
| 36 | * |
| 37 | * @var string |
| 38 | */ |
| 39 | const WPCOM_MERCHANT_INTEGRATION_ROUTE = '/paypal/platform/merchant-integration'; |
| 40 | |
| 41 | /** |
| 42 | * Make one Payment Links & Buttons call for the connected seller. |
| 43 | * |
| 44 | * PayPal's own status and body come back in the shape wp_remote_request() |
| 45 | * returns, so callers read the answer the same way as a direct call. |
| 46 | * |
| 47 | * @param string $method HTTP method (GET, POST, PUT, DELETE). |
| 48 | * @param string $path PayPal API path, with any query string. |
| 49 | * @param array|null $body Request body for POST and PUT. |
| 50 | * @param string $request_id PayPal-Request-Id idempotency key. |
| 51 | * @return array|\WP_Error A wp_remote_request()-shaped response, or WP_Error when the call never reached PayPal. |
| 52 | */ |
| 53 | public static function request( $method, $path, $body = null, $request_id = '' ) { |
| 54 | $merchant_id = PayPal_Partner_Onboarding::get_merchant_id(); |
| 55 | if ( '' === $merchant_id ) { |
| 56 | return new \WP_Error( |
| 57 | 'paypal_no_credentials', |
| 58 | __( 'PayPal API credentials are not configured. Please connect your PayPal account.', 'jetpack-paypal-payments' ) |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | $params = array( |
| 63 | 'environment' => PayPal_OAuth::get_environment(), |
| 64 | 'merchant_id' => $merchant_id, |
| 65 | 'method' => $method, |
| 66 | 'path' => $path, |
| 67 | 'body' => $body, |
| 68 | 'request_id' => $request_id, |
| 69 | ); |
| 70 | |
| 71 | // Proof this site referred the seller; a site onboarded before it was kept has none. |
| 72 | $tracking_id = PayPal_Partner_Onboarding::get_referral_tracking_id(); |
| 73 | if ( '' !== $tracking_id ) { |
| 74 | $params['tracking_id'] = $tracking_id; |
| 75 | } |
| 76 | |
| 77 | $result = self::call_wpcom( 'POST', self::WPCOM_REQUEST_ROUTE, $params ); |
| 78 | |
| 79 | if ( is_wp_error( $result ) ) { |
| 80 | return $result; |
| 81 | } |
| 82 | |
| 83 | if ( ! isset( $result['status'] ) ) { |
| 84 | return new \WP_Error( |
| 85 | 'paypal_platform_invalid_response', |
| 86 | __( 'WordPress.com returned an unexpected answer from PayPal.', 'jetpack-paypal-payments' ), |
| 87 | array( 'status' => 502 ) |
| 88 | ); |
| 89 | } |
| 90 | |
| 91 | return array( |
| 92 | 'headers' => array(), |
| 93 | 'body' => (string) ( $result['body'] ?? '' ), |
| 94 | 'response' => array( |
| 95 | 'code' => (int) $result['status'], |
| 96 | 'message' => '', |
| 97 | ), |
| 98 | 'cookies' => array(), |
| 99 | 'http_response' => null, |
| 100 | ); |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Read a referred seller's integration record. |
| 105 | * |
| 106 | * @param string $merchant_id The seller's PayPal merchant ID, when known. |
| 107 | * @param string $tracking_id The referral's tracking ID: alone, to find a seller who just finished |
| 108 | * onboarding; with a merchant ID, as proof this site referred them. |
| 109 | * @return array|\WP_Error PayPal's merchant integration, or WP_Error. |
| 110 | */ |
| 111 | public static function get_merchant_integration( $merchant_id = '', $tracking_id = '' ) { |
| 112 | $params = array( 'environment' => PayPal_OAuth::get_environment() ); |
| 113 | if ( '' !== $merchant_id ) { |
| 114 | $params['merchant_id'] = $merchant_id; |
| 115 | } |
| 116 | if ( '' !== $tracking_id ) { |
| 117 | $params['tracking_id'] = $tracking_id; |
| 118 | } |
| 119 | |
| 120 | return self::call_wpcom( 'GET', self::WPCOM_MERCHANT_INTEGRATION_ROUTE, $params ); |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Call one WordPress.com platform route as the blog. |
| 125 | * |
| 126 | * @param string $method HTTP method. |
| 127 | * @param string $route Route below wpcom/v2. |
| 128 | * @param array $params Query arguments for GET, JSON body otherwise. |
| 129 | * @return array|\WP_Error The decoded response, or WP_Error carrying WordPress.com's code and status. |
| 130 | */ |
| 131 | private static function call_wpcom( $method, $route, $params ) { |
| 132 | $body = null; |
| 133 | if ( 'GET' === $method ) { |
| 134 | $route = add_query_arg( array_map( 'rawurlencode', $params ), $route ); |
| 135 | } else { |
| 136 | $body = wp_json_encode( $params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); |
| 137 | } |
| 138 | |
| 139 | $response = Client::wpcom_json_api_request_as_blog( |
| 140 | $route, |
| 141 | '2', |
| 142 | array( |
| 143 | 'method' => $method, |
| 144 | 'timeout' => 30, |
| 145 | 'headers' => array( |
| 146 | 'Content-Type' => 'application/json', |
| 147 | 'Accept' => 'application/json', |
| 148 | ), |
| 149 | ), |
| 150 | $body, |
| 151 | 'wpcom' |
| 152 | ); |
| 153 | |
| 154 | if ( is_wp_error( $response ) ) { |
| 155 | return new \WP_Error( |
| 156 | 'paypal_api_request_failed', |
| 157 | sprintf( |
| 158 | /* translators: %s: error message from the HTTP request */ |
| 159 | __( 'PayPal API request failed: %s', 'jetpack-paypal-payments' ), |
| 160 | $response->get_error_message() |
| 161 | ), |
| 162 | array( 'status' => 0 ) |
| 163 | ); |
| 164 | } |
| 165 | |
| 166 | $status_code = (int) wp_remote_retrieve_response_code( $response ); |
| 167 | $data = json_decode( wp_remote_retrieve_body( $response ), true ); |
| 168 | |
| 169 | if ( 200 !== $status_code || ! is_array( $data ) ) { |
| 170 | return self::wpcom_error( $status_code, is_array( $data ) ? $data : array() ); |
| 171 | } |
| 172 | |
| 173 | return $data; |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * Turn a WordPress.com error answer into a WP_Error the site can act on. |
| 178 | * |
| 179 | * The code, message and data are WordPress.com's own, so a merchant this |
| 180 | * site did not refer, or an unprovisioned platform, is reported as such. |
| 181 | * |
| 182 | * @param int $status_code HTTP status from WordPress.com. |
| 183 | * @param array $data Decoded error body. |
| 184 | * @return \WP_Error |
| 185 | */ |
| 186 | private static function wpcom_error( $status_code, array $data ) { |
| 187 | $error_data = isset( $data['data'] ) && is_array( $data['data'] ) ? $data['data'] : array(); |
| 188 | |
| 189 | // 0 means WordPress.com itself never answered. |
| 190 | $error_data['status'] = $status_code > 0 ? $status_code : 503; |
| 191 | |
| 192 | return new \WP_Error( |
| 193 | ! empty( $data['code'] ) ? sanitize_key( $data['code'] ) : 'paypal_platform_request_failed', |
| 194 | ! empty( $data['message'] ) |
| 195 | ? sanitize_text_field( $data['message'] ) |
| 196 | : __( 'WordPress.com could not reach PayPal on your behalf. Please try again.', 'jetpack-paypal-payments' ), |
| 197 | $error_data |
| 198 | ); |
| 199 | } |
| 200 | } |