Completed
Push — fusion-sync/annemirasol/r21345... ( da41c8 )
by
unknown
124:11 queued 114:36
created

class.json-api.php (12 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
defined( 'WPCOM_JSON_API__DEBUG' ) or define( 'WPCOM_JSON_API__DEBUG', false );
4
5
require_once dirname( __FILE__ ) . '/sal/class.json-api-platform.php';
6
7
class WPCOM_JSON_API {
8
	static $self = null;
9
10
	public $endpoints = array();
11
12
	public $token_details = array();
13
14
	public $method       = '';
15
	public $url          = '';
16
	public $path         = '';
17
	public $version      = null;
18
	public $query        = array();
19
	public $post_body    = null;
20
	public $files        = null;
21
	public $content_type = null;
22
	public $accept       = '';
23
24
	public $_server_https;
25
	public $exit              = true;
26
	public $public_api_scheme = 'https';
27
28
	public $output_status_code = 200;
29
30
	public $trapped_error = null;
31
	public $did_output    = false;
32
33
	public $extra_headers = array();
34
35
	public $amp_source_origin = null;
36
37
	/**
38
	 * @return WPCOM_JSON_API instance
39
	 */
40
	static function init( $method = null, $url = null, $post_body = null ) {
41
		if ( ! self::$self ) {
42
			$class      = function_exists( 'get_called_class' ) ? get_called_class() : __CLASS__; // phpcs:ignore PHPCompatibility.PHP.NewFunctions.get_called_classFound
43
			self::$self = new $class( $method, $url, $post_body );
44
		}
45
		return self::$self;
46
	}
47
48
	function add( WPCOM_JSON_API_Endpoint $endpoint ) {
49
		$path_versions = serialize(
50
			array(
51
				$endpoint->path,
52
				$endpoint->min_version,
53
				$endpoint->max_version,
54
			)
55
		);
56
		if ( ! isset( $this->endpoints[ $path_versions ] ) ) {
57
			$this->endpoints[ $path_versions ] = array();
58
		}
59
		$this->endpoints[ $path_versions ][ $endpoint->method ] = $endpoint;
60
	}
61
62
	static function is_truthy( $value ) {
63
		switch ( strtolower( (string) $value ) ) {
64
			case '1':
65
			case 't':
66
			case 'true':
67
				return true;
68
		}
69
70
		return false;
71
	}
72
73
	static function is_falsy( $value ) {
74
		switch ( strtolower( (string) $value ) ) {
75
			case '0':
76
			case 'f':
77
			case 'false':
78
				return true;
79
		}
80
81
		return false;
82
	}
83
84
	function __construct( ...$args ) {
85
		call_user_func_array( array( $this, 'setup_inputs' ), $args );
86
	}
87
88
	function setup_inputs( $method = null, $url = null, $post_body = null ) {
89
		if ( is_null( $method ) ) {
90
			$this->method = strtoupper( $_SERVER['REQUEST_METHOD'] );
91
		} else {
92
			$this->method = strtoupper( $method );
93
		}
94
		if ( is_null( $url ) ) {
95
			$this->url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
96
		} else {
97
			$this->url = $url;
98
		}
99
100
		$parsed = wp_parse_url( $this->url );
101
		if ( ! empty( $parsed['path'] ) ) {
102
			$this->path = $parsed['path'];
103
		}
104
105
		if ( ! empty( $parsed['query'] ) ) {
106
			wp_parse_str( $parsed['query'], $this->query );
107
		}
108
109
		if ( isset( $_SERVER['HTTP_ACCEPT'] ) && $_SERVER['HTTP_ACCEPT'] ) {
110
			$this->accept = $_SERVER['HTTP_ACCEPT'];
111
		}
112
113
		if ( 'POST' === $this->method ) {
114
			if ( is_null( $post_body ) ) {
115
				$this->post_body = file_get_contents( 'php://input' );
116
117
				if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) && $_SERVER['HTTP_CONTENT_TYPE'] ) {
118
					$this->content_type = $_SERVER['HTTP_CONTENT_TYPE'];
119
				} elseif ( isset( $_SERVER['CONTENT_TYPE'] ) && $_SERVER['CONTENT_TYPE'] ) {
120
					$this->content_type = $_SERVER['CONTENT_TYPE'];
121
				} elseif ( '{' === $this->post_body[0] ) {
122
					$this->content_type = 'application/json';
123
				} else {
124
					$this->content_type = 'application/x-www-form-urlencoded';
125
				}
126
127
				if ( 0 === strpos( strtolower( $this->content_type ), 'multipart/' ) ) {
128
					$this->post_body    = http_build_query( stripslashes_deep( $_POST ) );
129
					$this->files        = $_FILES;
130
					$this->content_type = 'multipart/form-data';
131
				}
132
			} else {
133
				$this->post_body    = $post_body;
134
				$this->content_type = '{' === isset( $this->post_body[0] ) && $this->post_body[0] ? 'application/json' : 'application/x-www-form-urlencoded';
135
			}
136
		} else {
137
			$this->post_body    = null;
138
			$this->content_type = null;
139
		}
140
141
		$this->_server_https = array_key_exists( 'HTTPS', $_SERVER ) ? $_SERVER['HTTPS'] : '--UNset--';
142
	}
143
144
	function initialize() {
145
		$this->token_details['blog_id'] = Jetpack_Options::get_option( 'id' );
146
	}
147
148
	function serve( $exit = true ) {
149
		ini_set( 'display_errors', false );
150
151
		$this->exit = (bool) $exit;
152
153
		// This was causing problems with Jetpack, but is necessary for wpcom
154
		// @see https://github.com/Automattic/jetpack/pull/2603
155
		// @see r124548-wpcom
156
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
157
			add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
158
		}
159
160
		add_filter( 'user_can_richedit', '__return_true' );
161
162
		add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
163
164
		$initialization = $this->initialize();
0 ignored issues
show
Are you sure the assignment to $initialization is correct as $this->initialize() (which targets WPCOM_JSON_API::initialize()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
165
		if ( 'OPTIONS' == $this->method ) {
166
			/**
167
			 * Fires before the page output.
168
			 * Can be used to specify custom header options.
169
			 *
170
			 * @module json-api
171
			 *
172
			 * @since 3.1.0
173
			 */
174
			do_action( 'wpcom_json_api_options' );
175
			return $this->output( 200, '', 'text/plain' );
176
		}
177
178
		if ( is_wp_error( $initialization ) ) {
179
			$this->output_error( $initialization );
180
			return;
181
		}
182
183
		// Normalize path and extract API version
184
		$this->path = untrailingslashit( $this->path );
185
		preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
186
		$this->path    = substr( $this->path, strlen( $matches[0] ) );
187
		$this->version = $matches[1];
188
189
		$allowed_methods = array( 'GET', 'POST' );
190
		$four_oh_five    = false;
191
192
		$is_help            = preg_match( '#/help/?$#i', $this->path );
193
		$matching_endpoints = array();
194
195
		if ( $is_help ) {
196
			$origin = get_http_origin();
197
198
			if ( ! empty( $origin ) && 'GET' == $this->method ) {
199
				header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
200
			}
201
202
			$this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
203
			// Show help for all matching endpoints regardless of method
204
			$methods                     = $allowed_methods;
205
			$find_all_matching_endpoints = true;
206
			// How deep to truncate each endpoint's path to see if it matches this help request
207
			$depth = substr_count( $this->path, '/' ) + 1;
208
			if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
209
				$help_content_type = 'json';
210
			} else {
211
				$help_content_type = 'html';
212
			}
213
		} else {
214
			if ( in_array( $this->method, $allowed_methods ) ) {
215
				// Only serve requested method
216
				$methods                     = array( $this->method );
217
				$find_all_matching_endpoints = false;
218
			} else {
219
				// We don't allow this requested method - find matching endpoints and send 405
220
				$methods                     = $allowed_methods;
221
				$find_all_matching_endpoints = true;
222
				$four_oh_five                = true;
223
			}
224
		}
225
226
		// Find which endpoint to serve
227
		$found = false;
228
		foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
229
			$endpoint_path_versions = unserialize( $endpoint_path_versions );
230
			$endpoint_path          = $endpoint_path_versions[0];
231
			$endpoint_min_version   = $endpoint_path_versions[1];
232
			$endpoint_max_version   = $endpoint_path_versions[2];
233
234
			// Make sure max_version is not less than min_version
235
			if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
236
				$endpoint_max_version = $endpoint_min_version;
237
			}
238
239
			foreach ( $methods as $method ) {
240
				if ( ! isset( $endpoints_by_method[ $method ] ) ) {
241
					continue;
242
				}
243
244
				// Normalize
245
				$endpoint_path = untrailingslashit( $endpoint_path );
246
				if ( $is_help ) {
247
					// Truncate path at help depth
248
					$endpoint_path = join( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
0 ignored issues
show
The variable $depth does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
249
				}
250
251
				// Generate regular expression from sprintf()
252
				$endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
253
254
				if ( ! preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
255
					// This endpoint does not match the requested path.
256
					continue;
257
				}
258
259
				if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
260
					// This endpoint does not match the requested version.
261
					continue;
262
				}
263
264
				$found = true;
265
266
				if ( $find_all_matching_endpoints ) {
267
					$matching_endpoints[] = array( $endpoints_by_method[ $method ], $path_pieces );
268
				} else {
269
					// The method parameters are now in $path_pieces
270
					$endpoint = $endpoints_by_method[ $method ];
271
					break 2;
272
				}
273
			}
274
		}
275
276
		if ( ! $found ) {
277
			return $this->output( 404, '', 'text/plain' );
278
		}
279
280
		if ( $four_oh_five ) {
281
			$allowed_methods = array();
282
			foreach ( $matching_endpoints as $matching_endpoint ) {
283
				$allowed_methods[] = $matching_endpoint[0]->method;
284
			}
285
286
			header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
287
			return $this->output(
288
				405,
289
				array(
290
					'error'         => 'not_allowed',
291
					'error_message' => 'Method not allowed',
292
				)
293
			);
294
		}
295
296
		if ( $is_help ) {
297
			/**
298
			 * Fires before the API output.
299
			 *
300
			 * @since 1.9.0
301
			 *
302
			 * @param string help.
303
			 */
304
			do_action( 'wpcom_json_api_output', 'help' );
305
			$proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
306
			if ( 'json' === $help_content_type ) {
0 ignored issues
show
The variable $help_content_type does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
307
				$docs = array();
308
				foreach ( $matching_endpoints as $matching_endpoint ) {
309
					if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
310
						$docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
311
					}
312
				}
313
				return $this->output( 200, $docs );
314
			} else {
315
				status_header( 200 );
316
				foreach ( $matching_endpoints as $matching_endpoint ) {
317
					if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
318
						call_user_func( array( $matching_endpoint[0], 'document' ) );
319
					}
320
				}
321
			}
322
			exit;
323
		}
324
325
		if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
0 ignored issues
show
The variable $endpoint does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
326
			return $this->output( 404, '', 'text/plain' );
327
		}
328
329
		/** This action is documented in class.json-api.php */
330
		do_action( 'wpcom_json_api_output', $endpoint->stat );
331
332
		$response = $this->process_request( $endpoint, $path_pieces );
0 ignored issues
show
The variable $path_pieces does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
333
334
		if ( ! $response && ! is_array( $response ) ) {
335
			return $this->output( 500, '', 'text/plain' );
336
		} elseif ( is_wp_error( $response ) ) {
337
			return $this->output_error( $response );
338
		}
339
340
		$output_status_code = $this->output_status_code;
341
		$this->set_output_status_code();
342
343
		return $this->output( $output_status_code, $response, 'application/json', $this->extra_headers );
344
	}
345
346
	function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
347
		$this->endpoint = $endpoint;
0 ignored issues
show
The property endpoint does not seem to exist. Did you mean endpoints?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
348
		return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
349
	}
350
351
	function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
352
		$exit       = $this->exit;
353
		$this->exit = false;
354
		if ( is_wp_error( $response ) ) {
355
			$this->output_error( $response );
356
		} else {
357
			$this->output( $status_code, $response, $content_type );
358
		}
359
		$this->exit = $exit;
360
		if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
361
			$this->finish_request();
362
		}
363
	}
364
365
	function set_output_status_code( $code = 200 ) {
366
		$this->output_status_code = $code;
367
	}
368
369
	function output( $status_code, $response = null, $content_type = 'application/json', $extra = array() ) {
370
		// In case output() was called before the callback returned
371
		if ( $this->did_output ) {
372
			if ( $this->exit ) {
373
				exit;
374
			}
375
			return $content_type;
376
		}
377
		$this->did_output = true;
378
379
		// 400s and 404s are allowed for all origins
380
		if ( 404 == $status_code || 400 == $status_code ) {
381
			header( 'Access-Control-Allow-Origin: *' );
382
		}
383
384
		/* Add headers for form submission from <amp-form/> */
385
		if ( $this->amp_source_origin ) {
386
			header( 'Access-Control-Allow-Origin: ' . wp_unslash( $this->amp_source_origin ) );
387
			header( 'Access-Control-Allow-Credentials: true' );
388
		}
389
390
391
		if ( is_null( $response ) ) {
392
			$response = new stdClass();
393
		}
394
395
		if ( 'text/plain' === $content_type ||
396
			'text/html' === $content_type ) {
397
			status_header( (int) $status_code );
398
			header( 'Content-Type: ' . $content_type );
399
			foreach ( $extra as $key => $value ) {
400
				header( "$key: $value" );
401
			}
402
			echo $response;
403
			if ( $this->exit ) {
404
				exit;
405
			}
406
407
			return $content_type;
408
		}
409
410
		$response = $this->filter_fields( $response );
411
412
		if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
413
			$headers = array(
414
				array(
415
					'name'  => 'Content-Type',
416
					'value' => $content_type,
417
				),
418
			);
419
420
			foreach ( $extra as $key => $value ) {
421
				$headers[] = array(
422
					'name'  => $key,
423
					'value' => $value,
424
				);
425
			}
426
427
			$response     = array(
428
				'code'    => (int) $status_code,
429
				'headers' => $headers,
430
				'body'    => $response,
431
			);
432
			$status_code  = 200;
433
			$content_type = 'application/json';
434
		}
435
436
		status_header( (int) $status_code );
437
		header( "Content-Type: $content_type" );
438
		if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
439
			$callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
440
		} else {
441
			$callback = false;
442
		}
443
444
		if ( $callback ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $callback of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
445
			// Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
446
			// and by prepending the JSONP response with a JS comment.
447
			// [1] https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html
448
			echo "/**/$callback(";
449
450
		}
451
		echo $this->json_encode( $response );
452
		if ( $callback ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $callback of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
453
			echo ');';
454
		}
455
456
		if ( $this->exit ) {
457
			exit;
458
		}
459
460
		return $content_type;
461
	}
462
463
	public static function serializable_error( $error ) {
464
465
		$status_code = $error->get_error_data();
466
467
		if ( is_array( $status_code ) ) {
468
			$status_code = $status_code['status_code'];
469
		}
470
471
		if ( ! $status_code ) {
472
			$status_code = 400;
473
		}
474
		$response = array(
475
			'error'   => $error->get_error_code(),
476
			'message' => $error->get_error_message(),
477
		);
478
479
		if ( $additional_data = $error->get_error_data( 'additional_data' ) ) {
480
			$response['data'] = $additional_data;
481
		}
482
483
		return array(
484
			'status_code' => $status_code,
485
			'errors'      => $response,
486
		);
487
	}
488
489
	function output_error( $error ) {
490
		$error_response = $this->serializable_error( $error );
491
492
		return $this->output( $error_response['status_code'], $error_response['errors'] );
493
	}
494
495
	function filter_fields( $response ) {
496
		if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) ) {
0 ignored issues
show
The property endpoint does not seem to exist. Did you mean endpoints?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
497
			return $response;
498
		}
499
500
		$fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
501
502
		if ( is_object( $response ) ) {
503
			$response = (array) $response;
504
		}
505
506
		$has_filtered = false;
507
		if ( is_array( $response ) && empty( $response['ID'] ) ) {
508
			$keys_to_filter = array(
509
				'categories',
510
				'comments',
511
				'connections',
512
				'domains',
513
				'groups',
514
				'likes',
515
				'media',
516
				'notes',
517
				'posts',
518
				'services',
519
				'sites',
520
				'suggestions',
521
				'tags',
522
				'themes',
523
				'topics',
524
				'users',
525
			);
526
527
			foreach ( $keys_to_filter as $key_to_filter ) {
528
				if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
529
					continue;
530
				}
531
532
				foreach ( $response[ $key_to_filter ] as $key => $values ) {
533
					if ( is_object( $values ) ) {
534
						if ( is_object( $response[ $key_to_filter ] ) ) {
535
							$response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
536 View Code Duplication
						} elseif ( is_array( $response[ $key_to_filter ] ) ) {
537
							$response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
538
						}
539 View Code Duplication
					} elseif ( is_array( $values ) ) {
540
						$response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
541
					}
542
				}
543
544
				$has_filtered = true;
545
			}
546
		}
547
548
		if ( ! $has_filtered ) {
549
			if ( is_object( $response ) ) {
550
				$response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
551
			} elseif ( is_array( $response ) ) {
552
				$response = array_intersect_key( $response, array_flip( $fields ) );
553
			}
554
		}
555
556
		return $response;
557
	}
558
559
	function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
560
		if ( $original_scheme ) {
561
			return $url;
562
		}
563
564
		return preg_replace( '#^https:#', 'http:', $url );
565
	}
566
567
	function comment_edit_pre( $comment_content ) {
568
		return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
569
	}
570
571
	function json_encode( $data ) {
572
		return wp_json_encode( $data );
573
	}
574
575
	function ends_with( $haystack, $needle ) {
576
		return $needle === substr( $haystack, -strlen( $needle ) );
577
	}
578
579
	// Returns the site's blog_id in the WP.com ecosystem
580
	function get_blog_id_for_output() {
581
		return $this->token_details['blog_id'];
582
	}
583
584
	// Returns the site's local blog_id
585
	function get_blog_id( $blog_id ) {
586
		return $GLOBALS['blog_id'];
587
	}
588
589
	function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
590
		if ( $this->is_restricted_blog( $blog_id ) ) {
591
			return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
592
		}
593
594
		if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
595
			return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
596
		}
597
598
		return $blog_id;
599
	}
600
601
	// Returns true if the specified blog ID is a restricted blog
602
	function is_restricted_blog( $blog_id ) {
603
		/**
604
		 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
605
		 *
606
		 * @module json-api
607
		 *
608
		 * @since 3.4.0
609
		 *
610
		 * @param array $array Array of Blog IDs.
611
		 */
612
		$restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
613
		return true === in_array( $blog_id, $restricted_blog_ids );
614
	}
615
616
	function post_like_count( $blog_id, $post_id ) {
617
		return 0;
618
	}
619
620
	function is_liked( $blog_id, $post_id ) {
621
		return false;
622
	}
623
624
	function is_reblogged( $blog_id, $post_id ) {
625
		return false;
626
	}
627
628
	function is_following( $blog_id ) {
629
		return false;
630
	}
631
632
	function add_global_ID( $blog_id, $post_id ) {
633
		return '';
634
	}
635
636
	function get_avatar_url( $email, $avatar_size = null ) {
637
		if ( function_exists( 'wpcom_get_avatar_url' ) ) {
638
			return null === $avatar_size
639
				? wpcom_get_avatar_url( $email )
640
				: wpcom_get_avatar_url( $email, $avatar_size );
641
		} else {
642
			return null === $avatar_size
643
				? get_avatar_url( $email )
644
				: get_avatar_url( $email, $avatar_size );
645
		}
646
	}
647
648
	/**
649
	 * Counts the number of comments on a site, excluding certain comment types.
650
	 *
651
	 * @param $post_id int Post ID.
652
	 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
653
	 */
654
	public function wp_count_comments( $post_id ) {
655
		global $wpdb;
656
		if ( 0 !== $post_id ) {
657
			return wp_count_comments( $post_id );
658
		}
659
660
		$counts = array(
661
			'total_comments' => 0,
662
			'all'            => 0,
663
		);
664
665
		/**
666
		 * Exclude certain comment types from comment counts in the REST API.
667
		 *
668
		 * @since 6.9.0
669
		 * @module json-api
670
		 *
671
		 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
672
		 */
673
		$exclude = apply_filters(
674
			'jetpack_api_exclude_comment_types_count',
675
			array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
676
		);
677
678
		if ( empty( $exclude ) ) {
679
			return wp_count_comments( $post_id );
680
		}
681
682
		array_walk( $exclude, 'esc_sql' );
683
		$where = sprintf(
684
			"WHERE comment_type NOT IN ( '%s' )",
685
			implode( "','", $exclude )
686
		);
687
688
		$count = $wpdb->get_results(
689
			"SELECT comment_approved, COUNT(*) AS num_comments
690
				FROM $wpdb->comments
691
				{$where}
692
				GROUP BY comment_approved
693
			"
694
		);
695
696
		$approved = array(
697
			'0'            => 'moderated',
698
			'1'            => 'approved',
699
			'spam'         => 'spam',
700
			'trash'        => 'trash',
701
			'post-trashed' => 'post-trashed',
702
		);
703
704
		// https://developer.wordpress.org/reference/functions/get_comment_count/#source
705
		foreach ( $count as $row ) {
706
			if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
707
				$counts['all']            += $row->num_comments;
708
				$counts['total_comments'] += $row->num_comments;
709
			} elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
710
				$counts['total_comments'] += $row->num_comments;
711
			}
712
			if ( isset( $approved[ $row->comment_approved ] ) ) {
713
				$counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
714
			}
715
		}
716
717
		foreach ( $approved as $key ) {
718
			if ( empty( $counts[ $key ] ) ) {
719
				$counts[ $key ] = 0;
720
			}
721
		}
722
723
		$counts = (object) $counts;
724
725
		return $counts;
726
	}
727
728
	/**
729
	 * traps `wp_die()` calls and outputs a JSON response instead.
730
	 * The result is always output, never returned.
731
	 *
732
	 * @param string|null $error_code  Call with string to start the trapping.  Call with null to stop.
733
	 * @param int         $http_status  HTTP status code, 400 by default.
734
	 */
735
	function trap_wp_die( $error_code = null, $http_status = 400 ) {
736
		if ( is_null( $error_code ) ) {
737
			$this->trapped_error = null;
738
			// Stop trapping
739
			remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
740
			return;
741
		}
742
743
		// If API called via PHP, bail: don't do our custom wp_die().  Do the normal wp_die().
744
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
745
			if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
746
				return;
747
			}
748
		} else {
749
			if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
750
				return;
751
			}
752
		}
753
754
		$this->trapped_error = array(
755
			'status'  => $http_status,
756
			'code'    => $error_code,
757
			'message' => '',
758
		);
759
		// Start trapping
760
		add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
761
	}
762
763
	function wp_die_handler_callback() {
764
		return array( $this, 'wp_die_handler' );
765
	}
766
767
	function wp_die_handler( $message, $title = '', $args = array() ) {
768
		// Allow wp_die calls to override HTTP status code...
769
		$args = wp_parse_args(
770
			$args,
771
			array(
0 ignored issues
show
array('response' => $thi...rapped_error['status']) is of type array<string,?,{"response":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
772
				'response' => $this->trapped_error['status'],
773
			)
774
		);
775
776
		// ... unless it's 500
777
		if ( (int) $args['response'] !== 500 ) {
778
			$this->trapped_error['status'] = $args['response'];
779
		}
780
781
		if ( $title ) {
782
			$message = "$title: $message";
783
		}
784
785
		$this->trapped_error['message'] = wp_kses( $message, array() );
786
787
		switch ( $this->trapped_error['code'] ) {
788
			case 'comment_failure':
789
				if ( did_action( 'comment_duplicate_trigger' ) ) {
790
					$this->trapped_error['code'] = 'comment_duplicate';
791
				} elseif ( did_action( 'comment_flood_trigger' ) ) {
792
					$this->trapped_error['code'] = 'comment_flood';
793
				}
794
				break;
795
		}
796
797
		// We still want to exit so that code execution stops where it should.
798
		// Attach the JSON output to the WordPress shutdown handler
799
		add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
800
		exit;
801
	}
802
803
	function output_trapped_error() {
804
		$this->exit = false; // We're already exiting once.  Don't do it twice.
805
		$this->output(
806
			$this->trapped_error['status'],
807
			(object) array(
808
				'error'   => $this->trapped_error['code'],
809
				'message' => $this->trapped_error['message'],
810
			)
811
		);
812
	}
813
814
	function finish_request() {
815
		if ( function_exists( 'fastcgi_finish_request' ) ) {
816
			return fastcgi_finish_request();
817
		}
818
	}
819
}
820