Completed
Push — renovate/react-monorepo ( 545e89...65a81e )
by
unknown
268:48 queued 253:47
created

WPCOM_JSON_API_Endpoint   F

Complexity

Total Complexity 357

Size/Duplication

Total Lines 2157
Duplicated Lines 10.66 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 230
loc 2157
rs 0.8
c 0
b 0
f 0
wmc 357
lcom 1
cbo 7

38 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 10 105 7
A query_args() 0 9 2
D input() 0 57 19
A get_secure_body() 0 12 2
C cast_and_filter() 0 61 16
F cast_and_filter_item() 62 394 71
A parse_types() 0 27 5
A is_publicly_documentable() 0 3 2
D document() 0 118 11
A add_http_build_query_to_php_content_example() 0 7 1
A generate_doc_description() 0 19 3
C generate_documentation() 0 86 14
C user_can_view_post() 29 78 17
F get_author() 0 120 22
B get_media_item() 0 34 7
F get_media_item_v1_1() 31 175 33
A get_taxonomy() 0 10 3
B format_taxonomy() 3 40 7
A format_date() 0 3 1
A parse_date() 0 30 5
C load_theme_functions() 0 77 11
B copy_hooks() 0 37 9
B get_reflection() 0 21 8
A current_user_can_access_post_type() 15 15 5
B is_post_type_allowed() 27 27 7
A _get_whitelisted_post_types() 16 16 1
F handle_media_creation_v1_1() 33 128 29
C handle_media_sideload() 4 44 12
A is_file_supported_for_sideloading() 0 3 1
C allow_video_uploads() 0 61 12
A is_current_site_multi_user() 0 14 2
A allows_cross_origin_requests() 0 3 2
A allows_unauthorized_requests() 0 3 3
A accepts_site_based_authentication() 0 4 2
A get_platform() 0 3 1
A force_wpcom_request() 0 3 1
A get_amp_cache_origins() 0 27 3
callback() 0 1 ?

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WPCOM_JSON_API_Endpoint often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WPCOM_JSON_API_Endpoint, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use Automattic\Jetpack\Connection\Client;
4
5
require_once dirname( __FILE__ ) . '/json-api-config.php';
6
require_once dirname( __FILE__ ) . '/sal/class.json-api-links.php';
7
require_once dirname( __FILE__ ) . '/sal/class.json-api-metadata.php';
8
require_once dirname( __FILE__ ) . '/sal/class.json-api-date.php';
9
10
// Endpoint
11
abstract class WPCOM_JSON_API_Endpoint {
12
	// The API Object
13
	public $api;
14
15
	// The link-generating utility class
16
	public $links;
17
18
	public $pass_wpcom_user_details = false;
19
20
	// One liner.
21
	public $description;
22
23
	// Object Grouping For Documentation (Users, Posts, Comments)
24
	public $group;
25
26
	// Stats extra value to bump
27
	public $stat;
28
29
	// HTTP Method
30
	public $method = 'GET';
31
32
	// Minimum version of the api for which to serve this endpoint
33
	public $min_version = '0';
34
35
	// Maximum version of the api for which to serve this endpoint
36
	public $max_version = WPCOM_JSON_API__CURRENT_VERSION;
37
38
	// Path at which to serve this endpoint: sprintf() format.
39
	public $path = '';
40
41
	// Identifiers to fill sprintf() formatted $path
42
	public $path_labels = array();
43
44
	// Accepted query parameters
45
	public $query = array(
46
		// Parameter name
47
		'context'       => array(
48
			// Default value => description
49
			'display' => 'Formats the output as HTML for display.  Shortcodes are parsed, paragraph tags are added, etc..',
50
			// Other possible values => description
51
			'edit'    => 'Formats the output for editing.  Shortcodes are left unparsed, significant whitespace is kept, etc..',
52
		),
53
		'http_envelope' => array(
54
			'false' => '',
55
			'true'  => 'Some environments (like in-browser JavaScript or Flash) block or divert responses with a non-200 HTTP status code.  Setting this parameter will force the HTTP status code to always be 200.  The JSON response is wrapped in an "envelope" containing the "real" HTTP status code and headers.',
56
		),
57
		'pretty'        => array(
58
			'false' => '',
59
			'true'  => 'Output pretty JSON',
60
		),
61
		'meta'          => "(string) Optional. Loads data from the endpoints found in the 'meta' part of the response. Comma-separated list. Example: meta=site,likes",
62
		'fields'        => '(string) Optional. Returns specified fields only. Comma-separated list. Example: fields=ID,title',
63
		// Parameter name => description (default value is empty)
64
		'callback'      => '(string) An optional JSONP callback function.',
65
	);
66
67
	// Response format
68
	public $response_format = array();
69
70
	// Request format
71
	public $request_format = array();
72
73
	// Is this endpoint still in testing phase?  If so, not available to the public.
74
	public $in_testing = false;
75
76
	// Is this endpoint still allowed if the site in question is flagged?
77
	public $allowed_if_flagged = false;
78
79
	// Is this endpoint allowed if the site is red flagged?
80
	public $allowed_if_red_flagged = false;
81
82
	// Is this endpoint allowed if the site is deleted?
83
	public $allowed_if_deleted = false;
84
85
	/**
86
	 * @var string Version of the API
87
	 */
88
	public $version = '';
89
90
	/**
91
	 * @var string Example request to make
92
	 */
93
	public $example_request = '';
94
95
	/**
96
	 * @var string Example request data (for POST methods)
97
	 */
98
	public $example_request_data = '';
99
100
	/**
101
	 * @var string Example response from $example_request
102
	 */
103
	public $example_response = '';
104
105
	/**
106
	 * @var bool Set to true if the endpoint implements its own filtering instead of the standard `fields` query method
107
	 */
108
	public $custom_fields_filtering = false;
109
110
	/**
111
	 * @var bool Set to true if the endpoint accepts all cross origin requests. You probably should not set this flag.
112
	 */
113
	public $allow_cross_origin_request = false;
114
115
	/**
116
	 * @var bool Set to true if the endpoint can recieve unauthorized POST requests.
117
	 */
118
	public $allow_unauthorized_request = false;
119
120
	/**
121
	 * @var bool Set to true if the endpoint should accept site based (not user based) authentication.
122
	 */
123
	public $allow_jetpack_site_auth = false;
124
125
	/**
126
	 * @var bool Set to true if the endpoint should accept auth from an upload token.
127
	 */
128
	public $allow_upload_token_auth = false;
129
130
	/**
131
	 * @var bool Set to true if the endpoint should require auth from a Rewind auth token.
132
	 */
133
	public $require_rewind_auth = false;
134
135
	/**
136
	 * Whether this endpoint allows falling back to a blog token for making requests to remote Jetpack sites.
137
	 *
138
	 * @var bool
139
	 */
140
	public $allow_fallback_to_jetpack_blog_token = false;
141
142
	function __construct( $args ) {
143
		$defaults = array(
144
			'in_testing'                           => false,
145
			'allowed_if_flagged'                   => false,
146
			'allowed_if_red_flagged'               => false,
147
			'allowed_if_deleted'                   => false,
148
			'description'                          => '',
149
			'group'                                => '',
150
			'method'                               => 'GET',
151
			'path'                                 => '/',
152
			'min_version'                          => '0',
153
			'max_version'                          => WPCOM_JSON_API__CURRENT_VERSION,
154
			'force'                                => '',
155
			'deprecated'                           => false,
156
			'new_version'                          => WPCOM_JSON_API__CURRENT_VERSION,
157
			'jp_disabled'                          => false,
158
			'path_labels'                          => array(),
159
			'request_format'                       => array(),
160
			'response_format'                      => array(),
161
			'query_parameters'                     => array(),
162
			'version'                              => 'v1',
163
			'example_request'                      => '',
164
			'example_request_data'                 => '',
165
			'example_response'                     => '',
166
			'required_scope'                       => '',
167
			'pass_wpcom_user_details'              => false,
168
			'custom_fields_filtering'              => false,
169
			'allow_cross_origin_request'           => false,
170
			'allow_unauthorized_request'           => false,
171
			'allow_jetpack_site_auth'              => false,
172
			'allow_upload_token_auth'              => false,
173
			'allow_fallback_to_jetpack_blog_token' => false,
174
		);
175
176
		$args = wp_parse_args( $args, $defaults );
0 ignored issues
show
Documentation introduced by
$defaults is of type array<string,false|strin...k_blog_token":"false"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
177
178
		$this->in_testing = $args['in_testing'];
179
180
		$this->allowed_if_flagged     = $args['allowed_if_flagged'];
181
		$this->allowed_if_red_flagged = $args['allowed_if_red_flagged'];
182
		$this->allowed_if_deleted     = $args['allowed_if_deleted'];
183
184
		$this->description = $args['description'];
185
		$this->group       = $args['group'];
186
		$this->stat        = $args['stat'];
187
		$this->force       = $args['force'];
0 ignored issues
show
Bug introduced by
The property force does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
188
		$this->jp_disabled = $args['jp_disabled'];
0 ignored issues
show
Bug introduced by
The property jp_disabled does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
189
190
		$this->method      = $args['method'];
191
		$this->path        = $args['path'];
192
		$this->path_labels = $args['path_labels'];
193
		$this->min_version = $args['min_version'];
194
		$this->max_version = $args['max_version'];
195
		$this->deprecated  = $args['deprecated'];
0 ignored issues
show
Bug introduced by
The property deprecated does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
196
		$this->new_version = $args['new_version'];
0 ignored issues
show
Bug introduced by
The property new_version does not seem to exist. Did you mean version?

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...
197
198
		// Ensure max version is not less than min version
199
		if ( version_compare( $this->min_version, $this->max_version, '>' ) ) {
200
			$this->max_version = $this->min_version;
201
		}
202
203
		$this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
204
		$this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
205
206
		$this->allow_cross_origin_request           = (bool) $args['allow_cross_origin_request'];
207
		$this->allow_unauthorized_request           = (bool) $args['allow_unauthorized_request'];
208
		$this->allow_jetpack_site_auth              = (bool) $args['allow_jetpack_site_auth'];
209
		$this->allow_upload_token_auth              = (bool) $args['allow_upload_token_auth'];
210
		$this->allow_fallback_to_jetpack_blog_token = (bool) $args['allow_fallback_to_jetpack_blog_token'];
211
		$this->require_rewind_auth                  = isset( $args['require_rewind_auth'] ) ? (bool) $args['require_rewind_auth'] : false;
212
213
		$this->version = $args['version'];
214
215
		$this->required_scope = $args['required_scope'];
0 ignored issues
show
Bug introduced by
The property required_scope does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
216
217 View Code Duplication
		if ( $this->request_format ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->request_format of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
218
			$this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
219
		} else {
220
			$this->request_format = $args['request_format'];
221
		}
222
223 View Code Duplication
		if ( $this->response_format ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->response_format of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
224
			$this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
225
		} else {
226
			$this->response_format = $args['response_format'];
227
		}
228
229
		if ( false === $args['query_parameters'] ) {
230
			$this->query = array();
231
		} elseif ( is_array( $args['query_parameters'] ) ) {
232
			$this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
233
		}
234
235
		$this->api   = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
236
		$this->links = WPCOM_JSON_API_Links::getInstance();
237
238
		/** Example Request/Response */
239
240
		// Examples for endpoint documentation request
241
		$this->example_request      = $args['example_request'];
242
		$this->example_request_data = $args['example_request_data'];
243
		$this->example_response     = $args['example_response'];
244
245
		$this->api->add( $this );
246
	}
247
248
	// Get all query args.  Prefill with defaults
249
	function query_args( $return_default_values = true, $cast_and_filter = true ) {
250
		$args = array_intersect_key( $this->api->query, $this->query );
251
252
		if ( ! $cast_and_filter ) {
253
			return $args;
254
		}
255
256
		return $this->cast_and_filter( $args, $this->query, $return_default_values );
257
	}
258
259
	// Get POST body data
260
	function input( $return_default_values = true, $cast_and_filter = true ) {
261
		$input        = trim( $this->api->post_body );
262
		$content_type = $this->api->content_type;
263
		if ( $content_type ) {
264
			list ( $content_type ) = explode( ';', $content_type );
265
		}
266
		$content_type = trim( $content_type );
267
		switch ( $content_type ) {
268
			case 'application/json':
269
			case 'application/x-javascript':
270
			case 'text/javascript':
271
			case 'text/x-javascript':
272
			case 'text/x-json':
273
			case 'text/json':
274
				$return = json_decode( $input, true );
275
276
				if ( function_exists( 'json_last_error' ) ) {
277
					if ( JSON_ERROR_NONE !== json_last_error() ) { // phpcs:ignore PHPCompatibility
278
						return null;
279
					}
280
				} else {
281
					if ( is_null( $return ) && json_encode( null ) !== $input ) {
282
						return null;
283
					}
284
				}
285
286
				break;
287
			case 'multipart/form-data':
288
				$return = array_merge( stripslashes_deep( $_POST ), $_FILES );
289
				break;
290
			case 'application/x-www-form-urlencoded':
291
				// attempt JSON first, since probably a curl command
292
				$return = json_decode( $input, true );
293
294
				if ( is_null( $return ) ) {
295
					wp_parse_str( $input, $return );
296
				}
297
298
				break;
299
			default:
300
				wp_parse_str( $input, $return );
0 ignored issues
show
Bug introduced by
The variable $return seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
301
				break;
302
		}
303
304
		if ( isset( $this->api->query['force'] )
305
			&& 'secure' === $this->api->query['force']
306
			&& isset( $return['secure_key'] ) ) {
307
			$this->api->post_body      = $this->get_secure_body( $return['secure_key'] );
308
			$this->api->query['force'] = false;
309
			return $this->input( $return_default_values, $cast_and_filter );
310
		}
311
312
		if ( $cast_and_filter ) {
313
			$return = $this->cast_and_filter( $return, $this->request_format, $return_default_values );
0 ignored issues
show
Bug introduced by
The variable $return 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...
314
		}
315
		return $return;
316
	}
317
318
319
	protected function get_secure_body( $secure_key ) {
320
		$response = Client::wpcom_json_api_request_as_blog(
321
			sprintf( '/sites/%d/secure-request', Jetpack_Options::get_option( 'id' ) ),
322
			'1.1',
323
			array( 'method' => 'POST' ),
324
			array( 'secure_key' => $secure_key )
0 ignored issues
show
Documentation introduced by
array('secure_key' => $secure_key) is of type array<string,?,{"secure_key":"?"}>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
325
		);
326
		if ( 200 !== $response['response']['code'] ) {
327
			return null;
328
		}
329
		return json_decode( $response['body'], true );
330
	}
331
332
	function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
333
		$return_as_object = false;
334
		if ( is_object( $data ) ) {
335
			// @todo this should probably be a deep copy if $data can ever have nested objects
336
			$data             = (array) $data;
337
			$return_as_object = true;
338
		} elseif ( ! is_array( $data ) ) {
339
			return $data;
340
		}
341
342
		$boolean_arg = array( 'false', 'true' );
343
		$naeloob_arg = array( 'true', 'false' );
344
345
		$return = array();
346
347
		foreach ( $documentation as $key => $description ) {
348
			if ( is_array( $description ) ) {
349
				// String or boolean array keys only
350
				$whitelist = array_keys( $description );
351
352
				if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
353
					// Truthiness
354
					if ( isset( $data[ $key ] ) ) {
355
						$return[ $key ] = (bool) WPCOM_JSON_API::is_truthy( $data[ $key ] );
356
					} elseif ( $return_default_values ) {
357
						$return[ $key ] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg.
358
					}
359
				} elseif ( isset( $data[ $key ] ) && isset( $description[ $data[ $key ] ] ) ) {
360
					// String Key
361
					$return[ $key ] = (string) $data[ $key ];
362
				} elseif ( $return_default_values ) {
363
					// Default value
364
					$return[ $key ] = (string) current( $whitelist );
365
				}
366
367
				continue;
368
			}
369
370
			$types = $this->parse_types( $description );
371
			$type  = array_shift( $types );
372
373
			// Explicit default - string and int only for now.  Always set these reguardless of $return_default_values
374
			if ( isset( $type['default'] ) ) {
375
				if ( ! isset( $data[ $key ] ) ) {
376
					$data[ $key ] = $type['default'];
377
				}
378
			}
379
380
			if ( ! isset( $data[ $key ] ) ) {
381
				continue;
382
			}
383
384
			$this->cast_and_filter_item( $return, $type, $key, $data[ $key ], $types, $for_output );
385
		}
386
387
		if ( $return_as_object ) {
388
			return (object) $return;
389
		}
390
391
		return $return;
392
	}
393
394
	/**
395
	 * Casts $value according to $type.
396
	 * Handles fallbacks for certain values of $type when $value is not that $type
397
	 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way),
398
	 * and string -> object (one way)
399
	 *
400
	 * Handles "child types" - array:URL, object:category
401
	 * array:URL means an array of URLs
402
	 * object:category means a hash of categories
403
	 *
404
	 * Handles object typing - object>post means an object of type post
405
	 */
406
	function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
407
		if ( is_string( $type ) ) {
408
			$type = compact( 'type' );
409
		}
410
411
		switch ( $type['type'] ) {
412
			case 'false':
413
				$return[ $key ] = false;
414
				break;
415
			case 'url':
416
				if ( is_object( $value ) && isset( $value->url ) && false !== strpos( $value->url, 'https://videos.files.wordpress.com/' ) ) {
417
					$value = $value->url;
418
				}
419
				// Check for string since esc_url_raw() expects one.
420
				if ( ! is_string( $value ) ) {
421
					break;
422
				}
423
				$return[ $key ] = (string) esc_url_raw( $value );
424
				break;
425
			case 'string':
426
				// Fallback string -> array, or for string -> object
427
				if ( is_array( $value ) || is_object( $value ) ) {
428 View Code Duplication
					if ( ! empty( $types[0] ) ) {
429
						$next_type = array_shift( $types );
430
						return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
431
					}
432
				}
433
434
				// Fallback string -> false
435 View Code Duplication
				if ( ! is_string( $value ) ) {
436
					if ( ! empty( $types[0] ) && 'false' === $types[0]['type'] ) {
437
						$next_type = array_shift( $types );
438
						return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
439
					}
440
				}
441
				$return[ $key ] = (string) $value;
442
				break;
443
			case 'html':
444
				$return[ $key ] = (string) $value;
445
				break;
446
			case 'safehtml':
447
				$return[ $key ] = wp_kses( (string) $value, wp_kses_allowed_html() );
448
				break;
449
			case 'zip':
450
			case 'media':
451
				if ( is_array( $value ) ) {
452
					if ( isset( $value['name'] ) && is_array( $value['name'] ) ) {
453
						// It's a $_FILES array
454
						// Reformat into array of $_FILES items
455
						$files = array();
456
457
						foreach ( $value['name'] as $k => $v ) {
458
							$files[ $k ] = array();
459
							foreach ( array_keys( $value ) as $file_key ) {
460
								$files[ $k ][ $file_key ] = $value[ $file_key ][ $k ];
461
							}
462
						}
463
464
						$return[ $key ] = $files;
465
						break;
466
					}
467
				} else {
468
					// no break - treat as 'array'
469
				}
470
				// nobreak
471
			case 'array':
472
				// Fallback array -> string
473 View Code Duplication
				if ( is_string( $value ) ) {
474
					if ( ! empty( $types[0] ) ) {
475
						$next_type = array_shift( $types );
476
						return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
477
					}
478
				}
479
480 View Code Duplication
				if ( isset( $type['children'] ) ) {
481
					$children = array();
482
					foreach ( (array) $value as $k => $child ) {
483
						$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
484
					}
485
					$return[ $key ] = (array) $children;
486
					break;
487
				}
488
489
				$return[ $key ] = (array) $value;
490
				break;
491
			case 'iso 8601 datetime':
492
			case 'datetime':
493
				// (string)s
494
				$dates = $this->parse_date( (string) $value );
495
				if ( $for_output ) {
496
					$return[ $key ] = $this->format_date( $dates[1], $dates[0] );
497
				} else {
498
					list( $return[ $key ], $return[ "{$key}_gmt" ] ) = $dates;
499
				}
500
				break;
501
			case 'float':
502
				$return[ $key ] = (float) $value;
503
				break;
504
			case 'int':
505
			case 'integer':
506
				$return[ $key ] = (int) $value;
507
				break;
508
			case 'bool':
509
			case 'boolean':
510
				$return[ $key ] = (bool) WPCOM_JSON_API::is_truthy( $value );
511
				break;
512
			case 'object':
513
				// Fallback object -> false
514 View Code Duplication
				if ( is_scalar( $value ) || is_null( $value ) ) {
515
					if ( ! empty( $types[0] ) && 'false' === $types[0]['type'] ) {
516
						return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
517
					}
518
				}
519
520 View Code Duplication
				if ( isset( $type['children'] ) ) {
521
					$children = array();
522
					foreach ( (array) $value as $k => $child ) {
523
						$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
524
					}
525
					$return[ $key ] = (object) $children;
526
					break;
527
				}
528
529
				if ( isset( $type['subtype'] ) ) {
530
					return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
531
				}
532
533
				$return[ $key ] = (object) $value;
534
				break;
535
			case 'post':
536
				$return[ $key ] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
537
				break;
538
			case 'comment':
539
				$return[ $key ] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
540
				break;
541
			case 'tag':
542
			case 'category':
543
				$docs = array(
544
					'ID'          => '(int)',
545
					'name'        => '(string)',
546
					'slug'        => '(string)',
547
					'description' => '(HTML)',
548
					'post_count'  => '(int)',
549
					'feed_url'    => '(string)',
550
					'meta'        => '(object)',
551
				);
552
				if ( 'category' === $type['type'] ) {
553
					$docs['parent'] = '(int)';
554
				}
555
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
556
				break;
557
			case 'post_reference':
558 View Code Duplication
			case 'comment_reference':
559
				$docs           = array(
560
					'ID'    => '(int)',
561
					'type'  => '(string)',
562
					'title' => '(string)',
563
					'link'  => '(URL)',
564
				);
565
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
566
				break;
567 View Code Duplication
			case 'geo':
568
				$docs           = array(
569
					'latitude'  => '(float)',
570
					'longitude' => '(float)',
571
					'address'   => '(string)',
572
				);
573
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
574
				break;
575
			case 'author':
576
				$docs           = array(
577
					'ID'             => '(int)',
578
					'user_login'     => '(string)',
579
					'login'          => '(string)',
580
					'email'          => '(string|false)',
581
					'name'           => '(string)',
582
					'first_name'     => '(string)',
583
					'last_name'      => '(string)',
584
					'nice_name'      => '(string)',
585
					'URL'            => '(URL)',
586
					'avatar_URL'     => '(URL)',
587
					'profile_URL'    => '(URL)',
588
					'is_super_admin' => '(bool)',
589
					'roles'          => '(array:string)',
590
					'ip_address'     => '(string|false)',
591
				);
592
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
593
				break;
594 View Code Duplication
			case 'role':
595
				$docs           = array(
596
					'name'         => '(string)',
597
					'display_name' => '(string)',
598
					'capabilities' => '(object:boolean)',
599
				);
600
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
601
				break;
602
			case 'attachment':
603
				$docs           = array(
604
					'ID'        => '(int)',
605
					'URL'       => '(URL)',
606
					'guid'      => '(string)',
607
					'mime_type' => '(string)',
608
					'width'     => '(int)',
609
					'height'    => '(int)',
610
					'duration'  => '(int)',
611
				);
612
				$return[ $key ] = (object) $this->cast_and_filter(
613
					$value,
614
					/**
615
					* Filter the documentation returned for a post attachment.
616
					*
617
					* @module json-api
618
					*
619
					* @since 1.9.0
620
					*
621
					* @param array $docs Array of documentation about a post attachment.
622
					*/
623
					apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
624
					false,
625
					$for_output
626
				);
627
				break;
628
			case 'metadata':
629
				$docs           = array(
630
					'id'             => '(int)',
631
					'key'            => '(string)',
632
					'value'          => '(string|false|float|int|array|object)',
633
					'previous_value' => '(string)',
634
					'operation'      => '(string)',
635
				);
636
				$return[ $key ] = (object) $this->cast_and_filter(
637
					$value,
638
					/** This filter is documented in class.json-api-endpoints.php */
639
					apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
640
					false,
641
					$for_output
642
				);
643
				break;
644
			case 'plugin':
645
				$docs           = array(
646
					'id'           => '(safehtml) The plugin\'s ID',
647
					'slug'         => '(safehtml) The plugin\'s Slug',
648
					'active'       => '(boolean)  The plugin status.',
649
					'update'       => '(object)   The plugin update info.',
650
					'name'         => '(safehtml) The name of the plugin.',
651
					'plugin_url'   => '(url)      Link to the plugin\'s web site.',
652
					'version'      => '(safehtml) The plugin version number.',
653
					'description'  => '(safehtml) Description of what the plugin does and/or notes from the author',
654
					'author'       => '(safehtml) The plugin author\'s name',
655
					'author_url'   => '(url)      The plugin author web site address',
656
					'network'      => '(boolean)  Whether the plugin can only be activated network wide.',
657
					'autoupdate'   => '(boolean)  Whether the plugin is auto updated',
658
					'log'          => '(array:safehtml) An array of update log strings.',
659
					'action_links' => '(array) An array of action links that the plugin uses.',
660
				);
661
				$return[ $key ] = (object) $this->cast_and_filter(
662
					$value,
663
					/**
664
					* Filter the documentation returned for a plugin.
665
					*
666
					* @module json-api
667
					*
668
					* @since 3.1.0
669
					*
670
					* @param array $docs Array of documentation about a plugin.
671
					*/
672
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
673
					false,
674
					$for_output
675
				);
676
				break;
677
			case 'plugin_v1_2':
678
				$docs           = class_exists( 'Jetpack_JSON_API_Get_Plugins_v1_2_Endpoint' )
679
				? Jetpack_JSON_API_Get_Plugins_v1_2_Endpoint::$_response_format
680
				: Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2;
0 ignored issues
show
Bug introduced by
The property _response_format_v1_2 cannot be accessed from this context as it is declared private in class Jetpack_JSON_API_Plugins_Endpoint.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
681
				$return[ $key ] = (object) $this->cast_and_filter(
682
					$value,
683
					/**
684
					* Filter the documentation returned for a plugin.
685
					*
686
					* @module json-api
687
					*
688
					* @since 3.1.0
689
					*
690
					* @param array $docs Array of documentation about a plugin.
691
					*/
692
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
693
					false,
694
					$for_output
695
				);
696
				break;
697
			case 'file_mod_capabilities':
698
				$docs           = array(
699
					'reasons_modify_files_unavailable' => '(array) The reasons why files can\'t be modified',
700
					'reasons_autoupdate_unavailable'   => '(array) The reasons why autoupdates aren\'t allowed',
701
					'modify_files'                     => '(boolean) true if files can be modified',
702
					'autoupdate_files'                 => '(boolean) true if autoupdates are allowed',
703
				);
704
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
705
				break;
706
			case 'jetpackmodule':
707
				$docs           = array(
708
					'id'          => '(string)   The module\'s ID',
709
					'active'      => '(boolean)  The module\'s status.',
710
					'name'        => '(string)   The module\'s name.',
711
					'description' => '(safehtml) The module\'s description.',
712
					'sort'        => '(int)      The module\'s display order.',
713
					'introduced'  => '(string)   The Jetpack version when the module was introduced.',
714
					'changed'     => '(string)   The Jetpack version when the module was changed.',
715
					'free'        => '(boolean)  The module\'s Free or Paid status.',
716
					'module_tags' => '(array)    The module\'s tags.',
717
					'override'    => '(string)   The module\'s override. Empty if no override, otherwise \'active\' or \'inactive\'',
718
				);
719
				$return[ $key ] = (object) $this->cast_and_filter(
720
					$value,
721
					/** This filter is documented in class.json-api-endpoints.php */
722
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
723
					false,
724
					$for_output
725
				);
726
				break;
727
			case 'sharing_button':
728
				$docs           = array(
729
					'ID'         => '(string)',
730
					'name'       => '(string)',
731
					'URL'        => '(string)',
732
					'icon'       => '(string)',
733
					'enabled'    => '(bool)',
734
					'visibility' => '(string)',
735
				);
736
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
737
				break;
738
			case 'sharing_button_service':
739
				$docs           = array(
740
					'ID'               => '(string) The service identifier',
741
					'name'             => '(string) The service name',
742
					'class_name'       => '(string) Class name for custom style sharing button elements',
743
					'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
744
					'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
745
					'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview',
746
				);
747
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
748
				break;
749
			case 'site_keyring':
750
				$docs           = array(
751
					'keyring_id'       => '(int) Keyring ID',
752
					'service'          => '(string) The service name',
753
					'external_user_id' => '(string) External user id for the service',
754
				);
755
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
756
				break;
757
			case 'taxonomy':
758
				$docs           = array(
759
					'name'         => '(string) The taxonomy slug',
760
					'label'        => '(string) The taxonomy human-readable name',
761
					'labels'       => '(object) Mapping of labels for the taxonomy',
762
					'description'  => '(string) The taxonomy description',
763
					'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
764
					'public'       => '(bool) Whether the taxonomy is public',
765
					'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
766
				);
767
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
768
				break;
769
770
			case 'visibility':
771
				// This is needed to fix a bug in WPAndroid where `public: "PUBLIC"` is sent in place of `public: 1`
772
				if ( 'public' === strtolower( $value ) ) {
773
					$return[ $key ] = 1;
774
				} else if ( 'private' === strtolower( $value ) ) {
775
					$return[ $key ] = -1;
776
				} else {
777
					$return[ $key ] = (int) $value;
778
				}
779
				break;
780
781
			default:
782
				$method_name = $type['type'] . '_docs';
783
				if ( method_exists( 'WPCOM_JSON_API_Jetpack_Overrides', $method_name ) ) {
784
					$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
785
				}
786
787
				if ( ! empty( $docs ) ) {
788
					$return[ $key ] = (object) $this->cast_and_filter(
789
						$value,
790
						/** This filter is documented in class.json-api-endpoints.php */
791
						apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
792
						false,
793
						$for_output
794
					);
795
				} else {
796
					trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
797
				}
798
		}
799
	}
800
801
	function parse_types( $text ) {
802
		if ( ! preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
803
			return 'none';
804
		}
805
806
		$types  = explode( '|', strtolower( $matches[1] ) );
807
		$return = array();
808
		foreach ( $types as $type ) {
809
			foreach ( array(
810
				':' => 'children',
811
				'>' => 'subtype',
812
				'=' => 'default',
813
			) as $operator => $meaning ) {
814
				if ( false !== strpos( $type, $operator ) ) {
815
					$item     = explode( $operator, $type, 2 );
816
					$return[] = array(
817
						'type'   => $item[0],
818
						$meaning => $item[1],
819
					);
820
					continue 2;
821
				}
822
			}
823
			$return[] = compact( 'type' );
824
		}
825
826
		return $return;
827
	}
828
829
	/**
830
	 * Checks if the endpoint is publicly displayable
831
	 */
832
	function is_publicly_documentable() {
833
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
834
	}
835
836
	/**
837
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
838
	 * Echoes HTML.
839
	 */
840
	function document( $show_description = true ) {
841
		global $wpdb;
842
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
843
		unset( $GLOBALS['post'] );
844
845
		$doc = $this->generate_documentation();
846
847
		if ( $show_description ) :
848
			?>
849
<caption>
850
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
851
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
852
</caption>
853
854
<?php endif; ?>
855
856
		<?php if ( true === $this->deprecated ) { ?>
857
<p><strong>This endpoint is deprecated in favor of version <?php echo (float) $this->new_version; ?></strong></p>
0 ignored issues
show
Bug introduced by
The property new_version does not seem to exist. Did you mean version?

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...
858
<?php } ?>
859
860
<section class="resource-info">
861
	<h2 id="apidoc-resource-info">Resource Information</h2>
862
863
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
864
865
	<thead>
866
		<tr>
867
			<th class="api-index-title" scope="column">&nbsp;</th>
868
			<th class="api-index-title" scope="column">&nbsp;</th>
869
		</tr>
870
	</thead>
871
	<tbody>
872
873
		<tr class="api-index-item">
874
			<th scope="row" class="parameter api-index-item-title">Method</th>
875
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
876
		</tr>
877
878
		<tr class="api-index-item">
879
			<th scope="row" class="parameter api-index-item-title">URL</th>
880
			<?php
881
			$version = WPCOM_JSON_API__CURRENT_VERSION;
882
			if ( ! empty( $this->max_version ) ) {
883
				$version = $this->max_version;
884
			}
885
			?>
886
			<td class="type api-index-item-title">https://public-api.wordpress.com/rest/v<?php echo (float) $version; ?><?php echo wp_kses_post( $doc['path_labeled'] ); ?></td>
887
		</tr>
888
889
		<tr class="api-index-item">
890
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
891
			<?php
892
			$requires_auth = $wpdb->get_row( $wpdb->prepare( 'SELECT requires_authentication FROM rest_api_documentation WHERE `version` = %s AND `path` = %s AND `method` = %s LIMIT 1', $version, untrailingslashit( $doc['path_labeled'] ), $doc['method'] ) );
893
			?>
894
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
895
		</tr>
896
897
	</tbody>
898
	</table>
899
900
</section>
901
902
		<?php
903
904
		foreach ( array(
905
			'path'     => 'Method Parameters',
906
			'query'    => 'Query Parameters',
907
			'body'     => 'Request Parameters',
908
			'response' => 'Response Parameters',
909
		) as $doc_section_key => $label ) :
910
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][ $doc_section_key ];
911
			if ( ! $doc_section ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $doc_section of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
912
				continue;
913
			}
914
915
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
916
			?>
917
918
<section class="<?php echo $param_label; ?>">
919
920
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
921
922
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
923
924
<thead>
925
	<tr>
926
		<th class="api-index-title" scope="column">Parameter</th>
927
		<th class="api-index-title" scope="column">Type</th>
928
		<th class="api-index-title" scope="column">Description</th>
929
	</tr>
930
</thead>
931
<tbody>
932
933
			<?php foreach ( $doc_section as $key => $item ) : ?>
934
935
	<tr class="api-index-item">
936
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
937
		<td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
938
		<td class="description api-index-item-body">
939
				<?php
940
941
				$this->generate_doc_description( $item['description'] );
942
943
				?>
944
		</td>
945
	</tr>
946
947
			<?php endforeach; ?>
948
</tbody>
949
</table>
950
</section>
951
<?php endforeach; ?>
952
953
		<?php
954
		if ( 'unset' !== $original_post ) {
955
			$GLOBALS['post'] = $original_post;
956
		}
957
	}
958
959
	function add_http_build_query_to_php_content_example( $matches ) {
960
		$trimmed_match = ltrim( $matches[0] );
961
		$pad           = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
962
		$pad           = ltrim( $pad, ' ' );
963
		$return        = '  ' . str_replace( "\n", "\n  ", $matches[0] );
964
		return " http_build_query({$return}{$pad})";
965
	}
966
967
	/**
968
	 * Recursively generates the <dl>'s to document item descriptions.
969
	 * Echoes HTML.
970
	 */
971
	function generate_doc_description( $item ) {
972
		if ( is_array( $item ) ) :
973
			?>
974
975
		<dl>
976
			<?php	foreach ( $item as $description_key => $description_value ) : ?>
977
978
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
979
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
980
981
			<?php	endforeach; ?>
982
983
		</dl>
984
985
			<?php
986
		else :
987
			echo wp_kses_post( $item );
988
		endif;
989
	}
990
991
	/**
992
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
993
	 * Echoes HTML.
994
	 */
995
	function generate_documentation() {
996
		$format       = str_replace( '%d', '%s', $this->path );
997
		$path_labeled = $format;
998
		if ( ! empty( $this->path_labels ) ) {
999
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
1000
		}
1001
		$boolean_arg = array( 'false', 'true' );
1002
		$naeloob_arg = array( 'true', 'false' );
1003
1004
		$doc = array(
1005
			'description'  => $this->description,
1006
			'method'       => $this->method,
1007
			'path_format'  => $this->path,
1008
			'path_labeled' => $path_labeled,
1009
			'group'        => $this->group,
1010
			'request'      => array(
1011
				'path'  => array(),
1012
				'query' => array(),
1013
				'body'  => array(),
1014
			),
1015
			'response'     => array(
1016
				'body' => array(),
1017
			),
1018
		);
1019
1020
		foreach ( array(
1021
			'path_labels'     => 'path',
1022
			'query'           => 'query',
1023
			'request_format'  => 'body',
1024
			'response_format' => 'body',
1025
		) as $_property => $doc_item ) {
1026
			foreach ( (array) $this->$_property as $key => $description ) {
1027
				if ( is_array( $description ) ) {
1028
					$description_keys = array_keys( $description );
1029
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
1030
						$type = '(bool)';
1031
					} else {
1032
						$type = '(string)';
1033
					}
1034
1035
					if ( 'response_format' !== $_property ) {
1036
						// hack - don't show "(default)" in response format
1037
						reset( $description );
1038
						$description_key                 = key( $description );
1039
						$description[ $description_key ] = "(default) {$description[$description_key]}";
1040
					}
1041
				} else {
1042
					$types   = $this->parse_types( $description );
1043
					$type    = array();
1044
					$default = '';
1045
1046
					if ( 'none' == $types ) {
1047
						$types           = array();
1048
						$types[]['type'] = 'none';
1049
					}
1050
1051
					foreach ( $types as $type_array ) {
0 ignored issues
show
Bug introduced by
The expression $types of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1052
						$type[] = $type_array['type'];
1053
						if ( isset( $type_array['default'] ) ) {
1054
							$default = $type_array['default'];
1055
							if ( 'string' === $type_array['type'] ) {
1056
								$default = "'$default'";
1057
							}
1058
						}
1059
					}
1060
					$type                       = '(' . join( '|', $type ) . ')';
1061
					$noop                       = ''; // skip an index in list below
0 ignored issues
show
Unused Code introduced by
$noop is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1062
					list( $noop, $description ) = explode( ')', $description, 2 );
0 ignored issues
show
Unused Code introduced by
The assignment to $noop is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1063
					$description                = trim( $description );
1064
					if ( $default ) {
1065
						$description .= " Default: $default.";
1066
					}
1067
				}
1068
1069
				$item = compact( 'type', 'description' );
1070
1071
				if ( 'response_format' === $_property ) {
1072
					$doc['response'][ $doc_item ][ $key ] = $item;
1073
				} else {
1074
					$doc['request'][ $doc_item ][ $key ] = $item;
1075
				}
1076
			}
1077
		}
1078
1079
		return $doc;
1080
	}
1081
1082
	function user_can_view_post( $post_id ) {
1083
		$post = get_post( $post_id );
1084
		if ( ! $post || is_wp_error( $post ) ) {
1085
			return false;
1086
		}
1087
1088 View Code Duplication
		if ( 'inherit' === $post->post_status ) {
1089
			$parent_post     = get_post( $post->post_parent );
1090
			$post_status_obj = get_post_status_object( $parent_post->post_status );
1091
		} else {
1092
			$post_status_obj = get_post_status_object( $post->post_status );
1093
		}
1094
1095
		if ( ! $post_status_obj->public ) {
1096
			if ( is_user_logged_in() ) {
1097
				if ( $post_status_obj->protected ) {
1098
					if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1099
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1100
					}
1101
				} elseif ( $post_status_obj->private ) {
1102
					if ( ! current_user_can( 'read_post', $post->ID ) ) {
1103
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1104
					}
1105
				} elseif ( in_array( $post->post_status, array( 'inherit', 'trash' ) ) ) {
1106
					if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1107
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1108
					}
1109
				} elseif ( 'auto-draft' === $post->post_status ) {
1110
					// allow auto-drafts
1111
				} else {
1112
					return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1113
				}
1114
			} else {
1115
				return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1116
			}
1117
		}
1118
1119 View Code Duplication
		if (
1120
			-1 == get_option( 'blog_public' ) &&
1121
			/**
1122
			 * Filter access to a specific post.
1123
			 *
1124
			 * @module json-api
1125
			 *
1126
			 * @since 3.4.0
1127
			 *
1128
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1129
			 * @param WP_Post $post Post data.
1130
			 */
1131
			! apply_filters(
1132
				'wpcom_json_api_user_can_view_post',
1133
				current_user_can( 'read_post', $post->ID ),
1134
				$post
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post.

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...
1135
			)
1136
		) {
1137
			return new WP_Error(
1138
				'unauthorized',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1139
				'User cannot view post',
1140
				array(
1141
					'status_code' => 403,
1142
					'error'       => 'private_blog',
1143
				)
1144
			);
1145
		}
1146
1147 View Code Duplication
		if ( strlen( $post->post_password ) && ! current_user_can( 'edit_post', $post->ID ) ) {
1148
			return new WP_Error(
1149
				'unauthorized',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1150
				'User cannot view password protected post',
1151
				array(
1152
					'status_code' => 403,
1153
					'error'       => 'password_protected',
1154
				)
1155
			);
1156
		}
1157
1158
		return true;
1159
	}
1160
1161
	/**
1162
	 * Returns author object.
1163
	 *
1164
	 * @param object $author user ID, user row, WP_User object, comment row, post row
1165
	 * @param bool   $show_email_and_ip output the author's email address and IP address?
1166
	 *
1167
	 * @return object
1168
	 */
1169
	function get_author( $author, $show_email_and_ip = false ) {
1170
		$ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1171
1172
		if ( isset( $author->comment_author_email ) ) {
1173
			$ID          = 0;
1174
			$login       = '';
1175
			$email       = $author->comment_author_email;
1176
			$name        = $author->comment_author;
1177
			$first_name  = '';
1178
			$last_name   = '';
1179
			$URL         = $author->comment_author_url;
1180
			$avatar_URL  = $this->api->get_avatar_url( $author );
1181
			$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1182
			$nice        = '';
1183
			$site_id     = -1;
1184
1185
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1186
			// "&" is the only email/URL character altered by wp_kses()
1187
			foreach ( array( 'email', 'URL' ) as $field ) {
1188
				$$field = str_replace( '&amp;', '&', $$field );
1189
			}
1190
		} else {
1191
			if ( isset( $author->user_id ) && $author->user_id ) {
1192
				$author = $author->user_id;
1193
			} elseif ( isset( $author->user_email ) ) {
1194
				$author = $author->ID;
1195
			} elseif ( isset( $author->post_author ) ) {
1196
				// then $author is a Post Object.
1197
				if ( 0 == $author->post_author ) {
1198
					return null;
1199
				}
1200
				/**
1201
				 * Filter whether the current site is a Jetpack site.
1202
				 *
1203
				 * @module json-api
1204
				 *
1205
				 * @since 3.3.0
1206
				 *
1207
				 * @param bool false Is the current site a Jetpack site. Default to false.
1208
				 * @param int get_current_blog_id() Blog ID.
1209
				 */
1210
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with get_current_blog_id().

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...
1211
				$post_id    = $author->ID;
1212
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1213
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1214
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1215
					$login      = '';
1216
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1217
					$first_name = '';
1218
					$last_name  = '';
1219
					$URL        = '';
1220
					$nice       = '';
1221
				} else {
1222
					$author = $author->post_author;
1223
				}
1224
			}
1225
1226
			if ( ! isset( $ID ) ) {
1227
				$user = get_user_by( 'id', $author );
1228
				if ( ! $user || is_wp_error( $user ) ) {
1229
					trigger_error( 'Unknown user', E_USER_WARNING );
1230
1231
					return null;
1232
				}
1233
				$ID         = $user->ID;
1234
				$email      = $user->user_email;
1235
				$login      = $user->user_login;
1236
				$name       = $user->display_name;
1237
				$first_name = $user->first_name;
1238
				$last_name  = $user->last_name;
1239
				$URL        = $user->user_url;
1240
				$nice       = $user->user_nicename;
1241
			}
1242
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
0 ignored issues
show
Bug introduced by
The variable $is_jetpack 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...
1243
				$active_blog = get_active_blog_for_user( $ID );
1244
				$site_id     = $active_blog->blog_id;
1245
				if ( $site_id > -1 ) {
1246
					$site_visible = (
1247
						-1 != $active_blog->public ||
1248
						is_private_blog_user( $site_id, get_current_user_id() )
1249
					);
1250
				}
1251
				$profile_URL = "https://en.gravatar.com/{$login}";
0 ignored issues
show
Bug introduced by
The variable $login 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...
1252
			} else {
1253
				$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
0 ignored issues
show
Bug introduced by
The variable $email 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...
1254
				$site_id     = -1;
1255
			}
1256
1257
			$avatar_URL = $this->api->get_avatar_url( $email );
1258
		}
1259
1260
		if ( $show_email_and_ip ) {
1261
			$email      = (string) $email;
1262
			$ip_address = (string) $ip_address;
1263
		} else {
1264
			$email      = false;
1265
			$ip_address = false;
1266
		}
1267
1268
		$author = array(
1269
			'ID'          => (int) $ID,
1270
			'login'       => (string) $login,
1271
			'email'       => $email, // (string|bool)
1272
			'name'        => (string) $name,
0 ignored issues
show
Bug introduced by
The variable $name 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...
1273
			'first_name'  => (string) $first_name,
0 ignored issues
show
Bug introduced by
The variable $first_name 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...
1274
			'last_name'   => (string) $last_name,
0 ignored issues
show
Bug introduced by
The variable $last_name 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...
1275
			'nice_name'   => (string) $nice,
0 ignored issues
show
Bug introduced by
The variable $nice 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...
1276
			'URL'         => (string) esc_url_raw( $URL ),
0 ignored issues
show
Bug introduced by
The variable $URL 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...
1277
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1278
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1279
			'ip_address'  => $ip_address, // (string|bool)
1280
		);
1281
1282
		if ( $site_id > -1 ) {
1283
			$author['site_ID']      = (int) $site_id;
1284
			$author['site_visible'] = $site_visible;
0 ignored issues
show
Bug introduced by
The variable $site_visible 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...
1285
		}
1286
1287
		return (object) $author;
1288
	}
1289
1290
	function get_media_item( $media_id ) {
1291
		$media_item = get_post( $media_id );
1292
1293
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1294
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unknown_media'.

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...
1295
		}
1296
1297
		$response = array(
1298
			'id'          => (string) $media_item->ID,
1299
			'date'        => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1300
			'parent'      => $media_item->post_parent,
1301
			'link'        => wp_get_attachment_url( $media_item->ID ),
1302
			'title'       => $media_item->post_title,
1303
			'caption'     => $media_item->post_excerpt,
1304
			'description' => $media_item->post_content,
1305
			'metadata'    => wp_get_attachment_metadata( $media_item->ID ),
1306
		);
1307
1308
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1309
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1310
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1311
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1312
		}
1313
1314
		$response['meta'] = (object) array(
1315
			'links' => (object) array(
1316
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1317
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1318
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1319
			),
1320
		);
1321
1322
		return (object) $response;
1323
	}
1324
1325
	function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1326
1327
		if ( ! $media_item ) {
1328
			$media_item = get_post( $media_id );
1329
		}
1330
1331
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1332
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unknown_media'.

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...
1333
		}
1334
1335
		$attachment_file = get_attached_file( $media_item->ID );
1336
1337
		$file      = basename( $attachment_file ? $attachment_file : $file );
1338
		$file_info = pathinfo( $file );
1339
		$ext       = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1340
1341
		// File operations are handled differently on WordPress.com.
1342
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1343
			$attachment_metadata = wp_get_attachment_metadata( $media_item->ID );
1344
			$filesize            = ! empty( $attachment_metadata['filesize'] )
1345
				? $attachment_metadata['filesize']
1346
				: 0;
1347
		} else {
1348
			$filesize = filesize( $attachment_file );
1349
		}
1350
1351
		$response = array(
1352
			'ID'          => $media_item->ID,
1353
			'URL'         => wp_get_attachment_url( $media_item->ID ),
1354
			'guid'        => $media_item->guid,
1355
			'date'        => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1356
			'post_ID'     => $media_item->post_parent,
1357
			'author_ID'   => (int) $media_item->post_author,
1358
			'file'        => $file,
1359
			'mime_type'   => $media_item->post_mime_type,
1360
			'extension'   => $ext,
1361
			'title'       => $media_item->post_title,
1362
			'caption'     => $media_item->post_excerpt,
1363
			'description' => $media_item->post_content,
1364
			'alt'         => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1365
			'icon'        => wp_mime_type_icon( $media_item->ID ),
1366
			'size'        => size_format( (int) $filesize, 2 ),
1367
			'thumbnails'  => array(),
1368
		);
1369
1370
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1371
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1372
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1373
				$response['height'] = $metadata['height'];
1374
				$response['width']  = $metadata['width'];
1375
			}
1376
1377
			if ( isset( $metadata['sizes'] ) ) {
1378
				/**
1379
				 * Filter the thumbnail sizes available for each attachment ID.
1380
				 *
1381
				 * @module json-api
1382
				 *
1383
				 * @since 3.9.0
1384
				 *
1385
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1386
				 * @param string $media_id Attachment ID.
1387
				 */
1388
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $media_item->ID.

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...
1389 View Code Duplication
				if ( is_array( $sizes ) ) {
1390
					foreach ( $sizes as $size => $size_details ) {
1391
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1392
					}
1393
					/**
1394
					 * Filter the thumbnail URLs for attachment files.
1395
					 *
1396
					 * @module json-api
1397
					 *
1398
					 * @since 7.1.0
1399
					 *
1400
					 * @param array $metadata['sizes'] Array with thumbnail sizes as keys and URLs as values.
1401
					 */
1402
					$response['thumbnails'] = apply_filters( 'rest_api_thumbnail_size_urls', $response['thumbnails'] );
1403
				}
1404
			}
1405
1406
			if ( isset( $metadata['image_meta'] ) ) {
1407
				$response['exif'] = $metadata['image_meta'];
1408
			}
1409
		}
1410
1411
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1412
			$metadata           = wp_get_attachment_metadata( $media_item->ID );
1413
			$response['length'] = $metadata['length'];
1414
			$response['exif']   = $metadata;
1415
		}
1416
1417
		$is_video = false;
1418
1419
		if (
1420
			in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1421
			||
1422
			$response['mime_type'] === 'video/videopress'
1423
		) {
1424
			$is_video = true;
1425
		}
1426
1427
		if ( $is_video ) {
1428
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1429
1430
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1431
				$response['height'] = $metadata['height'];
1432
				$response['width']  = $metadata['width'];
1433
			}
1434
1435
			if ( isset( $metadata['length'] ) ) {
1436
				$response['length'] = $metadata['length'];
1437
			}
1438
1439
			// add VideoPress info
1440
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1441
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1442
1443
				// If we failed to get VideoPress info, but it exists in the meta data (for some reason)
1444
				// then let's use that.
1445
				if ( false === $info && isset( $metadata['videopress'] ) ) {
1446
					$info = (object) $metadata['videopress'];
1447
				}
1448
1449
				// Thumbnails
1450 View Code Duplication
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1451
					$response['thumbnails'] = array(
1452
						'fmt_hd'  => '',
1453
						'fmt_dvd' => '',
1454
						'fmt_std' => '',
1455
					);
1456
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1457
						if ( video_format_done( $info, $size ) ) {
1458
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1459
						} else {
1460
							unset( $response['thumbnails'][ $size ] );
1461
						}
1462
					}
1463
				}
1464
1465
				// If we didn't get VideoPress information (for some reason) then let's
1466
				// not try and include it in the response.
1467
				if ( isset( $info->guid ) ) {
1468
					$response['videopress_guid']            = $info->guid;
1469
					$response['videopress_processing_done'] = true;
1470
					if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1471
						$response['videopress_processing_done'] = false;
1472
					}
1473
				}
1474
			}
1475
		}
1476
1477
		$response['thumbnails'] = (object) $response['thumbnails'];
1478
1479
		$response['meta'] = (object) array(
1480
			'links' => (object) array(
1481
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1482
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1483
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1484
			),
1485
		);
1486
1487
		// add VideoPress link to the meta
1488
		if ( isset( $response['videopress_guid'] ) ) {
1489 View Code Duplication
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1490
				$response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1491
			}
1492
		}
1493
1494 View Code Duplication
		if ( $media_item->post_parent > 0 ) {
1495
			$response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1496
		}
1497
1498
		return (object) $response;
1499
	}
1500
1501
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1502
1503
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1504
		// keep updating this function
1505
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1506
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unknown_taxonomy'.

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...
1507
		}
1508
1509
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1510
	}
1511
1512
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1513
		// Permissions
1514
		switch ( $context ) {
1515
			case 'edit':
1516
				$tax = get_taxonomy( $taxonomy_type );
1517
				if ( ! current_user_can( $tax->cap->edit_terms ) ) {
1518
					return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1519
				}
1520
				break;
1521
			case 'display':
1522 View Code Duplication
				if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1523
					return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1524
				}
1525
				break;
1526
			default:
1527
				return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_context'.

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...
1528
		}
1529
1530
		$response                = array();
1531
		$response['ID']          = (int) $taxonomy->term_id;
1532
		$response['name']        = (string) $taxonomy->name;
1533
		$response['slug']        = (string) $taxonomy->slug;
1534
		$response['description'] = (string) $taxonomy->description;
1535
		$response['post_count']  = (int) $taxonomy->count;
1536
		$response['feed_url']    = get_term_feed_link( $taxonomy->term_id, $taxonomy_type );
1537
1538
		if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1539
			$response['parent'] = (int) $taxonomy->parent;
1540
		}
1541
1542
		$response['meta'] = (object) array(
1543
			'links' => (object) array(
1544
				'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1545
				'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1546
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1547
			),
1548
		);
1549
1550
		return (object) $response;
1551
	}
1552
1553
	/**
1554
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1555
	 *
1556
	 * @param $date_gmt (string) GMT datetime string.
1557
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1558
	 *
1559
	 * @return string
1560
	 */
1561
	function format_date( $date_gmt, $date = null ) {
1562
		return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1563
	}
1564
1565
	/**
1566
	 * Parses a date string and returns the local and GMT representations
1567
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1568
	 * timezones or offsets. If the parsed datetime was not localized to a
1569
	 * particular timezone or offset we will assume it was given in GMT
1570
	 * relative to now and will convert it to local time using either the
1571
	 * timezone set in the options table for the blog or the GMT offset.
1572
	 *
1573
	 * @param datetime string $date_string Date to parse.
1574
	 *
1575
	 * @return array( $local_time_string, $gmt_time_string )
0 ignored issues
show
Documentation introduced by
The doc-type array( could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1576
	 */
1577
	public function parse_date( $date_string ) {
1578
		$date_string_info = date_parse( $date_string );
1579
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1580
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1581
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1582
				$dt_utc   = new DateTime( $date_string );
1583
				$dt_local = clone $dt_utc;
1584
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1585
				return array(
1586
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1587
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1588
				);
1589
			}
1590
1591
			// It's parseable but no TZ info so assume UTC.
1592
			$dt_utc   = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1593
			$dt_local = clone $dt_utc;
1594
		} else {
1595
			// Could not parse time, use now in UTC.
1596
			$dt_utc   = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1597
			$dt_local = clone $dt_utc;
1598
		}
1599
1600
		$dt_local->setTimezone( wp_timezone() );
1601
1602
		return array(
1603
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1604
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1605
		);
1606
	}
1607
1608
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1609
	function load_theme_functions() {
1610
		if ( false === defined( 'STYLESHEETPATH' ) ) {
1611
			wp_templating_constants();
1612
		}
1613
1614
		// bail if we've done this already (can happen when calling /batch endpoint)
1615
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) ) {
1616
			return;
1617
		}
1618
1619
		// VIP context loading is handled elsewhere, so bail to prevent
1620
		// duplicate loading. See `switch_to_blog_and_validate_user()`
1621
		if ( defined( 'WPCOM_IS_VIP_ENV' ) && WPCOM_IS_VIP_ENV ) {
1622
			return;
1623
		}
1624
1625
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1626
1627
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1628
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1629
1630
		$copy_dirs = array( get_template_directory() );
1631
1632
		// Is this a child theme? Load the child theme's functions file.
1633
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1634
			foreach ( $function_files as $function_file ) {
1635
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1636
					require_once get_stylesheet_directory() . $function_file;
1637
				}
1638
			}
1639
			$copy_dirs[] = get_stylesheet_directory();
1640
		}
1641
1642
		foreach ( $function_files as $function_file ) {
1643
			if ( file_exists( get_template_directory() . $function_file ) ) {
1644
				require_once get_template_directory() . $function_file;
1645
			}
1646
		}
1647
1648
		// add inc/wpcom.php and/or includes/wpcom.php
1649
		wpcom_load_theme_compat_file();
1650
1651
		// Enable including additional directories or files in actions to be copied
1652
		$copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1653
1654
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1655
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1656
1657
		/**
1658
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1659
		 *
1660
		 * The REST API does not load the theme when processing requests.
1661
		 * To enable theme-based functionality, the API will load the '/functions.php',
1662
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1663
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1664
		 *
1665
		 * @module json-api
1666
		 *
1667
		 * @since 3.2.0
1668
		 */
1669
		do_action( 'restapi_theme_after_setup_theme' );
1670
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1671
1672
		/**
1673
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1674
		 *
1675
		 * The REST API does not load the theme when processing requests.
1676
		 * To enable theme-based functionality, the API will load the '/functions.php',
1677
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1678
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1679
		 *
1680
		 * @module json-api
1681
		 *
1682
		 * @since 3.2.0
1683
		 */
1684
		do_action( 'restapi_theme_init' );
1685
	}
1686
1687
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1688
		global $wp_filter;
1689
		foreach ( $wp_filter as $hook => $actions ) {
1690
1691
			if ( $from_hook != $hook ) {
1692
				continue;
1693
			}
1694
			if ( ! has_action( $hook ) ) {
1695
				continue;
1696
			}
1697
1698
			foreach ( $actions as $priority => $callbacks ) {
1699
				foreach ( $callbacks as $callback_key => $callback_data ) {
1700
					$callback = $callback_data['function'];
1701
1702
					// use reflection api to determine filename where function is defined
1703
					$reflection = $this->get_reflection( $callback );
1704
1705
					if ( false !== $reflection ) {
1706
						$file_name = $reflection->getFileName();
1707
						foreach ( $base_paths as $base_path ) {
1708
1709
							// only copy hooks with functions which are part of the specified files
1710
							if ( 0 === strpos( $file_name, $base_path ) ) {
1711
								add_action(
1712
									$to_hook,
1713
									$callback_data['function'],
1714
									$priority,
1715
									$callback_data['accepted_args']
1716
								);
1717
							}
1718
						}
1719
					}
1720
				}
1721
			}
1722
		}
1723
	}
1724
1725
	function get_reflection( $callback ) {
1726
		if ( is_array( $callback ) ) {
1727
			list( $class, $method ) = $callback;
1728
			return new ReflectionMethod( $class, $method );
1729
		}
1730
1731
		if ( is_string( $callback ) && strpos( $callback, '::' ) !== false ) {
1732
			list( $class, $method ) = explode( '::', $callback );
1733
			return new ReflectionMethod( $class, $method );
1734
		}
1735
1736
		if ( method_exists( $callback, "__invoke" ) ) {
1737
			return new ReflectionMethod( $callback, "__invoke" );
1738
		}
1739
1740
		if ( is_string( $callback ) && strpos( $callback, '::' ) == false && function_exists( $callback ) ) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing strpos($callback, '::') of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
1741
			return new ReflectionFunction( $callback );
1742
		}
1743
1744
		return false;
1745
	}
1746
1747
	/**
1748
	 * Check whether a user can view or edit a post type
1749
	 *
1750
	 * @param string $post_type              post type to check
1751
	 * @param string $context                'display' or 'edit'
1752
	 * @return bool
1753
	 */
1754 View Code Duplication
	function current_user_can_access_post_type( $post_type, $context = 'display' ) {
1755
		$post_type_object = get_post_type_object( $post_type );
1756
		if ( ! $post_type_object ) {
1757
			return false;
1758
		}
1759
1760
		switch ( $context ) {
1761
			case 'edit':
1762
				return current_user_can( $post_type_object->cap->edit_posts );
1763
			case 'display':
1764
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1765
			default:
1766
				return false;
1767
		}
1768
	}
1769
1770 View Code Duplication
	function is_post_type_allowed( $post_type ) {
1771
		// if the post type is empty, that's fine, WordPress will default to post
1772
		if ( empty( $post_type ) ) {
1773
			return true;
1774
		}
1775
1776
		// allow special 'any' type
1777
		if ( 'any' == $post_type ) {
1778
			return true;
1779
		}
1780
1781
		// check for allowed types
1782
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1783
			return true;
1784
		}
1785
1786
		if ( $post_type_object = get_post_type_object( $post_type ) ) {
1787
			if ( ! empty( $post_type_object->show_in_rest ) ) {
1788
				return $post_type_object->show_in_rest;
1789
			}
1790
			if ( ! empty( $post_type_object->publicly_queryable ) ) {
1791
				return $post_type_object->publicly_queryable;
1792
			}
1793
		}
1794
1795
		return ! empty( $post_type_object->public );
1796
	}
1797
1798
	/**
1799
	 * Gets the whitelisted post types that JP should allow access to.
1800
	 *
1801
	 * @return array Whitelisted post types.
1802
	 */
1803 View Code Duplication
	protected function _get_whitelisted_post_types() {
1804
		$allowed_types = array( 'post', 'page', 'revision' );
1805
1806
		/**
1807
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1808
		 *
1809
		 * @module json-api
1810
		 *
1811
		 * @since 2.2.3
1812
		 *
1813
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1814
		 */
1815
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1816
1817
		return array_unique( $allowed_types );
1818
	}
1819
1820
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1821
1822
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1823
1824
		$media_ids             = $errors = array();
1825
		$user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
0 ignored issues
show
Bug introduced by
The method is_authorized_with_upload_token() does not seem to exist on object<WPCOM_JSON_API>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1826
		$media_attrs           = array_values( $media_attrs ); // reset the keys
1827
		$i                     = 0;
1828
1829
		if ( ! empty( $media_files ) ) {
1830
			$this->api->trap_wp_die( 'upload_error' );
1831
			foreach ( $media_files as $media_item ) {
1832
				$_FILES['.api.media.item.'] = $media_item;
1833 View Code Duplication
				if ( ! $user_can_upload_files ) {
1834
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1835
				} else {
1836
					if ( $force_parent_id ) {
1837
						$parent_id = absint( $force_parent_id );
1838
					} elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1839
						$parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1840
					} else {
1841
						$parent_id = 0;
1842
					}
1843
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1844
				}
1845
				if ( is_wp_error( $media_id ) ) {
1846
					$errors[ $i ]['file']    = $media_item['name'];
1847
					$errors[ $i ]['error']   = $media_id->get_error_code();
1848
					$errors[ $i ]['message'] = $media_id->get_error_message();
1849
				} else {
1850
					$media_ids[ $i ] = $media_id;
1851
				}
1852
1853
				$i++;
1854
			}
1855
			$this->api->trap_wp_die( null );
1856
			unset( $_FILES['.api.media.item.'] );
1857
		}
1858
1859
		if ( ! empty( $media_urls ) ) {
1860
			foreach ( $media_urls as $url ) {
1861 View Code Duplication
				if ( ! $user_can_upload_files ) {
1862
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1863
				} else {
1864
					if ( $force_parent_id ) {
1865
						$parent_id = absint( $force_parent_id );
1866
					} elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1867
						$parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1868
					} else {
1869
						$parent_id = 0;
1870
					}
1871
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1872
				}
1873
				if ( is_wp_error( $media_id ) ) {
1874
					$errors[ $i ] = array(
1875
						'file'    => $url,
1876
						'error'   => $media_id->get_error_code(),
1877
						'message' => $media_id->get_error_message(),
1878
					);
1879
				} elseif ( ! empty( $media_id ) ) {
1880
					$media_ids[ $i ] = $media_id;
1881
				}
1882
1883
				$i++;
1884
			}
1885
		}
1886
1887
		if ( ! empty( $media_attrs ) ) {
1888
			foreach ( $media_ids as $index => $media_id ) {
1889
				if ( empty( $media_attrs[ $index ] ) ) {
1890
					continue;
1891
				}
1892
1893
				$attrs  = $media_attrs[ $index ];
1894
				$insert = array();
1895
1896
				// Attributes: Title, Caption, Description
1897
1898
				if ( isset( $attrs['title'] ) ) {
1899
					$insert['post_title'] = $attrs['title'];
1900
				}
1901
1902
				if ( isset( $attrs['caption'] ) ) {
1903
					$insert['post_excerpt'] = $attrs['caption'];
1904
				}
1905
1906
				if ( isset( $attrs['description'] ) ) {
1907
					$insert['post_content'] = $attrs['description'];
1908
				}
1909
1910
				if ( ! empty( $insert ) ) {
1911
					$insert['ID'] = $media_id;
1912
					wp_update_post( (object) $insert );
1913
				}
1914
1915
				// Attributes: Alt
1916
1917 View Code Duplication
				if ( isset( $attrs['alt'] ) ) {
1918
					$alt = wp_strip_all_tags( $attrs['alt'], true );
1919
					update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1920
				}
1921
1922
				// Attributes: Artist, Album
1923
1924
				$id3_meta = array();
1925
1926 View Code Duplication
				foreach ( array( 'artist', 'album' ) as $key ) {
1927
					if ( isset( $attrs[ $key ] ) ) {
1928
						$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1929
					}
1930
				}
1931
1932
				if ( ! empty( $id3_meta ) ) {
1933
					// Before updating metadata, ensure that the item is audio
1934
					$item = $this->get_media_item_v1_1( $media_id );
1935
					if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1936
						wp_update_attachment_metadata( $media_id, $id3_meta );
1937
					}
1938
				}
1939
			}
1940
		}
1941
1942
		return array(
1943
			'media_ids' => $media_ids,
1944
			'errors'    => $errors,
1945
		);
1946
1947
	}
1948
1949
	function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1950
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) ) {
1951
			return false;
1952
		}
1953
1954
		// if we didn't get a URL, let's bail
1955
		$parsed = wp_parse_url( $url );
1956
		if ( empty( $parsed ) ) {
1957
			return false;
1958
		}
1959
1960
		$tmp = download_url( $url );
1961
		if ( is_wp_error( $tmp ) ) {
1962
			return $tmp;
1963
		}
1964
1965
		// First check to see if we get a mime-type match by file, otherwise, check to
1966
		// see if WordPress supports this file as an image. If neither, then it is not supported.
1967 View Code Duplication
		if ( ! $this->is_file_supported_for_sideloading( $tmp ) || 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1968
			@unlink( $tmp );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1969
			return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_input'.

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...
1970
		}
1971
1972
		// emulate a $_FILES entry
1973
		$file_array = array(
1974
			'name'     => basename( wp_parse_url( $url, PHP_URL_PATH ) ),
1975
			'tmp_name' => $tmp,
1976
		);
1977
1978
		$id = media_handle_sideload( $file_array, $parent_post_id );
1979
		if ( file_exists( $tmp ) ) {
1980
			@unlink( $tmp );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1981
		}
1982
1983
		if ( is_wp_error( $id ) ) {
1984
			return $id;
1985
		}
1986
1987
		if ( ! $id || ! is_int( $id ) ) {
1988
			return false;
1989
		}
1990
1991
		return $id;
1992
	}
1993
1994
	/**
1995
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1996
	 *
1997
	 * @param string $file Path to file to get its mime type.
1998
	 *
1999
	 * @return bool
2000
	 */
2001
	protected function is_file_supported_for_sideloading( $file ) {
2002
		return jetpack_is_file_supported_for_sideloading( $file );
2003
	}
2004
2005
	function allow_video_uploads( $mimes ) {
2006
		// if we are on Jetpack, bail - Videos are already allowed
2007
		if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
2008
			return $mimes;
2009
		}
2010
2011
		// extra check that this filter is only ever applied during REST API requests
2012
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
2013
			return $mimes;
2014
		}
2015
2016
		// bail early if they already have the upgrade..
2017
		if ( get_option( 'video_upgrade' ) == '1' ) {
2018
			return $mimes;
2019
		}
2020
2021
		// lets whitelist to only specific clients right now
2022
		$clients_allowed_video_uploads = array();
2023
		/**
2024
		 * Filter the list of whitelisted video clients.
2025
		 *
2026
		 * @module json-api
2027
		 *
2028
		 * @since 3.2.0
2029
		 *
2030
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
2031
		 */
2032
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
2033
		if ( ! in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
2034
			return $mimes;
2035
		}
2036
2037
		$mime_list = wp_get_mime_types();
2038
2039
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
0 ignored issues
show
Unused Code introduced by
The call to get_site_option() has too many arguments starting with false.

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...
2040
		/**
2041
		 * Filter the video filetypes allowed on the site.
2042
		 *
2043
		 * @module json-api
2044
		 *
2045
		 * @since 3.2.0
2046
		 *
2047
		 * @param array $video_exts Array of video filetypes allowed on the site.
2048
		 */
2049
		$video_exts  = apply_filters( 'video_upload_filetypes', $video_exts );
2050
		$video_mimes = array();
2051
2052
		if ( ! empty( $video_exts ) ) {
2053
			foreach ( $video_exts as $ext ) {
2054
				foreach ( $mime_list as $ext_pattern => $mime ) {
2055
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false ) {
2056
						$video_mimes[ $ext_pattern ] = $mime;
2057
					}
2058
				}
2059
			}
2060
2061
			$mimes = array_merge( $mimes, $video_mimes );
2062
		}
2063
2064
		return $mimes;
2065
	}
2066
2067
	function is_current_site_multi_user() {
2068
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2069
		if ( false === $users ) {
2070
			$user_query = new WP_User_Query(
2071
				array(
2072
					'blog_id' => get_current_blog_id(),
2073
					'fields'  => 'ID',
2074
				)
2075
			);
2076
			$users      = (int) $user_query->get_total();
2077
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2078
		}
2079
		return $users > 1;
2080
	}
2081
2082
	function allows_cross_origin_requests() {
2083
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2084
	}
2085
2086
	function allows_unauthorized_requests( $origin, $complete_access_origins ) {
2087
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2088
	}
2089
2090
	/**
2091
	 * Whether this endpoint accepts site based authentication for the current request.
2092
	 *
2093
	 * @since 9.1.0
2094
	 *
2095
	 * @return bool true, if Jetpack blog token is used and `allow_jetpack_site_auth` is true,
2096
	 * false otherwise.
2097
	 */
2098
	public function accepts_site_based_authentication() {
2099
		return $this->allow_jetpack_site_auth &&
2100
			$this->api->is_jetpack_authorized_for_site();
2101
	}
2102
2103
	function get_platform() {
2104
		return wpcom_get_sal_platform( $this->api->token_details );
2105
	}
2106
2107
	/**
2108
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2109
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
2110
	 *
2111
	 * Override this method if you want to do something different.
2112
	 *
2113
	 * @param  int $blog_id
2114
	 * @return bool
2115
	 */
2116
	function force_wpcom_request( $blog_id ) {
2117
		return false;
2118
	}
2119
2120
	/**
2121
	 * Get an array of all valid AMP origins for a blog's siteurl.
2122
	 *
2123
	 * @param string $siteurl Origin url of the API request.
2124
	 * @return array
2125
	 */
2126
	public function get_amp_cache_origins( $siteurl ) {
2127
		$host = parse_url( $siteurl, PHP_URL_HOST );
2128
2129
		/*
2130
		 * From AMP docs:
2131
		 * "When possible, the Google AMP Cache will create a subdomain for each AMP document's domain by first converting it
2132
		 * from IDN (punycode) to UTF-8. The caches replaces every - (dash) with -- (2 dashes) and replace every . (dot) with
2133
		 * - (dash). For example, pub.com will map to pub-com.cdn.ampproject.org."
2134
		 */
2135
		if ( function_exists( 'idn_to_utf8' ) ) {
2136
			// The third parameter is set explicitly to prevent issues with newer PHP versions compiled with an old ICU version.
2137
			// phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated, PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003DeprecatedRemoved
2138
			$host = idn_to_utf8( $host, IDNA_DEFAULT, defined( 'INTL_IDNA_VARIANT_UTS46' ) ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003 );
2139
		}
2140
		$subdomain = str_replace( array( '-', '.' ), array( '--', '-' ), $host );
2141
		return array(
2142
			$siteurl,
2143
			// Google AMP Cache (legacy).
2144
			'https://cdn.ampproject.org',
2145
			// Google AMP Cache subdomain.
2146
			sprintf( 'https://%s.cdn.ampproject.org', $subdomain ),
2147
			// Cloudflare AMP Cache.
2148
			sprintf( 'https://%s.amp.cloudflare.com', $subdomain ),
2149
			// Bing AMP Cache.
2150
			sprintf( 'https://%s.bing-amp.com', $subdomain ),
2151
		);
2152
	}
2153
2154
	/**
2155
	 * Return endpoint response
2156
	 *
2157
	 * @param string $path ... determined by ->$path.
2158
	 *
2159
	 * @return array|WP_Error
2160
	 *  falsy: HTTP 500, no response body
2161
	 *  WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2162
	 *  $data: HTTP 200, json_encode( $data ) response body
2163
	 */
2164
	abstract public function callback( $path = '' );
2165
2166
2167
}
2168
2169
require_once dirname( __FILE__ ) . '/json-endpoints.php';
2170