PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 14.4.2
Jetpack – WP Security, Backup, Speed, & Growth v14.4.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 year 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 3 years 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-affiliate.php 2 years ago class.jetpack-autoupdate.php 2 years ago class.jetpack-bbpress-json-api.compat.php 2 years 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 2 years 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 3 years ago functions.compat.php 2 years 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 global.d.ts 2 years ago jetpack.php 1 day ago json-api-config.php 3 years ago json-endpoints.php 2 years ago load-jetpack.php 2 years ago locales.php 4 years ago readme.txt 1 day ago uninstall.php 1 year ago wpml-config.xml 4 years ago
class.json-api.php
1332 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 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
610 }
611
612 /**
613 * Output a response or error without exiting.
614 *
615 * @param int $status_code HTTP status code.
616 * @param mixed $response Response data.
617 * @param string $content_type Content type of the response.
618 */
619 public function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
620 $exit = $this->exit;
621 $this->exit = false;
622 if ( is_wp_error( $response ) ) {
623 $this->output_error( $response );
624 } else {
625 $this->output( $status_code, $response, $content_type );
626 }
627 $this->exit = $exit;
628 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
629 $this->finish_request();
630 }
631 }
632
633 /**
634 * Set output status code.
635 *
636 * @param int $code HTTP status code.
637 */
638 public function set_output_status_code( $code = 200 ) {
639 $this->output_status_code = $code;
640 }
641
642 /**
643 * Output a response.
644 *
645 * @param int $status_code HTTP status code.
646 * @param mixed $response Response data.
647 * @param string $content_type Content type of the response.
648 * @param array $extra Additional HTTP headers.
649 * @return string Content type (assuming it didn't exit).
650 */
651 public function output( $status_code, $response = null, $content_type = 'application/json', $extra = array() ) {
652 $status_code = (int) $status_code;
653
654 // In case output() was called before the callback returned.
655 if ( $this->did_output ) {
656 if ( $this->exit ) {
657 exit( 0 );
658 }
659 return $content_type;
660 }
661 $this->did_output = true;
662
663 // 400s and 404s are allowed for all origins
664 if ( 404 === $status_code || 400 === $status_code ) {
665 header( 'Access-Control-Allow-Origin: *' );
666 }
667
668 /* Add headers for form submission from <amp-form/> */
669 if ( $this->amp_source_origin ) {
670 header( 'Access-Control-Allow-Origin: ' . wp_unslash( $this->amp_source_origin ) );
671 header( 'Access-Control-Allow-Credentials: true' );
672 }
673
674 if ( $response === null ) {
675 $response = new stdClass();
676 }
677
678 if ( 'text/plain' === $content_type ||
679 'text/html' === $content_type ) {
680 status_header( (int) $status_code );
681 header( 'Content-Type: ' . $content_type );
682 foreach ( $extra as $key => $value ) {
683 header( "$key: $value" );
684 }
685 echo $response; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
686 if ( $this->exit ) {
687 exit( 0 );
688 }
689
690 return $content_type;
691 }
692
693 $response = $this->filter_fields( $response );
694
695 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
696 $response = static::wrap_http_envelope( $status_code, $response, $content_type, $extra );
697
698 $status_code = 200;
699 $content_type = 'application/json';
700 }
701
702 status_header( (int) $status_code );
703 header( "Content-Type: $content_type" );
704 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
705 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
706 } else {
707 $callback = false;
708 }
709
710 if ( $callback ) {
711 // Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
712 // and by prepending the JSONP response with a JS comment.
713 // [1] <https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html>.
714 echo "/**/$callback("; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSONP output, not HTML.
715
716 }
717 echo $this->json_encode( $response ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSON or JSONP output, not HTML.
718 if ( $callback ) {
719 echo ');';
720 }
721
722 if ( $this->exit ) {
723 exit( 0 );
724 }
725
726 return $content_type;
727 }
728
729 /**
730 * Wrap JSON API response into an HTTP 200 one.
731 *
732 * @param int $status_code HTTP status code.
733 * @param mixed $response Response body.
734 * @param string $content_type Content type.
735 * @param array|null $extra Extra data.
736 *
737 * @return array
738 */
739 public static function wrap_http_envelope( $status_code, $response, $content_type, $extra = null ) {
740 $headers = array(
741 array(
742 'name' => 'Content-Type',
743 'value' => $content_type,
744 ),
745 );
746
747 if ( is_array( $extra ) ) {
748 foreach ( $extra as $key => $value ) {
749 $headers[] = array(
750 'name' => $key,
751 'value' => $value,
752 );
753 }
754 }
755
756 return array(
757 'code' => (int) $status_code,
758 'headers' => $headers,
759 'body' => $response,
760 );
761 }
762
763 /**
764 * Serialize an error.
765 *
766 * @param WP_Error $error Error.
767 * @return array with 'status_code' and 'errors' data.
768 */
769 public static function serializable_error( $error ) {
770
771 $status_code = $error->get_error_data();
772
773 if ( is_array( $status_code ) && isset( $status_code['status_code'] ) ) {
774 $status_code = $status_code['status_code'];
775 }
776
777 if ( ! $status_code ) {
778 $status_code = 400;
779 }
780 $response = array(
781 'error' => $error->get_error_code(),
782 'message' => $error->get_error_message(),
783 );
784
785 $additional_data = $error->get_error_data( 'additional_data' );
786 if ( $additional_data ) {
787 $response['data'] = $additional_data;
788 }
789
790 return array(
791 'status_code' => $status_code,
792 'errors' => $response,
793 );
794 }
795
796 /**
797 * Output an error.
798 *
799 * @param WP_Error $error Error.
800 * @return string Content type (assuming it didn't exit).
801 */
802 public function output_error( $error ) {
803 $error_response = static::serializable_error( $error );
804
805 return $this->output( $error_response['status_code'], $error_response['errors'] );
806 }
807
808 /**
809 * Filter fields in a response.
810 *
811 * @param array|object $response Response.
812 * @return array|object Filtered response.
813 */
814 public function filter_fields( $response ) {
815 if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) ) {
816 return $response;
817 }
818
819 $fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
820
821 if ( is_object( $response ) ) {
822 $response = (array) $response;
823 }
824
825 $has_filtered = false;
826 if ( is_array( $response ) && empty( $response['ID'] ) ) {
827 $keys_to_filter = array(
828 'categories',
829 'comments',
830 'connections',
831 'domains',
832 'groups',
833 'likes',
834 'media',
835 'notes',
836 'posts',
837 'services',
838 'sites',
839 'suggestions',
840 'tags',
841 'themes',
842 'topics',
843 'users',
844 );
845
846 foreach ( $keys_to_filter as $key_to_filter ) {
847 if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
848 continue;
849 }
850
851 foreach ( $response[ $key_to_filter ] as $key => $values ) {
852 if ( is_object( $values ) ) {
853 if ( is_object( $response[ $key_to_filter ] ) ) {
854 // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found -- False positive.
855 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
856 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
857 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
858 }
859 } elseif ( is_array( $values ) ) {
860 $response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
861 }
862 }
863
864 $has_filtered = true;
865 }
866 }
867
868 if ( ! $has_filtered ) {
869 if ( is_object( $response ) ) {
870 $response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
871 } elseif ( is_array( $response ) ) {
872 $response = array_intersect_key( $response, array_flip( $fields ) );
873 }
874 }
875
876 return $response;
877 }
878
879 /**
880 * Filter for `home_url`.
881 *
882 * If `$original_scheme` is null, turns an https URL to http.
883 *
884 * @param string $url The complete home URL including scheme and path.
885 * @param string $path Path relative to the home URL. Blank string if no path is specified.
886 * @param string|null $original_scheme Scheme to give the home URL context. Accepts 'http', 'https', 'relative', 'rest', or null.
887 * @return string URL.
888 */
889 public function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
890 if ( $original_scheme ) {
891 return $url;
892 }
893
894 return preg_replace( '#^https:#', 'http:', $url );
895 }
896
897 /**
898 * Decode HTML special characters in comment content.
899 *
900 * @param string $comment_content Comment content.
901 * @return string
902 */
903 public function comment_edit_pre( $comment_content ) {
904 return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
905 }
906
907 /**
908 * JSON encode.
909 *
910 * @param mixed $data Data.
911 * @return string|false
912 */
913 public function json_encode( $data ) {
914 return wp_json_encode( $data );
915 }
916
917 /**
918 * Test if a string ends with a string.
919 *
920 * @param string $haystack String to check.
921 * @param string $needle Suffix to check.
922 * @return bool
923 */
924 public function ends_with( $haystack, $needle ) {
925 return substr( $haystack, -strlen( $needle ) ) === $needle;
926 }
927
928 /**
929 * Returns the site's blog_id in the WP.com ecosystem
930 *
931 * @return int
932 */
933 public function get_blog_id_for_output() {
934 return $this->token_details['blog_id'];
935 }
936
937 /**
938 * Returns the site's local blog_id.
939 *
940 * @param int $blog_id Blog ID.
941 * @return int
942 */
943 public function get_blog_id( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
944 return $GLOBALS['blog_id'];
945 }
946
947 /**
948 * Switch to blog and validate user.
949 *
950 * @param int $blog_id Blog ID.
951 * @param bool $verify_token_for_blog Whether to verify the token.
952 * @return int Blog ID.
953 */
954 public function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
955 if ( $this->is_restricted_blog( $blog_id ) ) {
956 return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
957 }
958 /**
959 * If this is a private site we check for 2 things:
960 * 1. In case of user based authentication, we need to check if the logged-in user has the 'read' capability.
961 * 2. In case of site based authentication, make sure the endpoint accepts it.
962 */
963 if ( ( new Status() )->is_private_site() &&
964 ! current_user_can( 'read' ) &&
965 ! $this->endpoint->accepts_site_based_authentication()
966 ) {
967 return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
968 }
969
970 return $blog_id;
971 }
972
973 /**
974 * Returns true if the specified blog ID is a restricted blog
975 *
976 * @param int $blog_id Blog ID.
977 * @return bool
978 */
979 public function is_restricted_blog( $blog_id ) {
980 /**
981 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
982 *
983 * @module json-api
984 *
985 * @since 3.4.0
986 *
987 * @param array $array Array of Blog IDs.
988 */
989 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
990 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.
991 }
992
993 /**
994 * Post like count.
995 *
996 * @param int $blog_id Blog ID.
997 * @param int $post_id Post ID.
998 * @return int
999 */
1000 public function post_like_count( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1001 return 0;
1002 }
1003
1004 /**
1005 * Is liked?
1006 *
1007 * @param int $blog_id Blog ID.
1008 * @param int $post_id Post ID.
1009 * @return bool
1010 */
1011 public function is_liked( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1012 return false;
1013 }
1014
1015 /**
1016 * Is reblogged?
1017 *
1018 * @param int $blog_id Blog ID.
1019 * @param int $post_id Post ID.
1020 * @return bool
1021 */
1022 public function is_reblogged( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1023 return false;
1024 }
1025
1026 /**
1027 * Is following?
1028 *
1029 * @param int $blog_id Blog ID.
1030 * @return bool
1031 */
1032 public function is_following( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1033 return false;
1034 }
1035
1036 /**
1037 * Add global ID.
1038 *
1039 * @param int $blog_id Blog ID.
1040 * @param int $post_id Post ID.
1041 * @return string
1042 */
1043 public function add_global_ID( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
1044 return '';
1045 }
1046
1047 /**
1048 * Return a count of comment likes.
1049 * This method is overridden by a child class in WPCOM.
1050 *
1051 * @since 13.5
1052 * @return int
1053 */
1054 public function comment_like_count() {
1055 func_get_args(); // @phan-suppress-current-line PhanPluginUseReturnValueInternalKnown -- This is just here so Phan realizes the wpcom version does this.
1056 return 0;
1057 }
1058
1059 /**
1060 * Get avatar URL.
1061 *
1062 * @param string $email Email.
1063 * @param array $args Args for `get_avatar_url()`.
1064 * @return string|false
1065 */
1066 public function get_avatar_url( $email, $args = null ) {
1067 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
1068 $ret = wpcom_get_avatar_url( $email, $args['size'] ?? 96, $args['default'] ?? '', false, $args['force_default'] ?? false );
1069 return $ret ? $ret[0] : false;
1070 } else {
1071 return null === $args
1072 ? get_avatar_url( $email )
1073 : get_avatar_url( $email, $args );
1074 }
1075 }
1076
1077 /**
1078 * Counts the number of comments on a site, including certain comment types.
1079 *
1080 * @param int $post_id Post ID.
1081 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
1082 */
1083 public function wp_count_comments( $post_id ) {
1084 global $wpdb;
1085 if ( 0 !== $post_id ) {
1086 return wp_count_comments( $post_id );
1087 }
1088
1089 $counts = array(
1090 'total_comments' => 0,
1091 'all' => 0,
1092 );
1093
1094 /**
1095 * Exclude certain comment types from comment counts in the REST API.
1096 *
1097 * @since 6.9.0
1098 * @deprecated 11.1
1099 * @module json-api
1100 *
1101 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
1102 */
1103 $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
1104
1105 /**
1106 * Include certain comment types in comment counts in the REST API.
1107 * Note: the default array of comment types includes an empty string,
1108 * to support comments posted before WP 5.5, that used an empty string as comment type.
1109 *
1110 * @since 11.1
1111 * @module json-api
1112 *
1113 * @param array Array of comment types to include (default: 'comment', 'pingback', 'trackback')
1114 */
1115 $include = apply_filters(
1116 'jetpack_api_include_comment_types_count',
1117 array( 'comment', 'pingback', 'trackback', '' )
1118 );
1119
1120 if ( empty( $include ) ) {
1121 return wp_count_comments( $post_id );
1122 }
1123
1124 // The following caching mechanism is based on what the get_comments() function uses.
1125
1126 $key = md5( serialize( $include ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1127 $last_changed = wp_cache_get_last_changed( 'comment' );
1128
1129 $cache_key = "wp_count_comments:$key:$last_changed";
1130 $count = wp_cache_get( $cache_key, 'jetpack-json-api' );
1131
1132 if ( false === $count ) {
1133 array_walk( $include, 'esc_sql' );
1134 $where = sprintf(
1135 "WHERE comment_type IN ( '%s' )",
1136 implode( "','", $include )
1137 );
1138
1139 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1140 $count = $wpdb->get_results(
1141 "SELECT comment_approved, COUNT(*) AS num_comments
1142 FROM $wpdb->comments
1143 {$where}
1144 GROUP BY comment_approved
1145 "
1146 );
1147 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1148
1149 wp_cache_add( $cache_key, $count, 'jetpack-json-api' );
1150 }
1151
1152 $approved = array(
1153 '0' => 'moderated',
1154 '1' => 'approved',
1155 'spam' => 'spam',
1156 'trash' => 'trash',
1157 'post-trashed' => 'post-trashed',
1158 );
1159
1160 // <https://developer.wordpress.org/reference/functions/get_comment_count/#source>
1161 foreach ( $count as $row ) {
1162 if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
1163 $counts['all'] += $row->num_comments;
1164 $counts['total_comments'] += $row->num_comments;
1165 } elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
1166 $counts['total_comments'] += $row->num_comments;
1167 }
1168 if ( isset( $approved[ $row->comment_approved ] ) ) {
1169 $counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
1170 }
1171 }
1172
1173 foreach ( $approved as $key ) {
1174 if ( empty( $counts[ $key ] ) ) {
1175 $counts[ $key ] = 0;
1176 }
1177 }
1178
1179 $counts = (object) $counts;
1180
1181 return $counts;
1182 }
1183
1184 /**
1185 * Traps `wp_die()` calls and outputs a JSON response instead.
1186 * The result is always output, never returned.
1187 *
1188 * @param string|null $error_code Call with string to start the trapping. Call with null to stop.
1189 * @param int $http_status HTTP status code, 400 by default.
1190 */
1191 public function trap_wp_die( $error_code = null, $http_status = 400 ) {
1192 // Determine the filter name; based on the conditionals inside the wp_die function.
1193 if ( wp_is_json_request() ) {
1194 $die_handler = 'wp_die_json_handler';
1195 } elseif ( wp_is_jsonp_request() ) {
1196 $die_handler = 'wp_die_jsonp_handler';
1197 } elseif ( wp_is_xml_request() ) {
1198 $die_handler = 'wp_die_xml_handler';
1199 } else {
1200 $die_handler = 'wp_die_handler';
1201 }
1202
1203 if ( $error_code === null ) {
1204 $this->trapped_error = null;
1205 // Stop trapping.
1206 remove_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
1207 return;
1208 }
1209
1210 // If API called via PHP, bail: don't do our custom wp_die(). Do the normal wp_die().
1211 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1212 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1213 return;
1214 }
1215 } elseif ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
1216 return;
1217 }
1218
1219 $this->trapped_error = array(
1220 'status' => $http_status,
1221 'code' => $error_code,
1222 'message' => '',
1223 );
1224 // Start trapping.
1225 add_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
1226 }
1227
1228 /**
1229 * Filter function for `wp_die_handler` and similar filters.
1230 *
1231 * @return callable
1232 */
1233 public function wp_die_handler_callback() {
1234 return array( $this, 'wp_die_handler' );
1235 }
1236
1237 /**
1238 * Handler for `wp_die` calls.
1239 *
1240 * @param string|WP_Error $message As for `wp_die()`.
1241 * @param string|int $title As for `wp_die()`.
1242 * @param string|array|int $args As for `wp_die()`.
1243 * @return never
1244 */
1245 public function wp_die_handler( $message, $title = '', $args = array() ) {
1246 // Allow wp_die calls to override HTTP status code...
1247 $args = wp_parse_args(
1248 $args,
1249 array(
1250 'response' => $this->trapped_error['status'],
1251 )
1252 );
1253
1254 // ... unless it's 500
1255 if ( 500 !== (int) $args['response'] ) {
1256 $this->trapped_error['status'] = $args['response'];
1257 }
1258
1259 if ( $title ) {
1260 $message = "$title: $message";
1261 }
1262
1263 $this->trapped_error['message'] = wp_kses( $message, array() );
1264
1265 switch ( $this->trapped_error['code'] ) {
1266 case 'comment_failure':
1267 if ( did_action( 'comment_duplicate_trigger' ) ) {
1268 $this->trapped_error['code'] = 'comment_duplicate';
1269 } elseif ( did_action( 'comment_flood_trigger' ) ) {
1270 $this->trapped_error['code'] = 'comment_flood';
1271 }
1272 break;
1273 }
1274
1275 // We still want to exit so that code execution stops where it should.
1276 // Attach the JSON output to the WordPress shutdown handler.
1277 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
1278 exit( 0 );
1279 }
1280
1281 /**
1282 * Output the trapped error.
1283 */
1284 public function output_trapped_error() {
1285 $this->exit = false; // We're already exiting once. Don't do it twice.
1286 $this->output(
1287 $this->trapped_error['status'],
1288 (object) array(
1289 'error' => $this->trapped_error['code'],
1290 'message' => $this->trapped_error['message'],
1291 )
1292 );
1293 }
1294
1295 /**
1296 * Finish the request.
1297 */
1298 public function finish_request() {
1299 if ( function_exists( 'fastcgi_finish_request' ) ) {
1300 return fastcgi_finish_request();
1301 }
1302 }
1303
1304 /**
1305 * Initialize the locale if different from 'en'.
1306 *
1307 * @param string $locale The locale to initialize.
1308 */
1309 public function init_locale( $locale ) {
1310 if ( 'en' !== $locale ) {
1311 // .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
1312 $new_locale = $locale;
1313 if ( str_contains( $locale, '-' ) ) {
1314 $locale_pieces = explode( '-', $locale );
1315 $new_locale = $locale_pieces[0];
1316 $new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
1317 } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
1318 // .com might pass 'fr' because thats what our language files are named as, where core seems
1319 // to do fr_FR - so try that if we don't think we can load the file.
1320 if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
1321 $new_locale = $locale . '_' . strtoupper( $locale );
1322 }
1323 }
1324
1325 if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
1326 unload_textdomain( 'default' );
1327 load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
1328 }
1329 }
1330 }
1331 }
1332