PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 8.9.1
Jetpack – WP Security, Backup, Speed, & Growth v8.9.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 5 years ago bin 5 years ago css 5 years ago extensions 5 years ago images 5 years ago json-endpoints 5 years ago modules 5 years ago sal 5 years ago src 6 years ago vendor 5 years ago views 6 years ago .svnignore 12 years ago CODE-OF-CONDUCT.md 9 years ago changelog.txt 5 years ago class-jetpack-wizard-banner.php 6 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 6 years ago class.jetpack-cli.php 5 years ago class.jetpack-client-server.php 6 years ago class.jetpack-connection-banner.php 5 years ago class.jetpack-data.php 6 years ago class.jetpack-debugger.php 7 years ago class.jetpack-error.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 6 years ago class.jetpack-modules-list-table.php 6 years ago class.jetpack-network-sites-list-table.php 6 years ago class.jetpack-network.php 5 years ago class.jetpack-plan.php 5 years ago class.jetpack-post-images.php 6 years ago class.jetpack-twitter-cards.php 5 years ago class.jetpack-user-agent.php 5 years ago class.jetpack-xmlrpc-server.php 6 years ago class.jetpack.php 5 years ago class.json-api-endpoints.php 6 years ago class.json-api.php 6 years ago class.photon.php 5 years ago composer.json 5 years ago functions.compat.php 6 years ago functions.cookies.php 6 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 5 years ago json-api-config.php 10 years ago json-endpoints.php 7 years ago load-jetpack.php 6 years ago locales.php 7 years ago readme.txt 5 years ago require-lib.php 6 years ago uninstall.php 6 years ago wpml-config.xml 10 years ago
class.json-api.php
820 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 function serve( $exit = true ) {
149 ini_set( 'display_errors', false );
150
151 $this->exit = (bool) $exit;
152
153 // This was causing problems with Jetpack, but is necessary for wpcom
154 // @see https://github.com/Automattic/jetpack/pull/2603
155 // @see r124548-wpcom
156 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
157 add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
158 }
159
160 add_filter( 'user_can_richedit', '__return_true' );
161
162 add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
163
164 $initialization = $this->initialize();
165 if ( 'OPTIONS' == $this->method ) {
166 /**
167 * Fires before the page output.
168 * Can be used to specify custom header options.
169 *
170 * @module json-api
171 *
172 * @since 3.1.0
173 */
174 do_action( 'wpcom_json_api_options' );
175 return $this->output( 200, '', 'text/plain' );
176 }
177
178 if ( is_wp_error( $initialization ) ) {
179 $this->output_error( $initialization );
180 return;
181 }
182
183 // Normalize path and extract API version
184 $this->path = untrailingslashit( $this->path );
185 preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
186 $this->path = substr( $this->path, strlen( $matches[0] ) );
187 $this->version = $matches[1];
188
189 $allowed_methods = array( 'GET', 'POST' );
190 $four_oh_five = false;
191
192 $is_help = preg_match( '#/help/?$#i', $this->path );
193 $matching_endpoints = array();
194
195 if ( $is_help ) {
196 $origin = get_http_origin();
197
198 if ( ! empty( $origin ) && 'GET' == $this->method ) {
199 header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
200 }
201
202 $this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
203 // Show help for all matching endpoints regardless of method
204 $methods = $allowed_methods;
205 $find_all_matching_endpoints = true;
206 // How deep to truncate each endpoint's path to see if it matches this help request
207 $depth = substr_count( $this->path, '/' ) + 1;
208 if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
209 $help_content_type = 'json';
210 } else {
211 $help_content_type = 'html';
212 }
213 } else {
214 if ( in_array( $this->method, $allowed_methods ) ) {
215 // Only serve requested method
216 $methods = array( $this->method );
217 $find_all_matching_endpoints = false;
218 } else {
219 // We don't allow this requested method - find matching endpoints and send 405
220 $methods = $allowed_methods;
221 $find_all_matching_endpoints = true;
222 $four_oh_five = true;
223 }
224 }
225
226 // Find which endpoint to serve
227 $found = false;
228 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
229 $endpoint_path_versions = unserialize( $endpoint_path_versions );
230 $endpoint_path = $endpoint_path_versions[0];
231 $endpoint_min_version = $endpoint_path_versions[1];
232 $endpoint_max_version = $endpoint_path_versions[2];
233
234 // Make sure max_version is not less than min_version
235 if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
236 $endpoint_max_version = $endpoint_min_version;
237 }
238
239 foreach ( $methods as $method ) {
240 if ( ! isset( $endpoints_by_method[ $method ] ) ) {
241 continue;
242 }
243
244 // Normalize
245 $endpoint_path = untrailingslashit( $endpoint_path );
246 if ( $is_help ) {
247 // Truncate path at help depth
248 $endpoint_path = join( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
249 }
250
251 // Generate regular expression from sprintf()
252 $endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
253
254 if ( ! preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
255 // This endpoint does not match the requested path.
256 continue;
257 }
258
259 if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
260 // This endpoint does not match the requested version.
261 continue;
262 }
263
264 $found = true;
265
266 if ( $find_all_matching_endpoints ) {
267 $matching_endpoints[] = array( $endpoints_by_method[ $method ], $path_pieces );
268 } else {
269 // The method parameters are now in $path_pieces
270 $endpoint = $endpoints_by_method[ $method ];
271 break 2;
272 }
273 }
274 }
275
276 if ( ! $found ) {
277 return $this->output( 404, '', 'text/plain' );
278 }
279
280 if ( $four_oh_five ) {
281 $allowed_methods = array();
282 foreach ( $matching_endpoints as $matching_endpoint ) {
283 $allowed_methods[] = $matching_endpoint[0]->method;
284 }
285
286 header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
287 return $this->output(
288 405,
289 array(
290 'error' => 'not_allowed',
291 'error_message' => 'Method not allowed',
292 )
293 );
294 }
295
296 if ( $is_help ) {
297 /**
298 * Fires before the API output.
299 *
300 * @since 1.9.0
301 *
302 * @param string help.
303 */
304 do_action( 'wpcom_json_api_output', 'help' );
305 $proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
306 if ( 'json' === $help_content_type ) {
307 $docs = array();
308 foreach ( $matching_endpoints as $matching_endpoint ) {
309 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
310 $docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
311 }
312 }
313 return $this->output( 200, $docs );
314 } else {
315 status_header( 200 );
316 foreach ( $matching_endpoints as $matching_endpoint ) {
317 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
318 call_user_func( array( $matching_endpoint[0], 'document' ) );
319 }
320 }
321 }
322 exit;
323 }
324
325 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
326 return $this->output( 404, '', 'text/plain' );
327 }
328
329 /** This action is documented in class.json-api.php */
330 do_action( 'wpcom_json_api_output', $endpoint->stat );
331
332 $response = $this->process_request( $endpoint, $path_pieces );
333
334 if ( ! $response && ! is_array( $response ) ) {
335 return $this->output( 500, '', 'text/plain' );
336 } elseif ( is_wp_error( $response ) ) {
337 return $this->output_error( $response );
338 }
339
340 $output_status_code = $this->output_status_code;
341 $this->set_output_status_code();
342
343 return $this->output( $output_status_code, $response, 'application/json', $this->extra_headers );
344 }
345
346 function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
347 $this->endpoint = $endpoint;
348 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
349 }
350
351 function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
352 $exit = $this->exit;
353 $this->exit = false;
354 if ( is_wp_error( $response ) ) {
355 $this->output_error( $response );
356 } else {
357 $this->output( $status_code, $response, $content_type );
358 }
359 $this->exit = $exit;
360 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
361 $this->finish_request();
362 }
363 }
364
365 function set_output_status_code( $code = 200 ) {
366 $this->output_status_code = $code;
367 }
368
369 function output( $status_code, $response = null, $content_type = 'application/json', $extra = array() ) {
370 // In case output() was called before the callback returned
371 if ( $this->did_output ) {
372 if ( $this->exit ) {
373 exit;
374 }
375 return $content_type;
376 }
377 $this->did_output = true;
378
379 // 400s and 404s are allowed for all origins
380 if ( 404 == $status_code || 400 == $status_code ) {
381 header( 'Access-Control-Allow-Origin: *' );
382 }
383
384 /* Add headers for form submission from <amp-form/> */
385 if ( $this->amp_source_origin ) {
386 header( 'Access-Control-Allow-Origin: ' . wp_unslash( $this->amp_source_origin ) );
387 header( 'Access-Control-Allow-Credentials: true' );
388 }
389
390
391 if ( is_null( $response ) ) {
392 $response = new stdClass();
393 }
394
395 if ( 'text/plain' === $content_type ||
396 'text/html' === $content_type ) {
397 status_header( (int) $status_code );
398 header( 'Content-Type: ' . $content_type );
399 foreach ( $extra as $key => $value ) {
400 header( "$key: $value" );
401 }
402 echo $response;
403 if ( $this->exit ) {
404 exit;
405 }
406
407 return $content_type;
408 }
409
410 $response = $this->filter_fields( $response );
411
412 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
413 $headers = array(
414 array(
415 'name' => 'Content-Type',
416 'value' => $content_type,
417 ),
418 );
419
420 foreach ( $extra as $key => $value ) {
421 $headers[] = array(
422 'name' => $key,
423 'value' => $value,
424 );
425 }
426
427 $response = array(
428 'code' => (int) $status_code,
429 'headers' => $headers,
430 'body' => $response,
431 );
432 $status_code = 200;
433 $content_type = 'application/json';
434 }
435
436 status_header( (int) $status_code );
437 header( "Content-Type: $content_type" );
438 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
439 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
440 } else {
441 $callback = false;
442 }
443
444 if ( $callback ) {
445 // Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
446 // and by prepending the JSONP response with a JS comment.
447 // [1] https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html
448 echo "/**/$callback(";
449
450 }
451 echo $this->json_encode( $response );
452 if ( $callback ) {
453 echo ');';
454 }
455
456 if ( $this->exit ) {
457 exit;
458 }
459
460 return $content_type;
461 }
462
463 public static function serializable_error( $error ) {
464
465 $status_code = $error->get_error_data();
466
467 if ( is_array( $status_code ) ) {
468 $status_code = $status_code['status_code'];
469 }
470
471 if ( ! $status_code ) {
472 $status_code = 400;
473 }
474 $response = array(
475 'error' => $error->get_error_code(),
476 'message' => $error->get_error_message(),
477 );
478
479 if ( $additional_data = $error->get_error_data( 'additional_data' ) ) {
480 $response['data'] = $additional_data;
481 }
482
483 return array(
484 'status_code' => $status_code,
485 'errors' => $response,
486 );
487 }
488
489 function output_error( $error ) {
490 $error_response = $this->serializable_error( $error );
491
492 return $this->output( $error_response['status_code'], $error_response['errors'] );
493 }
494
495 function filter_fields( $response ) {
496 if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) ) {
497 return $response;
498 }
499
500 $fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
501
502 if ( is_object( $response ) ) {
503 $response = (array) $response;
504 }
505
506 $has_filtered = false;
507 if ( is_array( $response ) && empty( $response['ID'] ) ) {
508 $keys_to_filter = array(
509 'categories',
510 'comments',
511 'connections',
512 'domains',
513 'groups',
514 'likes',
515 'media',
516 'notes',
517 'posts',
518 'services',
519 'sites',
520 'suggestions',
521 'tags',
522 'themes',
523 'topics',
524 'users',
525 );
526
527 foreach ( $keys_to_filter as $key_to_filter ) {
528 if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
529 continue;
530 }
531
532 foreach ( $response[ $key_to_filter ] as $key => $values ) {
533 if ( is_object( $values ) ) {
534 if ( is_object( $response[ $key_to_filter ] ) ) {
535 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
536 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
537 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
538 }
539 } elseif ( is_array( $values ) ) {
540 $response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
541 }
542 }
543
544 $has_filtered = true;
545 }
546 }
547
548 if ( ! $has_filtered ) {
549 if ( is_object( $response ) ) {
550 $response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
551 } elseif ( is_array( $response ) ) {
552 $response = array_intersect_key( $response, array_flip( $fields ) );
553 }
554 }
555
556 return $response;
557 }
558
559 function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
560 if ( $original_scheme ) {
561 return $url;
562 }
563
564 return preg_replace( '#^https:#', 'http:', $url );
565 }
566
567 function comment_edit_pre( $comment_content ) {
568 return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
569 }
570
571 function json_encode( $data ) {
572 return wp_json_encode( $data );
573 }
574
575 function ends_with( $haystack, $needle ) {
576 return $needle === substr( $haystack, -strlen( $needle ) );
577 }
578
579 // Returns the site's blog_id in the WP.com ecosystem
580 function get_blog_id_for_output() {
581 return $this->token_details['blog_id'];
582 }
583
584 // Returns the site's local blog_id
585 function get_blog_id( $blog_id ) {
586 return $GLOBALS['blog_id'];
587 }
588
589 function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
590 if ( $this->is_restricted_blog( $blog_id ) ) {
591 return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
592 }
593
594 if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
595 return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
596 }
597
598 return $blog_id;
599 }
600
601 // Returns true if the specified blog ID is a restricted blog
602 function is_restricted_blog( $blog_id ) {
603 /**
604 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
605 *
606 * @module json-api
607 *
608 * @since 3.4.0
609 *
610 * @param array $array Array of Blog IDs.
611 */
612 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
613 return true === in_array( $blog_id, $restricted_blog_ids );
614 }
615
616 function post_like_count( $blog_id, $post_id ) {
617 return 0;
618 }
619
620 function is_liked( $blog_id, $post_id ) {
621 return false;
622 }
623
624 function is_reblogged( $blog_id, $post_id ) {
625 return false;
626 }
627
628 function is_following( $blog_id ) {
629 return false;
630 }
631
632 function add_global_ID( $blog_id, $post_id ) {
633 return '';
634 }
635
636 function get_avatar_url( $email, $avatar_size = null ) {
637 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
638 return null === $avatar_size
639 ? wpcom_get_avatar_url( $email )
640 : wpcom_get_avatar_url( $email, $avatar_size );
641 } else {
642 return null === $avatar_size
643 ? get_avatar_url( $email )
644 : get_avatar_url( $email, $avatar_size );
645 }
646 }
647
648 /**
649 * Counts the number of comments on a site, excluding certain comment types.
650 *
651 * @param $post_id int Post ID.
652 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
653 */
654 public function wp_count_comments( $post_id ) {
655 global $wpdb;
656 if ( 0 !== $post_id ) {
657 return wp_count_comments( $post_id );
658 }
659
660 $counts = array(
661 'total_comments' => 0,
662 'all' => 0,
663 );
664
665 /**
666 * Exclude certain comment types from comment counts in the REST API.
667 *
668 * @since 6.9.0
669 * @module json-api
670 *
671 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
672 */
673 $exclude = apply_filters(
674 'jetpack_api_exclude_comment_types_count',
675 array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
676 );
677
678 if ( empty( $exclude ) ) {
679 return wp_count_comments( $post_id );
680 }
681
682 array_walk( $exclude, 'esc_sql' );
683 $where = sprintf(
684 "WHERE comment_type NOT IN ( '%s' )",
685 implode( "','", $exclude )
686 );
687
688 $count = $wpdb->get_results(
689 "SELECT comment_approved, COUNT(*) AS num_comments
690 FROM $wpdb->comments
691 {$where}
692 GROUP BY comment_approved
693 "
694 );
695
696 $approved = array(
697 '0' => 'moderated',
698 '1' => 'approved',
699 'spam' => 'spam',
700 'trash' => 'trash',
701 'post-trashed' => 'post-trashed',
702 );
703
704 // https://developer.wordpress.org/reference/functions/get_comment_count/#source
705 foreach ( $count as $row ) {
706 if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
707 $counts['all'] += $row->num_comments;
708 $counts['total_comments'] += $row->num_comments;
709 } elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
710 $counts['total_comments'] += $row->num_comments;
711 }
712 if ( isset( $approved[ $row->comment_approved ] ) ) {
713 $counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
714 }
715 }
716
717 foreach ( $approved as $key ) {
718 if ( empty( $counts[ $key ] ) ) {
719 $counts[ $key ] = 0;
720 }
721 }
722
723 $counts = (object) $counts;
724
725 return $counts;
726 }
727
728 /**
729 * traps `wp_die()` calls and outputs a JSON response instead.
730 * The result is always output, never returned.
731 *
732 * @param string|null $error_code Call with string to start the trapping. Call with null to stop.
733 * @param int $http_status HTTP status code, 400 by default.
734 */
735 function trap_wp_die( $error_code = null, $http_status = 400 ) {
736 if ( is_null( $error_code ) ) {
737 $this->trapped_error = null;
738 // Stop trapping
739 remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
740 return;
741 }
742
743 // If API called via PHP, bail: don't do our custom wp_die(). Do the normal wp_die().
744 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
745 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
746 return;
747 }
748 } else {
749 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
750 return;
751 }
752 }
753
754 $this->trapped_error = array(
755 'status' => $http_status,
756 'code' => $error_code,
757 'message' => '',
758 );
759 // Start trapping
760 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
761 }
762
763 function wp_die_handler_callback() {
764 return array( $this, 'wp_die_handler' );
765 }
766
767 function wp_die_handler( $message, $title = '', $args = array() ) {
768 // Allow wp_die calls to override HTTP status code...
769 $args = wp_parse_args(
770 $args,
771 array(
772 'response' => $this->trapped_error['status'],
773 )
774 );
775
776 // ... unless it's 500
777 if ( (int) $args['response'] !== 500 ) {
778 $this->trapped_error['status'] = $args['response'];
779 }
780
781 if ( $title ) {
782 $message = "$title: $message";
783 }
784
785 $this->trapped_error['message'] = wp_kses( $message, array() );
786
787 switch ( $this->trapped_error['code'] ) {
788 case 'comment_failure':
789 if ( did_action( 'comment_duplicate_trigger' ) ) {
790 $this->trapped_error['code'] = 'comment_duplicate';
791 } elseif ( did_action( 'comment_flood_trigger' ) ) {
792 $this->trapped_error['code'] = 'comment_flood';
793 }
794 break;
795 }
796
797 // We still want to exit so that code execution stops where it should.
798 // Attach the JSON output to the WordPress shutdown handler
799 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
800 exit;
801 }
802
803 function output_trapped_error() {
804 $this->exit = false; // We're already exiting once. Don't do it twice.
805 $this->output(
806 $this->trapped_error['status'],
807 (object) array(
808 'error' => $this->trapped_error['code'],
809 'message' => $this->trapped_error['message'],
810 )
811 );
812 }
813
814 function finish_request() {
815 if ( function_exists( 'fastcgi_finish_request' ) ) {
816 return fastcgi_finish_request();
817 }
818 }
819 }
820