PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 3.2.5
Jetpack – WP Security, Backup, Speed, & Growth v3.2.5
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / class.json-api.php

class.json-api.php in Jetpack – WP Security, Backup, Speed, & Growth 3.2.5, at class.json-api.php

639 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined( 'WPCOM_JSON_API__DEBUG' ) or define( 'WPCOM_JSON_API__DEBUG', false );
3 if( defined( 'WPCOM_JSON_API__DEBUG' ) && WPCOM_JSON_API__DEBUG )
4 require_once ABSPATH . 'wp-content/lib/statsd-client.php';
5
6
7
8 class WPCOM_JSON_API {
9 static $self = null;
10
11 var $endpoints = array();
12
13 var $token_details = array();
14
15 var $method = '';
16 var $url = '';
17 var $path = '';
18 var $version = null;
19 var $query = array();
20 var $post_body = null;
21 var $files = null;
22 var $content_type = null;
23 var $accept = '';
24
25 var $_server_https;
26 var $exit = true;
27 var $public_api_scheme = 'https';
28
29 var $trapped_error = null;
30 var $did_output = false;
31
32 static function init( $method = null, $url = null, $post_body = null ) {
33 if ( !self::$self ) {
34 $class = function_exists( 'get_called_class' ) ? get_called_class() : __CLASS__;
35 self::$self = new $class( $method, $url, $post_body );
36 }
37 return self::$self;
38 }
39
40 function add( WPCOM_JSON_API_Endpoint $endpoint ) {
41 $path_versions = serialize( array (
42 $endpoint->path,
43 $endpoint->min_version,
44 $endpoint->max_version,
45 ) );
46 if ( !isset( $this->endpoints[$path_versions] ) ) {
47 $this->endpoints[$path_versions] = array();
48 }
49 $this->endpoints[$path_versions][$endpoint->method] = $endpoint;
50 }
51
52 static function is_truthy( $value ) {
53 switch ( strtolower( (string) $value ) ) {
54 case '1' :
55 case 't' :
56 case 'true' :
57 return true;
58 }
59
60 return false;
61 }
62
63 function __construct() {
64 $args = func_get_args();
65 call_user_func_array( array( $this, 'setup_inputs' ), $args );
66 }
67
68 function setup_inputs( $method = null, $url = null, $post_body = null ) {
69 if ( is_null( $method ) ) {
70 $this->method = strtoupper( $_SERVER['REQUEST_METHOD'] );
71 } else {
72 $this->method = strtoupper( $method );
73 }
74 if ( is_null( $url ) ) {
75 $this->url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
76 } else {
77 $this->url = $url;
78 }
79
80 $parsed = parse_url( $this->url );
81 $this->path = $parsed['path'];
82
83 if ( !empty( $parsed['query'] ) ) {
84 wp_parse_str( $parsed['query'], $this->query );
85 }
86
87 if ( isset( $_SERVER['HTTP_ACCEPT'] ) && $_SERVER['HTTP_ACCEPT'] ) {
88 $this->accept = $_SERVER['HTTP_ACCEPT'];
89 }
90
91 if ( 'POST' === $this->method ) {
92 if ( is_null( $post_body ) ) {
93 $this->post_body = file_get_contents( 'php://input' );
94
95 if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) && $_SERVER['HTTP_CONTENT_TYPE'] ) {
96 $this->content_type = $_SERVER['HTTP_CONTENT_TYPE'];
97 } elseif ( isset( $_SERVER['CONTENT_TYPE'] ) && $_SERVER['CONTENT_TYPE'] ) {
98 $this->content_type = $_SERVER['CONTENT_TYPE'] ;
99 } elseif ( '{' === $this->post_body[0] ) {
100 $this->content_type = 'application/json';
101 } else {
102 $this->content_type = 'application/x-www-form-urlencoded';
103 }
104
105 if ( 0 === strpos( strtolower( $this->content_type ), 'multipart/' ) ) {
106 $this->post_body = http_build_query( stripslashes_deep( $_POST ) );
107 $this->files = $_FILES;
108 $this->content_type = 'multipart/form-data';
109 }
110 } else {
111 $this->post_body = $post_body;
112 $this->content_type = '{' === isset( $this->post_body[0] ) && $this->post_body[0] ? 'application/json' : 'application/x-www-form-urlencoded';
113 }
114 } else {
115 $this->post_body = null;
116 $this->content_type = null;
117 }
118
119 $this->_server_https = array_key_exists( 'HTTPS', $_SERVER ) ? $_SERVER['HTTPS'] : '--UNset--';
120 }
121
122 function initialize() {
123 $this->token_details['blog_id'] = Jetpack_Options::get_option( 'id' );
124 }
125
126 function serve( $exit = true ) {
127 ini_set( 'display_errors', false );
128
129 $this->exit = (bool) $exit;
130
131 add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
132
133 add_filter( 'user_can_richedit', '__return_true' );
134
135 add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
136
137 $initialization = $this->initialize();
138 if ( is_wp_error( $initialization ) ) {
139 $this->output_error( $initialization );
140 return;
141 }
142
143 // Normalize path and extract API version
144 $this->path = untrailingslashit( $this->path );
145 preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
146 $this->path = substr( $this->path, strlen( $matches[0] ) );
147 $this->version = $matches[1];
148
149 $allowed_methods = array( 'GET', 'POST' );
150 $four_oh_five = false;
151
152 $is_help = preg_match( '#/help/?$#i', $this->path );
153 $matching_endpoints = array();
154
155 if ( 'OPTIONS' == $this->method ) {
156 do_action( 'wpcom_json_api_options' );
157 exit;
158 }
159
160 if ( $is_help ) {
161 $origin = get_http_origin();
162
163 if ( !empty( $origin ) && 'GET' == $this->method ) {
164 header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
165 }
166
167 $this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
168 // Show help for all matching endpoints regardless of method
169 $methods = $allowed_methods;
170 $find_all_matching_endpoints = true;
171 // How deep to truncate each endpoint's path to see if it matches this help request
172 $depth = substr_count( $this->path, '/' ) + 1;
173 if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
174 $help_content_type = 'json';
175 } else {
176 $help_content_type = 'html';
177 }
178 } else {
179 if ( in_array( $this->method, $allowed_methods ) ) {
180 // Only serve requested method
181 $methods = array( $this->method );
182 $find_all_matching_endpoints = false;
183 } else {
184 // We don't allow this requested method - find matching endpoints and send 405
185 $methods = $allowed_methods;
186 $find_all_matching_endpoints = true;
187 $four_oh_five = true;
188 }
189 }
190
191 // Find which endpoint to serve
192 $found = false;
193 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
194 $endpoint_path_versions = unserialize( $endpoint_path_versions );
195 $endpoint_path = $endpoint_path_versions[0];
196 $endpoint_min_version = $endpoint_path_versions[1];
197 $endpoint_max_version = $endpoint_path_versions[2];
198 foreach ( $methods as $method ) {
199 if ( !isset( $endpoints_by_method[$method] ) ) {
200 continue;
201 }
202
203 // Normalize
204 $endpoint_path = untrailingslashit( $endpoint_path );
205 if ( $is_help ) {
206 // Truncate path at help depth
207 $endpoint_path = join( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
208 }
209
210 // Generate regular expression from sprintf()
211 $endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
212
213 if ( !preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
214 // This endpoint does not match the requested path.
215 continue;
216 }
217
218 if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
219 // This endpoint does not match the requested version.
220 continue;
221 }
222
223 $found = true;
224
225 if ( $find_all_matching_endpoints ) {
226 $matching_endpoints[] = array( $endpoints_by_method[$method], $path_pieces );
227 } else {
228 // The method parameters are now in $path_pieces
229 $endpoint = $endpoints_by_method[$method];
230 break 2;
231 }
232 }
233 }
234
235 if ( !$found ) {
236 return $this->output( 404, '', 'text/plain' );
237 }
238
239 if ( $four_oh_five ) {
240 $allowed_methods = array();
241 foreach ( $matching_endpoints as $matching_endpoint ) {
242 $allowed_methods[] = $matching_endpoint[0]->method;
243 }
244
245 header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
246 return $this->output( 405, array( 'error' => 'not_allowed', 'error_message' => 'Method not allowed' ) );
247 }
248
249 if ( $is_help ) {
250 do_action( 'wpcom_json_api_output', 'help' );
251 if ( 'json' === $help_content_type ) {
252 $docs = array();
253 foreach ( $matching_endpoints as $matching_endpoint ) {
254 if ( $matching_endpoint[0]->is_publicly_documentable() || WPCOM_JSON_API__DEBUG )
255 $docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
256 }
257 return $this->output( 200, $docs );
258 } else {
259 status_header( 200 );
260 foreach ( $matching_endpoints as $matching_endpoint ) {
261 if ( $matching_endpoint[0]->is_publicly_documentable() || WPCOM_JSON_API__DEBUG )
262 call_user_func( array( $matching_endpoint[0], 'document' ) );
263 }
264 }
265 exit;
266 }
267
268 if ( $endpoint->in_testing && !WPCOM_JSON_API__DEBUG ) {
269 return $this->output( 404, '', 'text/plain' );
270 }
271
272 do_action( 'wpcom_json_api_output', $endpoint->stat );
273
274 $response = $this->process_request( $endpoint, $path_pieces );
275
276 if ( !$response && !is_array( $response ) ) {
277 return $this->output( 500, '', 'text/plain' );
278 } elseif ( is_wp_error( $response ) ) {
279 return $this->output_error( $response );
280 }
281
282 return $this->output( 200, $response );
283 }
284
285 function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
286 $this->endpoint = $endpoint;
287
288 // Process API request and time it
289 $api_timer = microtime( true );
290 $response = call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
291 $api_timer = 1000 * ( microtime( true ) - $api_timer );
292 if( defined( 'WPCOM_JSON_API__DEBUG' ) && WPCOM_JSON_API__DEBUG ) {
293 // Don't track API timings per node / DC for now, maybe in the future
294 $statsd = new StatsD();
295 $statsd_prefix = 'com.wordpress.web.ALL.ALL.rest_api.method';
296 $statsd_name = str_replace( ':', '.', $endpoint->stat );
297
298 if ( ( !$response && !is_array( $response ) ) || is_wp_error( $response ) ) {
299 $statsd->timing( "{$statsd_prefix}.error.{$statsd_name}", $api_timer );
300 } else {
301 $statsd->timing( "{$statsd_prefix}.ok.{$statsd_name}", $api_timer );
302 }
303 }
304 return $response;
305 }
306
307 function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
308 $exit = $this->exit;
309 $this->exit = false;
310 if ( is_wp_error( $response ) )
311 $this->output_error( $response );
312 else
313 $this->output( $status_code, $response, $content_type );
314 $this->exit = $exit;
315 $this->finish_request();
316 }
317
318 function output( $status_code, $response = null, $content_type = 'application/json' ) {
319 // In case output() was called before the callback returned
320 if ( $this->did_output ) {
321 if ( $this->exit )
322 exit;
323 return $content_type;
324 }
325 $this->did_output = true;
326
327 // 400s and 404s are allowed for all origins
328 if ( 404 == $status_code || 400 == $status_code )
329 header( 'Access-Control-Allow-Origin: *' );
330
331 if ( is_null( $response ) ) {
332 $response = new stdClass;
333 }
334
335 if ( 'text/plain' === $content_type ) {
336 status_header( (int) $status_code );
337 header( 'Content-Type: text/plain' );
338 echo $response;
339 if ( $this->exit ) {
340 exit;
341 }
342
343 return $content_type;
344 }
345
346 $response = $this->filter_fields( $response );
347
348 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
349 $response = array(
350 'code' => (int) $status_code,
351 'headers' => array(
352 array(
353 'name' => 'Content-Type',
354 'value' => $content_type,
355 ),
356 ),
357 'body' => $response,
358 );
359 $status_code = 200;
360 $content_type = 'application/json';
361 }
362
363 status_header( (int) $status_code );
364 header( "Content-Type: $content_type" );
365 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
366 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
367 } else {
368 $callback = false;
369 }
370
371 if ( $callback ) {
372 // Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
373 // and by prepending the JSONP response with a JS comment.
374 // [1] http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
375 echo "/**/$callback(";
376
377 }
378 echo $this->json_encode( $response );
379 if ( $callback ) {
380 echo ");";
381 }
382
383 if ( $this->exit ) {
384 exit;
385 }
386
387 return $content_type;
388 }
389
390 public static function serializable_error ( $error ) {
391
392 $status_code = $error->get_error_data();
393
394 if ( is_array( $status_code ) )
395 $status_code = $status_code['status_code'];
396
397 if ( !$status_code ) {
398 $status_code = 400;
399 }
400 $response = array(
401 'error' => $error->get_error_code(),
402 'message' => $error->get_error_message(),
403 );
404 return array(
405 'status_code' => $status_code,
406 'errors' => $response
407 );
408 }
409
410 function output_error( $error ) {
411 if ( function_exists( 'bump_stats_extra' ) )
412 bump_stats_extra( 'rest-api-errors', $this->token_details['client_id'] );
413
414 $error_response = $this->serializable_error( $error );
415
416 return $this->output( $error_response[ 'status_code'], $error_response['errors'] );
417 }
418
419 function filter_fields( $response ) {
420 if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) )
421 return $response;
422
423 $fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
424
425 if ( is_object( $response ) ) {
426 $response = (array) $response;
427 }
428
429 $has_filtered = false;
430 if ( is_array( $response ) && empty( $response['ID'] ) ) {
431 $keys_to_filter = array(
432 'categories',
433 'comments',
434 'connections',
435 'domains',
436 'groups',
437 'likes',
438 'media',
439 'notes',
440 'posts',
441 'services',
442 'sites',
443 'suggestions',
444 'tags',
445 'themes',
446 'topics',
447 'users',
448 );
449
450 foreach ( $keys_to_filter as $key_to_filter ) {
451 if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered )
452 continue;
453
454 foreach ( $response[ $key_to_filter ] as $key => $values ) {
455 if ( is_object( $values ) ) {
456 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( (array) $values, array_flip( $fields ) );
457 } elseif ( is_array( $values ) ) {
458 $response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
459 }
460 }
461
462 $has_filtered = true;
463 }
464 }
465
466 if ( ! $has_filtered ) {
467 if ( is_object( $response ) ) {
468 $response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
469 } else if ( is_array( $response ) ) {
470 $response = array_intersect_key( $response, array_flip( $fields ) );
471 }
472 }
473
474 return $response;
475 }
476
477 function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
478 if ( $original_scheme ) {
479 return $url;
480 }
481
482 return preg_replace( '#^https:#', 'http:', $url );
483 }
484
485 function comment_edit_pre( $comment_content ) {
486 return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
487 }
488
489 function json_encode( $data ) {
490 return json_encode( $data );
491 }
492
493 function ends_with( $haystack, $needle ) {
494 return $needle === substr( $haystack, -strlen( $needle ) );
495 }
496
497 // Returns the site's blog_id in the WP.com ecosystem
498 function get_blog_id_for_output() {
499 return $this->token_details['blog_id'];
500 }
501
502 // Returns the site's local blog_id
503 function get_blog_id( $blog_id ) {
504 return $GLOBALS['blog_id'];
505 }
506
507 function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
508 if ( -1 == get_option( 'blog_public' ) && !current_user_can( 'read' ) ) {
509 return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
510 }
511
512 return $blog_id;
513 }
514
515 function post_like_count( $blog_id, $post_id ) {
516 return 0;
517 }
518
519 function is_liked( $blog_id, $post_id ) {
520 return false;
521 }
522
523 function is_reblogged( $blog_id, $post_id ) {
524 return false;
525 }
526
527 function is_following( $blog_id ) {
528 return false;
529 }
530
531 function add_global_ID( $blog_id, $post_id ) {
532 return '';
533 }
534
535 function get_avatar_url( $email, $avatar_size = 96 ) {
536 add_filter( 'pre_option_show_avatars', '__return_true', 999 );
537 $_SERVER['HTTPS'] = 'off';
538
539 $avatar_img_element = get_avatar( $email, $avatar_size, '' );
540
541 if ( !$avatar_img_element || is_wp_error( $avatar_img_element ) ) {
542 $return = '';
543 } elseif ( !preg_match( '#src=([\'"])?(.*?)(?(1)\\1|\s)#', $avatar_img_element, $matches ) ) {
544 $return = '';
545 } else {
546 $return = esc_url_raw( htmlspecialchars_decode( $matches[2] ) );
547 }
548
549 remove_filter( 'pre_option_show_avatars', '__return_true', 999 );
550 if ( '--UNset--' === $this->_server_https ) {
551 unset( $_SERVER['HTTPS'] );
552 } else {
553 $_SERVER['HTTPS'] = $this->_server_https;
554 }
555
556 return $return;
557 }
558
559 /**
560 * Traps `wp_die()` calls and outputs a JSON response instead.
561 * The result is always output, never returned.
562 *
563 * @param string|null $error_code. Call with string to start the trapping. Call with null to stop.
564 */
565 function trap_wp_die( $error_code = null ) {
566 // Stop trapping
567 if ( is_null( $error_code ) ) {
568 $this->trapped_error = null;
569 remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
570 return;
571 }
572
573 // If API called via PHP, bail: don't do our custom wp_die(). Do the normal wp_die().
574 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
575 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
576 return;
577 }
578 } else {
579 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
580 return;
581 }
582 }
583
584 // Start trapping
585 $this->trapped_error = array(
586 'status' => 500,
587 'code' => $error_code,
588 'message' => '',
589 );
590
591 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
592 }
593
594 function wp_die_handler_callback() {
595 return array( $this, 'wp_die_handler' );
596 }
597
598 function wp_die_handler( $message, $title = '', $args = array() ) {
599 $args = wp_parse_args( $args, array(
600 'response' => 500,
601 ) );
602
603 if ( $title ) {
604 $message = "$title: $message";
605 }
606
607 switch ( $this->trapped_error['code'] ) {
608 case 'comment_failure' :
609 if ( did_action( 'comment_duplicate_trigger' ) ) {
610 $this->trapped_error['code'] = 'comment_duplicate';
611 } else if ( did_action( 'comment_flood_trigger' ) ) {
612 $this->trapped_error['code'] = 'comment_flood';
613 }
614 break;
615 }
616
617 $this->trapped_error['status'] = $args['response'];
618 $this->trapped_error['message'] = wp_kses( $message, array() );
619
620 // We still want to exit so that code execution stops where it should.
621 // Attach the JSON output to WordPress' shutdown handler
622 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
623 exit;
624 }
625
626 function output_trapped_error() {
627 $this->exit = false; // We're already exiting once. Don't do it twice.
628 $this->output( $this->trapped_error['status'], (object) array(
629 'error' => $this->trapped_error['code'],
630 'message' => $this->trapped_error['message'],
631 ) );
632 }
633
634 function finish_request() {
635 if ( function_exists( 'fastcgi_finish_request' ) )
636 return fastcgi_finish_request();
637 }
638 }
639