PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 11.3.4
Jetpack – WP Security, Backup, Speed, & Growth v11.3.4
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 3 years ago _inc 3 years ago css 3 years ago extensions 3 years ago images 3 years ago jetpack_vendor 3 years ago json-endpoints 3 years ago modules 1 year ago sal 3 years ago src 4 years ago vendor 3 years ago views 4 years ago CHANGELOG.md 3 years ago LICENSE.txt 5 years ago SECURITY.md 5 years ago class-jetpack-connection-status.php 5 years ago class-jetpack-gallery-settings.php 4 years ago class-jetpack-pre-connection-jitms.php 4 years ago class-jetpack-recommendations-banner.php 3 years ago class-jetpack-stats-dashboard-widget.php 3 years ago class-jetpack-wizard-banner.php 5 years ago class-jetpack-xmlrpc-methods.php 4 years ago class.frame-nonce-preview.php 4 years ago class.jetpack-admin.php 3 years ago class.jetpack-affiliate.php 4 years ago class.jetpack-autoupdate.php 4 years ago class.jetpack-bbpress-json-api.compat.php 5 years ago class.jetpack-cli.php 3 years ago class.jetpack-client-server.php 4 years ago class.jetpack-connection-banner.php 3 years ago class.jetpack-data.php 5 years ago class.jetpack-gutenberg.php 3 years ago class.jetpack-heartbeat.php 4 years ago class.jetpack-idc.php 4 years ago class.jetpack-modules-list-table.php 3 years ago class.jetpack-network-sites-list-table.php 4 years ago class.jetpack-network.php 4 years ago class.jetpack-plan.php 3 years ago class.jetpack-post-images.php 3 years ago class.jetpack-twitter-cards.php 4 years ago class.jetpack-user-agent.php 4 years ago class.jetpack.php 3 years ago class.json-api-endpoints.php 3 years ago class.json-api.php 3 years ago class.photon.php 3 years ago composer.json 3 years ago enhanced-open-graph.php 4 years ago functions.compat.php 3 years ago functions.cookies.php 5 years ago functions.global.php 3 years ago functions.opengraph.php 4 years ago functions.photon.php 4 years ago jetpack.php 1 year ago json-api-config.php 5 years ago json-endpoints.php 4 years ago load-jetpack.php 3 years ago locales.php 4 years ago readme.txt 1 year ago require-lib.php 3 years ago uninstall.php 5 years ago wpml-config.xml 3 years ago
class.json-api.php
1222 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Jetpack JSON API.
4 *
5 * @package automattic/jetpack
6 */
7
8 if ( ! defined( 'WPCOM_JSON_API__DEBUG' ) ) {
9 define( 'WPCOM_JSON_API__DEBUG', false );
10 }
11
12 require_once __DIR__ . '/sal/class.json-api-platform.php';
13
14 /**
15 * Jetpack JSON API.
16 */
17 class WPCOM_JSON_API {
18 /**
19 * Static instance.
20 *
21 * @todo This should be private.
22 * @var self|null
23 */
24 public static $self = null;
25
26 /**
27 * Registered endpoints.
28 *
29 * @var WPCOM_JSON_API_Endpoint[]
30 */
31 public $endpoints = array();
32
33 /**
34 * Token details.
35 *
36 * @var array
37 */
38 public $token_details = array();
39
40 /**
41 * Request HTTP method.
42 *
43 * @var string
44 */
45 public $method = '';
46
47 /**
48 * Request URL.
49 *
50 * @var string
51 */
52 public $url = '';
53
54 /**
55 * Path part of the request URL.
56 *
57 * @var string
58 */
59 public $path = '';
60
61 /**
62 * Version extracted from the request URL.
63 *
64 * @var string|null
65 */
66 public $version = null;
67
68 /**
69 * Parsed query data.
70 *
71 * @var array
72 */
73 public $query = array();
74
75 /**
76 * Post body, if the request is a POST.
77 *
78 * @var string|null
79 */
80 public $post_body = null;
81
82 /**
83 * Copy of `$_FILES` if the request is a POST.
84 *
85 * @var null|array
86 */
87 public $files = null;
88
89 /**
90 * Content type of the request.
91 *
92 * @var string|null
93 */
94 public $content_type = null;
95
96 /**
97 * Value of `$_SERVER['HTTP_ACCEPT']`, if any
98 *
99 * @var string
100 */
101 public $accept = '';
102
103 /**
104 * Value of `$_SERVER['HTTPS']`, or "--UNset--" if unset.
105 *
106 * @var string
107 */
108 public $_server_https; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
109
110 /**
111 * Whether to exit after serving a response.
112 *
113 * @var bool
114 */
115 public $exit = true;
116
117 /**
118 * Public API scheme.
119 *
120 * @var string
121 */
122 public $public_api_scheme = 'https';
123
124 /**
125 * Output status code.
126 *
127 * @var int
128 */
129 public $output_status_code = 200;
130
131 /**
132 * Trapped error.
133 *
134 * @var null|array
135 */
136 public $trapped_error = null;
137
138 /**
139 * Whether output has been done.
140 *
141 * @var bool
142 */
143 public $did_output = false;
144
145 /**
146 * Extra HTTP headers.
147 *
148 * @var string
149 */
150 public $extra_headers = array();
151
152 /**
153 * AMP source origin.
154 *
155 * @var string
156 */
157 public $amp_source_origin = null;
158
159 /**
160 * Initialize.
161 *
162 * @param string|null $method As for `$this->setup_inputs()`.
163 * @param string|null $url As for `$this->setup_inputs()`.
164 * @param string|null $post_body As for `$this->setup_inputs()`.
165 * @return WPCOM_JSON_API instance
166 */
167 public static function init( $method = null, $url = null, $post_body = null ) {
168 if ( ! self::$self ) {
169 $class = function_exists( 'get_called_class' ) ? get_called_class() : __CLASS__; // phpcs:ignore PHPCompatibility.PHP.NewFunctions.get_called_classFound
170 self::$self = new $class( $method, $url, $post_body );
171 }
172 return self::$self;
173 }
174
175 /**
176 * Add an endpoint.
177 *
178 * @param WPCOM_JSON_API_Endpoint $endpoint Endpoint to add.
179 */
180 public function add( WPCOM_JSON_API_Endpoint $endpoint ) {
181 // @todo Determine if anything depends on this being serialized rather than e.g. JSON.
182 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Legacy, possibly depended on elsewhere.
183 $path_versions = serialize(
184 array(
185 $endpoint->path,
186 $endpoint->min_version,
187 $endpoint->max_version,
188 )
189 );
190 if ( ! isset( $this->endpoints[ $path_versions ] ) ) {
191 $this->endpoints[ $path_versions ] = array();
192 }
193 $this->endpoints[ $path_versions ][ $endpoint->method ] = $endpoint;
194 }
195
196 /**
197 * Determine if a string is truthy.
198 *
199 * @param string $value "1", "t", and "true" (case insensitive) are falsey, everything else isn't.
200 * @return bool
201 */
202 public static function is_truthy( $value ) {
203 switch ( strtolower( (string) $value ) ) {
204 case '1':
205 case 't':
206 case 'true':
207 return true;
208 }
209
210 return false;
211 }
212
213 /**
214 * Determine if a string is falsey.
215 *
216 * @param string $value "0", "f", and "false" (case insensitive) are falsey, everything else isn't.
217 * @return bool
218 */
219 public static function is_falsy( $value ) {
220 switch ( strtolower( (string) $value ) ) {
221 case '0':
222 case 'f':
223 case 'false':
224 return true;
225 }
226
227 return false;
228 }
229
230 /**
231 * Constructor.
232 *
233 * @todo This should be private.
234 * @param string|null $method As for `$this->setup_inputs()`.
235 * @param string|null $url As for `$this->setup_inputs()`.
236 * @param string|null $post_body As for `$this->setup_inputs()`.
237 */
238 public function __construct( $method = null, $url = null, $post_body = null ) {
239 $this->setup_inputs( $method, $url, $post_body );
240 }
241
242 /**
243 * Setup inputs.
244 *
245 * @param string|null $method Request HTTP method. Fetched from `$_SERVER` if null.
246 * @param string|null $url URL requested. Determined from `$_SERVER` if null.
247 * @param string|null $post_body POST body. Read from `php://input` if null and method is POST.
248 */
249 public function setup_inputs( $method = null, $url = null, $post_body = null ) {
250 if ( $method === null ) {
251 $this->method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : '';
252 } else {
253 $this->method = strtoupper( $method );
254 }
255 if ( $url === null ) {
256 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sniff misses the esc_url_raw.
257 $this->url = esc_url_raw( set_url_scheme( 'http://' . ( isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : '' ) . ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '' ) ) );
258 } else {
259 $this->url = $url;
260 }
261
262 $parsed = wp_parse_url( $this->url );
263 if ( ! empty( $parsed['path'] ) ) {
264 $this->path = $parsed['path'];
265 }
266
267 if ( ! empty( $parsed['query'] ) ) {
268 wp_parse_str( $parsed['query'], $this->query );
269 }
270
271 if ( ! empty( $_SERVER['HTTP_ACCEPT'] ) ) {
272 $this->accept = filter_var( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
273 }
274
275 if ( 'POST' === $this->method ) {
276 if ( $post_body === null ) {
277 $this->post_body = file_get_contents( 'php://input' );
278
279 if ( ! empty( $_SERVER['HTTP_CONTENT_TYPE'] ) ) {
280 $this->content_type = filter_var( wp_unslash( $_SERVER['HTTP_CONTENT_TYPE'] ) );
281 } elseif ( ! empty( $_SERVER['CONTENT_TYPE'] ) ) {
282 $this->content_type = filter_var( wp_unslash( $_SERVER['CONTENT_TYPE'] ) );
283 } elseif ( '{' === $this->post_body[0] ) {
284 $this->content_type = 'application/json';
285 } else {
286 $this->content_type = 'application/x-www-form-urlencoded';
287 }
288
289 if ( 0 === strpos( strtolower( $this->content_type ), 'multipart/' ) ) {
290 // phpcs:ignore WordPress.Security.NonceVerification.Missing
291 $this->post_body = http_build_query( stripslashes_deep( $_POST ) );
292 $this->files = $_FILES;
293 $this->content_type = 'multipart/form-data';
294 }
295 } else {
296 $this->post_body = $post_body;
297 $this->content_type = isset( $this->post_body[0] ) && '{' === $this->post_body[0] ? 'application/json' : 'application/x-www-form-urlencoded';
298 }
299 } else {
300 $this->post_body = null;
301 $this->content_type = null;
302 }
303
304 $this->_server_https = array_key_exists( 'HTTPS', $_SERVER ) ? filter_var( wp_unslash( $_SERVER['HTTPS'] ) ) : '--UNset--';
305 }
306
307 /**
308 * Initialize.
309 *
310 * @return null|WP_Error (although this implementation always returns null)
311 */
312 public function initialize() {
313 $this->token_details['blog_id'] = Jetpack_Options::get_option( 'id' );
314 return null;
315 }
316
317 /**
318 * Checks if the current request is authorized with a blog token.
319 * This method is overridden by a child class in WPCOM.
320 *
321 * @since 9.1.0
322 *
323 * @param boolean|int $site_id The site id.
324 * @return boolean
325 */
326 public function is_jetpack_authorized_for_site( $site_id = false ) {
327 if ( ! $this->token_details ) {
328 return false;
329 }
330
331 $token_details = (object) $this->token_details;
332
333 $site_in_token = (int) $token_details->blog_id;
334
335 if ( $site_in_token < 1 ) {
336 return false;
337 }
338
339 if ( $site_id && $site_in_token !== (int) $site_id ) {
340 return false;
341 }
342
343 if ( (int) get_current_user_id() !== 0 ) {
344 // If Jetpack blog token is used, no logged-in user should exist.
345 return false;
346 }
347
348 return true;
349 }
350
351 /**
352 * Serve.
353 *
354 * @param bool $exit Whether to exit.
355 * @return string|null Content type (assuming it didn't exit), or null in certain error cases.
356 */
357 public function serve( $exit = true ) {
358 ini_set( 'display_errors', false ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Blacklisted
359
360 $this->exit = (bool) $exit;
361
362 // This was causing problems with Jetpack, but is necessary for wpcom
363 // @see https://github.com/Automattic/jetpack/pull/2603
364 // @see r124548-wpcom .
365 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
366 add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
367 }
368
369 add_filter( 'user_can_richedit', '__return_true' );
370
371 add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
372
373 $initialization = $this->initialize();
374 if ( 'OPTIONS' === $this->method ) {
375 /**
376 * Fires before the page output.
377 * Can be used to specify custom header options.
378 *
379 * @module json-api
380 *
381 * @since 3.1.0
382 */
383 do_action( 'wpcom_json_api_options' );
384 return $this->output( 200, '', 'text/plain' );
385 }
386
387 if ( is_wp_error( $initialization ) ) {
388 $this->output_error( $initialization );
389 return;
390 }
391
392 // Normalize path and extract API version.
393 $this->path = untrailingslashit( $this->path );
394 preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
395 $this->path = substr( $this->path, strlen( $matches[0] ) );
396 $this->version = $matches[1];
397
398 $allowed_methods = array( 'GET', 'POST' );
399 $four_oh_five = false;
400
401 $is_help = preg_match( '#/help/?$#i', $this->path );
402 $matching_endpoints = array();
403
404 if ( $is_help ) {
405 $origin = get_http_origin();
406
407 if ( ! empty( $origin ) && 'GET' === $this->method ) {
408 header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
409 }
410
411 $this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
412 // Show help for all matching endpoints regardless of method.
413 $methods = $allowed_methods;
414 $find_all_matching_endpoints = true;
415 // How deep to truncate each endpoint's path to see if it matches this help request.
416 $depth = substr_count( $this->path, '/' ) + 1;
417 if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
418 $help_content_type = 'json';
419 } else {
420 $help_content_type = 'html';
421 }
422 } else {
423 if ( in_array( $this->method, $allowed_methods, true ) ) {
424 // Only serve requested method.
425 $methods = array( $this->method );
426 $find_all_matching_endpoints = false;
427 } else {
428 // We don't allow this requested method - find matching endpoints and send 405.
429 $methods = $allowed_methods;
430 $find_all_matching_endpoints = true;
431 $four_oh_five = true;
432 }
433 }
434
435 // Find which endpoint to serve.
436 $found = false;
437 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
438 // @todo Determine if anything depends on this being serialized rather than e.g. JSON.
439 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Legacy, possibly depended on elsewhere.
440 $endpoint_path_versions = unserialize( $endpoint_path_versions );
441 $endpoint_path = $endpoint_path_versions[0];
442 $endpoint_min_version = $endpoint_path_versions[1];
443 $endpoint_max_version = $endpoint_path_versions[2];
444
445 // Make sure max_version is not less than min_version.
446 if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
447 $endpoint_max_version = $endpoint_min_version;
448 }
449
450 foreach ( $methods as $method ) {
451 if ( ! isset( $endpoints_by_method[ $method ] ) ) {
452 continue;
453 }
454
455 // Normalize.
456 $endpoint_path = untrailingslashit( $endpoint_path );
457 if ( $is_help ) {
458 // Truncate path at help depth.
459 $endpoint_path = join( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
460 }
461
462 // Generate regular expression from sprintf().
463 $endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
464
465 if ( ! preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
466 // This endpoint does not match the requested path.
467 continue;
468 }
469
470 if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
471 // This endpoint does not match the requested version.
472 continue;
473 }
474
475 $found = true;
476
477 if ( $find_all_matching_endpoints ) {
478 $matching_endpoints[] = array( $endpoints_by_method[ $method ], $path_pieces );
479 } else {
480 // The method parameters are now in $path_pieces.
481 $endpoint = $endpoints_by_method[ $method ];
482 break 2;
483 }
484 }
485 }
486
487 if ( ! $found ) {
488 return $this->output( 404, '', 'text/plain' );
489 }
490
491 if ( $four_oh_five ) {
492 $allowed_methods = array();
493 foreach ( $matching_endpoints as $matching_endpoint ) {
494 $allowed_methods[] = $matching_endpoint[0]->method;
495 }
496
497 header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
498 return $this->output(
499 405,
500 array(
501 'error' => 'not_allowed',
502 'error_message' => 'Method not allowed',
503 )
504 );
505 }
506
507 if ( $is_help ) {
508 /**
509 * Fires before the API output.
510 *
511 * @since 1.9.0
512 *
513 * @param string help.
514 */
515 do_action( 'wpcom_json_api_output', 'help' );
516 $proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
517 if ( 'json' === $help_content_type ) {
518 $docs = array();
519 foreach ( $matching_endpoints as $matching_endpoint ) {
520 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
521 $docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
522 }
523 }
524 return $this->output( 200, $docs );
525 } else {
526 status_header( 200 );
527 foreach ( $matching_endpoints as $matching_endpoint ) {
528 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
529 call_user_func( array( $matching_endpoint[0], 'document' ) );
530 }
531 }
532 }
533 exit;
534 }
535
536 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
537 return $this->output( 404, '', 'text/plain' );
538 }
539
540 /** This action is documented in class.json-api.php */
541 do_action( 'wpcom_json_api_output', $endpoint->stat );
542
543 $response = $this->process_request( $endpoint, $path_pieces );
544
545 if ( ! $response && ! is_array( $response ) ) {
546 return $this->output( 500, '', 'text/plain' );
547 } elseif ( is_wp_error( $response ) ) {
548 return $this->output_error( $response );
549 }
550
551 $output_status_code = $this->output_status_code;
552 $this->set_output_status_code();
553
554 return $this->output( $output_status_code, $response, 'application/json', $this->extra_headers );
555 }
556
557 /**
558 * Process a request.
559 *
560 * @param WPCOM_JSON_API_Endpoint $endpoint Endpoint.
561 * @param array $path_pieces Path pieces.
562 * @return array|WP_Error Return value from the endpoint's callback.
563 */
564 public function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
565 $this->endpoint = $endpoint;
566 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
567 }
568
569 /**
570 * Output a response or error without exiting.
571 *
572 * @param int $status_code HTTP status code.
573 * @param mixed $response Response data.
574 * @param string $content_type Content type of the response.
575 */
576 public function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
577 $exit = $this->exit;
578 $this->exit = false;
579 if ( is_wp_error( $response ) ) {
580 $this->output_error( $response );
581 } else {
582 $this->output( $status_code, $response, $content_type );
583 }
584 $this->exit = $exit;
585 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
586 $this->finish_request();
587 }
588 }
589
590 /**
591 * Set output status code.
592 *
593 * @param int $code HTTP status code.
594 */
595 public function set_output_status_code( $code = 200 ) {
596 $this->output_status_code = $code;
597 }
598
599 /**
600 * Output a response.
601 *
602 * @param int $status_code HTTP status code.
603 * @param mixed $response Response data.
604 * @param string $content_type Content type of the response.
605 * @param array $extra Additional HTTP headers.
606 * @return string Content type (assuming it didn't exit).
607 */
608 public function output( $status_code, $response = null, $content_type = 'application/json', $extra = array() ) {
609 $status_code = (int) $status_code;
610
611 // In case output() was called before the callback returned.
612 if ( $this->did_output ) {
613 if ( $this->exit ) {
614 exit;
615 }
616 return $content_type;
617 }
618 $this->did_output = true;
619
620 // 400s and 404s are allowed for all origins
621 if ( 404 === $status_code || 400 === $status_code ) {
622 header( 'Access-Control-Allow-Origin: *' );
623 }
624
625 /* Add headers for form submission from <amp-form/> */
626 if ( $this->amp_source_origin ) {
627 header( 'Access-Control-Allow-Origin: ' . wp_unslash( $this->amp_source_origin ) );
628 header( 'Access-Control-Allow-Credentials: true' );
629 }
630
631 if ( $response === null ) {
632 $response = new stdClass();
633 }
634
635 if ( 'text/plain' === $content_type ||
636 'text/html' === $content_type ) {
637 status_header( (int) $status_code );
638 header( 'Content-Type: ' . $content_type );
639 foreach ( $extra as $key => $value ) {
640 header( "$key: $value" );
641 }
642 echo $response; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
643 if ( $this->exit ) {
644 exit;
645 }
646
647 return $content_type;
648 }
649
650 $response = $this->filter_fields( $response );
651
652 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
653 $headers = array(
654 array(
655 'name' => 'Content-Type',
656 'value' => $content_type,
657 ),
658 );
659
660 foreach ( $extra as $key => $value ) {
661 $headers[] = array(
662 'name' => $key,
663 'value' => $value,
664 );
665 }
666
667 $response = array(
668 'code' => (int) $status_code,
669 'headers' => $headers,
670 'body' => $response,
671 );
672 $status_code = 200;
673 $content_type = 'application/json';
674 }
675
676 status_header( (int) $status_code );
677 header( "Content-Type: $content_type" );
678 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
679 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
680 } else {
681 $callback = false;
682 }
683
684 if ( $callback ) {
685 // Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
686 // and by prepending the JSONP response with a JS comment.
687 // [1] <https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html>.
688 echo "/**/$callback("; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSONP output, not HTML.
689
690 }
691 echo $this->json_encode( $response ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSON or JSONP output, not HTML.
692 if ( $callback ) {
693 echo ');';
694 }
695
696 if ( $this->exit ) {
697 exit;
698 }
699
700 return $content_type;
701 }
702
703 /**
704 * Serialize an error.
705 *
706 * @param WP_Error $error Error.
707 * @return array with 'status_code' and 'errors' data.
708 */
709 public static function serializable_error( $error ) {
710
711 $status_code = $error->get_error_data();
712
713 if ( is_array( $status_code ) ) {
714 $status_code = $status_code['status_code'];
715 }
716
717 if ( ! $status_code ) {
718 $status_code = 400;
719 }
720 $response = array(
721 'error' => $error->get_error_code(),
722 'message' => $error->get_error_message(),
723 );
724
725 $additional_data = $error->get_error_data( 'additional_data' );
726 if ( $additional_data ) {
727 $response['data'] = $additional_data;
728 }
729
730 return array(
731 'status_code' => $status_code,
732 'errors' => $response,
733 );
734 }
735
736 /**
737 * Output an error.
738 *
739 * @param WP_Error $error Error.
740 * @return string Content type (assuming it didn't exit).
741 */
742 public function output_error( $error ) {
743 $error_response = $this->serializable_error( $error );
744
745 return $this->output( $error_response['status_code'], $error_response['errors'] );
746 }
747
748 /**
749 * Filter fields in a response.
750 *
751 * @param array|object $response Response.
752 * @return array|object Filtered response.
753 */
754 public function filter_fields( $response ) {
755 if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) ) {
756 return $response;
757 }
758
759 $fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
760
761 if ( is_object( $response ) ) {
762 $response = (array) $response;
763 }
764
765 $has_filtered = false;
766 if ( is_array( $response ) && empty( $response['ID'] ) ) {
767 $keys_to_filter = array(
768 'categories',
769 'comments',
770 'connections',
771 'domains',
772 'groups',
773 'likes',
774 'media',
775 'notes',
776 'posts',
777 'services',
778 'sites',
779 'suggestions',
780 'tags',
781 'themes',
782 'topics',
783 'users',
784 );
785
786 foreach ( $keys_to_filter as $key_to_filter ) {
787 if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
788 continue;
789 }
790
791 foreach ( $response[ $key_to_filter ] as $key => $values ) {
792 if ( is_object( $values ) ) {
793 if ( is_object( $response[ $key_to_filter ] ) ) {
794 // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found -- False positive.
795 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
796 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
797 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
798 }
799 } elseif ( is_array( $values ) ) {
800 $response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
801 }
802 }
803
804 $has_filtered = true;
805 }
806 }
807
808 if ( ! $has_filtered ) {
809 if ( is_object( $response ) ) {
810 $response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
811 } elseif ( is_array( $response ) ) {
812 $response = array_intersect_key( $response, array_flip( $fields ) );
813 }
814 }
815
816 return $response;
817 }
818
819 /**
820 * Filter for `home_url`.
821 *
822 * If `$original_scheme` is null, turns an https URL to http.
823 *
824 * @param string $url The complete home URL including scheme and path.
825 * @param string $path Path relative to the home URL. Blank string if no path is specified.
826 * @param string|null $original_scheme Scheme to give the home URL context. Accepts 'http', 'https', 'relative', 'rest', or null.
827 * @return string URL.
828 */
829 public function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
830 if ( $original_scheme ) {
831 return $url;
832 }
833
834 return preg_replace( '#^https:#', 'http:', $url );
835 }
836
837 /**
838 * Decode HTML special characters in comment content.
839 *
840 * @param string $comment_content Comment content.
841 * @return string
842 */
843 public function comment_edit_pre( $comment_content ) {
844 return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
845 }
846
847 /**
848 * JSON encode.
849 *
850 * @param mixed $data Data.
851 * @return string|false
852 */
853 public function json_encode( $data ) {
854 return wp_json_encode( $data );
855 }
856
857 /**
858 * Test if a string ends with a string.
859 *
860 * @param string $haystack String to check.
861 * @param string $needle Suffix to check.
862 * @return bool
863 */
864 public function ends_with( $haystack, $needle ) {
865 return substr( $haystack, -strlen( $needle ) ) === $needle;
866 }
867
868 /**
869 * Returns the site's blog_id in the WP.com ecosystem
870 *
871 * @return int
872 */
873 public function get_blog_id_for_output() {
874 return $this->token_details['blog_id'];
875 }
876
877 /**
878 * Returns the site's local blog_id.
879 *
880 * @param int $blog_id Blog ID.
881 * @return int
882 */
883 public function get_blog_id( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
884 return $GLOBALS['blog_id'];
885 }
886
887 /**
888 * Switch to blog and validate user.
889 *
890 * @param int $blog_id Blog ID.
891 * @param bool $verify_token_for_blog Whether to verify the token.
892 * @return int Blog ID.
893 */
894 public function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
895 if ( $this->is_restricted_blog( $blog_id ) ) {
896 return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
897 }
898 /**
899 * If this is a private site we check for 2 things:
900 * 1. In case of user based authentication, we need to check if the logged-in user has the 'read' capability.
901 * 2. In case of site based authentication, make sure the endpoint accepts it.
902 */
903 if ( -1 === (int) get_option( 'blog_public' ) &&
904 ! current_user_can( 'read' ) &&
905 ! $this->endpoint->accepts_site_based_authentication()
906 ) {
907 return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
908 }
909
910 return $blog_id;
911 }
912
913 /**
914 * Returns true if the specified blog ID is a restricted blog
915 *
916 * @param int $blog_id Blog ID.
917 * @return bool
918 */
919 public function is_restricted_blog( $blog_id ) {
920 /**
921 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
922 *
923 * @module json-api
924 *
925 * @since 3.4.0
926 *
927 * @param array $array Array of Blog IDs.
928 */
929 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
930 return true === in_array( $blog_id, $restricted_blog_ids ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- I don't trust filters to return the right types.
931 }
932
933 /**
934 * Post like count.
935 *
936 * @param int $blog_id Blog ID.
937 * @param int $post_id Post ID.
938 * @return int
939 */
940 public function post_like_count( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
941 return 0;
942 }
943
944 /**
945 * Is liked?
946 *
947 * @param int $blog_id Blog ID.
948 * @param int $post_id Post ID.
949 * @return bool
950 */
951 public function is_liked( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
952 return false;
953 }
954
955 /**
956 * Is reblogged?
957 *
958 * @param int $blog_id Blog ID.
959 * @param int $post_id Post ID.
960 * @return bool
961 */
962 public function is_reblogged( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
963 return false;
964 }
965
966 /**
967 * Is following?
968 *
969 * @param int $blog_id Blog ID.
970 * @return bool
971 */
972 public function is_following( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
973 return false;
974 }
975
976 /**
977 * Add global ID.
978 *
979 * @param int $blog_id Blog ID.
980 * @param int $post_id Post ID.
981 * @return string
982 */
983 public function add_global_ID( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
984 return '';
985 }
986
987 /**
988 * Get avatar URL.
989 *
990 * @param string $email Email.
991 * @param array $avatar_size Args for `get_avatar_url()`.
992 * @return string|false
993 */
994 public function get_avatar_url( $email, $avatar_size = null ) {
995 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
996 return null === $avatar_size
997 ? wpcom_get_avatar_url( $email )
998 : wpcom_get_avatar_url( $email, $avatar_size );
999 } else {
1000 return null === $avatar_size
1001 ? get_avatar_url( $email )
1002 : get_avatar_url( $email, $avatar_size );
1003 }
1004 }
1005
1006 /**
1007 * Counts the number of comments on a site, including certain comment types.
1008 *
1009 * @param int $post_id Post ID.
1010 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
1011 */
1012 public function wp_count_comments( $post_id ) {
1013 global $wpdb;
1014 if ( 0 !== $post_id ) {
1015 return wp_count_comments( $post_id );
1016 }
1017
1018 $counts = array(
1019 'total_comments' => 0,
1020 'all' => 0,
1021 );
1022
1023 /**
1024 * Exclude certain comment types from comment counts in the REST API.
1025 *
1026 * @since 6.9.0
1027 * @deprecated 11.1
1028 * @module json-api
1029 *
1030 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
1031 */
1032 $exclude = apply_filters_deprecated( 'jetpack_api_exclude_comment_types_count', array( 'order_note', 'webhook_delivery', 'review', 'action_log' ), 'jetpack-11.1', 'jetpack_api_include_comment_types_count' ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1033
1034 /**
1035 * Include certain comment types in comment counts in the REST API.
1036 * Note: the default array of comment types includes an empty string,
1037 * to support comments posted before WP 5.5, that used an empty string as comment type.
1038 *
1039 * @since 11.1
1040 * @module json-api
1041 *
1042 * @param array Array of comment types to include (default: 'comment', 'pingback', 'trackback')
1043 */
1044 $include = apply_filters(
1045 'jetpack_api_include_comment_types_count',
1046 array( 'comment', 'pingback', 'trackback', '' )
1047 );
1048
1049 if ( empty( $include ) ) {
1050 return wp_count_comments( $post_id );
1051 }
1052
1053 array_walk( $include, 'esc_sql' );
1054 $where = sprintf(
1055 "WHERE comment_type IN ( '%s' )",
1056 implode( "','", $include )
1057 );
1058
1059 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1060 $count = $wpdb->get_results(
1061 "SELECT comment_approved, COUNT(*) AS num_comments
1062 FROM $wpdb->comments
1063 {$where}
1064 GROUP BY comment_approved
1065 "
1066 );
1067 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1068
1069 $approved = array(
1070 '0' => 'moderated',
1071 '1' => 'approved',
1072 'spam' => 'spam',
1073 'trash' => 'trash',
1074 'post-trashed' => 'post-trashed',
1075 );
1076
1077 // <https://developer.wordpress.org/reference/functions/get_comment_count/#source>
1078 foreach ( $count as $row ) {
1079 if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
1080 $counts['all'] += $row->num_comments;
1081 $counts['total_comments'] += $row->num_comments;
1082 } elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
1083 $counts['total_comments'] += $row->num_comments;
1084 }
1085 if ( isset( $approved[ $row->comment_approved ] ) ) {
1086 $counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
1087 }
1088 }
1089
1090 foreach ( $approved as $key ) {
1091 if ( empty( $counts[ $key ] ) ) {
1092 $counts[ $key ] = 0;
1093 }
1094 }
1095
1096 $counts = (object) $counts;
1097
1098 return $counts;
1099 }
1100
1101 /**
1102 * Traps `wp_die()` calls and outputs a JSON response instead.
1103 * The result is always output, never returned.
1104 *
1105 * @param string|null $error_code Call with string to start the trapping. Call with null to stop.
1106 * @param int $http_status HTTP status code, 400 by default.
1107 */
1108 public function trap_wp_die( $error_code = null, $http_status = 400 ) {
1109 // Determine the filter name; based on the conditionals inside the wp_die function.
1110 if ( wp_is_json_request() ) {
1111 $die_handler = 'wp_die_json_handler';
1112 } elseif ( wp_is_jsonp_request() ) {
1113 $die_handler = 'wp_die_jsonp_handler';
1114 } elseif ( wp_is_xml_request() ) {
1115 $die_handler = 'wp_die_xml_handler';
1116 } else {
1117 $die_handler = 'wp_die_handler';
1118 }
1119
1120 if ( $error_code === null ) {
1121 $this->trapped_error = null;
1122 // Stop trapping.
1123 remove_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
1124 return;
1125 }
1126
1127 // If API called via PHP, bail: don't do our custom wp_die(). Do the normal wp_die().
1128 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1129 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1130 return;
1131 }
1132 } else {
1133 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
1134 return;
1135 }
1136 }
1137
1138 $this->trapped_error = array(
1139 'status' => $http_status,
1140 'code' => $error_code,
1141 'message' => '',
1142 );
1143 // Start trapping.
1144 add_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
1145 }
1146
1147 /**
1148 * Filter function for `wp_die_handler` and similar filters.
1149 *
1150 * @return callable
1151 */
1152 public function wp_die_handler_callback() {
1153 return array( $this, 'wp_die_handler' );
1154 }
1155
1156 /**
1157 * Handler for `wp_die` calls.
1158 *
1159 * @param string|WP_Error $message As for `wp_die()`.
1160 * @param string|int $title As for `wp_die()`.
1161 * @param string|array|int $args As for `wp_die()`.
1162 */
1163 public function wp_die_handler( $message, $title = '', $args = array() ) {
1164 // Allow wp_die calls to override HTTP status code...
1165 $args = wp_parse_args(
1166 $args,
1167 array(
1168 'response' => $this->trapped_error['status'],
1169 )
1170 );
1171
1172 // ... unless it's 500
1173 if ( 500 !== (int) $args['response'] ) {
1174 $this->trapped_error['status'] = $args['response'];
1175 }
1176
1177 if ( $title ) {
1178 $message = "$title: $message";
1179 }
1180
1181 $this->trapped_error['message'] = wp_kses( $message, array() );
1182
1183 switch ( $this->trapped_error['code'] ) {
1184 case 'comment_failure':
1185 if ( did_action( 'comment_duplicate_trigger' ) ) {
1186 $this->trapped_error['code'] = 'comment_duplicate';
1187 } elseif ( did_action( 'comment_flood_trigger' ) ) {
1188 $this->trapped_error['code'] = 'comment_flood';
1189 }
1190 break;
1191 }
1192
1193 // We still want to exit so that code execution stops where it should.
1194 // Attach the JSON output to the WordPress shutdown handler.
1195 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
1196 exit;
1197 }
1198
1199 /**
1200 * Output the trapped error.
1201 */
1202 public function output_trapped_error() {
1203 $this->exit = false; // We're already exiting once. Don't do it twice.
1204 $this->output(
1205 $this->trapped_error['status'],
1206 (object) array(
1207 'error' => $this->trapped_error['code'],
1208 'message' => $this->trapped_error['message'],
1209 )
1210 );
1211 }
1212
1213 /**
1214 * Finish the request.
1215 */
1216 public function finish_request() {
1217 if ( function_exists( 'fastcgi_finish_request' ) ) {
1218 return fastcgi_finish_request();
1219 }
1220 }
1221 }
1222