Completed
Push — update/wordads-reinstate-secti... ( 18a2e8...ec223d )
by
unknown
12:00 queued 01:12
created

WPCOM_JSON_API_Endpoint   D

Complexity

Total Complexity 342

Size/Duplication

Total Lines 2044
Duplicated Lines 13.06 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 267
loc 2044
rs 4.4102
c 0
b 0
f 0
wmc 342
lcom 1
cbo 6

36 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 10 102 6
A query_args() 0 9 2
C input() 0 57 19
A get_secure_body() 0 12 2
C cast_and_filter() 0 61 16
D cast_and_filter_item() 52 365 62
B parse_types() 0 20 5
A is_publicly_documentable() 0 3 2
D document() 0 116 11
A add_http_build_query_to_php_content_example() 0 7 1
A generate_doc_description() 0 18 3
C generate_documentation() 0 81 14
C user_can_view_post() 29 64 17
D get_author() 0 112 20
C get_media_item() 0 33 7
F get_media_item_v1_1() 30 151 30
A get_taxonomy() 0 10 3
C format_taxonomy() 0 38 7
A format_date() 0 3 1
C parse_date() 0 42 7
C load_theme_functions() 0 72 10
D copy_hooks() 0 37 9
B get_reflection() 0 21 9
B current_user_can_access_post_type() 15 15 5
A _get_whitelisted_post_types() 16 16 1
D handle_media_creation_v1_1() 40 124 29
C handle_media_sideload() 0 42 12
B is_file_supported_for_sideloading() 48 48 4
C allow_video_uploads() 0 60 12
A is_current_site_multi_user() 0 12 2
A allows_cross_origin_requests() 0 3 2
A allows_unauthorized_requests() 0 3 3
A get_platform() 0 3 1
A force_wpcom_request() 0 3 1
callback() 0 1 ?
C is_post_type_allowed() 27 27 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
2
3
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'];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 2 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
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
Bug introduced by
The property force does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

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

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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
Bug introduced by
The property deprecated does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

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

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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'];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 5 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
197
198
		$this->required_scope = $args['required_scope'];
0 ignored issues
show
Bug introduced by
The property required_scope does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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() ) {
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 :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
283
			wp_parse_str( $input, $return );
0 ignored issues
show
Bug introduced by
The variable $return seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

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

Let’s take a look at a simple example:

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

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

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

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

Loading history...
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
Bug introduced by
The variable $return does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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
Documentation introduced by
array('secure_key' => $secure_key) is of type array<string,?,{"secure_key":"?"}>, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
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
			$return[$key] = (string) esc_url_raw( $value );
400
			break;
401
		case 'string' :
402
			// Fallback string -> array, or for string -> object
403
			if ( is_array( $value ) || is_object( $value ) ) {
404
				if ( !empty( $types[0] ) ) {
405
					$next_type = array_shift( $types );
406
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
407
				}
408
			}
409
410
			// Fallback string -> false
411 View Code Duplication
			if ( !is_string( $value ) ) {
412
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
413
					$next_type = array_shift( $types );
414
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
415
				}
416
			}
417
			$return[$key] = (string) $value;
418
			break;
419
		case 'html' :
420
			$return[$key] = (string) $value;
421
			break;
422
		case 'safehtml' :
423
			$return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() );
424
			break;
425
		case 'zip' :
426
		case 'media' :
427
			if ( is_array( $value ) ) {
428
				if ( isset( $value['name'] ) && is_array( $value['name'] ) ) {
429
					// It's a $_FILES array
430
					// Reformat into array of $_FILES items
431
					$files = array();
432
433
					foreach ( $value['name'] as $k => $v ) {
434
						$files[$k] = array();
435
						foreach ( array_keys( $value ) as $file_key ) {
436
							$files[$k][$file_key] = $value[$file_key][$k];
437
						}
438
					}
439
440
					$return[$key] = $files;
441
					break;
442
				}
443
			} else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
444
				// no break - treat as 'array'
445
			}
446
			// nobreak
447
		case 'array' :
448
			// Fallback array -> string
449
			if ( is_string( $value ) ) {
450
				if ( !empty( $types[0] ) ) {
451
					$next_type = array_shift( $types );
452
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
453
				}
454
			}
455
456 View Code Duplication
			if ( isset( $type['children'] ) ) {
457
				$children = array();
458
				foreach ( (array) $value as $k => $child ) {
459
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
460
				}
461
				$return[$key] = (array) $children;
462
				break;
463
			}
464
465
			$return[$key] = (array) $value;
466
			break;
467
		case 'iso 8601 datetime' :
468
		case 'datetime' :
469
			// (string)s
470
			$dates = $this->parse_date( (string) $value );
471
			if ( $for_output ) {
472
				$return[$key] = $this->format_date( $dates[1], $dates[0] );
473
			} else {
474
				list( $return[$key], $return["{$key}_gmt"] ) = $dates;
475
			}
476
			break;
477
		case 'float' :
478
			$return[$key] = (float) $value;
479
			break;
480
		case 'int' :
481
		case 'integer' :
482
			$return[$key] = (int) $value;
483
			break;
484
		case 'bool' :
485
		case 'boolean' :
486
			$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value );
487
			break;
488
		case 'object' :
489
			// Fallback object -> false
490 View Code Duplication
			if ( is_scalar( $value ) || is_null( $value ) ) {
491
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
492
					return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
493
				}
494
			}
495
496 View Code Duplication
			if ( isset( $type['children'] ) ) {
497
				$children = array();
498
				foreach ( (array) $value as $k => $child ) {
499
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
500
				}
501
				$return[$key] = (object) $children;
502
				break;
503
			}
504
505
			if ( isset( $type['subtype'] ) ) {
506
				return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
507
			}
508
509
			$return[$key] = (object) $value;
510
			break;
511
		case 'post' :
512
			$return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
513
			break;
514
		case 'comment' :
515
			$return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
516
			break;
517
		case 'tag' :
518
		case 'category' :
519
			$docs = array(
520
				'ID'          => '(int)',
521
				'name'        => '(string)',
522
				'slug'        => '(string)',
523
				'description' => '(HTML)',
524
				'post_count'  => '(int)',
525
				'meta'        => '(object)',
526
			);
527
			if ( 'category' === $type['type'] ) {
528
				$docs['parent'] = '(int)';
529
			}
530
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
531
			break;
532
		case 'post_reference' :
533 View Code Duplication
		case 'comment_reference' :
534
			$docs = array(
535
				'ID'    => '(int)',
536
				'type'  => '(string)',
537
				'title' => '(string)',
538
				'link'  => '(URL)',
539
			);
540
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
541
			break;
542 View Code Duplication
		case 'geo' :
543
			$docs = array(
544
				'latitude'  => '(float)',
545
				'longitude' => '(float)',
546
				'address'   => '(string)',
547
			);
548
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
549
			break;
550
		case 'author' :
551
			$docs = array(
552
				'ID'             => '(int)',
553
				'user_login'     => '(string)',
554
				'login'          => '(string)',
555
				'email'          => '(string|false)',
556
				'name'           => '(string)',
557
				'first_name'     => '(string)',
558
				'last_name'      => '(string)',
559
				'nice_name'      => '(string)',
560
				'URL'            => '(URL)',
561
				'avatar_URL'     => '(URL)',
562
				'profile_URL'    => '(URL)',
563
				'is_super_admin' => '(bool)',
564
				'roles'          => '(array:string)',
565
				'ip_address'     => '(string|false)',
566
			);
567
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
568
			break;
569 View Code Duplication
		case 'role' :
570
			$docs = array(
571
				'name'         => '(string)',
572
				'display_name' => '(string)',
573
				'capabilities' => '(object:boolean)',
574
			);
575
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
576
			break;
577
		case 'attachment' :
578
			$docs = array(
579
				'ID'        => '(int)',
580
				'URL'       => '(URL)',
581
				'guid'      => '(string)',
582
				'mime_type' => '(string)',
583
				'width'     => '(int)',
584
				'height'    => '(int)',
585
				'duration'  => '(int)',
586
			);
587
			$return[$key] = (object) $this->cast_and_filter(
588
				$value,
589
				/**
590
				 * Filter the documentation returned for a post attachment.
591
				 *
592
				 * @module json-api
593
				 *
594
				 * @since 1.9.0
595
				 *
596
				 * @param array $docs Array of documentation about a post attachment.
597
				 */
598
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
599
				false,
600
				$for_output
601
			);
602
			break;
603
		case 'metadata' :
604
			$docs = array(
605
				'id'       => '(int)',
606
				'key'       => '(string)',
607
				'value'     => '(string|false|float|int|array|object)',
608
				'previous_value' => '(string)',
609
				'operation'  => '(string)',
610
			);
611
			$return[$key] = (object) $this->cast_and_filter(
612
				$value,
613
				/** This filter is documented in class.json-api-endpoints.php */
614
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
615
				false,
616
				$for_output
617
			);
618
			break;
619
		case 'plugin' :
620
			$docs = array(
621
				'id'            => '(safehtml) The plugin\'s ID',
622
				'slug'          => '(safehtml) The plugin\'s Slug',
623
				'active'        => '(boolean)  The plugin status.',
624
				'update'        => '(object)   The plugin update info.',
625
				'name'          => '(safehtml) The name of the plugin.',
626
				'plugin_url'    => '(url)      Link to the plugin\'s web site.',
627
				'version'       => '(safehtml) The plugin version number.',
628
				'description'   => '(safehtml) Description of what the plugin does and/or notes from the author',
629
				'author'        => '(safehtml) The plugin author\'s name',
630
				'author_url'    => '(url)      The plugin author web site address',
631
				'network'       => '(boolean)  Whether the plugin can only be activated network wide.',
632
				'autoupdate'    => '(boolean)  Whether the plugin is auto updated',
633
				'log'           => '(array:safehtml) An array of update log strings.',
634
        		'action_links'  => '(array) An array of action links that the plugin uses.',
635
			);
636
			$return[$key] = (object) $this->cast_and_filter(
637
				$value,
638
				/**
639
				 * Filter the documentation returned for a plugin.
640
				 *
641
				 * @module json-api
642
				 *
643
				 * @since 3.1.0
644
				 *
645
				 * @param array $docs Array of documentation about a plugin.
646
				 */
647
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
648
				false,
649
				$for_output
650
			);
651
			break;
652
		case 'plugin_v1_2' :
653
			$docs = Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2;
0 ignored issues
show
Bug introduced by
The property _response_format_v1_2 cannot be accessed from this context as it is declared private in class Jetpack_JSON_API_Plugins_Endpoint.

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

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

Loading history...
654
			$return[$key] = (object) $this->cast_and_filter(
655
				$value,
656
				/**
657
				 * Filter the documentation returned for a plugin.
658
				 *
659
				 * @module json-api
660
				 *
661
				 * @since 3.1.0
662
				 *
663
				 * @param array $docs Array of documentation about a plugin.
664
				 */
665
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
666
				false,
667
				$for_output
668
			);
669
			break;
670
		case 'file_mod_capabilities':
671
			$docs           = array(
672
				'reasons_modify_files_unavailable' => '(array) The reasons why files can\'t be modified',
673
				'reasons_autoupdate_unavailable'   => '(array) The reasons why autoupdates aren\'t allowed',
674
				'modify_files'                     => '(boolean) true if files can be modified',
675
				'autoupdate_files'                 => '(boolean) true if autoupdates are allowed',
676
			);
677
			$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
678
			break;
679
		case 'jetpackmodule' :
680
			$docs = array(
681
				'id'          => '(string)   The module\'s ID',
682
				'active'      => '(boolean)  The module\'s status.',
683
				'name'        => '(string)   The module\'s name.',
684
				'description' => '(safehtml) The module\'s description.',
685
				'sort'        => '(int)      The module\'s display order.',
686
				'introduced'  => '(string)   The Jetpack version when the module was introduced.',
687
				'changed'     => '(string)   The Jetpack version when the module was changed.',
688
				'free'        => '(boolean)  The module\'s Free or Paid status.',
689
				'module_tags' => '(array)    The module\'s tags.',
690
				'override'    => '(string)   The module\'s override. Empty if no override, otherwise \'active\' or \'inactive\'',
691
			);
692
			$return[$key] = (object) $this->cast_and_filter(
693
				$value,
694
				/** This filter is documented in class.json-api-endpoints.php */
695
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
696
				false,
697
				$for_output
698
			);
699
			break;
700
		case 'sharing_button' :
701
			$docs = array(
702
				'ID'         => '(string)',
703
				'name'       => '(string)',
704
				'URL'        => '(string)',
705
				'icon'       => '(string)',
706
				'enabled'    => '(bool)',
707
				'visibility' => '(string)',
708
			);
709
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
710
			break;
711
		case 'sharing_button_service':
712
			$docs = array(
713
				'ID'               => '(string) The service identifier',
714
				'name'             => '(string) The service name',
715
				'class_name'       => '(string) Class name for custom style sharing button elements',
716
				'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
717
				'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
718
				'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview'
719
			);
720
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
721
			break;
722
		case 'taxonomy':
723
			$docs = array(
724
				'name'         => '(string) The taxonomy slug',
725
				'label'        => '(string) The taxonomy human-readable name',
726
				'labels'       => '(object) Mapping of labels for the taxonomy',
727
				'description'  => '(string) The taxonomy description',
728
				'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
729
				'public'       => '(bool) Whether the taxonomy is public',
730
				'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
731
			);
732
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
733
			break;
734
735
		default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
736
			$method_name = $type['type'] . '_docs';
737
			if ( method_exists( WPCOM_JSON_API_Jetpack_Overrides, $method_name ) ) {
738
				$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
739
			}
740
741
			if ( ! empty( $docs ) ) {
742
				$return[$key] = (object) $this->cast_and_filter(
743
					$value,
744
					/** This filter is documented in class.json-api-endpoints.php */
745
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
746
					false,
747
					$for_output
748
				);
749
			} else {
750
				trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
751
			}
752
		}
753
	}
754
755
	function parse_types( $text ) {
756
		if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
757
			return 'none';
758
		}
759
760
		$types = explode( '|', strtolower( $matches[1] ) );
761
		$return = array();
762
		foreach ( $types as $type ) {
763
			foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
764
				if ( false !== strpos( $type, $operator ) ) {
765
					$item = explode( $operator, $type, 2 );
766
					$return[] = array( 'type' => $item[0], $meaning => $item[1] );
767
					continue 2;
768
				}
769
			}
770
			$return[] = compact( 'type' );
771
		}
772
773
		return $return;
774
	}
775
776
	/**
777
	 * Checks if the endpoint is publicly displayable
778
	 */
779
	function is_publicly_documentable() {
780
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
781
	}
782
783
	/**
784
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
785
	 * Echoes HTML.
786
	 */
787
	function document( $show_description = true ) {
788
		global $wpdb;
789
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
790
		unset( $GLOBALS['post'] );
791
792
		$doc = $this->generate_documentation();
793
794
		if ( $show_description ) :
795
?>
796
<caption>
797
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
798
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
799
</caption>
800
801
<?php endif; ?>
802
803
<?php if ( true === $this->deprecated ) { ?>
804
<p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $this->new_version ); ?></strong></p>
0 ignored issues
show
Bug introduced by
The property new_version does not seem to exist. Did you mean version?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
805
<?php } ?>
806
807
<section class="resource-info">
808
	<h2 id="apidoc-resource-info">Resource Information</h2>
809
810
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
811
812
	<thead>
813
		<tr>
814
			<th class="api-index-title" scope="column">&nbsp;</th>
815
			<th class="api-index-title" scope="column">&nbsp;</th>
816
		</tr>
817
	</thead>
818
	<tbody>
819
820
		<tr class="api-index-item">
821
			<th scope="row" class="parameter api-index-item-title">Method</th>
822
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
823
		</tr>
824
825
		<tr class="api-index-item">
826
			<th scope="row" class="parameter api-index-item-title">URL</th>
827
			<?php
828
			$version = WPCOM_JSON_API__CURRENT_VERSION;
829
			if ( !empty( $this->max_version ) ) {
830
				$version = $this->max_version;
831
			}
832
			?>
833
			<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>
834
		</tr>
835
836
		<tr class="api-index-item">
837
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
838
			<?php
839
			$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'] ) );
840
			?>
841
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
842
		</tr>
843
844
	</tbody>
845
	</table>
846
847
</section>
848
849
<?php
850
851
		foreach ( array(
852
			'path'     => 'Method Parameters',
853
			'query'    => 'Query Parameters',
854
			'body'     => 'Request Parameters',
855
			'response' => 'Response Parameters',
856
		) as $doc_section_key => $label ) :
857
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
858
			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...
859
				continue;
860
			}
861
862
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
863
?>
864
865
<section class="<?php echo $param_label; ?>">
866
867
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
868
869
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
870
871
<thead>
872
	<tr>
873
		<th class="api-index-title" scope="column">Parameter</th>
874
		<th class="api-index-title" scope="column">Type</th>
875
		<th class="api-index-title" scope="column">Description</th>
876
	</tr>
877
</thead>
878
<tbody>
879
880
<?php foreach ( $doc_section as $key => $item ) : ?>
881
882
	<tr class="api-index-item">
883
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
884
		<td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
885
		<td class="description api-index-item-body"><?php
886
887
		$this->generate_doc_description( $item['description'] );
888
889
		?></td>
890
	</tr>
891
892
<?php endforeach; ?>
893
</tbody>
894
</table>
895
</section>
896
<?php endforeach; ?>
897
898
<?php
899
		if ( 'unset' !== $original_post ) {
900
			$GLOBALS['post'] = $original_post;
901
		}
902
	}
903
904
	function add_http_build_query_to_php_content_example( $matches ) {
905
		$trimmed_match = ltrim( $matches[0] );
906
		$pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
907
		$pad = ltrim( $pad, ' ' );
908
		$return = '  ' . str_replace( "\n", "\n  ", $matches[0] );
909
		return " http_build_query({$return}{$pad})";
910
	}
911
912
	/**
913
	 * Recursively generates the <dl>'s to document item descriptions.
914
	 * Echoes HTML.
915
	 */
916
	function generate_doc_description( $item ) {
917
		if ( is_array( $item ) ) : ?>
918
919
		<dl>
920
<?php			foreach ( $item as $description_key => $description_value ) : ?>
921
922
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
923
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
924
925
<?php			endforeach; ?>
926
927
		</dl>
928
929
<?php
930
		else :
931
			echo wp_kses_post( $item );
932
		endif;
933
	}
934
935
	/**
936
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
937
	 * Echoes HTML.
938
	 */
939
	function generate_documentation() {
940
		$format       = str_replace( '%d', '%s', $this->path );
941
		$path_labeled = $format;
942
		if ( ! empty( $this->path_labels ) ) {
943
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
944
		}
945
		$boolean_arg  = array( 'false', 'true' );
946
		$naeloob_arg  = array( 'true', 'false' );
947
948
		$doc = array(
949
			'description'  => $this->description,
950
			'method'       => $this->method,
951
			'path_format'  => $this->path,
952
			'path_labeled' => $path_labeled,
953
			'group'        => $this->group,
954
			'request' => array(
955
				'path'  => array(),
956
				'query' => array(),
957
				'body'  => array(),
958
			),
959
			'response' => array(
960
				'body' => array(),
961
			)
962
		);
963
964
		foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
965
			foreach ( (array) $this->$_property as $key => $description ) {
966
				if ( is_array( $description ) ) {
967
					$description_keys = array_keys( $description );
968
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
969
						$type = '(bool)';
970
					} else {
971
						$type = '(string)';
972
					}
973
974
					if ( 'response_format' !== $_property ) {
975
						// hack - don't show "(default)" in response format
976
						reset( $description );
977
						$description_key = key( $description );
978
						$description[$description_key] = "(default) {$description[$description_key]}";
979
					}
980
				} else {
981
					$types   = $this->parse_types( $description );
982
					$type    = array();
983
					$default = '';
984
985
					if ( 'none' == $types ) {
986
						$types = array();
987
						$types[]['type'] = 'none';
988
					}
989
990
					foreach ( $types as $type_array ) {
0 ignored issues
show
Bug introduced by
The expression $types of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
991
						$type[] = $type_array['type'];
992
						if ( isset( $type_array['default'] ) ) {
993
							$default = $type_array['default'];
994
							if ( 'string' === $type_array['type'] ) {
995
								$default = "'$default'";
996
							}
997
						}
998
					}
999
					$type = '(' . join( '|', $type ) . ')';
1000
					$noop = ''; // skip an index in list below
0 ignored issues
show
Unused Code introduced by
$noop is not used, you could remove the assignment.

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

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

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

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

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

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1002
					$description = trim( $description );
1003
					if ( $default ) {
1004
						$description .= " Default: $default.";
1005
					}
1006
				}
1007
1008
				$item = compact( 'type', 'description' );
1009
1010
				if ( 'response_format' === $_property ) {
1011
					$doc['response'][$doc_item][$key] = $item;
1012
				} else {
1013
					$doc['request'][$doc_item][$key] = $item;
1014
				}
1015
			}
1016
		}
1017
1018
		return $doc;
1019
	}
1020
1021
	function user_can_view_post( $post_id ) {
1022
		$post = get_post( $post_id );
1023
		if ( !$post || is_wp_error( $post ) ) {
1024
			return false;
1025
		}
1026
1027 View Code Duplication
		if ( 'inherit' === $post->post_status ) {
1028
			$parent_post = get_post( $post->post_parent );
1029
			$post_status_obj = get_post_status_object( $parent_post->post_status );
1030
		} else {
1031
			$post_status_obj = get_post_status_object( $post->post_status );
1032
		}
1033
1034
		if ( !$post_status_obj->public ) {
1035
			if ( is_user_logged_in() ) {
1036
				if ( $post_status_obj->protected ) {
1037
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1038
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1039
					}
1040
				} elseif ( $post_status_obj->private ) {
1041
					if ( !current_user_can( 'read_post', $post->ID ) ) {
1042
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1043
					}
1044
				} elseif ( in_array( $post->post_status, array( 'inherit', 'trash' ) ) ) {
1045
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1046
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1047
					}
1048
				} elseif ( 'auto-draft' === $post->post_status ) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
1049
					//allow auto-drafts
1050
				} else {
1051
					return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1052
				}
1053
			} else {
1054
				return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1055
			}
1056
		}
1057
1058 View Code Duplication
		if (
1059
			-1 == get_option( 'blog_public' ) &&
1060
			/**
1061
			 * Filter access to a specific post.
1062
			 *
1063
			 * @module json-api
1064
			 *
1065
			 * @since 3.4.0
1066
			 *
1067
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1068
			 * @param WP_Post $post Post data.
1069
			 */
1070
			! apply_filters(
1071
				'wpcom_json_api_user_can_view_post',
1072
				current_user_can( 'read_post', $post->ID ),
1073
				$post
1074
			)
1075
		) {
1076
			return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
1077
		}
1078
1079 View Code Duplication
		if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
1080
			return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
1081
		}
1082
1083
		return true;
1084
	}
1085
1086
	/**
1087
	 * Returns author object.
1088
	 *
1089
	 * @param object $author user ID, user row, WP_User object, comment row, post row
1090
	 * @param bool $show_email_and_ip output the author's email address and IP address?
1091
	 *
1092
	 * @return object
1093
	 */
1094
	function get_author( $author, $show_email_and_ip = false ) {
1095
		$ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1096
1097
		if ( isset( $author->comment_author_email ) ) {
1098
			$ID          = 0;
1099
			$login       = '';
1100
			$email       = $author->comment_author_email;
1101
			$name        = $author->comment_author;
1102
			$first_name  = '';
1103
			$last_name   = '';
1104
			$URL         = $author->comment_author_url;
1105
			$avatar_URL  = $this->api->get_avatar_url( $author );
1106
			$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1107
			$nice        = '';
1108
			$site_id     = -1;
1109
1110
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1111
			// "&" is the only email/URL character altered by wp_kses()
1112
			foreach ( array( 'email', 'URL' ) as $field ) {
1113
				$$field = str_replace( '&amp;', '&', $$field );
1114
			}
1115
		} else {
1116
			if ( isset( $author->user_id ) && $author->user_id ) {
1117
				$author = $author->user_id;
1118
			} elseif ( isset( $author->user_email ) ) {
1119
				$author = $author->ID;
1120
			} elseif ( isset( $author->post_author ) ) {
1121
				// then $author is a Post Object.
1122
				if ( 0 == $author->post_author )
1123
					return null;
1124
				/**
1125
				 * Filter whether the current site is a Jetpack site.
1126
				 *
1127
				 * @module json-api
1128
				 *
1129
				 * @since 3.3.0
1130
				 *
1131
				 * @param bool false Is the current site a Jetpack site. Default to false.
1132
				 * @param int get_current_blog_id() Blog ID.
1133
				 */
1134
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1135
				$post_id = $author->ID;
1136
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1137
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1138
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1139
					$login      = '';
1140
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1141
					$first_name = '';
1142
					$last_name  = '';
1143
					$URL        = '';
1144
					$nice       = '';
1145
				} else {
1146
					$author = $author->post_author;
1147
				}
1148
			}
1149
1150
			if ( ! isset( $ID ) ) {
1151
				$user = get_user_by( 'id', $author );
1152
				if ( ! $user || is_wp_error( $user ) ) {
1153
					trigger_error( 'Unknown user', E_USER_WARNING );
1154
1155
					return null;
1156
				}
1157
				$ID         = $user->ID;
1158
				$email      = $user->user_email;
1159
				$login      = $user->user_login;
1160
				$name       = $user->display_name;
1161
				$first_name = $user->first_name;
1162
				$last_name  = $user->last_name;
1163
				$URL        = $user->user_url;
1164
				$nice       = $user->user_nicename;
1165
			}
1166
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
0 ignored issues
show
Bug introduced by
The variable $is_jetpack does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1167
				$active_blog = get_active_blog_for_user( $ID );
1168
				$site_id     = $active_blog->blog_id;
1169
				$profile_URL = "https://en.gravatar.com/{$login}";
0 ignored issues
show
Bug introduced by
The variable $login does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1170
			} else {
1171
				$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
0 ignored issues
show
Bug introduced by
The variable $email does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1172
				$site_id     = -1;
1173
			}
1174
1175
			$avatar_URL = $this->api->get_avatar_url( $email );
1176
		}
1177
1178
		if ( $show_email_and_ip ) {
1179
			$email = (string) $email;
1180
			$ip_address = (string) $ip_address;
1181
		} else {
1182
			$email = false;
1183
			$ip_address = false;
1184
		}
1185
1186
		$author = array(
1187
			'ID'          => (int) $ID,
1188
			'login'       => (string) $login,
1189
			'email'       => $email, // (string|bool)
1190
			'name'        => (string) $name,
0 ignored issues
show
Bug introduced by
The variable $name does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1191
			'first_name'  => (string) $first_name,
0 ignored issues
show
Bug introduced by
The variable $first_name does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1192
			'last_name'   => (string) $last_name,
0 ignored issues
show
Bug introduced by
The variable $last_name does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1193
			'nice_name'   => (string) $nice,
0 ignored issues
show
Bug introduced by
The variable $nice does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1194
			'URL'         => (string) esc_url_raw( $URL ),
0 ignored issues
show
Bug introduced by
The variable $URL does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1195
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1196
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1197
			'ip_address'  => $ip_address, // (string|bool)
1198
		);
1199
1200
		if ($site_id > -1) {
1201
			$author['site_ID'] = (int) $site_id;
1202
		}
1203
1204
		return (object) $author;
1205
	}
1206
1207
	function get_media_item( $media_id ) {
1208
		$media_item = get_post( $media_id );
1209
1210
		if ( !$media_item || is_wp_error( $media_item ) )
1211
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1212
1213
		$response = array(
1214
			'id'    => strval( $media_item->ID ),
1215
			'date' =>  (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1216
			'parent'           => $media_item->post_parent,
1217
			'link'             => wp_get_attachment_url( $media_item->ID ),
1218
			'title'            => $media_item->post_title,
1219
			'caption'          => $media_item->post_excerpt,
1220
			'description'      => $media_item->post_content,
1221
			'metadata'         => wp_get_attachment_metadata( $media_item->ID ),
1222
		);
1223
1224
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1225
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1226
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1227
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1228
		}
1229
1230
		$response['meta'] = (object) array(
1231
			'links' => (object) array(
1232
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1233
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1234
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1235
			),
1236
		);
1237
1238
		return (object) $response;
1239
	}
1240
1241
	function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1242
1243
		if ( ! $media_item ) {
1244
			$media_item = get_post( $media_id );
1245
		}
1246
1247
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1248
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1249
		}
1250
1251
		$attachment_file = get_attached_file( $media_item->ID );
1252
1253
		$file = basename( $attachment_file ? $attachment_file : $file );
1254
		$file_info = pathinfo( $file );
1255
		$ext  = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1256
1257
		$response = array(
1258
			'ID'           => $media_item->ID,
1259
			'URL'          => wp_get_attachment_url( $media_item->ID ),
1260
			'guid'         => $media_item->guid,
1261
			'date'         => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1262
			'post_ID'      => $media_item->post_parent,
1263
			'author_ID'    => (int) $media_item->post_author,
1264
			'file'         => $file,
1265
			'mime_type'    => $media_item->post_mime_type,
1266
			'extension'    => $ext,
1267
			'title'        => $media_item->post_title,
1268
			'caption'      => $media_item->post_excerpt,
1269
			'description'  => $media_item->post_content,
1270
			'alt'          => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1271
			'icon'         => wp_mime_type_icon( $media_item->ID ),
1272
			'thumbnails'   => array()
1273
		);
1274
1275 View Code Duplication
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1276
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1277
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1278
				$response['height'] = $metadata['height'];
1279
				$response['width'] = $metadata['width'];
1280
			}
1281
1282
			if ( isset( $metadata['sizes'] ) ) {
1283
				/**
1284
				 * Filter the thumbnail sizes available for each attachment ID.
1285
				 *
1286
				 * @module json-api
1287
				 *
1288
				 * @since 3.9.0
1289
				 *
1290
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1291
				 * @param string $media_id Attachment ID.
1292
				 */
1293
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
1294
				if ( is_array( $sizes ) ) {
1295
					foreach ( $sizes as $size => $size_details ) {
1296
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1297
					}
1298
				}
1299
			}
1300
1301
			if ( isset( $metadata['image_meta'] ) ) {
1302
				$response['exif'] = $metadata['image_meta'];
1303
			}
1304
		}
1305
1306
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1307
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1308
			$response['length'] = $metadata['length'];
1309
			$response['exif']   = $metadata;
1310
		}
1311
1312
		$is_video = false;
1313
1314
		if (
1315
			in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1316
			||
1317
			$response['mime_type'] === 'video/videopress'
1318
		) {
1319
			$is_video = true;
1320
		}
1321
1322
1323
		if ( $is_video ) {
1324
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1325
1326
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1327
				$response['height'] = $metadata['height'];
1328
				$response['width']  = $metadata['width'];
1329
			}
1330
1331
			if ( isset( $metadata['length'] ) ) {
1332
				$response['length'] = $metadata['length'];
1333
			}
1334
1335
			// add VideoPress info
1336
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1337
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1338
1339
				// If we failed to get VideoPress info, but it exists in the meta data (for some reason)
1340
				// then let's use that.
1341
				if ( false === $info && isset( $metadata['videopress'] ) ) {
1342
				    $info = (object) $metadata['videopress'];
1343
				}
1344
1345
				// Thumbnails
1346
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1347
					$response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' );
1348
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1349
						if ( video_format_done( $info, $size ) ) {
1350
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1351
						} else {
1352
							unset( $response['thumbnails'][ $size ] );
1353
						}
1354
					}
1355
				}
1356
1357
				// If we didn't get VideoPress information (for some reason) then let's
1358
				// not try and include it in the response.
1359
				if ( isset( $info->guid ) ) {
1360
					$response['videopress_guid']            = $info->guid;
1361
					$response['videopress_processing_done'] = true;
1362
					if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1363
						$response['videopress_processing_done'] = false;
1364
					}
1365
				}
1366
			}
1367
		}
1368
1369
		$response['thumbnails'] = (object) $response['thumbnails'];
1370
1371
		$response['meta'] = (object) array(
1372
			'links' => (object) array(
1373
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1374
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1375
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1376
			),
1377
		);
1378
1379
		// add VideoPress link to the meta
1380
		if ( isset ( $response['videopress_guid'] ) ) {
1381
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1382
				$response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1383
			}
1384
		}
1385
1386
		if ( $media_item->post_parent > 0 ) {
1387
			$response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1388
		}
1389
1390
		return (object) $response;
1391
	}
1392
1393
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1394
1395
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1396
		/// keep updating this function
1397
		if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1398
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1399
		}
1400
1401
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1402
	}
1403
1404
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1405
		// Permissions
1406
		switch ( $context ) {
1407
		case 'edit' :
1408
			$tax = get_taxonomy( $taxonomy_type );
1409
			if ( !current_user_can( $tax->cap->edit_terms ) )
1410
				return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1411
			break;
1412
		case 'display' :
1413
			if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1414
				return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1415
			}
1416
			break;
1417
		default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1418
			return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1419
		}
1420
1421
		$response                = array();
1422
		$response['ID']          = (int) $taxonomy->term_id;
1423
		$response['name']        = (string) $taxonomy->name;
1424
		$response['slug']        = (string) $taxonomy->slug;
1425
		$response['description'] = (string) $taxonomy->description;
1426
		$response['post_count']  = (int) $taxonomy->count;
1427
1428
		if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1429
			$response['parent'] = (int) $taxonomy->parent;
1430
		}
1431
1432
		$response['meta'] = (object) array(
1433
			'links' => (object) array(
1434
				'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1435
				'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1436
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1437
			),
1438
		);
1439
1440
		return (object) $response;
1441
	}
1442
1443
	/**
1444
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1445
	 *
1446
	 * @param $date_gmt (string) GMT datetime string.
1447
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1448
	 *
1449
	 * @return string
1450
	 */
1451
	function format_date( $date_gmt, $date = null ) {
1452
		return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1453
	}
1454
1455
	/**
1456
	 * Parses a date string and returns the local and GMT representations
1457
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1458
	 * timezones or offsets. If the parsed datetime was not localized to a
1459
	 * particular timezone or offset we will assume it was given in GMT
1460
	 * relative to now and will convert it to local time using either the
1461
	 * timezone set in the options table for the blog or the GMT offset.
1462
	 *
1463
	 * @param datetime string
1464
	 *
1465
	 * @return array( $local_time_string, $gmt_time_string )
0 ignored issues
show
Documentation introduced by
The doc-type array( could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

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

Loading history...
1466
	 */
1467
	function parse_date( $date_string ) {
1468
		$date_string_info = date_parse( $date_string );
1469
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1470
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1471
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1472
				$dt_local = clone $dt_utc = new DateTime( $date_string );
1473
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1474
				return array(
1475
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1476
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1477
				);
1478
			}
1479
1480
			// It's parseable but no TZ info so assume UTC
1481
			$dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1482
		} else {
1483
			// Could not parse time, use now in UTC
1484
			$dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1485
		}
1486
1487
		// First try to use timezone as it's daylight savings aware.
1488
		$timezone_string = get_option( 'timezone_string' );
1489
		if ( $timezone_string ) {
1490
			$tz = timezone_open( $timezone_string );
1491
			if ( $tz ) {
1492
				$dt_local->setTimezone( $tz );
1493
				return array(
1494
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1495
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1496
				);
1497
			}
1498
		}
1499
1500
		// Fallback to GMT offset (in hours)
1501
		// NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1502
		$gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1503
		$dt_local->modify("+{$gmt_offset_seconds} seconds");
1504
		return array(
1505
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1506
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1507
		);
1508
	}
1509
1510
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1511
	function load_theme_functions() {
1512
		// bail if we've done this already (can happen when calling /batch endpoint)
1513
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) )
1514
			return;
1515
1516
		// VIP context loading is handled elsewhere, so bail to prevent
1517
		// duplicate loading. See `switch_to_blog_and_validate_user()`
1518
		if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1519
			return;
1520
		}
1521
1522
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1523
1524
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1525
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1526
1527
		$copy_dirs = array( get_template_directory() );
1528
1529
		// Is this a child theme? Load the child theme's functions file.
1530
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1531
			foreach ( $function_files as $function_file ) {
1532
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1533
					require_once(  get_stylesheet_directory() . $function_file );
1534
				}
1535
			}
1536
			$copy_dirs[] = get_stylesheet_directory();
1537
		}
1538
1539
		foreach ( $function_files as $function_file ) {
1540
			if ( file_exists( get_template_directory() . $function_file ) ) {
1541
				require_once(  get_template_directory() . $function_file );
1542
			}
1543
		}
1544
1545
		// add inc/wpcom.php and/or includes/wpcom.php
1546
		wpcom_load_theme_compat_file();
1547
1548
		// Enable including additional directories or files in actions to be copied
1549
		$copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1550
1551
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1552
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1553
1554
		/**
1555
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1556
		 *
1557
		 * The REST API does not load the theme when processing requests.
1558
		 * To enable theme-based functionality, the API will load the '/functions.php',
1559
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1560
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1561
		 *
1562
		 * @module json-api
1563
		 *
1564
		 * @since 3.2.0
1565
		 */
1566
		do_action( 'restapi_theme_after_setup_theme' );
1567
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1568
1569
		/**
1570
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1571
		 *
1572
		 * The REST API does not load the theme when processing requests.
1573
		 * To enable theme-based functionality, the API will load the '/functions.php',
1574
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1575
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1576
		 *
1577
		 * @module json-api
1578
		 *
1579
		 * @since 3.2.0
1580
		 */
1581
		do_action( 'restapi_theme_init' );
1582
	}
1583
1584
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1585
		global $wp_filter;
1586
		foreach ( $wp_filter as $hook => $actions ) {
1587
1588
			if ( $from_hook != $hook ) {
1589
				continue;
1590
			}
1591
			if ( ! has_action( $hook ) ) {
1592
				continue;
1593
			}
1594
1595
			foreach ( $actions as $priority => $callbacks ) {
1596
				foreach( $callbacks as $callback_key => $callback_data ) {
1597
					$callback = $callback_data['function'];
1598
1599
					// use reflection api to determine filename where function is defined
1600
					$reflection = $this->get_reflection( $callback );
1601
1602
					if ( false !== $reflection ) {
1603
						$file_name = $reflection->getFileName();
1604
						foreach( $base_paths as $base_path ) {
1605
1606
							// only copy hooks with functions which are part of the specified files
1607
							if ( 0 === strpos( $file_name, $base_path ) ) {
1608
								add_action(
1609
									$to_hook,
1610
									$callback_data['function'],
1611
									$priority,
1612
									$callback_data['accepted_args']
1613
								);
1614
							}
1615
						}
1616
					}
1617
				}
1618
			}
1619
		}
1620
	}
1621
1622
	function get_reflection( $callback ) {
1623
		if ( is_array( $callback ) ) {
1624
			list( $class, $method ) = $callback;
1625
			return new ReflectionMethod( $class, $method );
1626
		}
1627
1628
		if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) {
1629
			list( $class, $method ) = explode( "::", $callback );
1630
			return new ReflectionMethod( $class, $method );
1631
		}
1632
1633
		if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) {
1634
			return new ReflectionMethod( $callback, "__invoke" );
1635
		}
1636
1637
		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...
1638
			return new ReflectionFunction( $callback );
1639
		}
1640
1641
		return false;
1642
	}
1643
1644
	/**
1645
	* Check whether a user can view or edit a post type
1646
	* @param string $post_type              post type to check
1647
	* @param string $context                'display' or 'edit'
1648
	* @return bool
1649
	*/
1650 View Code Duplication
	function current_user_can_access_post_type( $post_type, $context='display' ) {
1651
		$post_type_object = get_post_type_object( $post_type );
1652
		if ( ! $post_type_object ) {
1653
			return false;
1654
		}
1655
1656
		switch( $context ) {
1657
			case 'edit':
1658
				return current_user_can( $post_type_object->cap->edit_posts );
1659
			case 'display':
1660
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1661
			default:
1662
				return false;
1663
		}
1664
	}
1665
1666 View Code Duplication
	function is_post_type_allowed( $post_type ) {
1667
		// if the post type is empty, that's fine, WordPress will default to post
1668
		if ( empty( $post_type ) ) {
1669
			return true;
1670
		}
1671
1672
		// allow special 'any' type
1673
		if ( 'any' == $post_type ) {
1674
			return true;
1675
		}
1676
1677
		// check for allowed types
1678
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1679
			return true;
1680
		}
1681
1682
		if ( $post_type_object = get_post_type_object( $post_type ) ) {
1683
			if ( ! empty( $post_type_object->show_in_rest ) ) {
1684
				return $post_type_object->show_in_rest;
1685
			}
1686
			if ( ! empty( $post_type_object->publicly_queryable ) ) {
1687
				return $post_type_object->publicly_queryable;
1688
			}
1689
		}
1690
1691
		return ! empty( $post_type_object->public );
1692
	}
1693
1694
	/**
1695
	 * Gets the whitelisted post types that JP should allow access to.
1696
	 *
1697
	 * @return array Whitelisted post types.
1698
	 */
1699 View Code Duplication
	protected function _get_whitelisted_post_types() {
1700
		$allowed_types = array( 'post', 'page', 'revision' );
1701
1702
		/**
1703
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1704
		 *
1705
		 * @module json-api
1706
		 *
1707
		 * @since 2.2.3
1708
		 *
1709
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1710
		 */
1711
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1712
1713
		return array_unique( $allowed_types );
1714
	}
1715
1716
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1717
1718
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1719
1720
		$media_ids = $errors = array();
1721
		$user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
0 ignored issues
show
Bug introduced by
The method is_authorized_with_upload_token() does not seem to exist on object<WPCOM_JSON_API>.

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

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

Loading history...
1722
		$media_attrs = array_values( $media_attrs ); // reset the keys
1723
		$i = 0;
1724
1725
		if ( ! empty( $media_files ) ) {
1726
			$this->api->trap_wp_die( 'upload_error' );
1727
			foreach ( $media_files as $media_item ) {
1728
				$_FILES['.api.media.item.'] = $media_item;
1729 View Code Duplication
				if ( ! $user_can_upload_files ) {
1730
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1731
				} else {
1732
					if ( $force_parent_id ) {
1733
						$parent_id = absint( $force_parent_id );
1734
					} elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1735
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1736
					} else {
1737
						$parent_id = 0;
1738
					}
1739
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1740
				}
1741
				if ( is_wp_error( $media_id ) ) {
1742
					$errors[$i]['file']   = $media_item['name'];
1743
					$errors[$i]['error']   = $media_id->get_error_code();
1744
					$errors[$i]['message'] = $media_id->get_error_message();
1745
				} else {
1746
					$media_ids[$i] = $media_id;
1747
				}
1748
1749
				$i++;
1750
			}
1751
			$this->api->trap_wp_die( null );
1752
			unset( $_FILES['.api.media.item.'] );
1753
		}
1754
1755
		if ( ! empty( $media_urls ) ) {
1756
			foreach ( $media_urls as $url ) {
1757 View Code Duplication
				if ( ! $user_can_upload_files ) {
1758
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1759
				} else {
1760
					if ( $force_parent_id ) {
1761
						$parent_id = absint( $force_parent_id );
1762
					} else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1763
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1764
					} else {
1765
						$parent_id = 0;
1766
					}
1767
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1768
				}
1769
				if ( is_wp_error( $media_id ) ) {
1770
					$errors[$i] = array(
1771
						'file'    => $url,
1772
						'error'   => $media_id->get_error_code(),
1773
						'message' => $media_id->get_error_message(),
1774
					);
1775
				} elseif ( ! empty( $media_id ) ) {
1776
					$media_ids[$i] = $media_id;
1777
				}
1778
1779
				$i++;
1780
			}
1781
		}
1782
1783
		if ( ! empty( $media_attrs ) ) {
1784
			foreach ( $media_ids as $index => $media_id ) {
1785
				if ( empty( $media_attrs[$index] ) )
1786
					continue;
1787
1788
				$attrs = $media_attrs[$index];
1789
				$insert = array();
1790
1791
				// Attributes: Title, Caption, Description
1792
1793
				if ( isset( $attrs['title'] ) ) {
1794
					$insert['post_title'] = $attrs['title'];
1795
				}
1796
1797
				if ( isset( $attrs['caption'] ) ) {
1798
					$insert['post_excerpt'] = $attrs['caption'];
1799
				}
1800
1801
				if ( isset( $attrs['description'] ) ) {
1802
					$insert['post_content'] = $attrs['description'];
1803
				}
1804
1805
				if ( ! empty( $insert ) ) {
1806
					$insert['ID'] = $media_id;
1807
					wp_update_post( (object) $insert );
1808
				}
1809
1810
				// Attributes: Alt
1811
1812 View Code Duplication
				if ( isset( $attrs['alt'] ) ) {
1813
					$alt = wp_strip_all_tags( $attrs['alt'], true );
1814
					update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1815
				}
1816
1817
				// Attributes: Artist, Album
1818
1819
				$id3_meta = array();
1820
1821 View Code Duplication
				foreach ( array( 'artist', 'album' ) as $key ) {
1822
					if ( isset( $attrs[ $key ] ) ) {
1823
						$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1824
					}
1825
				}
1826
1827 View Code Duplication
				if ( ! empty( $id3_meta ) ) {
1828
					// Before updating metadata, ensure that the item is audio
1829
					$item = $this->get_media_item_v1_1( $media_id );
1830
					if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1831
						wp_update_attachment_metadata( $media_id, $id3_meta );
1832
					}
1833
				}
1834
			}
1835
		}
1836
1837
		return array( 'media_ids' => $media_ids, 'errors' => $errors );
1838
1839
	}
1840
1841
	function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1842
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1843
			return false;
1844
1845
		// if we didn't get a URL, let's bail
1846
		$parsed = @parse_url( $url );
1847
		if ( empty( $parsed ) )
1848
			return false;
1849
1850
		$tmp = download_url( $url );
1851
		if ( is_wp_error( $tmp ) ) {
1852
			return $tmp;
1853
		}
1854
1855
		// First check to see if we get a mime-type match by file, otherwise, check to
1856
		// see if WordPress supports this file as an image. If neither, then it is not supported.
1857
		if ( ! $this->is_file_supported_for_sideloading( $tmp ) && 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1858
			@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...
1859
			return false;
1860
		}
1861
1862
		// emulate a $_FILES entry
1863
		$file_array = array(
1864
			'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1865
			'tmp_name' => $tmp,
1866
		);
1867
1868
		$id = media_handle_sideload( $file_array, $parent_post_id );
1869
		if ( file_exists( $tmp ) ) {
1870
			@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...
1871
		}
1872
1873
		if ( is_wp_error( $id ) ) {
1874
			return $id;
1875
		}
1876
1877
		if ( ! $id || ! is_int( $id ) ) {
1878
			return false;
1879
		}
1880
1881
		return $id;
1882
	}
1883
1884
	/**
1885
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1886
	 *
1887
	 * @param string $file Path to file to get its mime type.
1888
	 *
1889
	 * @return bool
1890
	 */
1891 View Code Duplication
	protected function is_file_supported_for_sideloading( $file ) {
1892
		if ( class_exists( 'finfo' ) ) { // php 5.3+
1893
			$finfo = new finfo( FILEINFO_MIME );
1894
			$mime = explode( '; ', $finfo->file( $file ) );
1895
			$type = $mime[0];
1896
1897
		} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
1898
			$type = mime_content_type( $file );
1899
1900
		} else {
1901
			return false;
1902
		}
1903
1904
		/**
1905
		 * Filter the list of supported mime types for media sideloading.
1906
		 *
1907
		 * @since 4.0.0
1908
		 *
1909
		 * @module json-api
1910
		 *
1911
		 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
1912
		 */
1913
		$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
1914
			'image/png',
1915
			'image/jpeg',
1916
			'image/gif',
1917
			'image/bmp',
1918
			'video/quicktime',
1919
			'video/mp4',
1920
			'video/mpeg',
1921
			'video/ogg',
1922
			'video/3gpp',
1923
			'video/3gpp2',
1924
			'video/h261',
1925
			'video/h262',
1926
			'video/h264',
1927
			'video/x-msvideo',
1928
			'video/x-ms-wmv',
1929
			'video/x-ms-asf',
1930
		) );
1931
1932
		// If the type returned was not an array as expected, then we know we don't have a match.
1933
		if ( ! is_array( $supported_mime_types ) ) {
1934
			return false;
1935
		}
1936
1937
		return in_array( $type, $supported_mime_types );
1938
	}
1939
1940
	function allow_video_uploads( $mimes ) {
1941
		// if we are on Jetpack, bail - Videos are already allowed
1942
		if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1943
			return $mimes;
1944
		}
1945
1946
		// extra check that this filter is only ever applied during REST API requests
1947
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1948
			return $mimes;
1949
		}
1950
1951
		// bail early if they already have the upgrade..
1952
		if ( get_option( 'video_upgrade' ) == '1' ) {
1953
			return $mimes;
1954
		}
1955
1956
		// lets whitelist to only specific clients right now
1957
		$clients_allowed_video_uploads = array();
1958
		/**
1959
		 * Filter the list of whitelisted video clients.
1960
		 *
1961
		 * @module json-api
1962
		 *
1963
		 * @since 3.2.0
1964
		 *
1965
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1966
		 */
1967
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1968
		if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1969
			return $mimes;
1970
		}
1971
1972
		$mime_list = wp_get_mime_types();
1973
1974
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1975
		/**
1976
		 * Filter the video filetypes allowed on the site.
1977
		 *
1978
		 * @module json-api
1979
		 *
1980
		 * @since 3.2.0
1981
		 *
1982
		 * @param array $video_exts Array of video filetypes allowed on the site.
1983
		 */
1984
		$video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
1985
		$video_mimes = array();
1986
1987
		if ( !empty( $video_exts ) ) {
1988
			foreach ( $video_exts as $ext ) {
1989
				foreach ( $mime_list as $ext_pattern => $mime ) {
1990
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
1991
						$video_mimes[$ext_pattern] = $mime;
1992
				}
1993
			}
1994
1995
			$mimes = array_merge( $mimes, $video_mimes );
1996
		}
1997
1998
		return $mimes;
1999
	}
2000
2001
	function is_current_site_multi_user() {
2002
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2003
		if ( false === $users ) {
2004
			$user_query = new WP_User_Query( array(
2005
				'blog_id' => get_current_blog_id(),
2006
				'fields'  => 'ID',
2007
			) );
2008
			$users = (int) $user_query->get_total();
2009
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2010
		}
2011
		return $users > 1;
2012
	}
2013
2014
	function allows_cross_origin_requests() {
2015
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2016
	}
2017
2018
	function allows_unauthorized_requests( $origin, $complete_access_origins  ) {
2019
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2020
	}
2021
2022
	function get_platform() {
2023
		return wpcom_get_sal_platform( $this->api->token_details );
2024
	}
2025
2026
	/**
2027
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2028
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
2029
	 *
2030
	 * Override this method if you want to do something different.
2031
	 *
2032
	 * @param  int  $blog_id
2033
	 * @return bool
2034
	 */
2035
	function force_wpcom_request( $blog_id ) {
2036
		return false;
2037
	}
2038
2039
	/**
2040
	 * Return endpoint response
2041
	 *
2042
	 * @param ... determined by ->$path
2043
	 *
2044
	 * @return
2045
	 * 	falsy: HTTP 500, no response body
2046
	 *	WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2047
	 *	$data: HTTP 200, json_encode( $data ) response body
2048
	 */
2049
	abstract function callback( $path = '' );
2050
2051
2052
}
2053
2054
require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
2055