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