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