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