Completed
Push — fix/linting-wpcom-site-setting... ( aa549a...77ff68 )
by Jeremy
213:00 queued 204:43
created

class.json-api.php (14 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
	/**
149
	 * Checks if the current request is authorized with a blog token.
150
	 * This method is overridden by a child class in WPCOM.
151
	 *
152
	 * @since 9.1.0
153
	 *
154
	 * @param  boolean|int $site_id The site id.
155
	 * @return boolean
156
	 */
157
	public function is_jetpack_authorized_for_site( $site_id = false ) {
158
		if ( ! $this->token_details ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->token_details of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
159
			return false;
160
		}
161
162
		$token_details = (object) $this->token_details;
163
164
		$site_in_token = (int) $token_details->blog_id;
165
166
		if ( $site_in_token < 1 ) {
167
			return false;
168
		}
169
170
		if ( $site_id && $site_in_token !== (int) $site_id ) {
171
			return false;
172
		}
173
174
		if ( (int) get_current_user_id() !== 0 ) {
175
			// If Jetpack blog token is used, no logged-in user should exist.
176
			return false;
177
		}
178
179
		return true;
180
	}
181
182
	function serve( $exit = true ) {
183
		ini_set( 'display_errors', false );
184
185
		$this->exit = (bool) $exit;
186
187
		// This was causing problems with Jetpack, but is necessary for wpcom
188
		// @see https://github.com/Automattic/jetpack/pull/2603
189
		// @see r124548-wpcom
190
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
191
			add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
192
		}
193
194
		add_filter( 'user_can_richedit', '__return_true' );
195
196
		add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
197
198
		$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...
199
		if ( 'OPTIONS' == $this->method ) {
200
			/**
201
			 * Fires before the page output.
202
			 * Can be used to specify custom header options.
203
			 *
204
			 * @module json-api
205
			 *
206
			 * @since 3.1.0
207
			 */
208
			do_action( 'wpcom_json_api_options' );
209
			return $this->output( 200, '', 'text/plain' );
210
		}
211
212
		if ( is_wp_error( $initialization ) ) {
213
			$this->output_error( $initialization );
214
			return;
215
		}
216
217
		// Normalize path and extract API version
218
		$this->path = untrailingslashit( $this->path );
219
		preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
220
		$this->path    = substr( $this->path, strlen( $matches[0] ) );
221
		$this->version = $matches[1];
222
223
		$allowed_methods = array( 'GET', 'POST' );
224
		$four_oh_five    = false;
225
226
		$is_help            = preg_match( '#/help/?$#i', $this->path );
227
		$matching_endpoints = array();
228
229
		if ( $is_help ) {
230
			$origin = get_http_origin();
231
232
			if ( ! empty( $origin ) && 'GET' == $this->method ) {
233
				header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
234
			}
235
236
			$this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
237
			// Show help for all matching endpoints regardless of method
238
			$methods                     = $allowed_methods;
239
			$find_all_matching_endpoints = true;
240
			// How deep to truncate each endpoint's path to see if it matches this help request
241
			$depth = substr_count( $this->path, '/' ) + 1;
242
			if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
243
				$help_content_type = 'json';
244
			} else {
245
				$help_content_type = 'html';
246
			}
247
		} else {
248
			if ( in_array( $this->method, $allowed_methods ) ) {
249
				// Only serve requested method
250
				$methods                     = array( $this->method );
251
				$find_all_matching_endpoints = false;
252
			} else {
253
				// We don't allow this requested method - find matching endpoints and send 405
254
				$methods                     = $allowed_methods;
255
				$find_all_matching_endpoints = true;
256
				$four_oh_five                = true;
257
			}
258
		}
259
260
		// Find which endpoint to serve
261
		$found = false;
262
		foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
263
			$endpoint_path_versions = unserialize( $endpoint_path_versions );
264
			$endpoint_path          = $endpoint_path_versions[0];
265
			$endpoint_min_version   = $endpoint_path_versions[1];
266
			$endpoint_max_version   = $endpoint_path_versions[2];
267
268
			// Make sure max_version is not less than min_version
269
			if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
270
				$endpoint_max_version = $endpoint_min_version;
271
			}
272
273
			foreach ( $methods as $method ) {
274
				if ( ! isset( $endpoints_by_method[ $method ] ) ) {
275
					continue;
276
				}
277
278
				// Normalize
279
				$endpoint_path = untrailingslashit( $endpoint_path );
280
				if ( $is_help ) {
281
					// Truncate path at help depth
282
					$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...
283
				}
284
285
				// Generate regular expression from sprintf()
286
				$endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
287
288
				if ( ! preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
289
					// This endpoint does not match the requested path.
290
					continue;
291
				}
292
293
				if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
294
					// This endpoint does not match the requested version.
295
					continue;
296
				}
297
298
				$found = true;
299
300
				if ( $find_all_matching_endpoints ) {
301
					$matching_endpoints[] = array( $endpoints_by_method[ $method ], $path_pieces );
302
				} else {
303
					// The method parameters are now in $path_pieces
304
					$endpoint = $endpoints_by_method[ $method ];
305
					break 2;
306
				}
307
			}
308
		}
309
310
		if ( ! $found ) {
311
			return $this->output( 404, '', 'text/plain' );
312
		}
313
314
		if ( $four_oh_five ) {
315
			$allowed_methods = array();
316
			foreach ( $matching_endpoints as $matching_endpoint ) {
317
				$allowed_methods[] = $matching_endpoint[0]->method;
318
			}
319
320
			header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
321
			return $this->output(
322
				405,
323
				array(
324
					'error'         => 'not_allowed',
325
					'error_message' => 'Method not allowed',
326
				)
327
			);
328
		}
329
330
		if ( $is_help ) {
331
			/**
332
			 * Fires before the API output.
333
			 *
334
			 * @since 1.9.0
335
			 *
336
			 * @param string help.
337
			 */
338
			do_action( 'wpcom_json_api_output', 'help' );
339
			$proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
340
			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...
341
				$docs = array();
342
				foreach ( $matching_endpoints as $matching_endpoint ) {
343
					if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
344
						$docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
345
					}
346
				}
347
				return $this->output( 200, $docs );
348
			} else {
349
				status_header( 200 );
350
				foreach ( $matching_endpoints as $matching_endpoint ) {
351
					if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
352
						call_user_func( array( $matching_endpoint[0], 'document' ) );
353
					}
354
				}
355
			}
356
			exit;
357
		}
358
359
		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...
360
			return $this->output( 404, '', 'text/plain' );
361
		}
362
363
		/** This action is documented in class.json-api.php */
364
		do_action( 'wpcom_json_api_output', $endpoint->stat );
365
366
		$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...
367
368
		if ( ! $response && ! is_array( $response ) ) {
369
			return $this->output( 500, '', 'text/plain' );
370
		} elseif ( is_wp_error( $response ) ) {
371
			return $this->output_error( $response );
372
		}
373
374
		$output_status_code = $this->output_status_code;
375
		$this->set_output_status_code();
376
377
		return $this->output( $output_status_code, $response, 'application/json', $this->extra_headers );
378
	}
379
380
	function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
381
		$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...
382
		return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
383
	}
384
385
	function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
386
		$exit       = $this->exit;
387
		$this->exit = false;
388
		if ( is_wp_error( $response ) ) {
389
			$this->output_error( $response );
390
		} else {
391
			$this->output( $status_code, $response, $content_type );
392
		}
393
		$this->exit = $exit;
394
		if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
395
			$this->finish_request();
396
		}
397
	}
398
399
	function set_output_status_code( $code = 200 ) {
400
		$this->output_status_code = $code;
401
	}
402
403
	function output( $status_code, $response = null, $content_type = 'application/json', $extra = array() ) {
404
		// In case output() was called before the callback returned
405
		if ( $this->did_output ) {
406
			if ( $this->exit ) {
407
				exit;
408
			}
409
			return $content_type;
410
		}
411
		$this->did_output = true;
412
413
		// 400s and 404s are allowed for all origins
414
		if ( 404 == $status_code || 400 == $status_code ) {
415
			header( 'Access-Control-Allow-Origin: *' );
416
		}
417
418
		/* Add headers for form submission from <amp-form/> */
419
		if ( $this->amp_source_origin ) {
420
			header( 'Access-Control-Allow-Origin: ' . wp_unslash( $this->amp_source_origin ) );
421
			header( 'Access-Control-Allow-Credentials: true' );
422
		}
423
424
425
		if ( is_null( $response ) ) {
426
			$response = new stdClass();
427
		}
428
429
		if ( 'text/plain' === $content_type ||
430
			'text/html' === $content_type ) {
431
			status_header( (int) $status_code );
432
			header( 'Content-Type: ' . $content_type );
433
			foreach ( $extra as $key => $value ) {
434
				header( "$key: $value" );
435
			}
436
			echo $response;
437
			if ( $this->exit ) {
438
				exit;
439
			}
440
441
			return $content_type;
442
		}
443
444
		$response = $this->filter_fields( $response );
445
446
		if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
447
			$headers = array(
448
				array(
449
					'name'  => 'Content-Type',
450
					'value' => $content_type,
451
				),
452
			);
453
454
			foreach ( $extra as $key => $value ) {
455
				$headers[] = array(
456
					'name'  => $key,
457
					'value' => $value,
458
				);
459
			}
460
461
			$response     = array(
462
				'code'    => (int) $status_code,
463
				'headers' => $headers,
464
				'body'    => $response,
465
			);
466
			$status_code  = 200;
467
			$content_type = 'application/json';
468
		}
469
470
		status_header( (int) $status_code );
471
		header( "Content-Type: $content_type" );
472
		if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
473
			$callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
474
		} else {
475
			$callback = false;
476
		}
477
478
		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...
479
			// Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
480
			// and by prepending the JSONP response with a JS comment.
481
			// [1] https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html
482
			echo "/**/$callback(";
483
484
		}
485
		echo $this->json_encode( $response );
486
		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...
487
			echo ');';
488
		}
489
490
		if ( $this->exit ) {
491
			exit;
492
		}
493
494
		return $content_type;
495
	}
496
497
	public static function serializable_error( $error ) {
498
499
		$status_code = $error->get_error_data();
500
501
		if ( is_array( $status_code ) ) {
502
			$status_code = $status_code['status_code'];
503
		}
504
505
		if ( ! $status_code ) {
506
			$status_code = 400;
507
		}
508
		$response = array(
509
			'error'   => $error->get_error_code(),
510
			'message' => $error->get_error_message(),
511
		);
512
513
		if ( $additional_data = $error->get_error_data( 'additional_data' ) ) {
514
			$response['data'] = $additional_data;
515
		}
516
517
		return array(
518
			'status_code' => $status_code,
519
			'errors'      => $response,
520
		);
521
	}
522
523
	function output_error( $error ) {
524
		$error_response = $this->serializable_error( $error );
525
526
		return $this->output( $error_response['status_code'], $error_response['errors'] );
527
	}
528
529
	function filter_fields( $response ) {
530
		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...
531
			return $response;
532
		}
533
534
		$fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
535
536
		if ( is_object( $response ) ) {
537
			$response = (array) $response;
538
		}
539
540
		$has_filtered = false;
541
		if ( is_array( $response ) && empty( $response['ID'] ) ) {
542
			$keys_to_filter = array(
543
				'categories',
544
				'comments',
545
				'connections',
546
				'domains',
547
				'groups',
548
				'likes',
549
				'media',
550
				'notes',
551
				'posts',
552
				'services',
553
				'sites',
554
				'suggestions',
555
				'tags',
556
				'themes',
557
				'topics',
558
				'users',
559
			);
560
561
			foreach ( $keys_to_filter as $key_to_filter ) {
562
				if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
563
					continue;
564
				}
565
566
				foreach ( $response[ $key_to_filter ] as $key => $values ) {
567
					if ( is_object( $values ) ) {
568
						if ( is_object( $response[ $key_to_filter ] ) ) {
569
							$response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
570 View Code Duplication
						} elseif ( is_array( $response[ $key_to_filter ] ) ) {
571
							$response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
572
						}
573 View Code Duplication
					} elseif ( is_array( $values ) ) {
574
						$response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
575
					}
576
				}
577
578
				$has_filtered = true;
579
			}
580
		}
581
582
		if ( ! $has_filtered ) {
583
			if ( is_object( $response ) ) {
584
				$response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
585
			} elseif ( is_array( $response ) ) {
586
				$response = array_intersect_key( $response, array_flip( $fields ) );
587
			}
588
		}
589
590
		return $response;
591
	}
592
593
	function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
594
		if ( $original_scheme ) {
595
			return $url;
596
		}
597
598
		return preg_replace( '#^https:#', 'http:', $url );
599
	}
600
601
	function comment_edit_pre( $comment_content ) {
602
		return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
603
	}
604
605
	function json_encode( $data ) {
606
		return wp_json_encode( $data );
607
	}
608
609
	function ends_with( $haystack, $needle ) {
610
		return $needle === substr( $haystack, -strlen( $needle ) );
611
	}
612
613
	// Returns the site's blog_id in the WP.com ecosystem
614
	function get_blog_id_for_output() {
615
		return $this->token_details['blog_id'];
616
	}
617
618
	// Returns the site's local blog_id
619
	function get_blog_id( $blog_id ) {
620
		return $GLOBALS['blog_id'];
621
	}
622
623
	function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
624
		if ( $this->is_restricted_blog( $blog_id ) ) {
625
			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...
626
		}
627
		/**
628
		 * If this is a private site we check for 2 things:
629
		 * 1. In case of user based authentication, we need to check if the logged-in user has the 'read' capability.
630
		 * 2. In case of site based authentication, make sure the endpoint accepts it.
631
		 */
632
		if ( -1 === (int) get_option( 'blog_public' ) &&
633
			! current_user_can( 'read' ) &&
634
			! $this->endpoint->accepts_site_based_authentication()
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...
635
		) {
636
			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...
637
		}
638
639
		return $blog_id;
640
	}
641
642
	// Returns true if the specified blog ID is a restricted blog
643
	function is_restricted_blog( $blog_id ) {
644
		/**
645
		 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
646
		 *
647
		 * @module json-api
648
		 *
649
		 * @since 3.4.0
650
		 *
651
		 * @param array $array Array of Blog IDs.
652
		 */
653
		$restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
654
		return true === in_array( $blog_id, $restricted_blog_ids );
655
	}
656
657
	function post_like_count( $blog_id, $post_id ) {
658
		return 0;
659
	}
660
661
	function is_liked( $blog_id, $post_id ) {
662
		return false;
663
	}
664
665
	function is_reblogged( $blog_id, $post_id ) {
666
		return false;
667
	}
668
669
	function is_following( $blog_id ) {
670
		return false;
671
	}
672
673
	function add_global_ID( $blog_id, $post_id ) {
674
		return '';
675
	}
676
677
	function get_avatar_url( $email, $avatar_size = null ) {
678
		if ( function_exists( 'wpcom_get_avatar_url' ) ) {
679
			return null === $avatar_size
680
				? wpcom_get_avatar_url( $email )
681
				: wpcom_get_avatar_url( $email, $avatar_size );
682
		} else {
683
			return null === $avatar_size
684
				? get_avatar_url( $email )
685
				: get_avatar_url( $email, $avatar_size );
686
		}
687
	}
688
689
	/**
690
	 * Counts the number of comments on a site, excluding certain comment types.
691
	 *
692
	 * @param $post_id int Post ID.
693
	 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
694
	 */
695
	public function wp_count_comments( $post_id ) {
696
		global $wpdb;
697
		if ( 0 !== $post_id ) {
698
			return wp_count_comments( $post_id );
699
		}
700
701
		$counts = array(
702
			'total_comments' => 0,
703
			'all'            => 0,
704
		);
705
706
		/**
707
		 * Exclude certain comment types from comment counts in the REST API.
708
		 *
709
		 * @since 6.9.0
710
		 * @module json-api
711
		 *
712
		 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
713
		 */
714
		$exclude = apply_filters(
715
			'jetpack_api_exclude_comment_types_count',
716
			array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
717
		);
718
719
		if ( empty( $exclude ) ) {
720
			return wp_count_comments( $post_id );
721
		}
722
723
		array_walk( $exclude, 'esc_sql' );
724
		$where = sprintf(
725
			"WHERE comment_type NOT IN ( '%s' )",
726
			implode( "','", $exclude )
727
		);
728
729
		$count = $wpdb->get_results(
730
			"SELECT comment_approved, COUNT(*) AS num_comments
731
				FROM $wpdb->comments
732
				{$where}
733
				GROUP BY comment_approved
734
			"
735
		);
736
737
		$approved = array(
738
			'0'            => 'moderated',
739
			'1'            => 'approved',
740
			'spam'         => 'spam',
741
			'trash'        => 'trash',
742
			'post-trashed' => 'post-trashed',
743
		);
744
745
		// https://developer.wordpress.org/reference/functions/get_comment_count/#source
746
		foreach ( $count as $row ) {
747
			if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
748
				$counts['all']            += $row->num_comments;
749
				$counts['total_comments'] += $row->num_comments;
750
			} elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
751
				$counts['total_comments'] += $row->num_comments;
752
			}
753
			if ( isset( $approved[ $row->comment_approved ] ) ) {
754
				$counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
755
			}
756
		}
757
758
		foreach ( $approved as $key ) {
759
			if ( empty( $counts[ $key ] ) ) {
760
				$counts[ $key ] = 0;
761
			}
762
		}
763
764
		$counts = (object) $counts;
765
766
		return $counts;
767
	}
768
769
	/**
770
	 * traps `wp_die()` calls and outputs a JSON response instead.
771
	 * The result is always output, never returned.
772
	 *
773
	 * @param string|null $error_code  Call with string to start the trapping.  Call with null to stop.
774
	 * @param int         $http_status  HTTP status code, 400 by default.
775
	 */
776
	function trap_wp_die( $error_code = null, $http_status = 400 ) {
777
		if ( is_null( $error_code ) ) {
778
			$this->trapped_error = null;
779
			// Stop trapping
780
			remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
781
			return;
782
		}
783
784
		// If API called via PHP, bail: don't do our custom wp_die().  Do the normal wp_die().
785
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
786
			if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
787
				return;
788
			}
789
		} else {
790
			if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
791
				return;
792
			}
793
		}
794
795
		$this->trapped_error = array(
796
			'status'  => $http_status,
797
			'code'    => $error_code,
798
			'message' => '',
799
		);
800
		// Start trapping
801
		add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
802
	}
803
804
	function wp_die_handler_callback() {
805
		return array( $this, 'wp_die_handler' );
806
	}
807
808
	function wp_die_handler( $message, $title = '', $args = array() ) {
809
		// Allow wp_die calls to override HTTP status code...
810
		$args = wp_parse_args(
811
			$args,
812
			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...
813
				'response' => $this->trapped_error['status'],
814
			)
815
		);
816
817
		// ... unless it's 500
818
		if ( (int) $args['response'] !== 500 ) {
819
			$this->trapped_error['status'] = $args['response'];
820
		}
821
822
		if ( $title ) {
823
			$message = "$title: $message";
824
		}
825
826
		$this->trapped_error['message'] = wp_kses( $message, array() );
827
828
		switch ( $this->trapped_error['code'] ) {
829
			case 'comment_failure':
830
				if ( did_action( 'comment_duplicate_trigger' ) ) {
831
					$this->trapped_error['code'] = 'comment_duplicate';
832
				} elseif ( did_action( 'comment_flood_trigger' ) ) {
833
					$this->trapped_error['code'] = 'comment_flood';
834
				}
835
				break;
836
		}
837
838
		// We still want to exit so that code execution stops where it should.
839
		// Attach the JSON output to the WordPress shutdown handler
840
		add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
841
		exit;
842
	}
843
844
	function output_trapped_error() {
845
		$this->exit = false; // We're already exiting once.  Don't do it twice.
846
		$this->output(
847
			$this->trapped_error['status'],
848
			(object) array(
849
				'error'   => $this->trapped_error['code'],
850
				'message' => $this->trapped_error['message'],
851
			)
852
		);
853
	}
854
855
	function finish_request() {
856
		if ( function_exists( 'fastcgi_finish_request' ) ) {
857
			return fastcgi_finish_request();
858
		}
859
	}
860
}
861