Completed
Push — update/date-time ( 50c4ab )
by
unknown
06:46
created

WPCOM_JSON_API_Endpoint::parse_date()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 3
nop 1
dl 0
loc 27
rs 9.1768
c 0
b 0
f 0
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
	function __construct( $args ) {
131
		$defaults = array(
132
			'in_testing'                 => false,
133
			'allowed_if_flagged'         => false,
134
			'allowed_if_red_flagged'     => false,
135
			'allowed_if_deleted'         => false,
136
			'description'                => '',
137
			'group'                      => '',
138
			'method'                     => 'GET',
139
			'path'                       => '/',
140
			'min_version'                => '0',
141
			'max_version'                => WPCOM_JSON_API__CURRENT_VERSION,
142
			'force'                      => '',
143
			'deprecated'                 => false,
144
			'new_version'                => WPCOM_JSON_API__CURRENT_VERSION,
145
			'jp_disabled'                => false,
146
			'path_labels'                => array(),
147
			'request_format'             => array(),
148
			'response_format'            => array(),
149
			'query_parameters'           => array(),
150
			'version'                    => 'v1',
151
			'example_request'            => '',
152
			'example_request_data'       => '',
153
			'example_response'           => '',
154
			'required_scope'             => '',
155
			'pass_wpcom_user_details'    => false,
156
			'custom_fields_filtering'    => false,
157
			'allow_cross_origin_request' => false,
158
			'allow_unauthorized_request' => false,
159
			'allow_jetpack_site_auth'    => false,
160
			'allow_upload_token_auth'    => false,
161
		);
162
163
		$args = wp_parse_args( $args, $defaults );
164
165
		$this->in_testing = $args['in_testing'];
166
167
		$this->allowed_if_flagged     = $args['allowed_if_flagged'];
168
		$this->allowed_if_red_flagged = $args['allowed_if_red_flagged'];
169
		$this->allowed_if_deleted     = $args['allowed_if_deleted'];
170
171
		$this->description = $args['description'];
172
		$this->group       = $args['group'];
173
		$this->stat        = $args['stat'];
174
		$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...
175
		$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...
176
177
		$this->method      = $args['method'];
178
		$this->path        = $args['path'];
179
		$this->path_labels = $args['path_labels'];
180
		$this->min_version = $args['min_version'];
181
		$this->max_version = $args['max_version'];
182
		$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...
183
		$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...
184
185
		// Ensure max version is not less than min version
186
		if ( version_compare( $this->min_version, $this->max_version, '>' ) ) {
187
			$this->max_version = $this->min_version;
188
		}
189
190
		$this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
191
		$this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
192
193
		$this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
194
		$this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
195
		$this->allow_jetpack_site_auth    = (bool) $args['allow_jetpack_site_auth'];
196
		$this->allow_upload_token_auth    = (bool) $args['allow_upload_token_auth'];
197
198
		$this->version = $args['version'];
199
200
		$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...
201
202 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...
203
			$this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
204
		} else {
205
			$this->request_format = $args['request_format'];
206
		}
207
208 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...
209
			$this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
210
		} else {
211
			$this->response_format = $args['response_format'];
212
		}
213
214
		if ( false === $args['query_parameters'] ) {
215
			$this->query = array();
216
		} elseif ( is_array( $args['query_parameters'] ) ) {
217
			$this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
218
		}
219
220
		$this->api   = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
221
		$this->links = WPCOM_JSON_API_Links::getInstance();
222
223
		/** Example Request/Response */
224
225
		// Examples for endpoint documentation request
226
		$this->example_request      = $args['example_request'];
227
		$this->example_request_data = $args['example_request_data'];
228
		$this->example_response     = $args['example_response'];
229
230
		$this->api->add( $this );
231
	}
232
233
	// Get all query args.  Prefill with defaults
234
	function query_args( $return_default_values = true, $cast_and_filter = true ) {
235
		$args = array_intersect_key( $this->api->query, $this->query );
236
237
		if ( ! $cast_and_filter ) {
238
			return $args;
239
		}
240
241
		return $this->cast_and_filter( $args, $this->query, $return_default_values );
242
	}
243
244
	// Get POST body data
245
	function input( $return_default_values = true, $cast_and_filter = true ) {
246
		$input        = trim( $this->api->post_body );
247
		$content_type = $this->api->content_type;
248
		if ( $content_type ) {
249
			list ( $content_type ) = explode( ';', $content_type );
250
		}
251
		$content_type = trim( $content_type );
252
		switch ( $content_type ) {
253
			case 'application/json':
254
			case 'application/x-javascript':
255
			case 'text/javascript':
256
			case 'text/x-javascript':
257
			case 'text/x-json':
258
			case 'text/json':
259
				$return = json_decode( $input, true );
260
261
				if ( function_exists( 'json_last_error' ) ) {
262
					if ( JSON_ERROR_NONE !== json_last_error() ) { // phpcs:ignore PHPCompatibility
263
						return null;
264
					}
265
				} else {
266
					if ( is_null( $return ) && json_encode( null ) !== $input ) {
267
						return null;
268
					}
269
				}
270
271
				break;
272
			case 'multipart/form-data':
273
				$return = array_merge( stripslashes_deep( $_POST ), $_FILES );
274
				break;
275
			case 'application/x-www-form-urlencoded':
276
				// attempt JSON first, since probably a curl command
277
				$return = json_decode( $input, true );
278
279
				if ( is_null( $return ) ) {
280
					wp_parse_str( $input, $return );
281
				}
282
283
				break;
284
			default:
285
				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...
286
				break;
287
		}
288
289
		if ( isset( $this->api->query['force'] )
290
			&& 'secure' === $this->api->query['force']
291
			&& isset( $return['secure_key'] ) ) {
292
			$this->api->post_body      = $this->get_secure_body( $return['secure_key'] );
293
			$this->api->query['force'] = false;
294
			return $this->input( $return_default_values, $cast_and_filter );
295
		}
296
297
		if ( $cast_and_filter ) {
298
			$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...
299
		}
300
		return $return;
301
	}
302
303
304
	protected function get_secure_body( $secure_key ) {
305
		$response = Client::wpcom_json_api_request_as_blog(
306
			sprintf( '/sites/%d/secure-request', Jetpack_Options::get_option( 'id' ) ),
307
			'1.1',
308
			array( 'method' => 'POST' ),
309
			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...
310
		);
311
		if ( 200 !== $response['response']['code'] ) {
312
			return null;
313
		}
314
		return json_decode( $response['body'], true );
315
	}
316
317
	function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
318
		$return_as_object = false;
319
		if ( is_object( $data ) ) {
320
			// @todo this should probably be a deep copy if $data can ever have nested objects
321
			$data             = (array) $data;
322
			$return_as_object = true;
323
		} elseif ( ! is_array( $data ) ) {
324
			return $data;
325
		}
326
327
		$boolean_arg = array( 'false', 'true' );
328
		$naeloob_arg = array( 'true', 'false' );
329
330
		$return = array();
331
332
		foreach ( $documentation as $key => $description ) {
333
			if ( is_array( $description ) ) {
334
				// String or boolean array keys only
335
				$whitelist = array_keys( $description );
336
337
				if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
338
					// Truthiness
339
					if ( isset( $data[ $key ] ) ) {
340
						$return[ $key ] = (bool) WPCOM_JSON_API::is_truthy( $data[ $key ] );
341
					} elseif ( $return_default_values ) {
342
						$return[ $key ] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg.
343
					}
344
				} elseif ( isset( $data[ $key ] ) && isset( $description[ $data[ $key ] ] ) ) {
345
					// String Key
346
					$return[ $key ] = (string) $data[ $key ];
347
				} elseif ( $return_default_values ) {
348
					// Default value
349
					$return[ $key ] = (string) current( $whitelist );
350
				}
351
352
				continue;
353
			}
354
355
			$types = $this->parse_types( $description );
356
			$type  = array_shift( $types );
357
358
			// Explicit default - string and int only for now.  Always set these reguardless of $return_default_values
359
			if ( isset( $type['default'] ) ) {
360
				if ( ! isset( $data[ $key ] ) ) {
361
					$data[ $key ] = $type['default'];
362
				}
363
			}
364
365
			if ( ! isset( $data[ $key ] ) ) {
366
				continue;
367
			}
368
369
			$this->cast_and_filter_item( $return, $type, $key, $data[ $key ], $types, $for_output );
370
		}
371
372
		if ( $return_as_object ) {
373
			return (object) $return;
374
		}
375
376
		return $return;
377
	}
378
379
	/**
380
	 * Casts $value according to $type.
381
	 * Handles fallbacks for certain values of $type when $value is not that $type
382
	 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way),
383
	 * and string -> object (one way)
384
	 *
385
	 * Handles "child types" - array:URL, object:category
386
	 * array:URL means an array of URLs
387
	 * object:category means a hash of categories
388
	 *
389
	 * Handles object typing - object>post means an object of type post
390
	 */
391
	function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
392
		if ( is_string( $type ) ) {
393
			$type = compact( 'type' );
394
		}
395
396
		switch ( $type['type'] ) {
397
			case 'false':
398
				$return[ $key ] = false;
399
				break;
400
			case 'url':
401
				if ( is_object( $value ) && isset( $value->url ) && false !== strpos( $value->url, 'https://videos.files.wordpress.com/' ) ) {
402
					$value = $value->url;
403
				}
404
				// Check for string since esc_url_raw() expects one.
405
				if ( ! is_string( $value ) ) {
406
					break;
407
				}
408
				$return[ $key ] = (string) esc_url_raw( $value );
409
				break;
410
			case 'string':
411
				// Fallback string -> array, or for string -> object
412
				if ( is_array( $value ) || is_object( $value ) ) {
413 View Code Duplication
					if ( ! empty( $types[0] ) ) {
414
						$next_type = array_shift( $types );
415
						return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
416
					}
417
				}
418
419
				// Fallback string -> false
420 View Code Duplication
				if ( ! is_string( $value ) ) {
421
					if ( ! empty( $types[0] ) && 'false' === $types[0]['type'] ) {
422
						$next_type = array_shift( $types );
423
						return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
424
					}
425
				}
426
				$return[ $key ] = (string) $value;
427
				break;
428
			case 'html':
429
				$return[ $key ] = (string) $value;
430
				break;
431
			case 'safehtml':
432
				$return[ $key ] = wp_kses( (string) $value, wp_kses_allowed_html() );
433
				break;
434
			case 'zip':
435
			case 'media':
436
				if ( is_array( $value ) ) {
437
					if ( isset( $value['name'] ) && is_array( $value['name'] ) ) {
438
						// It's a $_FILES array
439
						// Reformat into array of $_FILES items
440
						$files = array();
441
442
						foreach ( $value['name'] as $k => $v ) {
443
							$files[ $k ] = array();
444
							foreach ( array_keys( $value ) as $file_key ) {
445
								$files[ $k ][ $file_key ] = $value[ $file_key ][ $k ];
446
							}
447
						}
448
449
						$return[ $key ] = $files;
450
						break;
451
					}
452
				} else {
453
					// no break - treat as 'array'
454
				}
455
				// nobreak
456
			case 'array':
457
				// Fallback array -> string
458 View Code Duplication
				if ( is_string( $value ) ) {
459
					if ( ! empty( $types[0] ) ) {
460
						$next_type = array_shift( $types );
461
						return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
462
					}
463
				}
464
465 View Code Duplication
				if ( isset( $type['children'] ) ) {
466
					$children = array();
467
					foreach ( (array) $value as $k => $child ) {
468
						$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
469
					}
470
					$return[ $key ] = (array) $children;
471
					break;
472
				}
473
474
				$return[ $key ] = (array) $value;
475
				break;
476
			case 'iso 8601 datetime':
477
			case 'datetime':
478
				// (string)s
479
				$dates = $this->parse_date( (string) $value );
480
				if ( $for_output ) {
481
					$return[ $key ] = $this->format_date( $dates[1], $dates[0] );
482
				} else {
483
					list( $return[ $key ], $return[ "{$key}_gmt" ] ) = $dates;
484
				}
485
				break;
486
			case 'float':
487
				$return[ $key ] = (float) $value;
488
				break;
489
			case 'int':
490
			case 'integer':
491
				$return[ $key ] = (int) $value;
492
				break;
493
			case 'bool':
494
			case 'boolean':
495
				$return[ $key ] = (bool) WPCOM_JSON_API::is_truthy( $value );
496
				break;
497
			case 'object':
498
				// Fallback object -> false
499 View Code Duplication
				if ( is_scalar( $value ) || is_null( $value ) ) {
500
					if ( ! empty( $types[0] ) && 'false' === $types[0]['type'] ) {
501
						return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
502
					}
503
				}
504
505 View Code Duplication
				if ( isset( $type['children'] ) ) {
506
					$children = array();
507
					foreach ( (array) $value as $k => $child ) {
508
						$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
509
					}
510
					$return[ $key ] = (object) $children;
511
					break;
512
				}
513
514
				if ( isset( $type['subtype'] ) ) {
515
					return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
516
				}
517
518
				$return[ $key ] = (object) $value;
519
				break;
520
			case 'post':
521
				$return[ $key ] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
522
				break;
523
			case 'comment':
524
				$return[ $key ] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
525
				break;
526
			case 'tag':
527
			case 'category':
528
				$docs = array(
529
					'ID'          => '(int)',
530
					'name'        => '(string)',
531
					'slug'        => '(string)',
532
					'description' => '(HTML)',
533
					'post_count'  => '(int)',
534
					'feed_url'    => '(string)',
535
					'meta'        => '(object)',
536
				);
537
				if ( 'category' === $type['type'] ) {
538
					$docs['parent'] = '(int)';
539
				}
540
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
541
				break;
542
			case 'post_reference':
543 View Code Duplication
			case 'comment_reference':
544
				$docs           = array(
545
					'ID'    => '(int)',
546
					'type'  => '(string)',
547
					'title' => '(string)',
548
					'link'  => '(URL)',
549
				);
550
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
551
				break;
552 View Code Duplication
			case 'geo':
553
				$docs           = array(
554
					'latitude'  => '(float)',
555
					'longitude' => '(float)',
556
					'address'   => '(string)',
557
				);
558
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
559
				break;
560
			case 'author':
561
				$docs           = array(
562
					'ID'             => '(int)',
563
					'user_login'     => '(string)',
564
					'login'          => '(string)',
565
					'email'          => '(string|false)',
566
					'name'           => '(string)',
567
					'first_name'     => '(string)',
568
					'last_name'      => '(string)',
569
					'nice_name'      => '(string)',
570
					'URL'            => '(URL)',
571
					'avatar_URL'     => '(URL)',
572
					'profile_URL'    => '(URL)',
573
					'is_super_admin' => '(bool)',
574
					'roles'          => '(array:string)',
575
					'ip_address'     => '(string|false)',
576
				);
577
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
578
				break;
579 View Code Duplication
			case 'role':
580
				$docs           = array(
581
					'name'         => '(string)',
582
					'display_name' => '(string)',
583
					'capabilities' => '(object:boolean)',
584
				);
585
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
586
				break;
587
			case 'attachment':
588
				$docs           = array(
589
					'ID'        => '(int)',
590
					'URL'       => '(URL)',
591
					'guid'      => '(string)',
592
					'mime_type' => '(string)',
593
					'width'     => '(int)',
594
					'height'    => '(int)',
595
					'duration'  => '(int)',
596
				);
597
				$return[ $key ] = (object) $this->cast_and_filter(
598
					$value,
599
					/**
600
					* Filter the documentation returned for a post attachment.
601
					*
602
					* @module json-api
603
					*
604
					* @since 1.9.0
605
					*
606
					* @param array $docs Array of documentation about a post attachment.
607
					*/
608
					apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
609
					false,
610
					$for_output
611
				);
612
				break;
613
			case 'metadata':
614
				$docs           = array(
615
					'id'             => '(int)',
616
					'key'            => '(string)',
617
					'value'          => '(string|false|float|int|array|object)',
618
					'previous_value' => '(string)',
619
					'operation'      => '(string)',
620
				);
621
				$return[ $key ] = (object) $this->cast_and_filter(
622
					$value,
623
					/** This filter is documented in class.json-api-endpoints.php */
624
					apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
625
					false,
626
					$for_output
627
				);
628
				break;
629
			case 'plugin':
630
				$docs           = array(
631
					'id'           => '(safehtml) The plugin\'s ID',
632
					'slug'         => '(safehtml) The plugin\'s Slug',
633
					'active'       => '(boolean)  The plugin status.',
634
					'update'       => '(object)   The plugin update info.',
635
					'name'         => '(safehtml) The name of the plugin.',
636
					'plugin_url'   => '(url)      Link to the plugin\'s web site.',
637
					'version'      => '(safehtml) The plugin version number.',
638
					'description'  => '(safehtml) Description of what the plugin does and/or notes from the author',
639
					'author'       => '(safehtml) The plugin author\'s name',
640
					'author_url'   => '(url)      The plugin author web site address',
641
					'network'      => '(boolean)  Whether the plugin can only be activated network wide.',
642
					'autoupdate'   => '(boolean)  Whether the plugin is auto updated',
643
					'log'          => '(array:safehtml) An array of update log strings.',
644
					'action_links' => '(array) An array of action links that the plugin uses.',
645
				);
646
				$return[ $key ] = (object) $this->cast_and_filter(
647
					$value,
648
					/**
649
					* Filter the documentation returned for a plugin.
650
					*
651
					* @module json-api
652
					*
653
					* @since 3.1.0
654
					*
655
					* @param array $docs Array of documentation about a plugin.
656
					*/
657
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
658
					false,
659
					$for_output
660
				);
661
				break;
662
			case 'plugin_v1_2':
663
				$docs           = class_exists( 'Jetpack_JSON_API_Get_Plugins_v1_2_Endpoint' )
664
				? Jetpack_JSON_API_Get_Plugins_v1_2_Endpoint::$_response_format
665
				: 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...
666
				$return[ $key ] = (object) $this->cast_and_filter(
667
					$value,
668
					/**
669
					* Filter the documentation returned for a plugin.
670
					*
671
					* @module json-api
672
					*
673
					* @since 3.1.0
674
					*
675
					* @param array $docs Array of documentation about a plugin.
676
					*/
677
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
678
					false,
679
					$for_output
680
				);
681
				break;
682
			case 'file_mod_capabilities':
683
				$docs           = array(
684
					'reasons_modify_files_unavailable' => '(array) The reasons why files can\'t be modified',
685
					'reasons_autoupdate_unavailable'   => '(array) The reasons why autoupdates aren\'t allowed',
686
					'modify_files'                     => '(boolean) true if files can be modified',
687
					'autoupdate_files'                 => '(boolean) true if autoupdates are allowed',
688
				);
689
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
690
				break;
691
			case 'jetpackmodule':
692
				$docs           = array(
693
					'id'          => '(string)   The module\'s ID',
694
					'active'      => '(boolean)  The module\'s status.',
695
					'name'        => '(string)   The module\'s name.',
696
					'description' => '(safehtml) The module\'s description.',
697
					'sort'        => '(int)      The module\'s display order.',
698
					'introduced'  => '(string)   The Jetpack version when the module was introduced.',
699
					'changed'     => '(string)   The Jetpack version when the module was changed.',
700
					'free'        => '(boolean)  The module\'s Free or Paid status.',
701
					'module_tags' => '(array)    The module\'s tags.',
702
					'override'    => '(string)   The module\'s override. Empty if no override, otherwise \'active\' or \'inactive\'',
703
				);
704
				$return[ $key ] = (object) $this->cast_and_filter(
705
					$value,
706
					/** This filter is documented in class.json-api-endpoints.php */
707
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
708
					false,
709
					$for_output
710
				);
711
				break;
712
			case 'sharing_button':
713
				$docs           = array(
714
					'ID'         => '(string)',
715
					'name'       => '(string)',
716
					'URL'        => '(string)',
717
					'icon'       => '(string)',
718
					'enabled'    => '(bool)',
719
					'visibility' => '(string)',
720
				);
721
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
722
				break;
723
			case 'sharing_button_service':
724
				$docs           = array(
725
					'ID'               => '(string) The service identifier',
726
					'name'             => '(string) The service name',
727
					'class_name'       => '(string) Class name for custom style sharing button elements',
728
					'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
729
					'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
730
					'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview',
731
				);
732
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
733
				break;
734
			case 'site_keyring':
735
				$docs           = array(
736
					'keyring_id'       => '(int) Keyring ID',
737
					'service'          => '(string) The service name',
738
					'external_user_id' => '(string) External user id for the service',
739
				);
740
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
741
				break;
742
			case 'taxonomy':
743
				$docs           = array(
744
					'name'         => '(string) The taxonomy slug',
745
					'label'        => '(string) The taxonomy human-readable name',
746
					'labels'       => '(object) Mapping of labels for the taxonomy',
747
					'description'  => '(string) The taxonomy description',
748
					'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
749
					'public'       => '(bool) Whether the taxonomy is public',
750
					'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
751
				);
752
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
753
				break;
754
755
			default:
756
				$method_name = $type['type'] . '_docs';
757
				if ( method_exists( 'WPCOM_JSON_API_Jetpack_Overrides', $method_name ) ) {
758
					$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
759
				}
760
761
				if ( ! empty( $docs ) ) {
762
					$return[ $key ] = (object) $this->cast_and_filter(
763
						$value,
764
						/** This filter is documented in class.json-api-endpoints.php */
765
						apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
766
						false,
767
						$for_output
768
					);
769
				} else {
770
					trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
771
				}
772
		}
773
	}
774
775
	function parse_types( $text ) {
776
		if ( ! preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
777
			return 'none';
778
		}
779
780
		$types  = explode( '|', strtolower( $matches[1] ) );
781
		$return = array();
782
		foreach ( $types as $type ) {
783
			foreach ( array(
784
				':' => 'children',
785
				'>' => 'subtype',
786
				'=' => 'default',
787
			) as $operator => $meaning ) {
788
				if ( false !== strpos( $type, $operator ) ) {
789
					$item     = explode( $operator, $type, 2 );
790
					$return[] = array(
791
						'type'   => $item[0],
792
						$meaning => $item[1],
793
					);
794
					continue 2;
795
				}
796
			}
797
			$return[] = compact( 'type' );
798
		}
799
800
		return $return;
801
	}
802
803
	/**
804
	 * Checks if the endpoint is publicly displayable
805
	 */
806
	function is_publicly_documentable() {
807
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
808
	}
809
810
	/**
811
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
812
	 * Echoes HTML.
813
	 */
814
	function document( $show_description = true ) {
815
		global $wpdb;
816
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
817
		unset( $GLOBALS['post'] );
818
819
		$doc = $this->generate_documentation();
820
821
		if ( $show_description ) :
822
			?>
823
<caption>
824
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
825
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
826
</caption>
827
828
<?php endif; ?>
829
830
		<?php if ( true === $this->deprecated ) { ?>
831
<p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $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...
832
<?php } ?>
833
834
<section class="resource-info">
835
	<h2 id="apidoc-resource-info">Resource Information</h2>
836
837
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
838
839
	<thead>
840
		<tr>
841
			<th class="api-index-title" scope="column">&nbsp;</th>
842
			<th class="api-index-title" scope="column">&nbsp;</th>
843
		</tr>
844
	</thead>
845
	<tbody>
846
847
		<tr class="api-index-item">
848
			<th scope="row" class="parameter api-index-item-title">Method</th>
849
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
850
		</tr>
851
852
		<tr class="api-index-item">
853
			<th scope="row" class="parameter api-index-item-title">URL</th>
854
			<?php
855
			$version = WPCOM_JSON_API__CURRENT_VERSION;
856
			if ( ! empty( $this->max_version ) ) {
857
				$version = $this->max_version;
858
			}
859
			?>
860
			<td class="type api-index-item-title">https://public-api.wordpress.com/rest/v<?php echo floatval( $version ); ?><?php echo wp_kses_post( $doc['path_labeled'] ); ?></td>
861
		</tr>
862
863
		<tr class="api-index-item">
864
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
865
			<?php
866
			$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'] ) );
867
			?>
868
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
869
		</tr>
870
871
	</tbody>
872
	</table>
873
874
</section>
875
876
		<?php
877
878
		foreach ( array(
879
			'path'     => 'Method Parameters',
880
			'query'    => 'Query Parameters',
881
			'body'     => 'Request Parameters',
882
			'response' => 'Response Parameters',
883
		) as $doc_section_key => $label ) :
884
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][ $doc_section_key ];
885
			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...
886
				continue;
887
			}
888
889
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
890
			?>
891
892
<section class="<?php echo $param_label; ?>">
893
894
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
895
896
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
897
898
<thead>
899
	<tr>
900
		<th class="api-index-title" scope="column">Parameter</th>
901
		<th class="api-index-title" scope="column">Type</th>
902
		<th class="api-index-title" scope="column">Description</th>
903
	</tr>
904
</thead>
905
<tbody>
906
907
			<?php foreach ( $doc_section as $key => $item ) : ?>
908
909
	<tr class="api-index-item">
910
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
911
		<td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
912
		<td class="description api-index-item-body">
913
				<?php
914
915
				$this->generate_doc_description( $item['description'] );
916
917
				?>
918
		</td>
919
	</tr>
920
921
			<?php endforeach; ?>
922
</tbody>
923
</table>
924
</section>
925
<?php endforeach; ?>
926
927
		<?php
928
		if ( 'unset' !== $original_post ) {
929
			$GLOBALS['post'] = $original_post;
930
		}
931
	}
932
933
	function add_http_build_query_to_php_content_example( $matches ) {
934
		$trimmed_match = ltrim( $matches[0] );
935
		$pad           = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
936
		$pad           = ltrim( $pad, ' ' );
937
		$return        = '  ' . str_replace( "\n", "\n  ", $matches[0] );
938
		return " http_build_query({$return}{$pad})";
939
	}
940
941
	/**
942
	 * Recursively generates the <dl>'s to document item descriptions.
943
	 * Echoes HTML.
944
	 */
945
	function generate_doc_description( $item ) {
946
		if ( is_array( $item ) ) :
947
			?>
948
949
		<dl>
950
			<?php	foreach ( $item as $description_key => $description_value ) : ?>
951
952
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
953
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
954
955
			<?php	endforeach; ?>
956
957
		</dl>
958
959
			<?php
960
		else :
961
			echo wp_kses_post( $item );
962
		endif;
963
	}
964
965
	/**
966
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
967
	 * Echoes HTML.
968
	 */
969
	function generate_documentation() {
970
		$format       = str_replace( '%d', '%s', $this->path );
971
		$path_labeled = $format;
972
		if ( ! empty( $this->path_labels ) ) {
973
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
974
		}
975
		$boolean_arg = array( 'false', 'true' );
976
		$naeloob_arg = array( 'true', 'false' );
977
978
		$doc = array(
979
			'description'  => $this->description,
980
			'method'       => $this->method,
981
			'path_format'  => $this->path,
982
			'path_labeled' => $path_labeled,
983
			'group'        => $this->group,
984
			'request'      => array(
985
				'path'  => array(),
986
				'query' => array(),
987
				'body'  => array(),
988
			),
989
			'response'     => array(
990
				'body' => array(),
991
			),
992
		);
993
994
		foreach ( array(
995
			'path_labels'     => 'path',
996
			'query'           => 'query',
997
			'request_format'  => 'body',
998
			'response_format' => 'body',
999
		) as $_property => $doc_item ) {
1000
			foreach ( (array) $this->$_property as $key => $description ) {
1001
				if ( is_array( $description ) ) {
1002
					$description_keys = array_keys( $description );
1003
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
1004
						$type = '(bool)';
1005
					} else {
1006
						$type = '(string)';
1007
					}
1008
1009
					if ( 'response_format' !== $_property ) {
1010
						// hack - don't show "(default)" in response format
1011
						reset( $description );
1012
						$description_key                 = key( $description );
1013
						$description[ $description_key ] = "(default) {$description[$description_key]}";
1014
					}
1015
				} else {
1016
					$types   = $this->parse_types( $description );
1017
					$type    = array();
1018
					$default = '';
1019
1020
					if ( 'none' == $types ) {
1021
						$types           = array();
1022
						$types[]['type'] = 'none';
1023
					}
1024
1025
					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...
1026
						$type[] = $type_array['type'];
1027
						if ( isset( $type_array['default'] ) ) {
1028
							$default = $type_array['default'];
1029
							if ( 'string' === $type_array['type'] ) {
1030
								$default = "'$default'";
1031
							}
1032
						}
1033
					}
1034
					$type                       = '(' . join( '|', $type ) . ')';
1035
					$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...
1036
					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...
1037
					$description                = trim( $description );
1038
					if ( $default ) {
1039
						$description .= " Default: $default.";
1040
					}
1041
				}
1042
1043
				$item = compact( 'type', 'description' );
1044
1045
				if ( 'response_format' === $_property ) {
1046
					$doc['response'][ $doc_item ][ $key ] = $item;
1047
				} else {
1048
					$doc['request'][ $doc_item ][ $key ] = $item;
1049
				}
1050
			}
1051
		}
1052
1053
		return $doc;
1054
	}
1055
1056
	function user_can_view_post( $post_id ) {
1057
		$post = get_post( $post_id );
1058
		if ( ! $post || is_wp_error( $post ) ) {
1059
			return false;
1060
		}
1061
1062 View Code Duplication
		if ( 'inherit' === $post->post_status ) {
1063
			$parent_post     = get_post( $post->post_parent );
1064
			$post_status_obj = get_post_status_object( $parent_post->post_status );
1065
		} else {
1066
			$post_status_obj = get_post_status_object( $post->post_status );
1067
		}
1068
1069
		if ( ! $post_status_obj->public ) {
1070
			if ( is_user_logged_in() ) {
1071
				if ( $post_status_obj->protected ) {
1072
					if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1073
						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...
1074
					}
1075
				} elseif ( $post_status_obj->private ) {
1076
					if ( ! current_user_can( 'read_post', $post->ID ) ) {
1077
						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...
1078
					}
1079
				} elseif ( in_array( $post->post_status, array( 'inherit', 'trash' ) ) ) {
1080
					if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1081
						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...
1082
					}
1083
				} elseif ( 'auto-draft' === $post->post_status ) {
1084
					// allow auto-drafts
1085
				} else {
1086
					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...
1087
				}
1088
			} else {
1089
				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...
1090
			}
1091
		}
1092
1093 View Code Duplication
		if (
1094
			-1 == get_option( 'blog_public' ) &&
1095
			/**
1096
			 * Filter access to a specific post.
1097
			 *
1098
			 * @module json-api
1099
			 *
1100
			 * @since 3.4.0
1101
			 *
1102
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1103
			 * @param WP_Post $post Post data.
1104
			 */
1105
			! apply_filters(
1106
				'wpcom_json_api_user_can_view_post',
1107
				current_user_can( 'read_post', $post->ID ),
1108
				$post
1109
			)
1110
		) {
1111
			return new WP_Error(
1112
				'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...
1113
				'User cannot view post',
1114
				array(
1115
					'status_code' => 403,
1116
					'error'       => 'private_blog',
1117
				)
1118
			);
1119
		}
1120
1121 View Code Duplication
		if ( strlen( $post->post_password ) && ! current_user_can( 'edit_post', $post->ID ) ) {
1122
			return new WP_Error(
1123
				'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...
1124
				'User cannot view password protected post',
1125
				array(
1126
					'status_code' => 403,
1127
					'error'       => 'password_protected',
1128
				)
1129
			);
1130
		}
1131
1132
		return true;
1133
	}
1134
1135
	/**
1136
	 * Returns author object.
1137
	 *
1138
	 * @param object $author user ID, user row, WP_User object, comment row, post row
1139
	 * @param bool   $show_email_and_ip output the author's email address and IP address?
1140
	 *
1141
	 * @return object
1142
	 */
1143
	function get_author( $author, $show_email_and_ip = false ) {
1144
		$ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1145
1146
		if ( isset( $author->comment_author_email ) ) {
1147
			$ID          = 0;
1148
			$login       = '';
1149
			$email       = $author->comment_author_email;
1150
			$name        = $author->comment_author;
1151
			$first_name  = '';
1152
			$last_name   = '';
1153
			$URL         = $author->comment_author_url;
1154
			$avatar_URL  = $this->api->get_avatar_url( $author );
1155
			$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1156
			$nice        = '';
1157
			$site_id     = -1;
1158
1159
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1160
			// "&" is the only email/URL character altered by wp_kses()
1161
			foreach ( array( 'email', 'URL' ) as $field ) {
1162
				$$field = str_replace( '&amp;', '&', $$field );
1163
			}
1164
		} else {
1165
			if ( isset( $author->user_id ) && $author->user_id ) {
1166
				$author = $author->user_id;
1167
			} elseif ( isset( $author->user_email ) ) {
1168
				$author = $author->ID;
1169
			} elseif ( isset( $author->post_author ) ) {
1170
				// then $author is a Post Object.
1171
				if ( 0 == $author->post_author ) {
1172
					return null;
1173
				}
1174
				/**
1175
				 * Filter whether the current site is a Jetpack site.
1176
				 *
1177
				 * @module json-api
1178
				 *
1179
				 * @since 3.3.0
1180
				 *
1181
				 * @param bool false Is the current site a Jetpack site. Default to false.
1182
				 * @param int get_current_blog_id() Blog ID.
1183
				 */
1184
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1185
				$post_id    = $author->ID;
1186
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1187
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1188
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1189
					$login      = '';
1190
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1191
					$first_name = '';
1192
					$last_name  = '';
1193
					$URL        = '';
1194
					$nice       = '';
1195
				} else {
1196
					$author = $author->post_author;
1197
				}
1198
			}
1199
1200
			if ( ! isset( $ID ) ) {
1201
				$user = get_user_by( 'id', $author );
1202
				if ( ! $user || is_wp_error( $user ) ) {
1203
					trigger_error( 'Unknown user', E_USER_WARNING );
1204
1205
					return null;
1206
				}
1207
				$ID         = $user->ID;
1208
				$email      = $user->user_email;
1209
				$login      = $user->user_login;
1210
				$name       = $user->display_name;
1211
				$first_name = $user->first_name;
1212
				$last_name  = $user->last_name;
1213
				$URL        = $user->user_url;
1214
				$nice       = $user->user_nicename;
1215
			}
1216
			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...
1217
				$active_blog = get_active_blog_for_user( $ID );
1218
				$site_id     = $active_blog->blog_id;
1219
				if ( $site_id > -1 ) {
1220
					$site_visible = (
1221
						-1 != $active_blog->public ||
1222
						is_private_blog_user( $site_id, get_current_user_id() )
1223
					);
1224
				}
1225
				$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...
1226
			} else {
1227
				$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...
1228
				$site_id     = -1;
1229
			}
1230
1231
			$avatar_URL = $this->api->get_avatar_url( $email );
1232
		}
1233
1234
		if ( $show_email_and_ip ) {
1235
			$email      = (string) $email;
1236
			$ip_address = (string) $ip_address;
1237
		} else {
1238
			$email      = false;
1239
			$ip_address = false;
1240
		}
1241
1242
		$author = array(
1243
			'ID'          => (int) $ID,
1244
			'login'       => (string) $login,
1245
			'email'       => $email, // (string|bool)
1246
			'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...
1247
			'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...
1248
			'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...
1249
			'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...
1250
			'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...
1251
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1252
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1253
			'ip_address'  => $ip_address, // (string|bool)
1254
		);
1255
1256
		if ( $site_id > -1 ) {
1257
			$author['site_ID']      = (int) $site_id;
1258
			$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...
1259
		}
1260
1261
		return (object) $author;
1262
	}
1263
1264
	function get_media_item( $media_id ) {
1265
		$media_item = get_post( $media_id );
1266
1267
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1268
			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...
1269
		}
1270
1271
		$response = array(
1272
			'id'          => strval( $media_item->ID ),
1273
			'date'        => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1274
			'parent'      => $media_item->post_parent,
1275
			'link'        => wp_get_attachment_url( $media_item->ID ),
1276
			'title'       => $media_item->post_title,
1277
			'caption'     => $media_item->post_excerpt,
1278
			'description' => $media_item->post_content,
1279
			'metadata'    => wp_get_attachment_metadata( $media_item->ID ),
1280
		);
1281
1282
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1283
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1284
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1285
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1286
		}
1287
1288
		$response['meta'] = (object) array(
1289
			'links' => (object) array(
1290
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1291
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1292
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1293
			),
1294
		);
1295
1296
		return (object) $response;
1297
	}
1298
1299
	function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1300
1301
		if ( ! $media_item ) {
1302
			$media_item = get_post( $media_id );
1303
		}
1304
1305
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1306
			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...
1307
		}
1308
1309
		$attachment_file = get_attached_file( $media_item->ID );
1310
1311
		$file      = basename( $attachment_file ? $attachment_file : $file );
1312
		$file_info = pathinfo( $file );
1313
		$ext       = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1314
1315
		$response = array(
1316
			'ID'          => $media_item->ID,
1317
			'URL'         => wp_get_attachment_url( $media_item->ID ),
1318
			'guid'        => $media_item->guid,
1319
			'date'        => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1320
			'post_ID'     => $media_item->post_parent,
1321
			'author_ID'   => (int) $media_item->post_author,
1322
			'file'        => $file,
1323
			'mime_type'   => $media_item->post_mime_type,
1324
			'extension'   => $ext,
1325
			'title'       => $media_item->post_title,
1326
			'caption'     => $media_item->post_excerpt,
1327
			'description' => $media_item->post_content,
1328
			'alt'         => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1329
			'icon'        => wp_mime_type_icon( $media_item->ID ),
1330
			'thumbnails'  => array(),
1331
		);
1332
1333
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1334
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1335 View Code Duplication
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1336
				$response['height'] = $metadata['height'];
1337
				$response['width']  = $metadata['width'];
1338
			}
1339
1340
			if ( isset( $metadata['sizes'] ) ) {
1341
				/**
1342
				 * Filter the thumbnail sizes available for each attachment ID.
1343
				 *
1344
				 * @module json-api
1345
				 *
1346
				 * @since 3.9.0
1347
				 *
1348
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1349
				 * @param string $media_id Attachment ID.
1350
				 */
1351
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
1352 View Code Duplication
				if ( is_array( $sizes ) ) {
1353
					foreach ( $sizes as $size => $size_details ) {
1354
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1355
					}
1356
					/**
1357
					 * Filter the thumbnail URLs for attachment files.
1358
					 *
1359
					 * @module json-api
1360
					 *
1361
					 * @since 7.1.0
1362
					 *
1363
					 * @param array $metadata['sizes'] Array with thumbnail sizes as keys and URLs as values.
1364
					 */
1365
					$response['thumbnails'] = apply_filters( 'rest_api_thumbnail_size_urls', $response['thumbnails'] );
1366
				}
1367
			}
1368
1369
			if ( isset( $metadata['image_meta'] ) ) {
1370
				$response['exif'] = $metadata['image_meta'];
1371
			}
1372
		}
1373
1374 View Code Duplication
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1375
			$metadata           = wp_get_attachment_metadata( $media_item->ID );
1376
			$response['length'] = $metadata['length'];
1377
			$response['exif']   = $metadata;
1378
		}
1379
1380
		$is_video = false;
1381
1382
		if (
1383
			in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1384
			||
1385
			$response['mime_type'] === 'video/videopress'
1386
		) {
1387
			$is_video = true;
1388
		}
1389
1390
		if ( $is_video ) {
1391
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1392
1393 View Code Duplication
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1394
				$response['height'] = $metadata['height'];
1395
				$response['width']  = $metadata['width'];
1396
			}
1397
1398
			if ( isset( $metadata['length'] ) ) {
1399
				$response['length'] = $metadata['length'];
1400
			}
1401
1402
			// add VideoPress info
1403
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1404
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1405
1406
				// If we failed to get VideoPress info, but it exists in the meta data (for some reason)
1407
				// then let's use that.
1408
				if ( false === $info && isset( $metadata['videopress'] ) ) {
1409
					$info = (object) $metadata['videopress'];
1410
				}
1411
1412
				// Thumbnails
1413 View Code Duplication
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1414
					$response['thumbnails'] = array(
1415
						'fmt_hd'  => '',
1416
						'fmt_dvd' => '',
1417
						'fmt_std' => '',
1418
					);
1419
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1420
						if ( video_format_done( $info, $size ) ) {
1421
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1422
						} else {
1423
							unset( $response['thumbnails'][ $size ] );
1424
						}
1425
					}
1426
				}
1427
1428
				// If we didn't get VideoPress information (for some reason) then let's
1429
				// not try and include it in the response.
1430
				if ( isset( $info->guid ) ) {
1431
					$response['videopress_guid']            = $info->guid;
1432
					$response['videopress_processing_done'] = true;
1433
					if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1434
						$response['videopress_processing_done'] = false;
1435
					}
1436
				}
1437
			}
1438
		}
1439
1440
		$response['thumbnails'] = (object) $response['thumbnails'];
1441
1442
		$response['meta'] = (object) array(
1443
			'links' => (object) array(
1444
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1445
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1446
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1447
			),
1448
		);
1449
1450
		// add VideoPress link to the meta
1451
		if ( isset( $response['videopress_guid'] ) ) {
1452 View Code Duplication
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1453
				$response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1454
			}
1455
		}
1456
1457 View Code Duplication
		if ( $media_item->post_parent > 0 ) {
1458
			$response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1459
		}
1460
1461
		return (object) $response;
1462
	}
1463
1464
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1465
1466
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1467
		// keep updating this function
1468
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1469
			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...
1470
		}
1471
1472
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1473
	}
1474
1475
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1476
		// Permissions
1477 View Code Duplication
		switch ( $context ) {
1478
			case 'edit':
1479
				$tax = get_taxonomy( $taxonomy_type );
1480
				if ( ! current_user_can( $tax->cap->edit_terms ) ) {
1481
					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...
1482
				}
1483
				break;
1484
			case 'display':
1485
				if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1486
					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...
1487
				}
1488
				break;
1489
			default:
1490
				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...
1491
		}
1492
1493
		$response                = array();
1494
		$response['ID']          = (int) $taxonomy->term_id;
1495
		$response['name']        = (string) $taxonomy->name;
1496
		$response['slug']        = (string) $taxonomy->slug;
1497
		$response['description'] = (string) $taxonomy->description;
1498
		$response['post_count']  = (int) $taxonomy->count;
1499
		$response['feed_url']    = get_term_feed_link( $taxonomy->term_id, $taxonomy_type );
1500
1501
		if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1502
			$response['parent'] = (int) $taxonomy->parent;
1503
		}
1504
1505
		$response['meta'] = (object) array(
1506
			'links' => (object) array(
1507
				'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1508
				'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1509
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1510
			),
1511
		);
1512
1513
		return (object) $response;
1514
	}
1515
1516
	/**
1517
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1518
	 *
1519
	 * @param $date_gmt (string) GMT datetime string.
1520
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1521
	 *
1522
	 * @return string
1523
	 */
1524
	function format_date( $date_gmt, $date = null ) {
1525
		return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1526
	}
1527
1528
	/**
1529
	 * Parses a date string and returns the local and GMT representations
1530
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1531
	 * timezones or offsets. If the parsed datetime was not localized to a
1532
	 * particular timezone or offset we will assume it was given in GMT
1533
	 * relative to now and will convert it to local time using either the
1534
	 * timezone set in the options table for the blog or the GMT offset.
1535
	 *
1536
	 * @param datetime string
1537
	 *
1538
	 * @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...
1539
	 */
1540
	function parse_date( $date_string ) {
1541
		$date_string_info = date_parse( $date_string );
1542
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1543
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1544
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1545
				$dt_local = clone $dt_utc = new DateTime( $date_string );
1546
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1547
				return array(
1548
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1549
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1550
				);
1551
			}
1552
1553
			// It's parseable but no TZ info so assume UTC
1554
			$dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1555
		} else {
1556
			// Could not parse time, use now in UTC
1557
			$dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1558
		}
1559
1560
		$dt_local->setTimezone( wp_timezone() );
1561
1562
		return array(
1563
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1564
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1565
			);
1566
	}
1567
1568
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1569
	function load_theme_functions() {
1570
		// bail if we've done this already (can happen when calling /batch endpoint)
1571
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) ) {
1572
			return;
1573
		}
1574
1575
		// VIP context loading is handled elsewhere, so bail to prevent
1576
		// duplicate loading. See `switch_to_blog_and_validate_user()`
1577
		if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1578
			return;
1579
		}
1580
1581
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1582
1583
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1584
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1585
1586
		$copy_dirs = array( get_template_directory() );
1587
1588
		// Is this a child theme? Load the child theme's functions file.
1589
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1590
			foreach ( $function_files as $function_file ) {
1591
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1592
					require_once get_stylesheet_directory() . $function_file;
1593
				}
1594
			}
1595
			$copy_dirs[] = get_stylesheet_directory();
1596
		}
1597
1598
		foreach ( $function_files as $function_file ) {
1599
			if ( file_exists( get_template_directory() . $function_file ) ) {
1600
				require_once get_template_directory() . $function_file;
1601
			}
1602
		}
1603
1604
		// add inc/wpcom.php and/or includes/wpcom.php
1605
		wpcom_load_theme_compat_file();
1606
1607
		// Enable including additional directories or files in actions to be copied
1608
		$copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1609
1610
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1611
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1612
1613
		/**
1614
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1615
		 *
1616
		 * The REST API does not load the theme when processing requests.
1617
		 * To enable theme-based functionality, the API will load the '/functions.php',
1618
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1619
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1620
		 *
1621
		 * @module json-api
1622
		 *
1623
		 * @since 3.2.0
1624
		 */
1625
		do_action( 'restapi_theme_after_setup_theme' );
1626
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1627
1628
		/**
1629
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1630
		 *
1631
		 * The REST API does not load the theme when processing requests.
1632
		 * To enable theme-based functionality, the API will load the '/functions.php',
1633
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1634
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1635
		 *
1636
		 * @module json-api
1637
		 *
1638
		 * @since 3.2.0
1639
		 */
1640
		do_action( 'restapi_theme_init' );
1641
	}
1642
1643
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1644
		global $wp_filter;
1645
		foreach ( $wp_filter as $hook => $actions ) {
1646
1647
			if ( $from_hook != $hook ) {
1648
				continue;
1649
			}
1650
			if ( ! has_action( $hook ) ) {
1651
				continue;
1652
			}
1653
1654
			foreach ( $actions as $priority => $callbacks ) {
1655
				foreach ( $callbacks as $callback_key => $callback_data ) {
1656
					$callback = $callback_data['function'];
1657
1658
					// use reflection api to determine filename where function is defined
1659
					$reflection = $this->get_reflection( $callback );
1660
1661
					if ( false !== $reflection ) {
1662
						$file_name = $reflection->getFileName();
1663
						foreach ( $base_paths as $base_path ) {
1664
1665
							// only copy hooks with functions which are part of the specified files
1666
							if ( 0 === strpos( $file_name, $base_path ) ) {
1667
								add_action(
1668
									$to_hook,
1669
									$callback_data['function'],
1670
									$priority,
1671
									$callback_data['accepted_args']
1672
								);
1673
							}
1674
						}
1675
					}
1676
				}
1677
			}
1678
		}
1679
	}
1680
1681
	function get_reflection( $callback ) {
1682
		if ( is_array( $callback ) ) {
1683
			list( $class, $method ) = $callback;
1684
			return new ReflectionMethod( $class, $method );
1685
		}
1686
1687
		if ( is_string( $callback ) && strpos( $callback, '::' ) !== false ) {
1688
			list( $class, $method ) = explode( '::', $callback );
1689
			return new ReflectionMethod( $class, $method );
1690
		}
1691
1692
		if ( method_exists( $callback, "__invoke" ) ) {
1693
			return new ReflectionMethod( $callback, "__invoke" );
1694
		}
1695
1696
		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...
1697
			return new ReflectionFunction( $callback );
1698
		}
1699
1700
		return false;
1701
	}
1702
1703
	/**
1704
	 * Check whether a user can view or edit a post type
1705
	 *
1706
	 * @param string $post_type              post type to check
1707
	 * @param string $context                'display' or 'edit'
1708
	 * @return bool
1709
	 */
1710 View Code Duplication
	function current_user_can_access_post_type( $post_type, $context = 'display' ) {
1711
		$post_type_object = get_post_type_object( $post_type );
1712
		if ( ! $post_type_object ) {
1713
			return false;
1714
		}
1715
1716
		switch ( $context ) {
1717
			case 'edit':
1718
				return current_user_can( $post_type_object->cap->edit_posts );
1719
			case 'display':
1720
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1721
			default:
1722
				return false;
1723
		}
1724
	}
1725
1726 View Code Duplication
	function is_post_type_allowed( $post_type ) {
1727
		// if the post type is empty, that's fine, WordPress will default to post
1728
		if ( empty( $post_type ) ) {
1729
			return true;
1730
		}
1731
1732
		// allow special 'any' type
1733
		if ( 'any' == $post_type ) {
1734
			return true;
1735
		}
1736
1737
		// check for allowed types
1738
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1739
			return true;
1740
		}
1741
1742
		if ( $post_type_object = get_post_type_object( $post_type ) ) {
1743
			if ( ! empty( $post_type_object->show_in_rest ) ) {
1744
				return $post_type_object->show_in_rest;
1745
			}
1746
			if ( ! empty( $post_type_object->publicly_queryable ) ) {
1747
				return $post_type_object->publicly_queryable;
1748
			}
1749
		}
1750
1751
		return ! empty( $post_type_object->public );
1752
	}
1753
1754
	/**
1755
	 * Gets the whitelisted post types that JP should allow access to.
1756
	 *
1757
	 * @return array Whitelisted post types.
1758
	 */
1759 View Code Duplication
	protected function _get_whitelisted_post_types() {
1760
		$allowed_types = array( 'post', 'page', 'revision' );
1761
1762
		/**
1763
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1764
		 *
1765
		 * @module json-api
1766
		 *
1767
		 * @since 2.2.3
1768
		 *
1769
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1770
		 */
1771
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1772
1773
		return array_unique( $allowed_types );
1774
	}
1775
1776
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1777
1778
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1779
1780
		$media_ids             = $errors = array();
1781
		$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...
1782
		$media_attrs           = array_values( $media_attrs ); // reset the keys
1783
		$i                     = 0;
1784
1785
		if ( ! empty( $media_files ) ) {
1786
			$this->api->trap_wp_die( 'upload_error' );
1787
			foreach ( $media_files as $media_item ) {
1788
				$_FILES['.api.media.item.'] = $media_item;
1789 View Code Duplication
				if ( ! $user_can_upload_files ) {
1790
					$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...
1791
				} else {
1792
					if ( $force_parent_id ) {
1793
						$parent_id = absint( $force_parent_id );
1794
					} elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1795
						$parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1796
					} else {
1797
						$parent_id = 0;
1798
					}
1799
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1800
				}
1801
				if ( is_wp_error( $media_id ) ) {
1802
					$errors[ $i ]['file']    = $media_item['name'];
1803
					$errors[ $i ]['error']   = $media_id->get_error_code();
1804
					$errors[ $i ]['message'] = $media_id->get_error_message();
1805
				} else {
1806
					$media_ids[ $i ] = $media_id;
1807
				}
1808
1809
				$i++;
1810
			}
1811
			$this->api->trap_wp_die( null );
1812
			unset( $_FILES['.api.media.item.'] );
1813
		}
1814
1815
		if ( ! empty( $media_urls ) ) {
1816
			foreach ( $media_urls as $url ) {
1817 View Code Duplication
				if ( ! $user_can_upload_files ) {
1818
					$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...
1819
				} else {
1820
					if ( $force_parent_id ) {
1821
						$parent_id = absint( $force_parent_id );
1822
					} elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1823
						$parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1824
					} else {
1825
						$parent_id = 0;
1826
					}
1827
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1828
				}
1829
				if ( is_wp_error( $media_id ) ) {
1830
					$errors[ $i ] = array(
1831
						'file'    => $url,
1832
						'error'   => $media_id->get_error_code(),
1833
						'message' => $media_id->get_error_message(),
1834
					);
1835
				} elseif ( ! empty( $media_id ) ) {
1836
					$media_ids[ $i ] = $media_id;
1837
				}
1838
1839
				$i++;
1840
			}
1841
		}
1842
1843
		if ( ! empty( $media_attrs ) ) {
1844
			foreach ( $media_ids as $index => $media_id ) {
1845
				if ( empty( $media_attrs[ $index ] ) ) {
1846
					continue;
1847
				}
1848
1849
				$attrs  = $media_attrs[ $index ];
1850
				$insert = array();
1851
1852
				// Attributes: Title, Caption, Description
1853
1854
				if ( isset( $attrs['title'] ) ) {
1855
					$insert['post_title'] = $attrs['title'];
1856
				}
1857
1858
				if ( isset( $attrs['caption'] ) ) {
1859
					$insert['post_excerpt'] = $attrs['caption'];
1860
				}
1861
1862
				if ( isset( $attrs['description'] ) ) {
1863
					$insert['post_content'] = $attrs['description'];
1864
				}
1865
1866
				if ( ! empty( $insert ) ) {
1867
					$insert['ID'] = $media_id;
1868
					wp_update_post( (object) $insert );
1869
				}
1870
1871
				// Attributes: Alt
1872
1873 View Code Duplication
				if ( isset( $attrs['alt'] ) ) {
1874
					$alt = wp_strip_all_tags( $attrs['alt'], true );
1875
					update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1876
				}
1877
1878
				// Attributes: Artist, Album
1879
1880
				$id3_meta = array();
1881
1882 View Code Duplication
				foreach ( array( 'artist', 'album' ) as $key ) {
1883
					if ( isset( $attrs[ $key ] ) ) {
1884
						$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1885
					}
1886
				}
1887
1888
				if ( ! empty( $id3_meta ) ) {
1889
					// Before updating metadata, ensure that the item is audio
1890
					$item = $this->get_media_item_v1_1( $media_id );
1891
					if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1892
						wp_update_attachment_metadata( $media_id, $id3_meta );
1893
					}
1894
				}
1895
			}
1896
		}
1897
1898
		return array(
1899
			'media_ids' => $media_ids,
1900
			'errors'    => $errors,
1901
		);
1902
1903
	}
1904
1905
	function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1906
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) ) {
1907
			return false;
1908
		}
1909
1910
		// if we didn't get a URL, let's bail
1911
		$parsed = @parse_url( $url );
1912
		if ( empty( $parsed ) ) {
1913
			return false;
1914
		}
1915
1916
		$tmp = download_url( $url );
1917
		if ( is_wp_error( $tmp ) ) {
1918
			return $tmp;
1919
		}
1920
1921
		// First check to see if we get a mime-type match by file, otherwise, check to
1922
		// see if WordPress supports this file as an image. If neither, then it is not supported.
1923 View Code Duplication
		if ( ! $this->is_file_supported_for_sideloading( $tmp ) || 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1924
			@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...
1925
			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...
1926
		}
1927
1928
		// emulate a $_FILES entry
1929
		$file_array = array(
1930
			'name'     => basename( parse_url( $url, PHP_URL_PATH ) ),
1931
			'tmp_name' => $tmp,
1932
		);
1933
1934
		$id = media_handle_sideload( $file_array, $parent_post_id );
1935
		if ( file_exists( $tmp ) ) {
1936
			@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...
1937
		}
1938
1939
		if ( is_wp_error( $id ) ) {
1940
			return $id;
1941
		}
1942
1943
		if ( ! $id || ! is_int( $id ) ) {
1944
			return false;
1945
		}
1946
1947
		return $id;
1948
	}
1949
1950
	/**
1951
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1952
	 *
1953
	 * @param string $file Path to file to get its mime type.
1954
	 *
1955
	 * @return bool
1956
	 */
1957
	protected function is_file_supported_for_sideloading( $file ) {
1958
		return jetpack_is_file_supported_for_sideloading( $file );
1959
	}
1960
1961
	function allow_video_uploads( $mimes ) {
1962
		// if we are on Jetpack, bail - Videos are already allowed
1963
		if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
1964
			return $mimes;
1965
		}
1966
1967
		// extra check that this filter is only ever applied during REST API requests
1968
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1969
			return $mimes;
1970
		}
1971
1972
		// bail early if they already have the upgrade..
1973
		if ( get_option( 'video_upgrade' ) == '1' ) {
1974
			return $mimes;
1975
		}
1976
1977
		// lets whitelist to only specific clients right now
1978
		$clients_allowed_video_uploads = array();
1979
		/**
1980
		 * Filter the list of whitelisted video clients.
1981
		 *
1982
		 * @module json-api
1983
		 *
1984
		 * @since 3.2.0
1985
		 *
1986
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1987
		 */
1988
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1989
		if ( ! in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1990
			return $mimes;
1991
		}
1992
1993
		$mime_list = wp_get_mime_types();
1994
1995
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1996
		/**
1997
		 * Filter the video filetypes allowed on the site.
1998
		 *
1999
		 * @module json-api
2000
		 *
2001
		 * @since 3.2.0
2002
		 *
2003
		 * @param array $video_exts Array of video filetypes allowed on the site.
2004
		 */
2005
		$video_exts  = apply_filters( 'video_upload_filetypes', $video_exts );
2006
		$video_mimes = array();
2007
2008
		if ( ! empty( $video_exts ) ) {
2009
			foreach ( $video_exts as $ext ) {
2010
				foreach ( $mime_list as $ext_pattern => $mime ) {
2011
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false ) {
2012
						$video_mimes[ $ext_pattern ] = $mime;
2013
					}
2014
				}
2015
			}
2016
2017
			$mimes = array_merge( $mimes, $video_mimes );
2018
		}
2019
2020
		return $mimes;
2021
	}
2022
2023
	function is_current_site_multi_user() {
2024
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2025
		if ( false === $users ) {
2026
			$user_query = new WP_User_Query(
2027
				array(
2028
					'blog_id' => get_current_blog_id(),
2029
					'fields'  => 'ID',
2030
				)
2031
			);
2032
			$users      = (int) $user_query->get_total();
2033
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2034
		}
2035
		return $users > 1;
2036
	}
2037
2038
	function allows_cross_origin_requests() {
2039
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2040
	}
2041
2042
	function allows_unauthorized_requests( $origin, $complete_access_origins ) {
2043
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2044
	}
2045
2046
	function get_platform() {
2047
		return wpcom_get_sal_platform( $this->api->token_details );
2048
	}
2049
2050
	/**
2051
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2052
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
2053
	 *
2054
	 * Override this method if you want to do something different.
2055
	 *
2056
	 * @param  int $blog_id
2057
	 * @return bool
2058
	 */
2059
	function force_wpcom_request( $blog_id ) {
2060
		return false;
2061
	}
2062
2063
	/**
2064
	 * Return endpoint response
2065
	 *
2066
	 * @param ... determined by ->$path
2067
	 *
2068
	 * @return
2069
	 *  falsy: HTTP 500, no response body
2070
	 *  WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2071
	 *  $data: HTTP 200, json_encode( $data ) response body
2072
	 */
2073
	abstract function callback( $path = '' );
2074
2075
2076
}
2077
2078
require_once dirname( __FILE__ ) . '/json-endpoints.php';
2079