Completed
Push — fix/api-jetpack-comments-avata... ( 3dac26 )
by
unknown
25:51 queued 15:10
created

WPCOM_JSON_API_Endpoint   D

Complexity

Total Complexity 343

Size/Duplication

Total Lines 2036
Duplicated Lines 5.26 %

Coupling/Cohesion

Components 1
Dependencies 1
Metric Value
wmc 343
lcom 1
cbo 1
dl 107
loc 2036
rs 4.4102

46 Methods

Rating   Name   Duplication   Size   Complexity  
A is_publicly_documentable() 0 3 2
D cast_and_filter_item() 62 322 57
B __construct() 10 92 5
A query_args() 0 9 2
C input() 0 50 16
C cast_and_filter() 0 61 16
B parse_types() 0 20 5
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() 0 64 17
D get_author() 0 103 20
C get_media_item() 0 33 7
F get_media_item_v1_1() 8 121 22
A get_taxonomy() 0 10 3
C format_taxonomy() 3 37 7
C format_date() 0 43 8
C parse_date() 0 42 7
C load_theme_functions() 0 66 9
B copy_hooks() 0 21 8
B get_reflection() 0 21 9
C get_closest_version_of_endpoint() 0 55 13
B get_endpoint_path_versions() 0 39 3
A get_last_segment_of_relative_path() 0 9 2
B get_link() 0 33 4
A get_me_link() 0 3 1
A get_taxonomy_link() 0 6 2
A get_media_link() 0 3 1
A get_site_link() 0 3 1
A get_post_link() 0 3 1
A get_comment_link() 0 3 1
A get_publicize_connection_link() 0 3 1
A get_publicize_connections_link() 0 3 1
A get_keyring_connection_link() 0 3 1
A get_external_service_link() 0 3 1
B current_user_can_access_post_type() 0 15 5
A is_post_type_allowed() 0 15 4
A _get_whitelisted_post_types() 0 16 1
D handle_media_creation_v1_1() 24 96 23
D handle_media_sideload() 0 38 9
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
callback() 0 1 ?

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
2
3
require_once( dirname( __FILE__ ) . '/json-api-config.php' );
4
5
// Endpoint
6
abstract class WPCOM_JSON_API_Endpoint {
7
	// The API Object
8
	public $api;
9
10
	public $pass_wpcom_user_details = false;
11
	public $can_use_user_details_instead_of_blog_membership = false;
12
13
	// One liner.
14
	public $description;
15
16
	// Object Grouping For Documentation (Users, Posts, Comments)
17
	public $group;
18
19
	// Stats extra value to bump
20
	public $stat;
21
22
	// HTTP Method
23
	public $method = 'GET';
24
25
	// Minimum version of the api for which to serve this endpoint
26
	public $min_version = '0';
27
28
	// Maximum version of the api for which to serve this endpoint
29
	public $max_version = WPCOM_JSON_API__CURRENT_VERSION;
30
31
	// Path at which to serve this endpoint: sprintf() format.
32
	public $path = '';
33
34
	// Identifiers to fill sprintf() formatted $path
35
	public $path_labels = array();
36
37
	// Accepted query parameters
38
	public $query = array(
39
		// Parameter name
40
		'context' => array(
41
			// Default value => description
42
			'display' => 'Formats the output as HTML for display.  Shortcodes are parsed, paragraph tags are added, etc..',
43
			// Other possible values => description
44
			'edit'    => 'Formats the output for editing.  Shortcodes are left unparsed, significant whitespace is kept, etc..',
45
		),
46
		'http_envelope' => array(
47
			'false' => '',
48
			'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.',
49
		),
50
		'pretty' => array(
51
			'false' => '',
52
			'true'  => 'Output pretty JSON',
53
		),
54
		'meta' => "(string) Optional. Loads data from the endpoints found in the 'meta' part of the response. Comma-separated list. Example: meta=site,likes",
55
		'fields' => '(string) Optional. Returns specified fields only. Comma-separated list. Example: fields=ID,title',
56
		// Parameter name => description (default value is empty)
57
		'callback' => '(string) An optional JSONP callback function.',
58
	);
59
60
	// Response format
61
	public $response_format = array();
62
63
	// Request format
64
	public $request_format = array();
65
66
	// Is this endpoint still in testing phase?  If so, not available to the public.
67
	public $in_testing = false;
68
69
	// Is this endpoint still allowed if the site in question is flagged?
70
	public $allowed_if_flagged = false;
71
72
	/**
73
	 * @var string Version of the API
74
	 */
75
	public $version = '';
76
77
	/**
78
	 * @var string Example request to make
79
	 */
80
	public $example_request = '';
81
82
	/**
83
	 * @var string Example request data (for POST methods)
84
	 */
85
	public $example_request_data = '';
86
87
	/**
88
	 * @var string Example response from $example_request
89
	 */
90
	public $example_response = '';
91
92
	/**
93
	 * @var bool Set to true if the endpoint implements its own filtering instead of the standard `fields` query method
94
	 */
95
	public $custom_fields_filtering = false;
96
97
	/**
98
	 * @var bool Set to true if the endpoint accepts all cross origin requests. You probably should not set this flag.
99
	 */
100
	public $allow_cross_origin_request = false;
101
102
	/**
103
	 * @var bool Set to true if the endpoint can recieve unauthorized POST requests.
104
	 */
105
	public $allow_unauthorized_request = false;
106
107
	/**
108
	 * @var bool Set to true if the endpoint should accept site based (not user based) authentication.
109
	 */
110
	public $allow_jetpack_site_auth = false;
111
112
	function __construct( $args ) {
113
		$defaults = array(
114
			'in_testing'           => false,
115
			'allowed_if_flagged'   => false,
116
			'description'          => '',
117
			'group'	               => '',
118
			'method'               => 'GET',
119
			'path'                 => '/',
120
			'min_version'          => '0',
121
			'max_version'          => WPCOM_JSON_API__CURRENT_VERSION,
122
			'force'	               => '',
123
			'deprecated'           => false,
124
			'new_version'          => WPCOM_JSON_API__CURRENT_VERSION,
125
			'jp_disabled'          => false,
126
			'path_labels'          => array(),
127
			'request_format'       => array(),
128
			'response_format'      => array(),
129
			'query_parameters'     => array(),
130
			'version'              => 'v1',
131
			'example_request'      => '',
132
			'example_request_data' => '',
133
			'example_response'     => '',
134
			'required_scope'       => '',
135
			'pass_wpcom_user_details' => false,
136
			'can_use_user_details_instead_of_blog_membership' => false,
137
			'custom_fields_filtering' => false,
138
			'allow_cross_origin_request' => false,
139
			'allow_unauthorized_request' => false,
140
			'allow_jetpack_site_auth'    => false,
141
		);
142
143
		$args = wp_parse_args( $args, $defaults );
144
145
		$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...
146
147
		$this->allowed_if_flagged = $args['allowed_if_flagged'];
148
149
		$this->description = $args['description'];
150
		$this->group       = $args['group'];
151
		$this->stat        = $args['stat'];
152
		$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...
153
		$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...
154
155
		$this->method      = $args['method'];
156
		$this->path        = $args['path'];
157
		$this->path_labels = $args['path_labels'];
158
		$this->min_version = $args['min_version'];
159
		$this->max_version = $args['max_version'];
160
		$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...
161
		$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...
162
163
		$this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
164
		$this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
165
		$this->can_use_user_details_instead_of_blog_membership = $args['can_use_user_details_instead_of_blog_membership'];
166
167
		$this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
168
		$this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
169
		$this->allow_jetpack_site_auth    = (bool) $args['allow_jetpack_site_auth'];
170
171
		$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...
172
173
		$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...
174
175 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...
176
			$this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
177
		} else {
178
			$this->request_format = $args['request_format'];
179
		}
180
181 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...
182
			$this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
183
		} else {
184
			$this->response_format = $args['response_format'];
185
		}
186
187
		if ( false === $args['query_parameters'] ) {
188
			$this->query = array();
189
		} elseif ( is_array( $args['query_parameters'] ) ) {
190
			$this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
191
		}
192
193
		$this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
194
195
		/** Example Request/Response ******************************************/
196
197
		// Examples for endpoint documentation request
198
		$this->example_request      = $args['example_request'];
199
		$this->example_request_data = $args['example_request_data'];
200
		$this->example_response     = $args['example_response'];
201
202
		$this->api->add( $this );
203
	}
204
205
	// Get all query args.  Prefill with defaults
206
	function query_args( $return_default_values = true, $cast_and_filter = true ) {
207
		$args = array_intersect_key( $this->api->query, $this->query );
208
209
		if ( !$cast_and_filter ) {
210
			return $args;
211
		}
212
213
		return $this->cast_and_filter( $args, $this->query, $return_default_values );
214
	}
215
216
	// Get POST body data
217
	function input( $return_default_values = true, $cast_and_filter = true ) {
218
		$input = trim( $this->api->post_body );
219
		$content_type = $this->api->content_type;
220
		if ( $content_type ) {
221
			list ( $content_type ) = explode( ';', $content_type );
222
		}
223
		$content_type = trim( $content_type );
224
		switch ( $content_type ) {
225
		case 'application/json' :
226
		case 'application/x-javascript' :
227
		case 'text/javascript' :
228
		case 'text/x-javascript' :
229
		case 'text/x-json' :
230
		case 'text/json' :
231
			$return = json_decode( $input, true );
232
233
			if ( function_exists( 'json_last_error' ) ) {
234
				if ( JSON_ERROR_NONE !== json_last_error() ) {
235
					return null;
236
				}
237
			} else {
238
				if ( is_null( $return ) && json_encode( null ) !== $input ) {
239
					return null;
240
				}
241
			}
242
243
			break;
244
		case 'multipart/form-data' :
245
			$return = array_merge( stripslashes_deep( $_POST ), $_FILES );
246
			break;
247
		case 'application/x-www-form-urlencoded' :
248
			//attempt JSON first, since probably a curl command
249
			$return = json_decode( $input, true );
250
251
			if ( is_null( $return ) ) {
252
				wp_parse_str( $input, $return );
253
			}
254
255
			break;
256
		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...
257
			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...
258
			break;
259
		}
260
261
		if ( !$cast_and_filter ) {
262
			return $return;
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...
263
		}
264
265
		return $this->cast_and_filter( $return, $this->request_format, $return_default_values );
266
	}
267
268
	function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
269
		$return_as_object = false;
270
		if ( is_object( $data ) ) {
271
			// @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...
272
			$data = (array) $data;
273
			$return_as_object = true;
274
		} elseif ( !is_array( $data ) ) {
275
			return $data;
276
		}
277
278
		$boolean_arg = array( 'false', 'true' );
279
		$naeloob_arg = array( 'true', 'false' );
280
281
		$return = array();
282
283
		foreach ( $documentation as $key => $description ) {
284
			if ( is_array( $description ) ) {
285
				// String or boolean array keys only
286
				$whitelist = array_keys( $description );
287
288
				if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
289
					// Truthiness
290
					if ( isset( $data[$key] ) ) {
291
						$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $data[$key] );
292
					} elseif ( $return_default_values ) {
293
						$return[$key] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg.
294
					}
295
				} elseif ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) {
296
					// String Key
297
					$return[$key] = (string) $data[$key];
298
				} elseif ( $return_default_values ) {
299
					// Default value
300
					$return[$key] = (string) current( $whitelist );
301
				}
302
303
				continue;
304
			}
305
306
			$types = $this->parse_types( $description );
307
			$type = array_shift( $types );
308
309
			// Explicit default - string and int only for now.  Always set these reguardless of $return_default_values
310
			if ( isset( $type['default'] ) ) {
311
				if ( !isset( $data[$key] ) ) {
312
					$data[$key] = $type['default'];
313
				}
314
			}
315
316
			if ( !isset( $data[$key] ) ) {
317
				continue;
318
			}
319
320
			$this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output );
321
		}
322
323
		if ( $return_as_object ) {
324
			return (object) $return;
325
		}
326
327
		return $return;
328
	}
329
330
	/**
331
	 * Casts $value according to $type.
332
	 * Handles fallbacks for certain values of $type when $value is not that $type
333
	 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way)
334
	 *
335
	 * Handles "child types" - array:URL, object:category
336
	 * array:URL means an array of URLs
337
	 * object:category means a hash of categories
338
	 *
339
	 * Handles object typing - object>post means an object of type post
340
	 */
341
	function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
342
		if ( is_string( $type ) ) {
343
			$type = compact( 'type' );
344
		}
345
346
		switch ( $type['type'] ) {
347
		case 'false' :
348
			$return[$key] = false;
349
			break;
350
		case 'url' :
351
			$return[$key] = (string) esc_url_raw( $value );
352
			break;
353
		case 'string' :
354
			// Fallback string -> array, or string -> object
355
			if ( is_array( $value ) || is_object( $value ) ) {
356 View Code Duplication
				if ( !empty( $types[0] ) ) {
357
					$next_type = array_shift( $types );
358
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
359
				}
360
			}
361
362
			// Fallback string -> false
363 View Code Duplication
			if ( !is_string( $value ) ) {
364
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
365
					$next_type = array_shift( $types );
366
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
367
				}
368
			}
369
			$return[$key] = (string) $value;
370
			break;
371
		case 'html' :
372
			$return[$key] = (string) $value;
373
			break;
374
		case 'safehtml' :
375
			$return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() );
376
			break;
377
		case 'media' :
378
			if ( is_array( $value ) ) {
379
				if ( isset( $value['name'] ) ) {
380
					// It's a $_FILES array
381
					// Reformat into array of $_FILES items
382
383
					$files = array();
384
					foreach ( $value['name'] as $k => $v ) {
385
						$files[$k] = array();
386
						foreach ( array_keys( $value ) as $file_key ) {
387
							$files[$k][$file_key] = $value[$file_key][$k];
388
						}
389
					}
390
391
					$return[$key] = $files;
392
					break;
393
				}
394
			} 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...
395
				// no break - treat as 'array'
396
			}
397
			// nobreak
398
		case 'array' :
399
			// Fallback array -> string
400 View Code Duplication
			if ( is_string( $value ) ) {
401
				if ( !empty( $types[0] ) ) {
402
					$next_type = array_shift( $types );
403
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
404
				}
405
			}
406
407 View Code Duplication
			if ( isset( $type['children'] ) ) {
408
				$children = array();
409
				foreach ( (array) $value as $k => $child ) {
410
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
411
				}
412
				$return[$key] = (array) $children;
413
				break;
414
			}
415
416
			$return[$key] = (array) $value;
417
			break;
418
		case 'iso 8601 datetime' :
419
		case 'datetime' :
420
			// (string)s
421
			$dates = $this->parse_date( (string) $value );
422
			if ( $for_output ) {
423
				$return[$key] = $this->format_date( $dates[1], $dates[0] );
424
			} else {
425
				list( $return[$key], $return["{$key}_gmt"] ) = $dates;
426
			}
427
			break;
428
		case 'float' :
429
			$return[$key] = (float) $value;
430
			break;
431
		case 'int' :
432
		case 'integer' :
433
			$return[$key] = (int) $value;
434
			break;
435
		case 'bool' :
436
		case 'boolean' :
437
			$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value );
438
			break;
439
		case 'object' :
440
			// Fallback object -> false
441 View Code Duplication
			if ( is_scalar( $value ) || is_null( $value ) ) {
442
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
443
					return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
444
				}
445
			}
446
447 View Code Duplication
			if ( isset( $type['children'] ) ) {
448
				$children = array();
449
				foreach ( (array) $value as $k => $child ) {
450
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
451
				}
452
				$return[$key] = (object) $children;
453
				break;
454
			}
455
456
			if ( isset( $type['subtype'] ) ) {
457
				return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
458
			}
459
460
			$return[$key] = (object) $value;
461
			break;
462
		case 'post' :
463
			$return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
464
			break;
465
		case 'comment' :
466
			$return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
467
			break;
468
		case 'tag' :
469
		case 'category' :
470
			$docs = array(
471
				'ID'          => '(int)',
472
				'name'        => '(string)',
473
				'slug'        => '(string)',
474
				'description' => '(HTML)',
475
				'post_count'  => '(int)',
476
				'meta'        => '(object)',
477
			);
478
			if ( 'category' === $type['type'] ) {
479
				$docs['parent'] = '(int)';
480
			}
481
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
482
			break;
483
		case 'post_reference' :
484 View Code Duplication
		case 'comment_reference' :
485
			$docs = array(
486
				'ID'    => '(int)',
487
				'type'  => '(string)',
488
				'title' => '(string)',
489
				'link'  => '(URL)',
490
			);
491
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
492
			break;
493 View Code Duplication
		case 'geo' :
494
			$docs = array(
495
				'latitude'  => '(float)',
496
				'longitude' => '(float)',
497
				'address'   => '(string)',
498
			);
499
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
500
			break;
501
		case 'author' :
502
			$docs = array(
503
				'ID'             => '(int)',
504
				'user_login'     => '(string)',
505
				'login'          => '(string)',
506
				'email'          => '(string|false)',
507
				'name'           => '(string)',
508
				'first_name'     => '(string)',
509
				'last_name'      => '(string)',
510
				'nice_name'      => '(string)',
511
				'URL'            => '(URL)',
512
				'avatar_URL'     => '(URL)',
513
				'profile_URL'    => '(URL)',
514
				'is_super_admin' => '(bool)',
515
				'roles'          => '(array:string)'
516
			);
517
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
518
			break;
519 View Code Duplication
		case 'role' :
520
			$docs = array(
521
				'name'         => '(string)',
522
				'display_name' => '(string)',
523
				'capabilities' => '(object:boolean)',
524
			);
525
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
526
			break;
527
		case 'attachment' :
528
			$docs = array(
529
				'ID'        => '(int)',
530
				'URL'       => '(URL)',
531
				'guid'      => '(string)',
532
				'mime_type' => '(string)',
533
				'width'     => '(int)',
534
				'height'    => '(int)',
535
				'duration'  => '(int)',
536
			);
537
			$return[$key] = (object) $this->cast_and_filter(
538
				$value,
539
				/**
540
				 * Filter the documentation returned for a post attachment.
541
				 *
542
				 * @module json-api
543
				 *
544
				 * @since 1.9.0
545
				 *
546
				 * @param array $docs Array of documentation about a post attachment.
547
				 */
548
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
549
				false,
550
				$for_output
551
			);
552
			break;
553
		case 'metadata' :
554
			$docs = array(
555
				'id'       => '(int)',
556
				'key'       => '(string)',
557
				'value'     => '(string|false|float|int|array|object)',
558
				'previous_value' => '(string)',
559
				'operation'  => '(string)',
560
			);
561
			$return[$key] = (object) $this->cast_and_filter(
562
				$value,
563
				/** This filter is documented in class.json-api-endpoints.php */
564
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
565
				false,
566
				$for_output
567
			);
568
			break;
569
		case 'plugin' :
570
			$docs = array(
571
				'id'          => '(safehtml) The plugin\'s ID',
572
				'slug'        => '(safehtml) The plugin\'s Slug',
573
				'active'      => '(boolean)  The plugin status.',
574
				'update'      => '(object)   The plugin update info.',
575
				'name'        => '(safehtml) The name of the plugin.',
576
				'plugin_url'  => '(url)      Link to the plugin\'s web site.',
577
				'version'     => '(safehtml) The plugin version number.',
578
				'description' => '(safehtml) Description of what the plugin does and/or notes from the author',
579
				'author'      => '(safehtml) The plugin author\'s name',
580
				'author_url'  => '(url)      The plugin author web site address',
581
				'network'     => '(boolean)  Whether the plugin can only be activated network wide.',
582
				'autoupdate'  => '(boolean)  Whether the plugin is auto updated',
583
				'log'         => '(array:safehtml) An array of update log strings.',
584
			);
585
			$return[$key] = (object) $this->cast_and_filter(
586
				$value,
587
				/**
588
				 * Filter the documentation returned for a plugin.
589
				 *
590
				 * @module json-api
591
				 *
592
				 * @since 3.1.0
593
				 *
594
				 * @param array $docs Array of documentation about a plugin.
595
				 */
596
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
597
				false,
598
				$for_output
599
			);
600
			break;
601
		case 'jetpackmodule' :
602
			$docs = array(
603
				'id'          => '(string)   The module\'s ID',
604
				'active'      => '(boolean)  The module\'s status.',
605
				'name'        => '(string)   The module\'s name.',
606
				'description' => '(safehtml) The module\'s description.',
607
				'sort'        => '(int)      The module\'s display order.',
608
				'introduced'  => '(string)   The Jetpack version when the module was introduced.',
609
				'changed'     => '(string)   The Jetpack version when the module was changed.',
610
				'free'        => '(boolean)  The module\'s Free or Paid status.',
611
				'module_tags' => '(array)    The module\'s tags.'
612
			);
613
			$return[$key] = (object) $this->cast_and_filter(
614
				$value,
615
				/** This filter is documented in class.json-api-endpoints.php */
616
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
617
				false,
618
				$for_output
619
			);
620
			break;
621
		case 'sharing_button' :
622
			$docs = array(
623
				'ID'         => '(string)',
624
				'name'       => '(string)',
625
				'URL'        => '(string)',
626
				'icon'       => '(string)',
627
				'enabled'    => '(bool)',
628
				'visibility' => '(string)',
629
			);
630
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
631
			break;
632
		case 'sharing_button_service':
633
			$docs = array(
634
				'ID'               => '(string) The service identifier',
635
				'name'             => '(string) The service name',
636
				'class_name'       => '(string) Class name for custom style sharing button elements',
637
				'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
638
				'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
639
				'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview'
640
			);
641
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
642
			break;
643
644
		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...
645
			$method_name = $type['type'] . '_docs';
646
			if ( method_exists( WPCOM_JSON_API_Jetpack_Overrides, $method_name ) ) {
647
				$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
648
			}
649
650
			if ( ! empty( $docs ) ) {
651
				$return[$key] = (object) $this->cast_and_filter(
652
					$value,
653
					/** This filter is documented in class.json-api-endpoints.php */
654
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
655
					false,
656
					$for_output
657
				);
658
			} else {
659
				trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
660
			}
661
		}
662
	}
663
664
	function parse_types( $text ) {
665
		if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
666
			return 'none';
667
		}
668
669
		$types = explode( '|', strtolower( $matches[1] ) );
670
		$return = array();
671
		foreach ( $types as $type ) {
672
			foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
673
				if ( false !== strpos( $type, $operator ) ) {
674
					$item = explode( $operator, $type, 2 );
675
					$return[] = array( 'type' => $item[0], $meaning => $item[1] );
676
					continue 2;
677
				}
678
			}
679
			$return[] = compact( 'type' );
680
		}
681
682
		return $return;
683
	}
684
685
	/**
686
	 * Checks if the endpoint is publicly displayable
687
	 */
688
	function is_publicly_documentable() {
689
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
690
	}
691
692
	/**
693
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
694
	 * Echoes HTML.
695
	 */
696
	function document( $show_description = true ) {
697
		global $wpdb;
698
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
699
		unset( $GLOBALS['post'] );
700
701
		$doc = $this->generate_documentation();
702
703
		if ( $show_description ) :
704
?>
705
<caption>
706
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
707
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
708
</caption>
709
710
<?php endif; ?>
711
712
<?php if ( true === $this->deprecated ) { ?>
713
<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...
714
<?php } ?>
715
716
<section class="resource-info">
717
	<h2 id="apidoc-resource-info">Resource Information</h2>
718
719
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
720
721
	<thead>
722
		<tr>
723
			<th class="api-index-title" scope="column">&nbsp;</th>
724
			<th class="api-index-title" scope="column">&nbsp;</th>
725
		</tr>
726
	</thead>
727
	<tbody>
728
729
		<tr class="api-index-item">
730
			<th scope="row" class="parameter api-index-item-title">Method</th>
731
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
732
		</tr>
733
734
		<tr class="api-index-item">
735
			<th scope="row" class="parameter api-index-item-title">URL</th>
736
			<?php
737
			$version = WPCOM_JSON_API__CURRENT_VERSION;
738
			if ( !empty( $this->max_version ) ) {
739
				$version = $this->max_version;
740
			}
741
			?>
742
			<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>
743
		</tr>
744
745
		<tr class="api-index-item">
746
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
747
			<?php
748
			$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'] ) );
749
			?>
750
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
751
		</tr>
752
753
	</tbody>
754
	</table>
755
756
</section>
757
758
<?php
759
760
		foreach ( array(
761
			'path'     => 'Method Parameters',
762
			'query'    => 'Query Parameters',
763
			'body'     => 'Request Parameters',
764
			'response' => 'Response Parameters',
765
		) as $doc_section_key => $label ) :
766
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
767
			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...
768
				continue;
769
			}
770
771
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
772
?>
773
774
<section class="<?php echo $param_label; ?>">
775
776
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
777
778
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
779
780
<thead>
781
	<tr>
782
		<th class="api-index-title" scope="column">Parameter</th>
783
		<th class="api-index-title" scope="column">Type</th>
784
		<th class="api-index-title" scope="column">Description</th>
785
	</tr>
786
</thead>
787
<tbody>
788
789
<?php foreach ( $doc_section as $key => $item ) : ?>
790
791
	<tr class="api-index-item">
792
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
793
		<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...
794
		<td class="description api-index-item-body"><?php
795
796
		$this->generate_doc_description( $item['description'] );
797
798
		?></td>
799
	</tr>
800
801
<?php endforeach; ?>
802
</tbody>
803
</table>
804
</section>
805
<?php endforeach; ?>
806
807
<?php
808
		if ( 'unset' !== $original_post ) {
809
			$GLOBALS['post'] = $original_post;
810
		}
811
	}
812
813
	function add_http_build_query_to_php_content_example( $matches ) {
814
		$trimmed_match = ltrim( $matches[0] );
815
		$pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
816
		$pad = ltrim( $pad, ' ' );
817
		$return = '  ' . str_replace( "\n", "\n  ", $matches[0] );
818
		return " http_build_query({$return}{$pad})";
819
	}
820
821
	/**
822
	 * Recursively generates the <dl>'s to document item descriptions.
823
	 * Echoes HTML.
824
	 */
825
	function generate_doc_description( $item ) {
826
		if ( is_array( $item ) ) : ?>
827
828
		<dl>
829
<?php			foreach ( $item as $description_key => $description_value ) : ?>
830
831
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
832
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
833
834
<?php			endforeach; ?>
835
836
		</dl>
837
838
<?php
839
		else :
840
			echo wp_kses_post( $item );
841
		endif;
842
	}
843
844
	/**
845
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
846
	 * Echoes HTML.
847
	 */
848
	function generate_documentation() {
849
		$format       = str_replace( '%d', '%s', $this->path );
850
		$path_labeled = $format;
851
		if ( ! empty( $this->path_labels ) ) {
852
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
853
		}
854
		$boolean_arg  = array( 'false', 'true' );
855
		$naeloob_arg  = array( 'true', 'false' );
856
857
		$doc = array(
858
			'description'  => $this->description,
859
			'method'       => $this->method,
860
			'path_format'  => $this->path,
861
			'path_labeled' => $path_labeled,
862
			'group'        => $this->group,
863
			'request' => array(
864
				'path'  => array(),
865
				'query' => array(),
866
				'body'  => array(),
867
			),
868
			'response' => array(
869
				'body' => array(),
870
			)
871
		);
872
873
		foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
874
			foreach ( (array) $this->$_property as $key => $description ) {
875
				if ( is_array( $description ) ) {
876
					$description_keys = array_keys( $description );
877
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
878
						$type = '(bool)';
879
					} else {
880
						$type = '(string)';
881
					}
882
883
					if ( 'response_format' !== $_property ) {
884
						// hack - don't show "(default)" in response format
885
						reset( $description );
886
						$description_key = key( $description );
887
						$description[$description_key] = "(default) {$description[$description_key]}";
888
					}
889
				} else {
890
					$types   = $this->parse_types( $description );
891
					$type    = array();
892
					$default = '';
893
894
					if ( 'none' == $types ) {
895
						$types = array();
896
						$types[]['type'] = 'none';
897
					}
898
899
					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...
900
						$type[] = $type_array['type'];
901
						if ( isset( $type_array['default'] ) ) {
902
							$default = $type_array['default'];
903
							if ( 'string' === $type_array['type'] ) {
904
								$default = "'$default'";
905
							}
906
						}
907
					}
908
					$type = '(' . join( '|', $type ) . ')';
909
					$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...
910
					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...
911
					$description = trim( $description );
912
					if ( $default ) {
913
						$description .= " Default: $default.";
914
					}
915
				}
916
917
				$item = compact( 'type', 'description' );
918
919
				if ( 'response_format' === $_property ) {
920
					$doc['response'][$doc_item][$key] = $item;
921
				} else {
922
					$doc['request'][$doc_item][$key] = $item;
923
				}
924
			}
925
		}
926
927
		return $doc;
928
	}
929
930
	function user_can_view_post( $post_id ) {
931
		$post = get_post( $post_id );
932
		if ( !$post || is_wp_error( $post ) ) {
933
			return false;
934
		}
935
936
		if ( 'inherit' === $post->post_status ) {
937
			$parent_post = get_post( $post->post_parent );
938
			$post_status_obj = get_post_status_object( $parent_post->post_status );
939
		} else {
940
			$post_status_obj = get_post_status_object( $post->post_status );
941
		}
942
943
		if ( !$post_status_obj->public ) {
944
			if ( is_user_logged_in() ) {
945
				if ( $post_status_obj->protected ) {
946
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
947
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
948
					}
949
				} elseif ( $post_status_obj->private ) {
950
					if ( !current_user_can( 'read_post', $post->ID ) ) {
951
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
952
					}
953
				} elseif ( 'trash' === $post->post_status ) {
954
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
955
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
956
					}
957
				} 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...
958
					//allow auto-drafts
959
				} else {
960
					return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
961
				}
962
			} else {
963
				return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
964
			}
965
		}
966
967
		if (
968
			-1 == get_option( 'blog_public' ) &&
969
			/**
970
			 * Filter access to a specific post.
971
			 *
972
			 * @module json-api
973
			 *
974
			 * @since 3.4.0
975
			 *
976
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
977
			 * @param WP_Post $post Post data.
978
			 */
979
			! apply_filters(
980
				'wpcom_json_api_user_can_view_post',
981
				current_user_can( 'read_post', $post->ID ),
982
				$post
983
			)
984
		) {
985
			return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
986
		}
987
988
		if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
989
			return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
990
		}
991
992
		return true;
993
	}
994
995
	/**
996
	 * Returns author object.
997
	 *
998
	 * @param $author user ID, user row, WP_User object, comment row, post row
999
	 * @param $show_email output the author's email address?
1000
	 *
1001
	 * @return (object)
1002
	 */
1003
	function get_author( $author, $show_email = false ) {
1004
		if ( isset( $author->comment_author_email ) && !$author->user_id ) {
1005
			$ID          = 0;
1006
			$login       = '';
1007
			$email       = $author->comment_author_email;
1008
			$name        = $author->comment_author;
1009
			$first_name  = '';
1010
			$last_name   = '';
1011
			$URL         = $author->comment_author_url;
1012
			$avatar_URL  = $this->api->get_avatar_url( $author );
1013
			$profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1014
			$nice        = '';
1015
			$site_id     = -1;
1016
1017
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1018
			// "&" is the only email/URL character altered by wp_kses()
1019
			foreach ( array( 'email', 'URL' ) as $field ) {
1020
				$$field = str_replace( '&amp;', '&', $$field );
1021
			}
1022
		} else {
1023
			if ( isset( $author->post_author ) ) {
1024
				// then $author is a Post Object.
1025
				if ( 0 == $author->post_author )
1026
					return null;
1027
				/**
1028
				 * Filter whether the current site is a Jetpack site.
1029
				 *
1030
				 * @module json-api
1031
				 *
1032
				 * @since 3.3.0
1033
				 *
1034
				 * @param bool false Is the current site a Jetpack site. Default to false.
1035
				 * @param int get_current_blog_id() Blog ID.
1036
				 */
1037
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1038
				$post_id = $author->ID;
1039
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1040
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1041
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1042
					$login      = '';
1043
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1044
					$first_name = '';
1045
					$last_name  = '';
1046
					$URL        = '';
1047
					$nice       = '';
1048
				} else {
1049
					$author = $author->post_author;
1050
				}
1051
			} elseif ( isset( $author->user_id ) && $author->user_id ) {
1052
				$author = $author->user_id;
1053
			} elseif ( isset( $author->user_email ) ) {
1054
				$author = $author->ID;
1055
			}
1056
1057
			if ( ! isset( $ID ) ) {
1058
				$user = get_user_by( 'id', $author );
1059
				if ( ! $user || is_wp_error( $user ) ) {
1060
					trigger_error( 'Unknown user', E_USER_WARNING );
1061
1062
					return null;
1063
				}
1064
				$ID         = $user->ID;
1065
				$email      = $user->user_email;
1066
				$login      = $user->user_login;
1067
				$name       = $user->display_name;
1068
				$first_name = $user->first_name;
1069
				$last_name  = $user->last_name;
1070
				$URL        = $user->user_url;
1071
				$nice       = $user->user_nicename;
1072
			}
1073
			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...
1074
				$active_blog = get_active_blog_for_user( $ID );
1075
				$site_id     = $active_blog->blog_id;
1076
				$profile_URL = "http://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...
1077
			} else {
1078
				$profile_URL = 'http://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...
1079
				$site_id     = -1;
1080
			}
1081
1082
			$avatar_URL = $this->api->get_avatar_url( $email );
1083
		}
1084
1085
		$email = $show_email ? (string) $email : false;
1086
1087
		$author = array(
1088
			'ID'          => (int) $ID,
1089
			'login'       => (string) $login,
1090
			'email'       => $email, // (string|bool)
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1091
			'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...
1092
			'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...
1093
			'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...
1094
			'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...
1095
			'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...
1096
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1097
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1098
		);
1099
1100
		if ($site_id > -1) {
1101
			$author['site_ID'] = (int) $site_id;
1102
		}
1103
1104
		return (object) $author;
1105
	}
1106
1107
	function get_media_item( $media_id ) {
1108
		$media_item = get_post( $media_id );
1109
1110
		if ( !$media_item || is_wp_error( $media_item ) )
1111
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1112
1113
		$response = array(
1114
			'id'    => strval( $media_item->ID ),
1115
			'date' =>  (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1116
			'parent'           => $media_item->post_parent,
1117
			'link'             => wp_get_attachment_url( $media_item->ID ),
1118
			'title'            => $media_item->post_title,
1119
			'caption'          => $media_item->post_excerpt,
1120
			'description'      => $media_item->post_content,
1121
			'metadata'         => wp_get_attachment_metadata( $media_item->ID ),
1122
		);
1123
1124
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1125
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1126
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1127
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1128
		}
1129
1130
		$response['meta'] = (object) array(
1131
			'links' => (object) array(
1132
				'self' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1133
				'help' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1134
				'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
1135
			),
1136
		);
1137
1138
		return (object) $response;
1139
	}
1140
1141
	function get_media_item_v1_1( $media_id ) {
1142
		$media_item = get_post( $media_id );
1143
1144
		if ( ! $media_item || is_wp_error( $media_item ) )
1145
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1146
1147
		$file = basename( wp_get_attachment_url( $media_item->ID ) );
1148
		$file_info = pathinfo( $file );
1149
		$ext  = $file_info['extension'];
1150
1151
		$response = array(
1152
			'ID'           => $media_item->ID,
1153
			'URL'          => wp_get_attachment_url( $media_item->ID ),
1154
			'guid'         => $media_item->guid,
1155
			'date'         => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1156
			'post_ID'      => $media_item->post_parent,
1157
			'file'         => $file,
1158
			'mime_type'    => $media_item->post_mime_type,
1159
			'extension'    => $ext,
1160
			'title'        => $media_item->post_title,
1161
			'caption'      => $media_item->post_excerpt,
1162
			'description'  => $media_item->post_content,
1163
			'alt'          => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1164
			'thumbnails'   => array()
1165
		);
1166
1167
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1168
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1169 View Code Duplication
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1170
				$response['height'] = $metadata['height'];
1171
				$response['width'] = $metadata['width'];
1172
			}
1173
1174
			if ( isset( $metadata['sizes'] ) ) {
1175
				/**
1176
				 * Filter the thumbnail sizes available for each attachment ID.
1177
				 *
1178
				 * @module json-api
1179
				 *
1180
				 * @since 3.9.0
1181
				 *
1182
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1183
				 * @param string $media_id Attachment ID.
1184
				 */
1185
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_id );
1186
				if ( is_array( $sizes ) ) {
1187
					foreach ( $sizes as $size => $size_details ) {
1188
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1189
					}
1190
				}
1191
			}
1192
1193
			if ( isset( $metadata['image_meta'] ) ) {
1194
				$response['exif'] = $metadata['image_meta'];
1195
			}
1196
		}
1197
1198
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1199
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1200
			$response['length'] = $metadata['length'];
1201
			$response['exif']   = $metadata;
1202
		}
1203
1204
		if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) {
1205
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1206 View Code Duplication
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1207
				$response['height'] = $metadata['height'];
1208
				$response['width']  = $metadata['width'];
1209
			}
1210
1211
			if ( isset( $metadata['length'] ) ) {
1212
				$response['length'] = $metadata['length'];
1213
			}
1214
1215
			// add VideoPress info
1216
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1217
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_id );
1218
1219
				// Thumbnails
1220
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1221
					$response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' );
1222
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1223
						if ( video_format_done( $info, $size ) ) {
1224
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1225
						} else {
1226
							unset( $response['thumbnails'][ $size ] );
1227
						}
1228
					}
1229
				}
1230
1231
				$response['videopress_guid'] = $info->guid;
1232
				$response['videopress_processing_done'] = true;
1233
				if ( '0000-00-00 00:00:00' == $info->finish_date_gmt ) {
1234
					$response['videopress_processing_done'] = false;
1235
				}
1236
			}
1237
		}
1238
1239
		$response['thumbnails'] = (object) $response['thumbnails'];
1240
1241
		$response['meta'] = (object) array(
1242
			'links' => (object) array(
1243
				'self' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1244
				'help' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1245
				'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
1246
			),
1247
		);
1248
1249
		// add VideoPress link to the meta
1250
		if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) {
1251
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1252
				$response['meta']->links->videopress = (string) $this->get_link( '/videos/%s', $response['videopress_guid'], '' );
1253
			}
1254
		}
1255
1256
		if ( $media_item->post_parent > 0 ) {
1257
			$response['meta']->links->parent = (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1258
		}
1259
1260
		return (object) $response;
1261
	}
1262
1263
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1264
1265
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1266
		/// keep updating this function
1267
		if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1268
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1269
		}
1270
1271
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1272
	}
1273
1274
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1275
		// Permissions
1276
		switch ( $context ) {
1277
		case 'edit' :
1278
			$tax = get_taxonomy( $taxonomy_type );
1279
			if ( !current_user_can( $tax->cap->edit_terms ) )
1280
				return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1281
			break;
1282
		case 'display' :
1283 View Code Duplication
			if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1284
				return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1285
			}
1286
			break;
1287
		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...
1288
			return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1289
		}
1290
1291
		$response                = array();
1292
		$response['ID']          = (int) $taxonomy->term_id;
1293
		$response['name']        = (string) $taxonomy->name;
1294
		$response['slug']        = (string) $taxonomy->slug;
1295
		$response['description'] = (string) $taxonomy->description;
1296
		$response['post_count']  = (int) $taxonomy->count;
1297
1298
		if ( 'category' === $taxonomy_type )
1299
			$response['parent'] = (int) $taxonomy->parent;
1300
1301
		$response['meta'] = (object) array(
1302
			'links' => (object) array(
1303
				'self' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1304
				'help' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1305
				'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
1306
			),
1307
		);
1308
1309
		return (object) $response;
1310
	}
1311
1312
	/**
1313
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1314
	 *
1315
	 * @param $date_gmt (string) GMT datetime string.
1316
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1317
	 *
1318
	 * @return string
1319
	 */
1320
	function format_date( $date_gmt, $date = null ) {
1321
		$timestamp_gmt = strtotime( "$date_gmt+0000" );
1322
1323
		if ( null === $date ) {
1324
			$timestamp = $timestamp_gmt;
1325
			$hours     = $minutes = $west = 0;
1326
		} else {
1327
			$date_time = date_create( "$date+0000" );
1328
			if ( $date_time ) {
1329
				$timestamp = date_format(  $date_time, 'U' );
1330
			} else {
1331
				$timestamp = 0;
1332
			}
1333
1334
			// "0000-00-00 00:00:00" == -62169984000
1335
			if ( -62169984000 == $timestamp_gmt ) {
1336
				// WordPress sets post_date=now, post_date_gmt="0000-00-00 00:00:00" for all drafts
1337
				// WordPress sets post_modified=now, post_modified_gmt="0000-00-00 00:00:00" for new drafts
1338
1339
				// Try to guess the correct offset from the blog's options.
1340
				$timezone_string = get_option( 'timezone_string' );
1341
1342
				if ( $timezone_string && $date_time ) {
1343
					$timezone = timezone_open( $timezone_string );
1344
					if ( $timezone ) {
1345
						$offset = $timezone->getOffset( $date_time );
1346
					}
1347
				} else {
1348
					$offset = 3600 * get_option( 'gmt_offset' );
1349
				}
1350
			} else {
1351
				$offset = $timestamp - $timestamp_gmt;
1352
			}
1353
1354
			$west      = $offset < 0;
0 ignored issues
show
Bug introduced by
The variable $offset 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...
1355
			$offset    = abs( $offset );
1356
			$hours     = (int) floor( $offset / 3600 );
1357
			$offset   -= $hours * 3600;
1358
			$minutes   = (int) floor( $offset / 60 );
1359
		}
1360
1361
		return (string) gmdate( 'Y-m-d\\TH:i:s', $timestamp ) . sprintf( '%s%02d:%02d', $west ? '-' : '+', $hours, $minutes );
1362
	}
1363
1364
	/**
1365
	 * Parses a date string and returns the local and GMT representations
1366
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1367
	 * timezones or offsets. If the parsed datetime was not localized to a
1368
	 * particular timezone or offset we will assume it was given in GMT
1369
	 * relative to now and will convert it to local time using either the
1370
	 * timezone set in the options table for the blog or the GMT offset.
1371
	 *
1372
	 * @param datetime string
1373
	 *
1374
	 * @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...
1375
	 */
1376
	function parse_date( $date_string ) {
1377
		$date_string_info = date_parse( $date_string );
1378
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1379
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1380
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1381
				$dt_local = clone $dt_utc = new DateTime( $date_string );
1382
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1383
				return array(
1384
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1385
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1386
				);
1387
			}
1388
1389
			// It's parseable but no TZ info so assume UTC
1390
			$dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1391
		} else {
1392
			// Could not parse time, use now in UTC
1393
			$dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1394
		}
1395
1396
		// First try to use timezone as it's daylight savings aware.
1397
		$timezone_string = get_option( 'timezone_string' );
1398
		if ( $timezone_string ) {
1399
			$tz = timezone_open( $timezone_string );
1400
			if ( $tz ) {
1401
				$dt_local->setTimezone( $tz );
1402
				return array(
1403
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1404
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1405
				);
1406
			}
1407
		}
1408
1409
		// Fallback to GMT offset (in hours)
1410
		// NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1411
		$gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1412
		$dt_local->modify("+{$gmt_offset_seconds} seconds");
1413
		return array(
1414
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1415
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1416
		);
1417
	}
1418
1419
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1420
	function load_theme_functions() {
1421
		// bail if we've done this already (can happen when calling /batch endpoint)
1422
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) )
1423
			return;
1424
1425
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1426
1427
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1428
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1429
1430
		$copy_dirs = array( get_template_directory() );
1431
		if ( wpcom_is_vip() ) {
1432
			$copy_dirs[] = WP_CONTENT_DIR . '/themes/vip/plugins/';
1433
		}
1434
1435
		// Is this a child theme? Load the child theme's functions file.
1436
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1437
			foreach ( $function_files as $function_file ) {
1438
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1439
					require_once(  get_stylesheet_directory() . $function_file );
1440
				}
1441
			}
1442
			$copy_dirs[] = get_stylesheet_directory();
1443
		}
1444
1445
		foreach ( $function_files as $function_file ) {
1446
			if ( file_exists( get_template_directory() . $function_file ) ) {
1447
				require_once(  get_template_directory() . $function_file );
1448
			}
1449
		}
1450
1451
		// add inc/wpcom.php and/or includes/wpcom.php
1452
		wpcom_load_theme_compat_file();
1453
1454
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1455
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1456
1457
		/**
1458
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1459
		 *
1460
		 * The REST API does not load the theme when processing requests.
1461
		 * To enable theme-based functionality, the API will load the '/functions.php',
1462
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1463
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1464
		 *
1465
		 * @module json-api
1466
		 *
1467
		 * @since 3.2.0
1468
		 */
1469
		do_action( 'restapi_theme_after_setup_theme' );
1470
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1471
1472
		/**
1473
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1474
		 *
1475
		 * The REST API does not load the theme when processing requests.
1476
		 * To enable theme-based functionality, the API will load the '/functions.php',
1477
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1478
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1479
		 *
1480
		 * @module json-api
1481
		 *
1482
		 * @since 3.2.0
1483
		 */
1484
		do_action( 'restapi_theme_init' );
1485
	}
1486
1487
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1488
		global $wp_filter;
1489
		foreach ( $wp_filter as $hook => $actions ) {
1490
			if ( $from_hook <> $hook )
1491
				continue;
1492
			foreach ( (array) $actions as $priority => $callbacks ) {
1493
				foreach( $callbacks as $callback_key => $callback_data ) {
1494
					$callback = $callback_data['function'];
1495
					$reflection = $this->get_reflection( $callback ); // use reflection api to determine filename where function is defined
1496
					if ( false !== $reflection ) {
1497
						$file_name = $reflection->getFileName();
1498
						foreach( $base_paths as $base_path ) {
1499
							if ( 0 === strpos( $file_name, $base_path ) ) { // only copy hooks with functions which are part of the specified files
1500
								$wp_filter[ $to_hook ][ $priority ][ 'cph' . $callback_key ] = $callback_data;
1501
							}
1502
						}
1503
					}
1504
				}
1505
			}
1506
		}
1507
	}
1508
1509
	function get_reflection( $callback ) {
1510
		if ( is_array( $callback ) ) {
1511
			list( $class, $method ) = $callback;
1512
			return new ReflectionMethod( $class, $method );
1513
		}
1514
1515
		if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) {
1516
			list( $class, $method ) = explode( "::", $callback );
1517
			return new ReflectionMethod( $class, $method );
1518
		}
1519
1520
		if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) {
1521
			return new ReflectionMethod( $callback, "__invoke" );
1522
		}
1523
1524
		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...
1525
			return new ReflectionFunction( $callback );
1526
		}
1527
1528
		return false;
1529
	}
1530
1531
	/**
1532
	 * Try to find the closest supported version of an endpoint to the current endpoint
1533
	 *
1534
	 * For example, if we were looking at the path /animals/panda:
1535
	 * - if the current endpoint is v1.3 and there is a v1.3 of /animals/%s available, we return 1.3
1536
	 * - if the current endpoint is v1.3 and there is no v1.3 of /animals/%s known, we fall back to the
1537
	 *   maximum available version of /animals/%s, e.g. 1.1
1538
	 *
1539
	 * This method is used in get_link() to construct meta links for API responses.
1540
	 *
1541
	 * @param $path string The current endpoint path, relative to the version
1542
	 * @param $method string Request method used to access the endpoint path
1543
	 * @return string The current version, or otherwise the maximum version available
1544
	 */
1545
	function get_closest_version_of_endpoint( $path, $request_method = 'GET' ) {
1546
1547
		$path = untrailingslashit( $path );
1548
1549
		// /help is a special case - always use the current request version
1550
		if ( wp_endswith( $path, '/help' ) ) {
1551
			return $this->api->version;
1552
		}
1553
1554
		$endpoint_path_versions = $this->get_endpoint_path_versions();
1555
		$last_path_segment = $this->get_last_segment_of_relative_path( $path );
1556
		$max_version_found = null;
1557
1558
		foreach ( $endpoint_path_versions as $endpoint_last_path_segment => $endpoints ) {
1559
1560
			// Does the last part of the path match the path key? (e.g. 'posts')
1561
			// If the last part contains a placeholder (e.g. %s), we want to carry on
1562
			if ( $last_path_segment != $endpoint_last_path_segment && ! strstr( $endpoint_last_path_segment, '%' ) ) {
1563
				continue;
1564
			}
1565
1566
			foreach ( $endpoints as $endpoint ) {
1567
				// Does the request method match?
1568
				if ( ! in_array( $request_method, $endpoint['request_methods'] ) ) {
1569
					continue;
1570
				}
1571
1572
				$endpoint_path = untrailingslashit( $endpoint['path'] );
1573
				$endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
1574
1575
				if ( ! preg_match( "#^$endpoint_path_regex\$#", $path, $matches ) ) {
1576
					continue;
1577
				}
1578
1579
				// Make sure the endpoint exists at the same version
1580
				if ( version_compare( $this->api->version, $endpoint['min_version'], '>=') &&
1581
					 version_compare( $this->api->version, $endpoint['max_version'], '<=') ) {
1582
					return $this->api->version;
1583
				}
1584
1585
				// If the endpoint doesn't exist at the same version, record the max version we found
1586
				if ( empty( $max_version_found ) || version_compare( $max_version_found, $endpoint['max_version'], '<' ) ) {
1587
					$max_version_found = $endpoint['max_version'];
1588
				}
1589
			}
1590
		}
1591
1592
		// If the endpoint version is less than the requested endpoint version, return the max version found
1593
		if ( ! empty( $max_version_found ) ) {
1594
			return $max_version_found;
1595
		}
1596
1597
		// Otherwise, use the API version of the current request
1598
		return $this->api->version;
1599
	}
1600
1601
	/**
1602
	 * Get an array of endpoint paths with their associated versions
1603
	 *
1604
	 * The result is cached for 30 minutes.
1605
	 *
1606
	 * @return array Array of endpoint paths, min_versions and max_versions, keyed by last segment of path
1607
	 **/
1608
	protected function get_endpoint_path_versions() {
1609
1610
		// Do we already have the result of this method in the cache?
1611
		$cache_result = get_transient( 'endpoint_path_versions' );
1612
1613
		if ( ! empty ( $cache_result ) ) {
1614
			return $cache_result;
1615
		}
1616
1617
		/*
1618
		 * Create a map of endpoints and their min/max versions keyed by the last segment of the path (e.g. 'posts')
1619
		 * This reduces the search space when finding endpoint matches in get_closest_version_of_endpoint()
1620
		 */
1621
		$endpoint_path_versions = array();
1622
1623
		foreach ( $this->api->endpoints as $key => $endpoint_objects ) {
1624
1625
			// The key contains a serialized path, min_version and max_version
1626
			list( $path, $min_version, $max_version ) = unserialize( $key );
1627
1628
			// Grab the last component of the relative path to use as the top-level key
1629
			$last_path_segment = $this->get_last_segment_of_relative_path( $path );
1630
1631
			$endpoint_path_versions[ $last_path_segment ][] = array(
1632
				'path' => $path,
1633
				'min_version' => $min_version,
1634
				'max_version' => $max_version,
1635
				'request_methods' => array_keys( $endpoint_objects )
1636
			);
1637
		}
1638
1639
		set_transient(
1640
			'endpoint_path_versions',
1641
			$endpoint_path_versions,
1642
			(HOUR_IN_SECONDS / 2)
1643
		);
1644
1645
		return $endpoint_path_versions;
1646
	}
1647
1648
	/**
1649
	 * Grab the last segment of a relative path
1650
	 *
1651
	 * @param string $path Path
1652
	 * @return string Last path segment
1653
	 */
1654
	protected function get_last_segment_of_relative_path( $path) {
1655
		$path_parts = array_filter( explode( '/', $path ) );
1656
1657
		if ( empty( $path_parts ) ) {
1658
			return null;
1659
		}
1660
1661
		return end( $path_parts );
1662
	}
1663
1664
	/**
1665
	 * Generate a URL to an endpoint
1666
	 *
1667
	 * Used to construct meta links in API responses
1668
	 *
1669
	 * @param mixed $args Optional arguments to be appended to URL
0 ignored issues
show
Bug introduced by
There is no parameter named $args. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1670
	 * @return string Endpoint URL
1671
	 **/
1672
	function get_link() {
1673
		$args   = func_get_args();
1674
		$format = array_shift( $args );
1675
		$base = WPCOM_JSON_API__BASE;
1676
1677
		$path = array_pop( $args );
1678
1679
		if ( $path ) {
1680
			$path = '/' . ltrim( $path, '/' );
1681
		}
1682
1683
		$args[] = $path;
1684
1685
		// Escape any % in args before using sprintf
1686
		$escaped_args = array();
1687
		foreach ( $args as $arg_key => $arg_value ) {
1688
			$escaped_args[ $arg_key ] = str_replace( '%', '%%', $arg_value );
1689
		}
1690
1691
		$relative_path = vsprintf( "$format%s", $escaped_args );
1692
1693
		if ( ! wp_startswith( $relative_path, '.' ) ) {
1694
			// Generic version. Match the requested version as best we can
1695
			$api_version = $this->get_closest_version_of_endpoint( $relative_path );
1696
			$base        = substr( $base, 0, - 1 ) . $api_version;
1697
		}
1698
1699
		// escape any % in the relative path before running it through sprintf again
1700
		$relative_path = str_replace( '%', '%%', $relative_path );
1701
		// http, WPCOM_JSON_API__BASE, ...    , path
1702
		// %s  , %s                  , $format, %s
1703
		return esc_url_raw( sprintf( "%s://%s$relative_path", $this->api->public_api_scheme, $base ) );
1704
	}
1705
1706
	function get_me_link( $path = '' ) {
1707
		return $this->get_link( '/me', $path );
1708
	}
1709
1710
	function get_taxonomy_link( $blog_id, $taxonomy_id, $taxonomy_type, $path = '' ) {
1711
		if ( 'category' === $taxonomy_type )
1712
			return $this->get_link( '/sites/%d/categories/slug:%s', $blog_id, $taxonomy_id, $path );
1713
		else
1714
			return $this->get_link( '/sites/%d/tags/slug:%s', $blog_id, $taxonomy_id, $path );
1715
	}
1716
1717
	function get_media_link( $blog_id, $media_id, $path = '' ) {
1718
		return $this->get_link( '/sites/%d/media/%d', $blog_id, $media_id, $path );
1719
	}
1720
1721
	function get_site_link( $blog_id, $path = '' ) {
1722
		return $this->get_link( '/sites/%d', $blog_id, $path );
1723
	}
1724
1725
	function get_post_link( $blog_id, $post_id, $path = '' ) {
1726
		return $this->get_link( '/sites/%d/posts/%d', $blog_id, $post_id, $path );
1727
	}
1728
1729
	function get_comment_link( $blog_id, $comment_id, $path = '' ) {
1730
		return $this->get_link( '/sites/%d/comments/%d', $blog_id, $comment_id, $path );
1731
	}
1732
1733
	function get_publicize_connection_link( $blog_id, $publicize_connection_id, $path = '' ) {
1734
		return $this->get_link( '.1/sites/%d/publicize-connections/%d', $blog_id, $publicize_connection_id, $path );
1735
	}
1736
1737
	function get_publicize_connections_link( $keyring_token_id, $path = '' ) {
1738
		return $this->get_link( '.1/me/publicize-connections/?keyring_connection_ID=%d', $keyring_token_id, $path );
1739
	}
1740
1741
	function get_keyring_connection_link( $keyring_token_id, $path = '' ) {
1742
		return $this->get_link( '.1/me/keyring-connections/%d', $keyring_token_id, $path );
1743
	}
1744
1745
	function get_external_service_link( $external_service, $path = '' ) {
1746
		return $this->get_link( '.1/meta/external-services/%s', $external_service, $path );
1747
	}
1748
1749
1750
	/**
1751
	* Check whether a user can view or edit a post type
1752
	* @param string $post_type              post type to check
1753
	* @param string $context                'display' or 'edit'
1754
	* @return bool
1755
	*/
1756
	function current_user_can_access_post_type( $post_type, $context='display' ) {
1757
		$post_type_object = get_post_type_object( $post_type );
1758
		if ( ! $post_type_object ) {
1759
			return false;
1760
		}
1761
1762
		switch( $context ) {
1763
			case 'edit':
1764
				return current_user_can( $post_type_object->cap->edit_posts );
1765
			case 'display':
1766
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1767
			default:
1768
				return false;
1769
		}
1770
	}
1771
1772
	function is_post_type_allowed( $post_type ) {
1773
		// if the post type is empty, that's fine, WordPress will default to post
1774
		if ( empty( $post_type ) )
1775
			return true;
1776
1777
		// allow special 'any' type
1778
		if ( 'any' == $post_type )
1779
			return true;
1780
1781
		// check for allowed types
1782
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) )
1783
			return true;
1784
1785
		return false;
1786
	}
1787
1788
	/**
1789
	 * Gets the whitelisted post types that JP should allow access to.
1790
	 *
1791
	 * @return array Whitelisted post types.
1792
	 */
1793
	protected function _get_whitelisted_post_types() {
1794
		$allowed_types = array( 'post', 'page', 'revision' );
1795
1796
		/**
1797
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1798
		 *
1799
		 * @module json-api
1800
		 *
1801
		 * @since 2.2.3
1802
		 *
1803
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1804
		 */
1805
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1806
1807
		return array_unique( $allowed_types );
1808
	}
1809
1810
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1811
1812
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1813
1814
		$media_ids = $errors = array();
1815
		$user_can_upload_files = current_user_can( 'upload_files' );
1816
		$media_attrs = array_values( $media_attrs ); // reset the keys
1817
		$i = 0;
1818
1819
		if ( ! empty( $media_files ) ) {
1820
			$this->api->trap_wp_die( 'upload_error' );
1821
			foreach ( $media_files as $media_item ) {
1822
				$_FILES['.api.media.item.'] = $media_item;
1823 View Code Duplication
				if ( ! $user_can_upload_files ) {
1824
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1825
				} else {
1826
					if ( $force_parent_id ) {
1827
						$parent_id = absint( $force_parent_id );
1828
					} elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1829
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1830
					} else {
1831
						$parent_id = 0;
1832
					}
1833
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1834
				}
1835
				if ( is_wp_error( $media_id ) ) {
1836
					$errors[$i]['file']   = $media_item['name'];
1837
					$errors[$i]['error']   = $media_id->get_error_code();
1838
					$errors[$i]['message'] = $media_id->get_error_message();
1839
				} else {
1840
					$media_ids[$i] = $media_id;
1841
				}
1842
1843
				$i++;
1844
			}
1845
			$this->api->trap_wp_die( null );
1846
			unset( $_FILES['.api.media.item.'] );
1847
		}
1848
1849
		if ( ! empty( $media_urls ) ) {
1850
			foreach ( $media_urls as $url ) {
1851 View Code Duplication
				if ( ! $user_can_upload_files ) {
1852
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1853
				} else {
1854
					if ( $force_parent_id ) {
1855
						$parent_id = absint( $force_parent_id );
1856
					} else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1857
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1858
					} else {
1859
						$parent_id = 0;
1860
					}
1861
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1862
				}
1863
				if ( is_wp_error( $media_id ) ) {
1864
					$errors[$i] = array(
1865
						'file'    => $url,
1866
						'error'   => $media_id->get_error_code(),
1867
						'message' => $media_id->get_error_message(),
1868
					);
1869
				} elseif ( ! empty( $media_id ) ) {
1870
					$media_ids[$i] = $media_id;
1871
				}
1872
1873
				$i++;
1874
			}
1875
		}
1876
1877
		if ( ! empty( $media_attrs ) ) {
1878
			foreach ( $media_ids as $index => $media_id ) {
1879
				if ( empty( $media_attrs[$index] ) )
1880
					continue;
1881
1882
				$attrs = $media_attrs[$index];
1883
				$insert = array();
1884
1885
				if ( ! empty( $attrs['title'] ) ) {
1886
					$insert['post_title'] = $attrs['title'];
1887
				}
1888
1889
				if ( ! empty( $attrs['caption'] ) )
1890
					$insert['post_excerpt'] = $attrs['caption'];
1891
1892
				if ( ! empty( $attrs['description'] ) )
1893
					$insert['post_content'] = $attrs['description'];
1894
1895
				if ( empty( $insert ) )
1896
					continue;
1897
1898
				$insert['ID'] = $media_id;
1899
				wp_update_post( (object) $insert );
1900
			}
1901
		}
1902
1903
		return array( 'media_ids' => $media_ids, 'errors' => $errors );
1904
1905
	}
1906
1907
	function handle_media_sideload( $url, $parent_post_id = 0 ) {
1908
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1909
			return false;
1910
1911
		// if we didn't get a URL, let's bail
1912
		$parsed = @parse_url( $url );
1913
		if ( empty( $parsed ) )
1914
			return false;
1915
1916
		$tmp = download_url( $url );
1917
		if ( is_wp_error( $tmp ) ) {
1918
			return $tmp;
1919
		}
1920
1921
		if ( ! file_is_displayable_image( $tmp ) ) {
1922
			@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...
1923
			return false;
1924
		}
1925
1926
		// emulate a $_FILES entry
1927
		$file_array = array(
1928
			'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1929
			'tmp_name' => $tmp,
1930
		);
1931
1932
		$id = media_handle_sideload( $file_array, $parent_post_id );
1933
		@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...
1934
1935
		if ( is_wp_error( $id ) ) {
1936
			return $id;
1937
		}
1938
1939
		if ( ! $id || ! is_int( $id ) ) {
1940
			return false;
1941
		}
1942
1943
		return $id;
1944
	}
1945
1946
	function allow_video_uploads( $mimes ) {
1947
		// if we are on Jetpack, bail - Videos are already allowed
1948
		if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1949
			return $mimes;
1950
		}
1951
1952
		// extra check that this filter is only ever applied during REST API requests
1953
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1954
			return $mimes;
1955
		}
1956
1957
		// bail early if they already have the upgrade..
1958
		if ( get_option( 'video_upgrade' ) == '1' ) {
1959
			return $mimes;
1960
		}
1961
1962
		// lets whitelist to only specific clients right now
1963
		$clients_allowed_video_uploads = array();
1964
		/**
1965
		 * Filter the list of whitelisted video clients.
1966
		 *
1967
		 * @module json-api
1968
		 *
1969
		 * @since 3.2.0
1970
		 *
1971
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1972
		 */
1973
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1974
		if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1975
			return $mimes;
1976
		}
1977
1978
		$mime_list = wp_get_mime_types();
1979
1980
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1981
		/**
1982
		 * Filter the video filetypes allowed on the site.
1983
		 *
1984
		 * @module json-api
1985
		 *
1986
		 * @since 3.2.0
1987
		 *
1988
		 * @param array $video_exts Array of video filetypes allowed on the site.
1989
		 */
1990
		$video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
1991
		$video_mimes = array();
1992
1993
		if ( !empty( $video_exts ) ) {
1994
			foreach ( $video_exts as $ext ) {
1995
				foreach ( $mime_list as $ext_pattern => $mime ) {
1996
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
1997
						$video_mimes[$ext_pattern] = $mime;
1998
				}
1999
			}
2000
2001
			$mimes = array_merge( $mimes, $video_mimes );
2002
		}
2003
2004
		return $mimes;
2005
	}
2006
2007
	function is_current_site_multi_user() {
2008
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2009
		if ( false === $users ) {
2010
			$user_query = new WP_User_Query( array(
2011
				'blog_id' => get_current_blog_id(),
2012
				'fields'  => 'ID',
2013
			) );
2014
			$users = (int) $user_query->get_total();
2015
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2016
		}
2017
		return $users > 1;
2018
	}
2019
2020
	function allows_cross_origin_requests() {
2021
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2022
	}
2023
2024
	function allows_unauthorized_requests( $origin, $complete_access_origins  ) {
2025
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2026
	}
2027
2028
	/**
2029
	 * Return endpoint response
2030
	 *
2031
	 * @param ... determined by ->$path
2032
	 *
2033
	 * @return
2034
	 * 	falsy: HTTP 500, no response body
2035
	 *	WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2036
	 *	$data: HTTP 200, json_encode( $data ) response body
2037
	 */
2038
	abstract function callback( $path = '' );
2039
2040
2041
}
2042
2043
require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
2044