PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 9.8.1
Jetpack – WP Security, Backup, Speed, & Growth v9.8.1
15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / class.json-api.php
jetpack Last commit date
3rd-party 5 years ago _inc 4 years ago css 4 years ago extensions 4 years ago images 5 years ago json-endpoints 4 years ago modules 4 years ago sal 5 years ago src 5 years ago vendor 4 years ago views 5 years ago CHANGELOG.md 4 years ago LICENSE.txt 5 years ago SECURITY.md 5 years ago class-jetpack-connection-status.php 5 years ago class-jetpack-pre-connection-jitms.php 5 years ago class-jetpack-recommendations-banner.php 5 years ago class-jetpack-wizard-banner.php 5 years ago class-jetpack-xmlrpc-methods.php 5 years ago class.frame-nonce-preview.php 6 years ago class.jetpack-admin.php 5 years ago class.jetpack-affiliate.php 6 years ago class.jetpack-autoupdate.php 5 years ago class.jetpack-bbpress-json-api.compat.php 5 years ago class.jetpack-cli.php 5 years ago class.jetpack-client-server.php 5 years ago class.jetpack-connection-banner.php 5 years ago class.jetpack-data.php 5 years ago class.jetpack-gutenberg.php 5 years ago class.jetpack-heartbeat.php 5 years ago class.jetpack-idc.php 6 years ago class.jetpack-ixr-client.php 5 years ago class.jetpack-modules-list-table.php 5 years ago class.jetpack-network-sites-list-table.php 5 years ago class.jetpack-network.php 5 years ago class.jetpack-plan.php 5 years ago class.jetpack-post-images.php 5 years ago class.jetpack-twitter-cards.php 5 years ago class.jetpack-user-agent.php 5 years ago class.jetpack.php 5 years ago class.json-api-endpoints.php 5 years ago class.json-api.php 5 years ago class.photon.php 5 years ago composer.json 4 years ago functions.compat.php 5 years ago functions.cookies.php 5 years ago functions.gallery.php 6 years ago functions.global.php 5 years ago functions.opengraph.php 5 years ago functions.photon.php 5 years ago jest.config.js 5 years ago jetpack.php 4 years ago json-api-config.php 5 years ago json-endpoints.php 7 years ago load-jetpack.php 5 years ago locales.php 7 years ago readme.txt 4 years ago require-lib.php 5 years ago uninstall.php 5 years ago wpml-config.xml 10 years ago
class.json-api.php
872 lines
1 <?php
2
3 defined( 'WPCOM_JSON_API__DEBUG' ) or define( 'WPCOM_JSON_API__DEBUG', false );
4
5 require_once dirname( __FILE__ ) . '/sal/class.json-api-platform.php';
6
7 class WPCOM_JSON_API {
8 static $self = null;
9
10 public $endpoints = array();
11
12 public $token_details = array();
13
14 public $method = '';
15 public $url = '';
16 public $path = '';
17 public $version = null;
18 public $query = array();
19 public $post_body = null;
20 public $files = null;
21 public $content_type = null;
22 public $accept = '';
23
24 public $_server_https;
25 public $exit = true;
26 public $public_api_scheme = 'https';
27
28 public $output_status_code = 200;
29
30 public $trapped_error = null;
31 public $did_output = false;
32
33 public $extra_headers = array();
34
35 public $amp_source_origin = null;
36
37 /**
38 * @return WPCOM_JSON_API instance
39 */
40 static function init( $method = null, $url = null, $post_body = null ) {
41 if ( ! self::$self ) {
42 $class = function_exists( 'get_called_class' ) ? get_called_class() : __CLASS__; // phpcs:ignore PHPCompatibility.PHP.NewFunctions.get_called_classFound
43 self::$self = new $class( $method, $url, $post_body );
44 }
45 return self::$self;
46 }
47
48 function add( WPCOM_JSON_API_Endpoint $endpoint ) {
49 $path_versions = serialize(
50 array(
51 $endpoint->path,
52 $endpoint->min_version,
53 $endpoint->max_version,
54 )
55 );
56 if ( ! isset( $this->endpoints[ $path_versions ] ) ) {
57 $this->endpoints[ $path_versions ] = array();
58 }
59 $this->endpoints[ $path_versions ][ $endpoint->method ] = $endpoint;
60 }
61
62 static function is_truthy( $value ) {
63 switch ( strtolower( (string) $value ) ) {
64 case '1':
65 case 't':
66 case 'true':
67 return true;
68 }
69
70 return false;
71 }
72
73 static function is_falsy( $value ) {
74 switch ( strtolower( (string) $value ) ) {
75 case '0':
76 case 'f':
77 case 'false':
78 return true;
79 }
80
81 return false;
82 }
83
84 function __construct( ...$args ) {
85 call_user_func_array( array( $this, 'setup_inputs' ), $args );
86 }
87
88 function setup_inputs( $method = null, $url = null, $post_body = null ) {
89 if ( is_null( $method ) ) {
90 $this->method = strtoupper( $_SERVER['REQUEST_METHOD'] );
91 } else {
92 $this->method = strtoupper( $method );
93 }
94 if ( is_null( $url ) ) {
95 $this->url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
96 } else {
97 $this->url = $url;
98 }
99
100 $parsed = wp_parse_url( $this->url );
101 if ( ! empty( $parsed['path'] ) ) {
102 $this->path = $parsed['path'];
103 }
104
105 if ( ! empty( $parsed['query'] ) ) {
106 wp_parse_str( $parsed['query'], $this->query );
107 }
108
109 if ( isset( $_SERVER['HTTP_ACCEPT'] ) && $_SERVER['HTTP_ACCEPT'] ) {
110 $this->accept = $_SERVER['HTTP_ACCEPT'];
111 }
112
113 if ( 'POST' === $this->method ) {
114 if ( is_null( $post_body ) ) {
115 $this->post_body = file_get_contents( 'php://input' );
116
117 if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) && $_SERVER['HTTP_CONTENT_TYPE'] ) {
118 $this->content_type = $_SERVER['HTTP_CONTENT_TYPE'];
119 } elseif ( isset( $_SERVER['CONTENT_TYPE'] ) && $_SERVER['CONTENT_TYPE'] ) {
120 $this->content_type = $_SERVER['CONTENT_TYPE'];
121 } elseif ( '{' === $this->post_body[0] ) {
122 $this->content_type = 'application/json';
123 } else {
124 $this->content_type = 'application/x-www-form-urlencoded';
125 }
126
127 if ( 0 === strpos( strtolower( $this->content_type ), 'multipart/' ) ) {
128 $this->post_body = http_build_query( stripslashes_deep( $_POST ) );
129 $this->files = $_FILES;
130 $this->content_type = 'multipart/form-data';
131 }
132 } else {
133 $this->post_body = $post_body;
134 $this->content_type = '{' === isset( $this->post_body[0] ) && $this->post_body[0] ? 'application/json' : 'application/x-www-form-urlencoded';
135 }
136 } else {
137 $this->post_body = null;
138 $this->content_type = null;
139 }
140
141 $this->_server_https = array_key_exists( 'HTTPS', $_SERVER ) ? $_SERVER['HTTPS'] : '--UNset--';
142 }
143
144 function initialize() {
145 $this->token_details['blog_id'] = Jetpack_Options::get_option( 'id' );
146 }
147
148 /**
149 * Checks if the current request is authorized with a blog token.
150 * This method is overridden by a child class in WPCOM.
151 *
152 * @since 9.1.0
153 *
154 * @param boolean|int $site_id The site id.
155 * @return boolean
156 */
157 public function is_jetpack_authorized_for_site( $site_id = false ) {
158 if ( ! $this->token_details ) {
159 return false;
160 }
161
162 $token_details = (object) $this->token_details;
163
164 $site_in_token = (int) $token_details->blog_id;
165
166 if ( $site_in_token < 1 ) {
167 return false;
168 }
169
170 if ( $site_id && $site_in_token !== (int) $site_id ) {
171 return false;
172 }
173
174 if ( (int) get_current_user_id() !== 0 ) {
175 // If Jetpack blog token is used, no logged-in user should exist.
176 return false;
177 }
178
179 return true;
180 }
181
182 function serve( $exit = true ) {
183 ini_set( 'display_errors', false );
184
185 $this->exit = (bool) $exit;
186
187 // This was causing problems with Jetpack, but is necessary for wpcom
188 // @see https://github.com/Automattic/jetpack/pull/2603
189 // @see r124548-wpcom
190 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
191 add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
192 }
193
194 add_filter( 'user_can_richedit', '__return_true' );
195
196 add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
197
198 $initialization = $this->initialize();
199 if ( 'OPTIONS' == $this->method ) {
200 /**
201 * Fires before the page output.
202 * Can be used to specify custom header options.
203 *
204 * @module json-api
205 *
206 * @since 3.1.0
207 */
208 do_action( 'wpcom_json_api_options' );
209 return $this->output( 200, '', 'text/plain' );
210 }
211
212 if ( is_wp_error( $initialization ) ) {
213 $this->output_error( $initialization );
214 return;
215 }
216
217 // Normalize path and extract API version
218 $this->path = untrailingslashit( $this->path );
219 preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
220 $this->path = substr( $this->path, strlen( $matches[0] ) );
221 $this->version = $matches[1];
222
223 $allowed_methods = array( 'GET', 'POST' );
224 $four_oh_five = false;
225
226 $is_help = preg_match( '#/help/?$#i', $this->path );
227 $matching_endpoints = array();
228
229 if ( $is_help ) {
230 $origin = get_http_origin();
231
232 if ( ! empty( $origin ) && 'GET' == $this->method ) {
233 header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
234 }
235
236 $this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
237 // Show help for all matching endpoints regardless of method
238 $methods = $allowed_methods;
239 $find_all_matching_endpoints = true;
240 // How deep to truncate each endpoint's path to see if it matches this help request
241 $depth = substr_count( $this->path, '/' ) + 1;
242 if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
243 $help_content_type = 'json';
244 } else {
245 $help_content_type = 'html';
246 }
247 } else {
248 if ( in_array( $this->method, $allowed_methods ) ) {
249 // Only serve requested method
250 $methods = array( $this->method );
251 $find_all_matching_endpoints = false;
252 } else {
253 // We don't allow this requested method - find matching endpoints and send 405
254 $methods = $allowed_methods;
255 $find_all_matching_endpoints = true;
256 $four_oh_five = true;
257 }
258 }
259
260 // Find which endpoint to serve
261 $found = false;
262 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
263 $endpoint_path_versions = unserialize( $endpoint_path_versions );
264 $endpoint_path = $endpoint_path_versions[0];
265 $endpoint_min_version = $endpoint_path_versions[1];
266 $endpoint_max_version = $endpoint_path_versions[2];
267
268 // Make sure max_version is not less than min_version
269 if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
270 $endpoint_max_version = $endpoint_min_version;
271 }
272
273 foreach ( $methods as $method ) {
274 if ( ! isset( $endpoints_by_method[ $method ] ) ) {
275 continue;
276 }
277
278 // Normalize
279 $endpoint_path = untrailingslashit( $endpoint_path );
280 if ( $is_help ) {
281 // Truncate path at help depth
282 $endpoint_path = join( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
283 }
284
285 // Generate regular expression from sprintf()
286 $endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
287
288 if ( ! preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
289 // This endpoint does not match the requested path.
290 continue;
291 }
292
293 if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
294 // This endpoint does not match the requested version.
295 continue;
296 }
297
298 $found = true;
299
300 if ( $find_all_matching_endpoints ) {
301 $matching_endpoints[] = array( $endpoints_by_method[ $method ], $path_pieces );
302 } else {
303 // The method parameters are now in $path_pieces
304 $endpoint = $endpoints_by_method[ $method ];
305 break 2;
306 }
307 }
308 }
309
310 if ( ! $found ) {
311 return $this->output( 404, '', 'text/plain' );
312 }
313
314 if ( $four_oh_five ) {
315 $allowed_methods = array();
316 foreach ( $matching_endpoints as $matching_endpoint ) {
317 $allowed_methods[] = $matching_endpoint[0]->method;
318 }
319
320 header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
321 return $this->output(
322 405,
323 array(
324 'error' => 'not_allowed',
325 'error_message' => 'Method not allowed',
326 )
327 );
328 }
329
330 if ( $is_help ) {
331 /**
332 * Fires before the API output.
333 *
334 * @since 1.9.0
335 *
336 * @param string help.
337 */
338 do_action( 'wpcom_json_api_output', 'help' );
339 $proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
340 if ( 'json' === $help_content_type ) {
341 $docs = array();
342 foreach ( $matching_endpoints as $matching_endpoint ) {
343 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
344 $docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
345 }
346 }
347 return $this->output( 200, $docs );
348 } else {
349 status_header( 200 );
350 foreach ( $matching_endpoints as $matching_endpoint ) {
351 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
352 call_user_func( array( $matching_endpoint[0], 'document' ) );
353 }
354 }
355 }
356 exit;
357 }
358
359 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
360 return $this->output( 404, '', 'text/plain' );
361 }
362
363 /** This action is documented in class.json-api.php */
364 do_action( 'wpcom_json_api_output', $endpoint->stat );
365
366 $response = $this->process_request( $endpoint, $path_pieces );
367
368 if ( ! $response && ! is_array( $response ) ) {
369 return $this->output( 500, '', 'text/plain' );
370 } elseif ( is_wp_error( $response ) ) {
371 return $this->output_error( $response );
372 }
373
374 $output_status_code = $this->output_status_code;
375 $this->set_output_status_code();
376
377 return $this->output( $output_status_code, $response, 'application/json', $this->extra_headers );
378 }
379
380 function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
381 $this->endpoint = $endpoint;
382 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
383 }
384
385 function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
386 $exit = $this->exit;
387 $this->exit = false;
388 if ( is_wp_error( $response ) ) {
389 $this->output_error( $response );
390 } else {
391 $this->output( $status_code, $response, $content_type );
392 }
393 $this->exit = $exit;
394 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
395 $this->finish_request();
396 }
397 }
398
399 function set_output_status_code( $code = 200 ) {
400 $this->output_status_code = $code;
401 }
402
403 function output( $status_code, $response = null, $content_type = 'application/json', $extra = array() ) {
404 // In case output() was called before the callback returned
405 if ( $this->did_output ) {
406 if ( $this->exit ) {
407 exit;
408 }
409 return $content_type;
410 }
411 $this->did_output = true;
412
413 // 400s and 404s are allowed for all origins
414 if ( 404 == $status_code || 400 == $status_code ) {
415 header( 'Access-Control-Allow-Origin: *' );
416 }
417
418 /* Add headers for form submission from <amp-form/> */
419 if ( $this->amp_source_origin ) {
420 header( 'Access-Control-Allow-Origin: ' . wp_unslash( $this->amp_source_origin ) );
421 header( 'Access-Control-Allow-Credentials: true' );
422 }
423
424
425 if ( is_null( $response ) ) {
426 $response = new stdClass();
427 }
428
429 if ( 'text/plain' === $content_type ||
430 'text/html' === $content_type ) {
431 status_header( (int) $status_code );
432 header( 'Content-Type: ' . $content_type );
433 foreach ( $extra as $key => $value ) {
434 header( "$key: $value" );
435 }
436 echo $response;
437 if ( $this->exit ) {
438 exit;
439 }
440
441 return $content_type;
442 }
443
444 $response = $this->filter_fields( $response );
445
446 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
447 $headers = array(
448 array(
449 'name' => 'Content-Type',
450 'value' => $content_type,
451 ),
452 );
453
454 foreach ( $extra as $key => $value ) {
455 $headers[] = array(
456 'name' => $key,
457 'value' => $value,
458 );
459 }
460
461 $response = array(
462 'code' => (int) $status_code,
463 'headers' => $headers,
464 'body' => $response,
465 );
466 $status_code = 200;
467 $content_type = 'application/json';
468 }
469
470 status_header( (int) $status_code );
471 header( "Content-Type: $content_type" );
472 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
473 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
474 } else {
475 $callback = false;
476 }
477
478 if ( $callback ) {
479 // Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
480 // and by prepending the JSONP response with a JS comment.
481 // [1] https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html
482 echo "/**/$callback(";
483
484 }
485 echo $this->json_encode( $response );
486 if ( $callback ) {
487 echo ');';
488 }
489
490 if ( $this->exit ) {
491 exit;
492 }
493
494 return $content_type;
495 }
496
497 public static function serializable_error( $error ) {
498
499 $status_code = $error->get_error_data();
500
501 if ( is_array( $status_code ) ) {
502 $status_code = $status_code['status_code'];
503 }
504
505 if ( ! $status_code ) {
506 $status_code = 400;
507 }
508 $response = array(
509 'error' => $error->get_error_code(),
510 'message' => $error->get_error_message(),
511 );
512
513 if ( $additional_data = $error->get_error_data( 'additional_data' ) ) {
514 $response['data'] = $additional_data;
515 }
516
517 return array(
518 'status_code' => $status_code,
519 'errors' => $response,
520 );
521 }
522
523 function output_error( $error ) {
524 $error_response = $this->serializable_error( $error );
525
526 return $this->output( $error_response['status_code'], $error_response['errors'] );
527 }
528
529 function filter_fields( $response ) {
530 if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) ) {
531 return $response;
532 }
533
534 $fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
535
536 if ( is_object( $response ) ) {
537 $response = (array) $response;
538 }
539
540 $has_filtered = false;
541 if ( is_array( $response ) && empty( $response['ID'] ) ) {
542 $keys_to_filter = array(
543 'categories',
544 'comments',
545 'connections',
546 'domains',
547 'groups',
548 'likes',
549 'media',
550 'notes',
551 'posts',
552 'services',
553 'sites',
554 'suggestions',
555 'tags',
556 'themes',
557 'topics',
558 'users',
559 );
560
561 foreach ( $keys_to_filter as $key_to_filter ) {
562 if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
563 continue;
564 }
565
566 foreach ( $response[ $key_to_filter ] as $key => $values ) {
567 if ( is_object( $values ) ) {
568 if ( is_object( $response[ $key_to_filter ] ) ) {
569 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
570 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
571 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
572 }
573 } elseif ( is_array( $values ) ) {
574 $response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
575 }
576 }
577
578 $has_filtered = true;
579 }
580 }
581
582 if ( ! $has_filtered ) {
583 if ( is_object( $response ) ) {
584 $response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
585 } elseif ( is_array( $response ) ) {
586 $response = array_intersect_key( $response, array_flip( $fields ) );
587 }
588 }
589
590 return $response;
591 }
592
593 function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
594 if ( $original_scheme ) {
595 return $url;
596 }
597
598 return preg_replace( '#^https:#', 'http:', $url );
599 }
600
601 function comment_edit_pre( $comment_content ) {
602 return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
603 }
604
605 function json_encode( $data ) {
606 return wp_json_encode( $data );
607 }
608
609 function ends_with( $haystack, $needle ) {
610 return $needle === substr( $haystack, -strlen( $needle ) );
611 }
612
613 // Returns the site's blog_id in the WP.com ecosystem
614 function get_blog_id_for_output() {
615 return $this->token_details['blog_id'];
616 }
617
618 // Returns the site's local blog_id
619 function get_blog_id( $blog_id ) {
620 return $GLOBALS['blog_id'];
621 }
622
623 function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
624 if ( $this->is_restricted_blog( $blog_id ) ) {
625 return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
626 }
627 /**
628 * If this is a private site we check for 2 things:
629 * 1. In case of user based authentication, we need to check if the logged-in user has the 'read' capability.
630 * 2. In case of site based authentication, make sure the endpoint accepts it.
631 */
632 if ( -1 === (int) get_option( 'blog_public' ) &&
633 ! current_user_can( 'read' ) &&
634 ! $this->endpoint->accepts_site_based_authentication()
635 ) {
636 return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
637 }
638
639 return $blog_id;
640 }
641
642 // Returns true if the specified blog ID is a restricted blog
643 function is_restricted_blog( $blog_id ) {
644 /**
645 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
646 *
647 * @module json-api
648 *
649 * @since 3.4.0
650 *
651 * @param array $array Array of Blog IDs.
652 */
653 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
654 return true === in_array( $blog_id, $restricted_blog_ids );
655 }
656
657 function post_like_count( $blog_id, $post_id ) {
658 return 0;
659 }
660
661 function is_liked( $blog_id, $post_id ) {
662 return false;
663 }
664
665 function is_reblogged( $blog_id, $post_id ) {
666 return false;
667 }
668
669 function is_following( $blog_id ) {
670 return false;
671 }
672
673 function add_global_ID( $blog_id, $post_id ) {
674 return '';
675 }
676
677 function get_avatar_url( $email, $avatar_size = null ) {
678 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
679 return null === $avatar_size
680 ? wpcom_get_avatar_url( $email )
681 : wpcom_get_avatar_url( $email, $avatar_size );
682 } else {
683 return null === $avatar_size
684 ? get_avatar_url( $email )
685 : get_avatar_url( $email, $avatar_size );
686 }
687 }
688
689 /**
690 * Counts the number of comments on a site, excluding certain comment types.
691 *
692 * @param $post_id int Post ID.
693 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
694 */
695 public function wp_count_comments( $post_id ) {
696 global $wpdb;
697 if ( 0 !== $post_id ) {
698 return wp_count_comments( $post_id );
699 }
700
701 $counts = array(
702 'total_comments' => 0,
703 'all' => 0,
704 );
705
706 /**
707 * Exclude certain comment types from comment counts in the REST API.
708 *
709 * @since 6.9.0
710 * @module json-api
711 *
712 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
713 */
714 $exclude = apply_filters(
715 'jetpack_api_exclude_comment_types_count',
716 array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
717 );
718
719 if ( empty( $exclude ) ) {
720 return wp_count_comments( $post_id );
721 }
722
723 array_walk( $exclude, 'esc_sql' );
724 $where = sprintf(
725 "WHERE comment_type NOT IN ( '%s' )",
726 implode( "','", $exclude )
727 );
728
729 $count = $wpdb->get_results(
730 "SELECT comment_approved, COUNT(*) AS num_comments
731 FROM $wpdb->comments
732 {$where}
733 GROUP BY comment_approved
734 "
735 );
736
737 $approved = array(
738 '0' => 'moderated',
739 '1' => 'approved',
740 'spam' => 'spam',
741 'trash' => 'trash',
742 'post-trashed' => 'post-trashed',
743 );
744
745 // https://developer.wordpress.org/reference/functions/get_comment_count/#source
746 foreach ( $count as $row ) {
747 if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
748 $counts['all'] += $row->num_comments;
749 $counts['total_comments'] += $row->num_comments;
750 } elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
751 $counts['total_comments'] += $row->num_comments;
752 }
753 if ( isset( $approved[ $row->comment_approved ] ) ) {
754 $counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
755 }
756 }
757
758 foreach ( $approved as $key ) {
759 if ( empty( $counts[ $key ] ) ) {
760 $counts[ $key ] = 0;
761 }
762 }
763
764 $counts = (object) $counts;
765
766 return $counts;
767 }
768
769 /**
770 * traps `wp_die()` calls and outputs a JSON response instead.
771 * The result is always output, never returned.
772 *
773 * @param string|null $error_code Call with string to start the trapping. Call with null to stop.
774 * @param int $http_status HTTP status code, 400 by default.
775 */
776 function trap_wp_die( $error_code = null, $http_status = 400 ) {
777 // Determine the filter name; based on the conditionals inside the wp_die function.
778 if ( wp_is_json_request() ) {
779 $die_handler = 'wp_die_json_handler';
780 } elseif ( wp_is_jsonp_request() ) {
781 $die_handler = 'wp_die_jsonp_handler';
782 } elseif ( wp_is_xml_request() ) {
783 $die_handler = 'wp_die_xml_handler';
784 } else {
785 $die_handler = 'wp_die_handler';
786 }
787
788 if ( is_null( $error_code ) ) {
789 $this->trapped_error = null;
790 // Stop trapping
791 remove_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
792 return;
793 }
794
795 // If API called via PHP, bail: don't do our custom wp_die(). Do the normal wp_die().
796 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
797 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
798 return;
799 }
800 } else {
801 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
802 return;
803 }
804 }
805
806 $this->trapped_error = array(
807 'status' => $http_status,
808 'code' => $error_code,
809 'message' => '',
810 );
811 // Start trapping
812 add_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
813 }
814
815 function wp_die_handler_callback() {
816 return array( $this, 'wp_die_handler' );
817 }
818
819 function wp_die_handler( $message, $title = '', $args = array() ) {
820 // Allow wp_die calls to override HTTP status code...
821 $args = wp_parse_args(
822 $args,
823 array(
824 'response' => $this->trapped_error['status'],
825 )
826 );
827
828 // ... unless it's 500
829 if ( (int) $args['response'] !== 500 ) {
830 $this->trapped_error['status'] = $args['response'];
831 }
832
833 if ( $title ) {
834 $message = "$title: $message";
835 }
836
837 $this->trapped_error['message'] = wp_kses( $message, array() );
838
839 switch ( $this->trapped_error['code'] ) {
840 case 'comment_failure':
841 if ( did_action( 'comment_duplicate_trigger' ) ) {
842 $this->trapped_error['code'] = 'comment_duplicate';
843 } elseif ( did_action( 'comment_flood_trigger' ) ) {
844 $this->trapped_error['code'] = 'comment_flood';
845 }
846 break;
847 }
848
849 // We still want to exit so that code execution stops where it should.
850 // Attach the JSON output to the WordPress shutdown handler
851 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
852 exit;
853 }
854
855 function output_trapped_error() {
856 $this->exit = false; // We're already exiting once. Don't do it twice.
857 $this->output(
858 $this->trapped_error['status'],
859 (object) array(
860 'error' => $this->trapped_error['code'],
861 'message' => $this->trapped_error['message'],
862 )
863 );
864 }
865
866 function finish_request() {
867 if ( function_exists( 'fastcgi_finish_request' ) ) {
868 return fastcgi_finish_request();
869 }
870 }
871 }
872