Completed
Push — instant-search-master ( 9522da...404687 )
by
unknown
07:50 queued 10s
created

WPCOM_JSON_API::output()   F

Complexity

Conditions 18
Paths 290

Size

Total Lines 92

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
nc 290
nop 4
dl 0
loc 92
rs 2.5012
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
Bug introduced by
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
Bug introduced by
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
Bug introduced by
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
Bug introduced by
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
Bug introduced by
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
Bug introduced by
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
			status_header( (int) $status_code );
397
			header( 'Content-Type: text/plain' );
398
			foreach ( $extra as $key => $value ) {
399
				header( "$key: $value" );
400
			}
401
			echo $response;
402
			if ( $this->exit ) {
403
				exit;
404
			}
405
406
			return $content_type;
407
		}
408
409
		$response = $this->filter_fields( $response );
410
411
		if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
412
			$headers = array(
413
				array(
414
					'name'  => 'Content-Type',
415
					'value' => $content_type,
416
				),
417
			);
418
419
			foreach ( $extra as $key => $value ) {
420
				$headers[] = array(
421
					'name'  => $key,
422
					'value' => $value,
423
				);
424
			}
425
426
			$response     = array(
427
				'code'    => (int) $status_code,
428
				'headers' => $headers,
429
				'body'    => $response,
430
			);
431
			$status_code  = 200;
432
			$content_type = 'application/json';
433
		}
434
435
		status_header( (int) $status_code );
436
		header( "Content-Type: $content_type" );
437
		if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
438
			$callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
439
		} else {
440
			$callback = false;
441
		}
442
443
		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...
444
			// Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
445
			// and by prepending the JSONP response with a JS comment.
446
			// [1] https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html
447
			echo "/**/$callback(";
448
449
		}
450
		echo $this->json_encode( $response );
451
		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...
452
			echo ');';
453
		}
454
455
		if ( $this->exit ) {
456
			exit;
457
		}
458
459
		return $content_type;
460
	}
461
462
	public static function serializable_error( $error ) {
463
464
		$status_code = $error->get_error_data();
465
466
		if ( is_array( $status_code ) ) {
467
			$status_code = $status_code['status_code'];
468
		}
469
470
		if ( ! $status_code ) {
471
			$status_code = 400;
472
		}
473
		$response = array(
474
			'error'   => $error->get_error_code(),
475
			'message' => $error->get_error_message(),
476
		);
477
478
		if ( $additional_data = $error->get_error_data( 'additional_data' ) ) {
479
			$response['data'] = $additional_data;
480
		}
481
482
		return array(
483
			'status_code' => $status_code,
484
			'errors'      => $response,
485
		);
486
	}
487
488
	function output_error( $error ) {
489
		$error_response = $this->serializable_error( $error );
490
491
		return $this->output( $error_response['status_code'], $error_response['errors'] );
492
	}
493
494
	function filter_fields( $response ) {
495
		if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) ) {
0 ignored issues
show
Bug introduced by
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...
496
			return $response;
497
		}
498
499
		$fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
500
501
		if ( is_object( $response ) ) {
502
			$response = (array) $response;
503
		}
504
505
		$has_filtered = false;
506
		if ( is_array( $response ) && empty( $response['ID'] ) ) {
507
			$keys_to_filter = array(
508
				'categories',
509
				'comments',
510
				'connections',
511
				'domains',
512
				'groups',
513
				'likes',
514
				'media',
515
				'notes',
516
				'posts',
517
				'services',
518
				'sites',
519
				'suggestions',
520
				'tags',
521
				'themes',
522
				'topics',
523
				'users',
524
			);
525
526
			foreach ( $keys_to_filter as $key_to_filter ) {
527
				if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
528
					continue;
529
				}
530
531
				foreach ( $response[ $key_to_filter ] as $key => $values ) {
532
					if ( is_object( $values ) ) {
533
						if ( is_object( $response[ $key_to_filter ] ) ) {
534
							$response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
535 View Code Duplication
						} elseif ( is_array( $response[ $key_to_filter ] ) ) {
536
							$response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
537
						}
538 View Code Duplication
					} elseif ( is_array( $values ) ) {
539
						$response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
540
					}
541
				}
542
543
				$has_filtered = true;
544
			}
545
		}
546
547
		if ( ! $has_filtered ) {
548
			if ( is_object( $response ) ) {
549
				$response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
550
			} elseif ( is_array( $response ) ) {
551
				$response = array_intersect_key( $response, array_flip( $fields ) );
552
			}
553
		}
554
555
		return $response;
556
	}
557
558
	function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
559
		if ( $original_scheme ) {
560
			return $url;
561
		}
562
563
		return preg_replace( '#^https:#', 'http:', $url );
564
	}
565
566
	function comment_edit_pre( $comment_content ) {
567
		return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
568
	}
569
570
	function json_encode( $data ) {
571
		return wp_json_encode( $data );
572
	}
573
574
	function ends_with( $haystack, $needle ) {
575
		return $needle === substr( $haystack, -strlen( $needle ) );
576
	}
577
578
	// Returns the site's blog_id in the WP.com ecosystem
579
	function get_blog_id_for_output() {
580
		return $this->token_details['blog_id'];
581
	}
582
583
	// Returns the site's local blog_id
584
	function get_blog_id( $blog_id ) {
585
		return $GLOBALS['blog_id'];
586
	}
587
588
	function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
589
		if ( $this->is_restricted_blog( $blog_id ) ) {
590
			return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
0 ignored issues
show
Unused Code introduced by
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...
591
		}
592
593
		if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
594
			return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
0 ignored issues
show
Unused Code introduced by
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...
595
		}
596
597
		return $blog_id;
598
	}
599
600
	// Returns true if the specified blog ID is a restricted blog
601
	function is_restricted_blog( $blog_id ) {
602
		/**
603
		 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
604
		 *
605
		 * @module json-api
606
		 *
607
		 * @since 3.4.0
608
		 *
609
		 * @param array $array Array of Blog IDs.
610
		 */
611
		$restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
612
		return true === in_array( $blog_id, $restricted_blog_ids );
613
	}
614
615
	function post_like_count( $blog_id, $post_id ) {
616
		return 0;
617
	}
618
619
	function is_liked( $blog_id, $post_id ) {
620
		return false;
621
	}
622
623
	function is_reblogged( $blog_id, $post_id ) {
624
		return false;
625
	}
626
627
	function is_following( $blog_id ) {
628
		return false;
629
	}
630
631
	function add_global_ID( $blog_id, $post_id ) {
632
		return '';
633
	}
634
635
	function get_avatar_url( $email, $avatar_size = null ) {
636
		if ( function_exists( 'wpcom_get_avatar_url' ) ) {
637
			return null === $avatar_size
638
				? wpcom_get_avatar_url( $email )
639
				: wpcom_get_avatar_url( $email, $avatar_size );
640
		} else {
641
			return null === $avatar_size
642
				? get_avatar_url( $email )
643
				: get_avatar_url( $email, $avatar_size );
644
		}
645
	}
646
647
	/**
648
	 * Counts the number of comments on a site, excluding certain comment types.
649
	 *
650
	 * @param $post_id int Post ID.
651
	 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
652
	 */
653
	public function wp_count_comments( $post_id ) {
654
		global $wpdb;
655
		if ( 0 !== $post_id ) {
656
			return wp_count_comments( $post_id );
657
		}
658
659
		$counts = array(
660
			'total_comments' => 0,
661
			'all'            => 0,
662
		);
663
664
		/**
665
		 * Exclude certain comment types from comment counts in the REST API.
666
		 *
667
		 * @since 6.9.0
668
		 * @module json-api
669
		 *
670
		 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
671
		 */
672
		$exclude = apply_filters(
673
			'jetpack_api_exclude_comment_types_count',
674
			array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
675
		);
676
677
		if ( empty( $exclude ) ) {
678
			return wp_count_comments( $post_id );
679
		}
680
681
		array_walk( $exclude, 'esc_sql' );
682
		$where = sprintf(
683
			"WHERE comment_type NOT IN ( '%s' )",
684
			implode( "','", $exclude )
685
		);
686
687
		$count = $wpdb->get_results(
688
			"SELECT comment_approved, COUNT(*) AS num_comments
689
				FROM $wpdb->comments
690
				{$where}
691
				GROUP BY comment_approved
692
			"
693
		);
694
695
		$approved = array(
696
			'0'            => 'moderated',
697
			'1'            => 'approved',
698
			'spam'         => 'spam',
699
			'trash'        => 'trash',
700
			'post-trashed' => 'post-trashed',
701
		);
702
703
		// https://developer.wordpress.org/reference/functions/get_comment_count/#source
704
		foreach ( $count as $row ) {
705
			if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
706
				$counts['all']            += $row->num_comments;
707
				$counts['total_comments'] += $row->num_comments;
708
			} elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
709
				$counts['total_comments'] += $row->num_comments;
710
			}
711
			if ( isset( $approved[ $row->comment_approved ] ) ) {
712
				$counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
713
			}
714
		}
715
716
		foreach ( $approved as $key ) {
717
			if ( empty( $counts[ $key ] ) ) {
718
				$counts[ $key ] = 0;
719
			}
720
		}
721
722
		$counts = (object) $counts;
723
724
		return $counts;
725
	}
726
727
	/**
728
	 * traps `wp_die()` calls and outputs a JSON response instead.
729
	 * The result is always output, never returned.
730
	 *
731
	 * @param string|null $error_code  Call with string to start the trapping.  Call with null to stop.
732
	 * @param int         $http_status  HTTP status code, 400 by default.
733
	 */
734
	function trap_wp_die( $error_code = null, $http_status = 400 ) {
735
		if ( is_null( $error_code ) ) {
736
			$this->trapped_error = null;
737
			// Stop trapping
738
			remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
739
			return;
740
		}
741
742
		// If API called via PHP, bail: don't do our custom wp_die().  Do the normal wp_die().
743
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
744
			if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
745
				return;
746
			}
747
		} else {
748
			if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
749
				return;
750
			}
751
		}
752
753
		$this->trapped_error = array(
754
			'status'  => $http_status,
755
			'code'    => $error_code,
756
			'message' => '',
757
		);
758
		// Start trapping
759
		add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
760
	}
761
762
	function wp_die_handler_callback() {
763
		return array( $this, 'wp_die_handler' );
764
	}
765
766
	function wp_die_handler( $message, $title = '', $args = array() ) {
767
		// Allow wp_die calls to override HTTP status code...
768
		$args = wp_parse_args(
769
			$args,
770
			array(
771
				'response' => $this->trapped_error['status'],
772
			)
773
		);
774
775
		// ... unless it's 500
776
		if ( (int) $args['response'] !== 500 ) {
777
			$this->trapped_error['status'] = $args['response'];
778
		}
779
780
		if ( $title ) {
781
			$message = "$title: $message";
782
		}
783
784
		$this->trapped_error['message'] = wp_kses( $message, array() );
785
786
		switch ( $this->trapped_error['code'] ) {
787
			case 'comment_failure':
788
				if ( did_action( 'comment_duplicate_trigger' ) ) {
789
					$this->trapped_error['code'] = 'comment_duplicate';
790
				} elseif ( did_action( 'comment_flood_trigger' ) ) {
791
					$this->trapped_error['code'] = 'comment_flood';
792
				}
793
				break;
794
		}
795
796
		// We still want to exit so that code execution stops where it should.
797
		// Attach the JSON output to the WordPress shutdown handler
798
		add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
799
		exit;
800
	}
801
802
	function output_trapped_error() {
803
		$this->exit = false; // We're already exiting once.  Don't do it twice.
804
		$this->output(
805
			$this->trapped_error['status'],
806
			(object) array(
807
				'error'   => $this->trapped_error['code'],
808
				'message' => $this->trapped_error['message'],
809
			)
810
		);
811
	}
812
813
	function finish_request() {
814
		if ( function_exists( 'fastcgi_finish_request' ) ) {
815
			return fastcgi_finish_request();
816
		}
817
	}
818
}
819