PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 13.1.5
Jetpack – WP Security, Backup, Speed, & Growth v13.1.5
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 2 years ago _inc 2 years ago css 2 years ago extensions 2 years ago images 2 years ago jetpack_vendor 1 day ago json-endpoints 2 years ago modules 1 year ago sal 2 years ago src 3 years ago vendor 2 years ago views 3 years ago CHANGELOG.md 2 years ago LICENSE.txt 5 years ago SECURITY.md 2 years ago class-jetpack-connection-status.php 2 years ago class-jetpack-connection-widget.php 2 years ago class-jetpack-gallery-settings.php 3 years ago class-jetpack-pre-connection-jitms.php 4 years ago class-jetpack-recommendations-banner.php 2 years ago class-jetpack-stats-dashboard-widget.php 2 years ago class-jetpack-wizard-banner.php 5 years ago class-jetpack-xmlrpc-methods.php 2 years ago class.frame-nonce-preview.php 4 years ago class.jetpack-admin.php 2 years 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-boost-modules.php 3 years ago class.jetpack-cli.php 2 years ago class.jetpack-client-server.php 4 years ago class.jetpack-connection-banner.php 2 years ago class.jetpack-data.php 2 years ago class.jetpack-gutenberg.php 2 years ago class.jetpack-heartbeat.php 2 years ago class.jetpack-idc.php 5 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 3 years ago class.jetpack-plan.php 3 years ago class.jetpack-post-images.php 2 years ago class.jetpack-twitter-cards.php 2 years ago class.jetpack-user-agent.php 2 years ago class.jetpack.php 2 years ago class.json-api-endpoints.php 2 years ago class.json-api.php 2 years ago class.photon.php 3 years ago composer.json 2 years ago enhanced-open-graph.php 3 years ago functions.compat.php 2 years ago functions.cookies.php 2 years ago functions.global.php 2 years ago functions.is-mobile.php 2 years ago functions.opengraph.php 2 years 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 3 years ago locales.php 4 years ago readme.txt 1 day ago uninstall.php 2 years ago wpml-config.xml 4 years ago
class.json-api.php
1251 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 ( '{' === $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 * Serve.
386 *
387 * @param bool $exit Whether to exit.
388 * @return string|null Content type (assuming it didn't exit), or null in certain error cases.
389 */
390 public function serve( $exit = true ) {
391 ini_set( 'display_errors', false ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Blacklisted
392
393 $this->exit = (bool) $exit;
394
395 // This was causing problems with Jetpack, but is necessary for wpcom
396 // @see https://github.com/Automattic/jetpack/pull/2603
397 // @see r124548-wpcom .
398 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
399 add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
400 }
401
402 add_filter( 'user_can_richedit', '__return_true' );
403
404 add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
405
406 $initialization = $this->initialize();
407 if ( 'OPTIONS' === $this->method ) {
408 /**
409 * Fires before the page output.
410 * Can be used to specify custom header options.
411 *
412 * @module json-api
413 *
414 * @since 3.1.0
415 */
416 do_action( 'wpcom_json_api_options' );
417 return $this->output( 200, '', 'text/plain' );
418 }
419
420 if ( is_wp_error( $initialization ) ) {
421 $this->output_error( $initialization );
422 return;
423 }
424
425 // Normalize path and extract API version.
426 $this->path = untrailingslashit( $this->path );
427 preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
428 $this->path = substr( $this->path, strlen( $matches[0] ) );
429 $this->version = $matches[1];
430
431 $allowed_methods = array( 'GET', 'POST' );
432 $four_oh_five = false;
433
434 $is_help = preg_match( '#/help/?$#i', $this->path );
435 $matching_endpoints = array();
436
437 if ( $is_help ) {
438 $origin = get_http_origin();
439
440 if ( ! empty( $origin ) && 'GET' === $this->method ) {
441 header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
442 }
443
444 $this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
445 // Show help for all matching endpoints regardless of method.
446 $methods = $allowed_methods;
447 $find_all_matching_endpoints = true;
448 // How deep to truncate each endpoint's path to see if it matches this help request.
449 $depth = substr_count( $this->path, '/' ) + 1;
450 if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
451 $help_content_type = 'json';
452 } else {
453 $help_content_type = 'html';
454 }
455 } elseif ( in_array( $this->method, $allowed_methods, true ) ) {
456 // Only serve requested method.
457 $methods = array( $this->method );
458 $find_all_matching_endpoints = false;
459 } else {
460 // We don't allow this requested method - find matching endpoints and send 405.
461 $methods = $allowed_methods;
462 $find_all_matching_endpoints = true;
463 $four_oh_five = true;
464 }
465
466 // Find which endpoint to serve.
467 $found = false;
468 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
469 // @todo Determine if anything depends on this being serialized rather than e.g. JSON.
470 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Legacy, possibly depended on elsewhere.
471 $endpoint_path_versions = unserialize( $endpoint_path_versions );
472 $endpoint_path = $endpoint_path_versions[0];
473 $endpoint_min_version = $endpoint_path_versions[1];
474 $endpoint_max_version = $endpoint_path_versions[2];
475
476 // Make sure max_version is not less than min_version.
477 if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
478 $endpoint_max_version = $endpoint_min_version;
479 }
480
481 foreach ( $methods as $method ) {
482 if ( ! isset( $endpoints_by_method[ $method ] ) ) {
483 continue;
484 }
485
486 // Normalize.
487 $endpoint_path = untrailingslashit( $endpoint_path );
488 if ( $is_help ) {
489 // Truncate path at help depth.
490 $endpoint_path = implode( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
491 }
492
493 // Generate regular expression from sprintf().
494 $endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
495
496 if ( ! preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
497 // This endpoint does not match the requested path.
498 continue;
499 }
500
501 if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
502 // This endpoint does not match the requested version.
503 continue;
504 }
505
506 $found = true;
507
508 if ( $find_all_matching_endpoints ) {
509 $matching_endpoints[] = array( $endpoints_by_method[ $method ], $path_pieces );
510 } else {
511 // The method parameters are now in $path_pieces.
512 $endpoint = $endpoints_by_method[ $method ];
513 break 2;
514 }
515 }
516 }
517
518 if ( ! $found ) {
519 return $this->output( 404, '', 'text/plain' );
520 }
521
522 if ( $four_oh_five ) {
523 $allowed_methods = array();
524 foreach ( $matching_endpoints as $matching_endpoint ) {
525 $allowed_methods[] = $matching_endpoint[0]->method;
526 }
527
528 header( 'Allow: ' . strtoupper( implode( ',', array_unique( $allowed_methods ) ) ) );
529 return $this->output(
530 405,
531 array(
532 'error' => 'not_allowed',
533 'error_message' => 'Method not allowed',
534 )
535 );
536 }
537
538 if ( $is_help ) {
539 /**
540 * Fires before the API output.
541 *
542 * @since 1.9.0
543 *
544 * @param string help.
545 */
546 do_action( 'wpcom_json_api_output', 'help' );
547 $proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
548 if ( 'json' === $help_content_type ) {
549 $docs = array();
550 foreach ( $matching_endpoints as $matching_endpoint ) {
551 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
552 $docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
553 }
554 }
555 return $this->output( 200, $docs );
556 } else {
557 status_header( 200 );
558 foreach ( $matching_endpoints as $matching_endpoint ) {
559 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
560 call_user_func( array( $matching_endpoint[0], 'document' ) );
561 }
562 }
563 }
564 exit;
565 }
566
567 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
568 return $this->output( 404, '', 'text/plain' );
569 }
570
571 /** This action is documented in class.json-api.php */
572 do_action( 'wpcom_json_api_output', $endpoint->stat );
573
574 $response = $this->process_request( $endpoint, $path_pieces );
575
576 if ( ! $response && ! is_array( $response ) ) {
577 return $this->output( 500, '', 'text/plain' );
578 } elseif ( is_wp_error( $response ) ) {
579 return $this->output_error( $response );
580 }
581
582 $output_status_code = $this->output_status_code;
583 $this->set_output_status_code();
584
585 return $this->output( $output_status_code, $response, 'application/json', $this->extra_headers );
586 }
587
588 /**
589 * Process a request.
590 *
591 * @param WPCOM_JSON_API_Endpoint $endpoint Endpoint.
592 * @param array $path_pieces Path pieces.
593 * @return array|WP_Error Return value from the endpoint's callback.
594 */
595 public function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
596 $this->endpoint = $endpoint;
597 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
598 }
599
600 /**
601 * Output a response or error without exiting.
602 *
603 * @param int $status_code HTTP status code.
604 * @param mixed $response Response data.
605 * @param string $content_type Content type of the response.
606 */
607 public function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
608 $exit = $this->exit;
609 $this->exit = false;
610 if ( is_wp_error( $response ) ) {
611 $this->output_error( $response );
612 } else {
613 $this->output( $status_code, $response, $content_type );
614 }
615 $this->exit = $exit;
616 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
617 $this->finish_request();
618 }
619 }
620
621 /**
622 * Set output status code.
623 *
624 * @param int $code HTTP status code.
625 */
626 public function set_output_status_code( $code = 200 ) {
627 $this->output_status_code = $code;
628 }
629
630 /**
631 * Output a response.
632 *
633 * @param int $status_code HTTP status code.
634 * @param mixed $response Response data.
635 * @param string $content_type Content type of the response.
636 * @param array $extra Additional HTTP headers.
637 * @return string Content type (assuming it didn't exit).
638 */
639 public function output( $status_code, $response = null, $content_type = 'application/json', $extra = array() ) {
640 $status_code = (int) $status_code;
641
642 // In case output() was called before the callback returned.
643 if ( $this->did_output ) {
644 if ( $this->exit ) {
645 exit;
646 }
647 return $content_type;
648 }
649 $this->did_output = true;
650
651 // 400s and 404s are allowed for all origins
652 if ( 404 === $status_code || 400 === $status_code ) {
653 header( 'Access-Control-Allow-Origin: *' );
654 }
655
656 /* Add headers for form submission from <amp-form/> */
657 if ( $this->amp_source_origin ) {
658 header( 'Access-Control-Allow-Origin: ' . wp_unslash( $this->amp_source_origin ) );
659 header( 'Access-Control-Allow-Credentials: true' );
660 }
661
662 if ( $response === null ) {
663 $response = new stdClass();
664 }
665
666 if ( 'text/plain' === $content_type ||
667 'text/html' === $content_type ) {
668 status_header( (int) $status_code );
669 header( 'Content-Type: ' . $content_type );
670 foreach ( $extra as $key => $value ) {
671 header( "$key: $value" );
672 }
673 echo $response; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
674 if ( $this->exit ) {
675 exit;
676 }
677
678 return $content_type;
679 }
680
681 $response = $this->filter_fields( $response );
682
683 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
684 $headers = array(
685 array(
686 'name' => 'Content-Type',
687 'value' => $content_type,
688 ),
689 );
690
691 foreach ( $extra as $key => $value ) {
692 $headers[] = array(
693 'name' => $key,
694 'value' => $value,
695 );
696 }
697
698 $response = array(
699 'code' => (int) $status_code,
700 'headers' => $headers,
701 'body' => $response,
702 );
703 $status_code = 200;
704 $content_type = 'application/json';
705 }
706
707 status_header( (int) $status_code );
708 header( "Content-Type: $content_type" );
709 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
710 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
711 } else {
712 $callback = false;
713 }
714
715 if ( $callback ) {
716 // Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
717 // and by prepending the JSONP response with a JS comment.
718 // [1] <https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html>.
719 echo "/**/$callback("; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSONP output, not HTML.
720
721 }
722 echo $this->json_encode( $response ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSON or JSONP output, not HTML.
723 if ( $callback ) {
724 echo ');';
725 }
726
727 if ( $this->exit ) {
728 exit;
729 }
730
731 return $content_type;
732 }
733
734 /**
735 * Serialize an error.
736 *
737 * @param WP_Error $error Error.
738 * @return array with 'status_code' and 'errors' data.
739 */
740 public static function serializable_error( $error ) {
741
742 $status_code = $error->get_error_data();
743
744 if ( is_array( $status_code ) ) {
745 $status_code = $status_code['status_code'];
746 }
747
748 if ( ! $status_code ) {
749 $status_code = 400;
750 }
751 $response = array(
752 'error' => $error->get_error_code(),
753 'message' => $error->get_error_message(),
754 );
755
756 $additional_data = $error->get_error_data( 'additional_data' );
757 if ( $additional_data ) {
758 $response['data'] = $additional_data;
759 }
760
761 return array(
762 'status_code' => $status_code,
763 'errors' => $response,
764 );
765 }
766
767 /**
768 * Output an error.
769 *
770 * @param WP_Error $error Error.
771 * @return string Content type (assuming it didn't exit).
772 */
773 public function output_error( $error ) {
774 $error_response = static::serializable_error( $error );
775
776 return $this->output( $error_response['status_code'], $error_response['errors'] );
777 }
778
779 /**
780 * Filter fields in a response.
781 *
782 * @param array|object $response Response.
783 * @return array|object Filtered response.
784 */
785 public function filter_fields( $response ) {
786 if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) ) {
787 return $response;
788 }
789
790 $fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
791
792 if ( is_object( $response ) ) {
793 $response = (array) $response;
794 }
795
796 $has_filtered = false;
797 if ( is_array( $response ) && empty( $response['ID'] ) ) {
798 $keys_to_filter = array(
799 'categories',
800 'comments',
801 'connections',
802 'domains',
803 'groups',
804 'likes',
805 'media',
806 'notes',
807 'posts',
808 'services',
809 'sites',
810 'suggestions',
811 'tags',
812 'themes',
813 'topics',
814 'users',
815 );
816
817 foreach ( $keys_to_filter as $key_to_filter ) {
818 if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
819 continue;
820 }
821
822 foreach ( $response[ $key_to_filter ] as $key => $values ) {
823 if ( is_object( $values ) ) {
824 if ( is_object( $response[ $key_to_filter ] ) ) {
825 // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found -- False positive.
826 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
827 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
828 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
829 }
830 } elseif ( is_array( $values ) ) {
831 $response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
832 }
833 }
834
835 $has_filtered = true;
836 }
837 }
838
839 if ( ! $has_filtered ) {
840 if ( is_object( $response ) ) {
841 $response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
842 } elseif ( is_array( $response ) ) {
843 $response = array_intersect_key( $response, array_flip( $fields ) );
844 }
845 }
846
847 return $response;
848 }
849
850 /**
851 * Filter for `home_url`.
852 *
853 * If `$original_scheme` is null, turns an https URL to http.
854 *
855 * @param string $url The complete home URL including scheme and path.
856 * @param string $path Path relative to the home URL. Blank string if no path is specified.
857 * @param string|null $original_scheme Scheme to give the home URL context. Accepts 'http', 'https', 'relative', 'rest', or null.
858 * @return string URL.
859 */
860 public function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
861 if ( $original_scheme ) {
862 return $url;
863 }
864
865 return preg_replace( '#^https:#', 'http:', $url );
866 }
867
868 /**
869 * Decode HTML special characters in comment content.
870 *
871 * @param string $comment_content Comment content.
872 * @return string
873 */
874 public function comment_edit_pre( $comment_content ) {
875 return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
876 }
877
878 /**
879 * JSON encode.
880 *
881 * @param mixed $data Data.
882 * @return string|false
883 */
884 public function json_encode( $data ) {
885 return wp_json_encode( $data );
886 }
887
888 /**
889 * Test if a string ends with a string.
890 *
891 * @param string $haystack String to check.
892 * @param string $needle Suffix to check.
893 * @return bool
894 */
895 public function ends_with( $haystack, $needle ) {
896 return substr( $haystack, -strlen( $needle ) ) === $needle;
897 }
898
899 /**
900 * Returns the site's blog_id in the WP.com ecosystem
901 *
902 * @return int
903 */
904 public function get_blog_id_for_output() {
905 return $this->token_details['blog_id'];
906 }
907
908 /**
909 * Returns the site's local blog_id.
910 *
911 * @param int $blog_id Blog ID.
912 * @return int
913 */
914 public function get_blog_id( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
915 return $GLOBALS['blog_id'];
916 }
917
918 /**
919 * Switch to blog and validate user.
920 *
921 * @param int $blog_id Blog ID.
922 * @param bool $verify_token_for_blog Whether to verify the token.
923 * @return int Blog ID.
924 */
925 public function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
926 if ( $this->is_restricted_blog( $blog_id ) ) {
927 return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
928 }
929 /**
930 * If this is a private site we check for 2 things:
931 * 1. In case of user based authentication, we need to check if the logged-in user has the 'read' capability.
932 * 2. In case of site based authentication, make sure the endpoint accepts it.
933 */
934 if ( ( new Status() )->is_private_site() &&
935 ! current_user_can( 'read' ) &&
936 ! $this->endpoint->accepts_site_based_authentication()
937 ) {
938 return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
939 }
940
941 return $blog_id;
942 }
943
944 /**
945 * Returns true if the specified blog ID is a restricted blog
946 *
947 * @param int $blog_id Blog ID.
948 * @return bool
949 */
950 public function is_restricted_blog( $blog_id ) {
951 /**
952 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
953 *
954 * @module json-api
955 *
956 * @since 3.4.0
957 *
958 * @param array $array Array of Blog IDs.
959 */
960 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
961 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.
962 }
963
964 /**
965 * Post like count.
966 *
967 * @param int $blog_id Blog ID.
968 * @param int $post_id Post ID.
969 * @return int
970 */
971 public function post_like_count( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
972 return 0;
973 }
974
975 /**
976 * Is liked?
977 *
978 * @param int $blog_id Blog ID.
979 * @param int $post_id Post ID.
980 * @return bool
981 */
982 public function is_liked( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
983 return false;
984 }
985
986 /**
987 * Is reblogged?
988 *
989 * @param int $blog_id Blog ID.
990 * @param int $post_id Post ID.
991 * @return bool
992 */
993 public function is_reblogged( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
994 return false;
995 }
996
997 /**
998 * Is following?
999 *
1000 * @param int $blog_id Blog ID.
1001 * @return bool
1002 */
1003 public function is_following( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1004 return false;
1005 }
1006
1007 /**
1008 * Add global ID.
1009 *
1010 * @param int $blog_id Blog ID.
1011 * @param int $post_id Post ID.
1012 * @return string
1013 */
1014 public function add_global_ID( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
1015 return '';
1016 }
1017
1018 /**
1019 * Get avatar URL.
1020 *
1021 * @param string $email Email.
1022 * @param array $avatar_size Args for `get_avatar_url()`.
1023 * @return string|false
1024 */
1025 public function get_avatar_url( $email, $avatar_size = null ) {
1026 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
1027 return null === $avatar_size
1028 ? wpcom_get_avatar_url( $email )
1029 : wpcom_get_avatar_url( $email, $avatar_size );
1030 } else {
1031 return null === $avatar_size
1032 ? get_avatar_url( $email )
1033 : get_avatar_url( $email, $avatar_size );
1034 }
1035 }
1036
1037 /**
1038 * Counts the number of comments on a site, including certain comment types.
1039 *
1040 * @param int $post_id Post ID.
1041 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
1042 */
1043 public function wp_count_comments( $post_id ) {
1044 global $wpdb;
1045 if ( 0 !== $post_id ) {
1046 return wp_count_comments( $post_id );
1047 }
1048
1049 $counts = array(
1050 'total_comments' => 0,
1051 'all' => 0,
1052 );
1053
1054 /**
1055 * Exclude certain comment types from comment counts in the REST API.
1056 *
1057 * @since 6.9.0
1058 * @deprecated 11.1
1059 * @module json-api
1060 *
1061 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
1062 */
1063 $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
1064
1065 /**
1066 * Include certain comment types in comment counts in the REST API.
1067 * Note: the default array of comment types includes an empty string,
1068 * to support comments posted before WP 5.5, that used an empty string as comment type.
1069 *
1070 * @since 11.1
1071 * @module json-api
1072 *
1073 * @param array Array of comment types to include (default: 'comment', 'pingback', 'trackback')
1074 */
1075 $include = apply_filters(
1076 'jetpack_api_include_comment_types_count',
1077 array( 'comment', 'pingback', 'trackback', '' )
1078 );
1079
1080 if ( empty( $include ) ) {
1081 return wp_count_comments( $post_id );
1082 }
1083
1084 array_walk( $include, 'esc_sql' );
1085 $where = sprintf(
1086 "WHERE comment_type IN ( '%s' )",
1087 implode( "','", $include )
1088 );
1089
1090 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1091 $count = $wpdb->get_results(
1092 "SELECT comment_approved, COUNT(*) AS num_comments
1093 FROM $wpdb->comments
1094 {$where}
1095 GROUP BY comment_approved
1096 "
1097 );
1098 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1099
1100 $approved = array(
1101 '0' => 'moderated',
1102 '1' => 'approved',
1103 'spam' => 'spam',
1104 'trash' => 'trash',
1105 'post-trashed' => 'post-trashed',
1106 );
1107
1108 // <https://developer.wordpress.org/reference/functions/get_comment_count/#source>
1109 foreach ( $count as $row ) {
1110 if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
1111 $counts['all'] += $row->num_comments;
1112 $counts['total_comments'] += $row->num_comments;
1113 } elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
1114 $counts['total_comments'] += $row->num_comments;
1115 }
1116 if ( isset( $approved[ $row->comment_approved ] ) ) {
1117 $counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
1118 }
1119 }
1120
1121 foreach ( $approved as $key ) {
1122 if ( empty( $counts[ $key ] ) ) {
1123 $counts[ $key ] = 0;
1124 }
1125 }
1126
1127 $counts = (object) $counts;
1128
1129 return $counts;
1130 }
1131
1132 /**
1133 * Traps `wp_die()` calls and outputs a JSON response instead.
1134 * The result is always output, never returned.
1135 *
1136 * @param string|null $error_code Call with string to start the trapping. Call with null to stop.
1137 * @param int $http_status HTTP status code, 400 by default.
1138 */
1139 public function trap_wp_die( $error_code = null, $http_status = 400 ) {
1140 // Determine the filter name; based on the conditionals inside the wp_die function.
1141 if ( wp_is_json_request() ) {
1142 $die_handler = 'wp_die_json_handler';
1143 } elseif ( wp_is_jsonp_request() ) {
1144 $die_handler = 'wp_die_jsonp_handler';
1145 } elseif ( wp_is_xml_request() ) {
1146 $die_handler = 'wp_die_xml_handler';
1147 } else {
1148 $die_handler = 'wp_die_handler';
1149 }
1150
1151 if ( $error_code === null ) {
1152 $this->trapped_error = null;
1153 // Stop trapping.
1154 remove_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
1155 return;
1156 }
1157
1158 // If API called via PHP, bail: don't do our custom wp_die(). Do the normal wp_die().
1159 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1160 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1161 return;
1162 }
1163 } elseif ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
1164 return;
1165 }
1166
1167 $this->trapped_error = array(
1168 'status' => $http_status,
1169 'code' => $error_code,
1170 'message' => '',
1171 );
1172 // Start trapping.
1173 add_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
1174 }
1175
1176 /**
1177 * Filter function for `wp_die_handler` and similar filters.
1178 *
1179 * @return callable
1180 */
1181 public function wp_die_handler_callback() {
1182 return array( $this, 'wp_die_handler' );
1183 }
1184
1185 /**
1186 * Handler for `wp_die` calls.
1187 *
1188 * @param string|WP_Error $message As for `wp_die()`.
1189 * @param string|int $title As for `wp_die()`.
1190 * @param string|array|int $args As for `wp_die()`.
1191 */
1192 public function wp_die_handler( $message, $title = '', $args = array() ) {
1193 // Allow wp_die calls to override HTTP status code...
1194 $args = wp_parse_args(
1195 $args,
1196 array(
1197 'response' => $this->trapped_error['status'],
1198 )
1199 );
1200
1201 // ... unless it's 500
1202 if ( 500 !== (int) $args['response'] ) {
1203 $this->trapped_error['status'] = $args['response'];
1204 }
1205
1206 if ( $title ) {
1207 $message = "$title: $message";
1208 }
1209
1210 $this->trapped_error['message'] = wp_kses( $message, array() );
1211
1212 switch ( $this->trapped_error['code'] ) {
1213 case 'comment_failure':
1214 if ( did_action( 'comment_duplicate_trigger' ) ) {
1215 $this->trapped_error['code'] = 'comment_duplicate';
1216 } elseif ( did_action( 'comment_flood_trigger' ) ) {
1217 $this->trapped_error['code'] = 'comment_flood';
1218 }
1219 break;
1220 }
1221
1222 // We still want to exit so that code execution stops where it should.
1223 // Attach the JSON output to the WordPress shutdown handler.
1224 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
1225 exit;
1226 }
1227
1228 /**
1229 * Output the trapped error.
1230 */
1231 public function output_trapped_error() {
1232 $this->exit = false; // We're already exiting once. Don't do it twice.
1233 $this->output(
1234 $this->trapped_error['status'],
1235 (object) array(
1236 'error' => $this->trapped_error['code'],
1237 'message' => $this->trapped_error['message'],
1238 )
1239 );
1240 }
1241
1242 /**
1243 * Finish the request.
1244 */
1245 public function finish_request() {
1246 if ( function_exists( 'fastcgi_finish_request' ) ) {
1247 return fastcgi_finish_request();
1248 }
1249 }
1250 }
1251