Completed
Push — fix/7598 ( 25a20b...a8e820 )
by
unknown
24:48 queued 13:24
created

WPCOM_JSON_API::process_request()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
defined( 'WPCOM_JSON_API__DEBUG' ) or define( 'WPCOM_JSON_API__DEBUG', false );
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
4
5
require_once dirname( __FILE__ ) . '/sal/class.json-api-platform.php';
6
7
class WPCOM_JSON_API {
8
	static $self = null;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $self.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
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__;
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;
0 ignored issues
show
Coding Style Compatibility introduced by
The method serve() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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
	 * traps `wp_die()` calls and outputs a JSON response instead.
621
	 * The result is always output, never returned.
622
	 *
623
	 * @param string|null $error_code  Call with string to start the trapping.  Call with null to stop.
624
	 * @param int         $http_status  HTTP status code, 400 by default.
625
	 */
626
	function trap_wp_die( $error_code = null, $http_status = 400 ) {
627
		if ( is_null( $error_code ) ) {
628
			$this->trapped_error = null;
629
			// Stop trapping
630
			remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
631
			return;
632
		}
633
634
		// If API called via PHP, bail: don't do our custom wp_die().  Do the normal wp_die().
635
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
636
			if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
637
				return;
638
			}
639
		} else {
640
			if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
641
				return;
642
			}
643
		}
644
645
		$this->trapped_error = array(
646
			'status'  => $http_status,
647
			'code'    => $error_code,
648
			'message' => '',
649
		);
650
		// Start trapping
651
		add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
652
	}
653
654
	function wp_die_handler_callback() {
655
		return array( $this, 'wp_die_handler' );
656
	}
657
658
	function wp_die_handler( $message, $title = '', $args = array() ) {
659
		// Allow wp_die calls to override HTTP status code...
660
		$args = wp_parse_args( $args, array(
661
			'response' => $this->trapped_error['status'],
662
		) );
663
664
		// ... unless it's 500 ( see http://wp.me/pMz3w-5VV )
665
		if ( (int) $args['response'] !== 500 ) {
666
			$this->trapped_error['status'] = $args['response'];
667
		}
668
669
		if ( $title ) {
670
			$message = "$title: $message";
671
		}
672
673
		$this->trapped_error['message'] = wp_kses( $message, array() );
674
675
		switch ( $this->trapped_error['code'] ) {
676
			case 'comment_failure' :
677
				if ( did_action( 'comment_duplicate_trigger' ) ) {
678
					$this->trapped_error['code'] = 'comment_duplicate';
679
				} else if ( did_action( 'comment_flood_trigger' ) ) {
680
					$this->trapped_error['code'] = 'comment_flood';
681
				}
682
				break;
683
		}
684
685
		// We still want to exit so that code execution stops where it should.
686
		// Attach the JSON output to the WordPress shutdown handler
687
		add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
688
		exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method wp_die_handler() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
689
	}
690
691
	function output_trapped_error() {
692
		$this->exit = false; // We're already exiting once.  Don't do it twice.
693
		$this->output( $this->trapped_error['status'], (object) array(
694
			'error'   => $this->trapped_error['code'],
695
			'message' => $this->trapped_error['message'],
696
		) );
697
	}
698
699
	function finish_request() {
700
		if ( function_exists( 'fastcgi_finish_request' ) )
701
			return fastcgi_finish_request();
702
	}
703
}
704