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