Completed
Push — update/editorconfig ( d39bf6 )
by Jeremy
16:06 queued 07:59
created

class.json-api-endpoints.php (30 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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