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