Completed
Push — fix/contact-form-test-domeleme... ( 06be23...9956fb )
by
unknown
80:11 queued 71:56
created

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

Upgrade to new PHP Analysis Engine

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

Loading history...
1951
			'tmp_name' => $tmp,
1952
		);
1953
1954
		$id = media_handle_sideload( $file_array, $parent_post_id );
1955
		if ( file_exists( $tmp ) ) {
1956
			@unlink( $tmp );
1957
		}
1958
1959
		if ( is_wp_error( $id ) ) {
1960
			return $id;
1961
		}
1962
1963
		if ( ! $id || ! is_int( $id ) ) {
1964
			return false;
1965
		}
1966
1967
		return $id;
1968
	}
1969
1970
	/**
1971
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1972
	 *
1973
	 * @param string $file Path to file to get its mime type.
1974
	 *
1975
	 * @return bool
1976
	 */
1977
	protected function is_file_supported_for_sideloading( $file ) {
1978
		return jetpack_is_file_supported_for_sideloading( $file );
1979
	}
1980
1981
	function allow_video_uploads( $mimes ) {
1982
		// if we are on Jetpack, bail - Videos are already allowed
1983
		if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
1984
			return $mimes;
1985
		}
1986
1987
		// extra check that this filter is only ever applied during REST API requests
1988
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1989
			return $mimes;
1990
		}
1991
1992
		// bail early if they already have the upgrade..
1993
		if ( get_option( 'video_upgrade' ) == '1' ) {
1994
			return $mimes;
1995
		}
1996
1997
		// lets whitelist to only specific clients right now
1998
		$clients_allowed_video_uploads = array();
1999
		/**
2000
		 * Filter the list of whitelisted video clients.
2001
		 *
2002
		 * @module json-api
2003
		 *
2004
		 * @since 3.2.0
2005
		 *
2006
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
2007
		 */
2008
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
2009
		if ( ! in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
2010
			return $mimes;
2011
		}
2012
2013
		$mime_list = wp_get_mime_types();
2014
2015
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
2016
		/**
2017
		 * Filter the video filetypes allowed on the site.
2018
		 *
2019
		 * @module json-api
2020
		 *
2021
		 * @since 3.2.0
2022
		 *
2023
		 * @param array $video_exts Array of video filetypes allowed on the site.
2024
		 */
2025
		$video_exts  = apply_filters( 'video_upload_filetypes', $video_exts );
2026
		$video_mimes = array();
2027
2028
		if ( ! empty( $video_exts ) ) {
2029
			foreach ( $video_exts as $ext ) {
2030
				foreach ( $mime_list as $ext_pattern => $mime ) {
2031
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false ) {
2032
						$video_mimes[ $ext_pattern ] = $mime;
2033
					}
2034
				}
2035
			}
2036
2037
			$mimes = array_merge( $mimes, $video_mimes );
2038
		}
2039
2040
		return $mimes;
2041
	}
2042
2043
	function is_current_site_multi_user() {
2044
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2045
		if ( false === $users ) {
2046
			$user_query = new WP_User_Query(
2047
				array(
2048
					'blog_id' => get_current_blog_id(),
2049
					'fields'  => 'ID',
2050
				)
2051
			);
2052
			$users      = (int) $user_query->get_total();
2053
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2054
		}
2055
		return $users > 1;
2056
	}
2057
2058
	function allows_cross_origin_requests() {
2059
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2060
	}
2061
2062
	function allows_unauthorized_requests( $origin, $complete_access_origins ) {
2063
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2064
	}
2065
2066
	/**
2067
	 * Whether this endpoint accepts site based authentication for the current request.
2068
	 *
2069
	 * @since 9.1.0
2070
	 *
2071
	 * @return bool true, if Jetpack blog token is used and `allow_jetpack_site_auth` is true,
2072
	 * false otherwise.
2073
	 */
2074
	public function accepts_site_based_authentication() {
2075
		return $this->allow_jetpack_site_auth &&
2076
			$this->api->is_jetpack_authorized_for_site();
2077
	}
2078
2079
	function get_platform() {
2080
		return wpcom_get_sal_platform( $this->api->token_details );
2081
	}
2082
2083
	/**
2084
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2085
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
2086
	 *
2087
	 * Override this method if you want to do something different.
2088
	 *
2089
	 * @param  int $blog_id
2090
	 * @return bool
2091
	 */
2092
	function force_wpcom_request( $blog_id ) {
2093
		return false;
2094
	}
2095
2096
	/**
2097
	 * Get an array of all valid AMP origins for a blog's siteurl.
2098
	 *
2099
	 * @param string $siteurl Origin url of the API request.
2100
	 * @return array
2101
	 */
2102
	public function get_amp_cache_origins( $siteurl ) {
2103
		$host = parse_url( $siteurl, PHP_URL_HOST );
2104
2105
		/*
2106
		 * From AMP docs:
2107
		 * "When possible, the Google AMP Cache will create a subdomain for each AMP document's domain by first converting it
2108
		 * from IDN (punycode) to UTF-8. The caches replaces every - (dash) with -- (2 dashes) and replace every . (dot) with
2109
		 * - (dash). For example, pub.com will map to pub-com.cdn.ampproject.org."
2110
		 */
2111
		if ( function_exists( 'idn_to_utf8' ) ) {
2112
			// The third parameter is set explicitly to prevent issues with newer PHP versions compiled with an old ICU version.
2113
			// phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated
2114
			$host = idn_to_utf8( $host, IDNA_DEFAULT, defined( 'INTL_IDNA_VARIANT_UTS46' ) ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003 );
2115
		}
2116
		$subdomain = str_replace( array( '-', '.' ), array( '--', '-' ), $host );
2117
		return array(
2118
			$siteurl,
2119
			// Google AMP Cache (legacy).
2120
			'https://cdn.ampproject.org',
2121
			// Google AMP Cache subdomain.
2122
			sprintf( 'https://%s.cdn.ampproject.org', $subdomain ),
2123
			// Cloudflare AMP Cache.
2124
			sprintf( 'https://%s.amp.cloudflare.com', $subdomain ),
2125
			// Bing AMP Cache.
2126
			sprintf( 'https://%s.bing-amp.com', $subdomain ),
2127
		);
2128
	}
2129
2130
	/**
2131
	 * Return endpoint response
2132
	 *
2133
	 * @param string $path ... determined by ->$path.
2134
	 *
2135
	 * @return array|WP_Error
2136
	 *  falsy: HTTP 500, no response body
2137
	 *  WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2138
	 *  $data: HTTP 200, json_encode( $data ) response body
2139
	 */
2140
	abstract public function callback( $path = '' );
2141
2142
2143
}
2144
2145
require_once dirname( __FILE__ ) . '/json-endpoints.php';
2146