class.jetpack-client.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <?php
  2. class Jetpack_Client {
  3. const WPCOM_JSON_API_VERSION = '1.1';
  4. /**
  5. * Makes an authorized remote request using Jetpack_Signature
  6. *
  7. * @return array|WP_Error WP HTTP response on success
  8. */
  9. public static function remote_request( $args, $body = null ) {
  10. $defaults = array(
  11. 'url' => '',
  12. 'user_id' => 0,
  13. 'blog_id' => 0,
  14. 'auth_location' => JETPACK_CLIENT__AUTH_LOCATION,
  15. 'method' => 'POST',
  16. 'timeout' => 10,
  17. 'redirection' => 0,
  18. 'headers' => array(),
  19. 'stream' => false,
  20. 'filename' => null,
  21. 'sslverify' => true,
  22. );
  23. $args = wp_parse_args( $args, $defaults );
  24. $args['blog_id'] = (int) $args['blog_id'];
  25. if ( 'header' != $args['auth_location'] ) {
  26. $args['auth_location'] = 'query_string';
  27. }
  28. $token = Jetpack_Data::get_access_token( $args['user_id'] );
  29. if ( !$token ) {
  30. return new Jetpack_Error( 'missing_token' );
  31. }
  32. $method = strtoupper( $args['method'] );
  33. $timeout = intval( $args['timeout'] );
  34. $redirection = $args['redirection'];
  35. $stream = $args['stream'];
  36. $filename = $args['filename'];
  37. $sslverify = $args['sslverify'];
  38. $request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' );
  39. @list( $token_key, $secret ) = explode( '.', $token->secret );
  40. if ( empty( $token ) || empty( $secret ) ) {
  41. return new Jetpack_Error( 'malformed_token' );
  42. }
  43. $token_key = sprintf( '%s:%d:%d', $token_key, JETPACK__API_VERSION, $token->external_user_id );
  44. require_once JETPACK__PLUGIN_DIR . 'class.jetpack-signature.php';
  45. $time_diff = (int) Jetpack_Options::get_option( 'time_diff' );
  46. $jetpack_signature = new Jetpack_Signature( $token->secret, $time_diff );
  47. $timestamp = time() + $time_diff;
  48. if( function_exists( 'wp_generate_password' ) ) {
  49. $nonce = wp_generate_password( 10, false );
  50. } else {
  51. $nonce = substr( sha1( rand( 0, 1000000 ) ), 0, 10);
  52. }
  53. // Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing
  54. if ( is_null( $body ) ) {
  55. $body_hash = '';
  56. } else {
  57. // Allow arrays to be used in passing data.
  58. $body_to_hash = $body;
  59. if ( is_array( $body ) ) {
  60. // We cast this to a new variable, because the array form of $body needs to be
  61. // maintained so it can be passed into the request later on in the code.
  62. if ( count( $body ) > 0 ) {
  63. $body_to_hash = json_encode( self::_stringify_data( $body ) );
  64. } else {
  65. $body_to_hash = '';
  66. }
  67. }
  68. if ( ! is_string( $body_to_hash ) ) {
  69. return new Jetpack_Error( 'invalid_body', 'Body is malformed.' );
  70. }
  71. $body_hash = jetpack_sha1_base64( $body_to_hash );
  72. }
  73. $auth = array(
  74. 'token' => $token_key,
  75. 'timestamp' => $timestamp,
  76. 'nonce' => $nonce,
  77. 'body-hash' => $body_hash,
  78. );
  79. if ( false !== strpos( $args['url'], 'xmlrpc.php' ) ) {
  80. $url_args = array(
  81. 'for' => 'jetpack',
  82. 'wpcom_blog_id' => Jetpack_Options::get_option( 'id' ),
  83. );
  84. } else {
  85. $url_args = array();
  86. }
  87. if ( 'header' != $args['auth_location'] ) {
  88. $url_args += $auth;
  89. }
  90. $url = add_query_arg( urlencode_deep( $url_args ), $args['url'] );
  91. $url = Jetpack::fix_url_for_bad_hosts( $url );
  92. $signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false );
  93. if ( !$signature || is_wp_error( $signature ) ) {
  94. return $signature;
  95. }
  96. // Send an Authorization header so various caches/proxies do the right thing
  97. $auth['signature'] = $signature;
  98. $auth['version'] = JETPACK__VERSION;
  99. $header_pieces = array();
  100. foreach ( $auth as $key => $value ) {
  101. $header_pieces[] = sprintf( '%s="%s"', $key, $value );
  102. }
  103. $request['headers'] = array_merge( $args['headers'], array(
  104. 'Authorization' => "X_JETPACK " . join( ' ', $header_pieces ),
  105. ) );
  106. if ( 'header' != $args['auth_location'] ) {
  107. $url = add_query_arg( 'signature', urlencode( $signature ), $url );
  108. }
  109. return Jetpack_Client::_wp_remote_request( $url, $request );
  110. }
  111. /**
  112. * Wrapper for wp_remote_request(). Turns off SSL verification for certain SSL errors.
  113. * This is lame, but many, many, many hosts have misconfigured SSL.
  114. *
  115. * When Jetpack is registered, the jetpack_fallback_no_verify_ssl_certs option is set to the current time if:
  116. * 1. a certificate error is found AND
  117. * 2. not verifying the certificate works around the problem.
  118. *
  119. * The option is checked on each request.
  120. *
  121. * @internal
  122. * @see Jetpack::fix_url_for_bad_hosts()
  123. *
  124. * @return array|WP_Error WP HTTP response on success
  125. */
  126. public static function _wp_remote_request( $url, $args, $set_fallback = false ) {
  127. /**
  128. * SSL verification (`sslverify`) for the JetpackClient remote request
  129. * defaults to off, use this filter to force it on.
  130. *
  131. * Return `true` to ENABLE SSL verification, return `false`
  132. * to DISABLE SSL verification.
  133. *
  134. * @since 3.6.0
  135. *
  136. * @param bool Whether to force `sslverify` or not.
  137. */
  138. if ( apply_filters( 'jetpack_client_verify_ssl_certs', false ) ) {
  139. return wp_remote_request( $url, $args );
  140. }
  141. $fallback = Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' );
  142. if ( false === $fallback ) {
  143. Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', 0 );
  144. }
  145. if ( (int) $fallback ) {
  146. // We're flagged to fallback
  147. $args['sslverify'] = false;
  148. }
  149. $response = wp_remote_request( $url, $args );
  150. if (
  151. !$set_fallback // We're not allowed to set the flag on this request, so whatever happens happens
  152. ||
  153. isset( $args['sslverify'] ) && !$args['sslverify'] // No verification - no point in doing it again
  154. ||
  155. !is_wp_error( $response ) // Let it ride
  156. ) {
  157. Jetpack_Client::set_time_diff( $response, $set_fallback );
  158. return $response;
  159. }
  160. // At this point, we're not flagged to fallback and we are allowed to set the flag on this request.
  161. $message = $response->get_error_message();
  162. // Is it an SSL Certificate verification error?
  163. if (
  164. false === strpos( $message, '14090086' ) // OpenSSL SSL3 certificate error
  165. &&
  166. false === strpos( $message, '1407E086' ) // OpenSSL SSL2 certificate error
  167. &&
  168. false === strpos( $message, 'error setting certificate verify locations' ) // cURL CA bundle not found
  169. &&
  170. false === strpos( $message, 'Peer certificate cannot be authenticated with' ) // cURL CURLE_SSL_CACERT: CA bundle found, but not helpful
  171. // different versions of curl have different error messages
  172. // this string should catch them all
  173. &&
  174. false === strpos( $message, 'Problem with the SSL CA cert' ) // cURL CURLE_SSL_CACERT_BADFILE: probably access rights
  175. ) {
  176. // No, it is not.
  177. return $response;
  178. }
  179. // Redo the request without SSL certificate verification.
  180. $args['sslverify'] = false;
  181. $response = wp_remote_request( $url, $args );
  182. if ( !is_wp_error( $response ) ) {
  183. // The request went through this time, flag for future fallbacks
  184. Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', time() );
  185. Jetpack_Client::set_time_diff( $response, $set_fallback );
  186. }
  187. return $response;
  188. }
  189. public static function set_time_diff( &$response, $force_set = false ) {
  190. $code = wp_remote_retrieve_response_code( $response );
  191. // Only trust the Date header on some responses
  192. if ( 200 != $code && 304 != $code && 400 != $code && 401 != $code ) {
  193. return;
  194. }
  195. if ( !$date = wp_remote_retrieve_header( $response, 'date' ) ) {
  196. return;
  197. }
  198. if ( 0 >= $time = (int) strtotime( $date ) ) {
  199. return;
  200. }
  201. $time_diff = $time - time();
  202. if ( $force_set ) { // during register
  203. Jetpack_Options::update_option( 'time_diff', $time_diff );
  204. } else { // otherwise
  205. $old_diff = Jetpack_Options::get_option( 'time_diff' );
  206. if ( false === $old_diff || abs( $time_diff - (int) $old_diff ) > 10 ) {
  207. Jetpack_Options::update_option( 'time_diff', $time_diff );
  208. }
  209. }
  210. }
  211. /**
  212. * Queries the WordPress.com REST API with a user token.
  213. *
  214. * @param string $path REST API path.
  215. * @param string $version REST API version. Default is `2`.
  216. * @param array $args Arguments to {@see WP_Http}. Default is `array()`.
  217. * @param string $body Body passed to {@see WP_Http}. Default is `null`.
  218. * @param string $base_api_path REST API root. Default is `wpcom`.
  219. *
  220. * @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
  221. */
  222. public static function wpcom_json_api_request_as_user( $path, $version = '2', $args = array(), $body = null, $base_api_path = 'wpcom' ) {
  223. $base_api_path = trim( $base_api_path, '/' );
  224. $version = ltrim( $version, 'v' );
  225. $path = ltrim( $path, '/' );
  226. $args = array_intersect_key( $args, array(
  227. 'headers' => 'array',
  228. 'method' => 'string',
  229. 'timeout' => 'int',
  230. 'redirection' => 'int',
  231. 'stream' => 'boolean',
  232. 'filename' => 'string',
  233. 'sslverify' => 'boolean',
  234. ) );
  235. $args['user_id'] = get_current_user_id();
  236. $args['method'] = isset( $args['method'] ) ? strtoupper( $args['method'] ) : 'GET';
  237. $args['url'] = sprintf( '%s://%s/%s/v%s/%s', self::protocol(), JETPACK__WPCOM_JSON_API_HOST, $base_api_path, $version, $path );
  238. if ( isset( $body ) && ! isset( $args['headers'] ) && in_array( $args['method'], array( 'POST', 'PUT', 'PATCH' ), true ) ) {
  239. $args['headers'] = array( 'Content-Type' => 'application/json' );
  240. }
  241. if ( isset( $body ) && ! is_string( $body ) ) {
  242. $body = wp_json_encode( $body );
  243. }
  244. return self::remote_request( $args, $body );
  245. }
  246. /**
  247. * Query the WordPress.com REST API using the blog token
  248. *
  249. * @param string $path
  250. * @param string $version
  251. * @param array $args
  252. * @param string $body
  253. * @param string $base_api_path
  254. * @return array|WP_Error $response Data.
  255. */
  256. static function wpcom_json_api_request_as_blog( $path, $version = self::WPCOM_JSON_API_VERSION, $args = array(), $body = null, $base_api_path = 'rest' ) {
  257. $filtered_args = array_intersect_key( $args, array(
  258. 'headers' => 'array',
  259. 'method' => 'string',
  260. 'timeout' => 'int',
  261. 'redirection' => 'int',
  262. 'stream' => 'boolean',
  263. 'filename' => 'string',
  264. 'sslverify' => 'boolean',
  265. ) );
  266. // unprecedingslashit
  267. $_path = preg_replace( '/^\//', '', $path );
  268. // Use GET by default whereas `remote_request` uses POST
  269. $request_method = ( isset( $filtered_args['method'] ) ) ? $filtered_args['method'] : 'GET';
  270. $url = sprintf( '%s://%s/%s/v%s/%s', self::protocol(), JETPACK__WPCOM_JSON_API_HOST, $base_api_path, $version, $_path );
  271. $validated_args = array_merge( $filtered_args, array(
  272. 'url' => $url,
  273. 'blog_id' => (int) Jetpack_Options::get_option( 'id' ),
  274. 'method' => $request_method,
  275. ) );
  276. return Jetpack_Client::remote_request( $validated_args, $body );
  277. }
  278. /**
  279. * Takes an array or similar structure and recursively turns all values into strings. This is used to
  280. * make sure that body hashes are made ith the string version, which is what will be seen after a
  281. * server pulls up the data in the $_POST array.
  282. *
  283. * @param array|mixed $data
  284. *
  285. * @return array|string
  286. */
  287. public static function _stringify_data( $data ) {
  288. // Booleans are special, lets just makes them and explicit 1/0 instead of the 0 being an empty string.
  289. if ( is_bool( $data ) ) {
  290. return $data ? "1" : "0";
  291. }
  292. // Cast objects into arrays.
  293. if ( is_object( $data ) ) {
  294. $data = (array) $data;
  295. }
  296. // Non arrays at this point should be just converted to strings.
  297. if ( ! is_array( $data ) ) {
  298. return (string)$data;
  299. }
  300. foreach ( $data as $key => &$value ) {
  301. $value = self::_stringify_data( $value );
  302. }
  303. return $data;
  304. }
  305. /**
  306. * Gets protocol string.
  307. *
  308. * @return string `https` (if possible), else `http`.
  309. */
  310. public static function protocol() {
  311. /**
  312. * Determines whether Jetpack can send outbound https requests to the WPCOM api.
  313. *
  314. * @since 3.6.0
  315. *
  316. * @param bool $proto Defaults to true.
  317. */
  318. $https = apply_filters( 'jetpack_can_make_outbound_https', true );
  319. return $https ? 'https' : 'http';
  320. }
  321. }