Completed
Push — update/tracking-in-dev-mode ( 39a6d0...b21b20 )
by Jeremy
32:10 queued 24:55
created

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