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