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