Completed
Push — renovate/babel-plugin-add-modu... ( 6e5406...4d1bee )
by
unknown
06:36
created

WPCOM_JSON_API::is_falsy()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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