Completed
Push — fix/min-php-everywhere ( 582b89...e9c8a1 )
by
unknown
07:08
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
	function __construct( $args ) {
131
		$defaults = array(
132
			'in_testing'           => false,
133
			'allowed_if_flagged'   => false,
134
			'allowed_if_red_flagged' => false,
135
			'allowed_if_deleted'	=> false,
136
			'description'          => '',
137
			'group'	               => '',
138
			'method'               => 'GET',
139
			'path'                 => '/',
140
			'min_version'          => '0',
141
			'max_version'          => WPCOM_JSON_API__CURRENT_VERSION,
142
			'force'	               => '',
143
			'deprecated'           => false,
144
			'new_version'          => WPCOM_JSON_API__CURRENT_VERSION,
145
			'jp_disabled'          => false,
146
			'path_labels'          => array(),
147
			'request_format'       => array(),
148
			'response_format'      => array(),
149
			'query_parameters'     => array(),
150
			'version'              => 'v1',
151
			'example_request'      => '',
152
			'example_request_data' => '',
153
			'example_response'     => '',
154
			'required_scope'       => '',
155
			'pass_wpcom_user_details' => false,
156
			'custom_fields_filtering' => false,
157
			'allow_cross_origin_request' => false,
158
			'allow_unauthorized_request' => false,
159
			'allow_jetpack_site_auth'    => false,
160
			'allow_upload_token_auth'    => false,
161
		);
162
163
		$args = wp_parse_args( $args, $defaults );
164
165
		$this->in_testing  = $args['in_testing'];
166
167
		$this->allowed_if_flagged = $args['allowed_if_flagged'];
168
		$this->allowed_if_red_flagged = $args['allowed_if_red_flagged'];
169
		$this->allowed_if_deleted = $args['allowed_if_deleted'];
170
171
		$this->description = $args['description'];
172
		$this->group       = $args['group'];
173
		$this->stat        = $args['stat'];
174
		$this->force	   = $args['force'];
175
		$this->jp_disabled = $args['jp_disabled'];
176
177
		$this->method      = $args['method'];
178
		$this->path        = $args['path'];
179
		$this->path_labels = $args['path_labels'];
180
		$this->min_version = $args['min_version'];
181
		$this->max_version = $args['max_version'];
182
		$this->deprecated  = $args['deprecated'];
183
		$this->new_version = $args['new_version'];
184
185
		// Ensure max version is not less than min version
186
		if ( version_compare( $this->min_version, $this->max_version, '>' ) ) {
187
			$this->max_version = $this->min_version;
188
		}
189
190
		$this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
191
		$this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
192
193
		$this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
194
		$this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
195
		$this->allow_jetpack_site_auth    = (bool) $args['allow_jetpack_site_auth'];
196
		$this->allow_upload_token_auth    = (bool) $args['allow_upload_token_auth'];
197
198
		$this->version     = $args['version'];
199
200
		$this->required_scope = $args['required_scope'];
201
202 View Code Duplication
		if ( $this->request_format ) {
203
			$this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
204
		} else {
205
			$this->request_format = $args['request_format'];
206
		}
207
208 View Code Duplication
		if ( $this->response_format ) {
209
			$this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
210
		} else {
211
			$this->response_format = $args['response_format'];
212
		}
213
214
		if ( false === $args['query_parameters'] ) {
215
			$this->query = array();
216
		} elseif ( is_array( $args['query_parameters'] ) ) {
217
			$this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
218
		}
219
220
		$this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
221
		$this->links = WPCOM_JSON_API_Links::getInstance();
222
223
		/** Example Request/Response ******************************************/
224
225
		// Examples for endpoint documentation request
226
		$this->example_request      = $args['example_request'];
227
		$this->example_request_data = $args['example_request_data'];
228
		$this->example_response     = $args['example_response'];
229
230
		$this->api->add( $this );
231
	}
232
233
	// Get all query args.  Prefill with defaults
234
	function query_args( $return_default_values = true, $cast_and_filter = true ) {
235
		$args = array_intersect_key( $this->api->query, $this->query );
236
237
		if ( !$cast_and_filter ) {
238
			return $args;
239
		}
240
241
		return $this->cast_and_filter( $args, $this->query, $return_default_values );
242
	}
243
244
	// Get POST body data
245
	function input( $return_default_values = true, $cast_and_filter = true ) {
246
		$input = trim( $this->api->post_body );
247
		$content_type = $this->api->content_type;
248
		if ( $content_type ) {
249
			list ( $content_type ) = explode( ';', $content_type );
250
		}
251
		$content_type = trim( $content_type );
252
		switch ( $content_type ) {
253
		case 'application/json' :
254
		case 'application/x-javascript' :
255
		case 'text/javascript' :
256
		case 'text/x-javascript' :
257
		case 'text/x-json' :
258
		case 'text/json' :
259
			$return = json_decode( $input, true );
260
261
			if ( function_exists( 'json_last_error' ) ) {
262
				if ( JSON_ERROR_NONE !== json_last_error() ) { // phpcs:ignore PHPCompatibility
263
					return null;
264
				}
265
			} else {
266
				if ( is_null( $return ) && json_encode( null ) !== $input ) {
267
					return null;
268
				}
269
			}
270
271
			break;
272
		case 'multipart/form-data' :
273
			$return = array_merge( stripslashes_deep( $_POST ), $_FILES );
274
			break;
275
		case 'application/x-www-form-urlencoded' :
276
			//attempt JSON first, since probably a curl command
277
			$return = json_decode( $input, true );
278
279
			if ( is_null( $return ) ) {
280
				wp_parse_str( $input, $return );
281
			}
282
283
			break;
284
		default :
285
			wp_parse_str( $input, $return );
286
			break;
287
		}
288
289
		if ( isset( $this->api->query['force'] )
290
		    && 'secure' === $this->api->query['force']
291
		    && isset( $return['secure_key'] ) ) {
292
			$this->api->post_body = $this->get_secure_body( $return['secure_key'] );
293
			$this->api->query['force'] = false;
294
			return $this->input( $return_default_values, $cast_and_filter );
295
		}
296
297
		if ( $cast_and_filter ) {
298
			$return = $this->cast_and_filter( $return, $this->request_format, $return_default_values );
299
		}
300
		return $return;
301
	}
302
303
304
	protected function get_secure_body( $secure_key ) {
305
		$response = Client::wpcom_json_api_request_as_blog(
306
			sprintf( '/sites/%d/secure-request', Jetpack_Options::get_option('id' ) ),
307
			'1.1',
308
			array( 'method' => 'POST' ),
309
			array( 'secure_key' => $secure_key )
310
		);
311
		if ( 200 !== $response['response']['code'] ) {
312
			return null;
313
		}
314
		return json_decode( $response['body'], true );
315
	}
316
317
	function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
318
		$return_as_object = false;
319
		if ( is_object( $data ) ) {
320
			// @todo this should probably be a deep copy if $data can ever have nested objects
321
			$data = (array) $data;
322
			$return_as_object = true;
323
		} elseif ( !is_array( $data ) ) {
324
			return $data;
325
		}
326
327
		$boolean_arg = array( 'false', 'true' );
328
		$naeloob_arg = array( 'true', 'false' );
329
330
		$return = array();
331
332
		foreach ( $documentation as $key => $description ) {
333
			if ( is_array( $description ) ) {
334
				// String or boolean array keys only
335
				$whitelist = array_keys( $description );
336
337
				if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
338
					// Truthiness
339
					if ( isset( $data[$key] ) ) {
340
						$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $data[$key] );
341
					} elseif ( $return_default_values ) {
342
						$return[$key] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg.
343
					}
344
				} elseif ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) {
345
					// String Key
346
					$return[$key] = (string) $data[$key];
347
				} elseif ( $return_default_values ) {
348
					// Default value
349
					$return[$key] = (string) current( $whitelist );
350
				}
351
352
				continue;
353
			}
354
355
			$types = $this->parse_types( $description );
356
			$type = array_shift( $types );
357
358
			// Explicit default - string and int only for now.  Always set these reguardless of $return_default_values
359
			if ( isset( $type['default'] ) ) {
360
				if ( !isset( $data[$key] ) ) {
361
					$data[$key] = $type['default'];
362
				}
363
			}
364
365
			if ( !isset( $data[$key] ) ) {
366
				continue;
367
			}
368
369
			$this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output );
370
		}
371
372
		if ( $return_as_object ) {
373
			return (object) $return;
374
		}
375
376
		return $return;
377
	}
378
379
	/**
380
	 * Casts $value according to $type.
381
	 * Handles fallbacks for certain values of $type when $value is not that $type
382
	 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way),
383
	 * and string -> object (one way)
384
	 *
385
	 * Handles "child types" - array:URL, object:category
386
	 * array:URL means an array of URLs
387
	 * object:category means a hash of categories
388
	 *
389
	 * Handles object typing - object>post means an object of type post
390
	 */
391
	function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
392
		if ( is_string( $type ) ) {
393
			$type = compact( 'type' );
394
		}
395
396
		switch ( $type['type'] ) {
397
		case 'false' :
398
			$return[$key] = false;
399
			break;
400
		case 'url' :
401
			if ( is_object( $value ) && isset( $value->url ) && false !== strpos( $value->url, 'https://videos.files.wordpress.com/' ) ) {
402
				$value = $value->url;
403
			}
404
			// Check for string since esc_url_raw() expects one.
405
			if ( ! is_string( $value ) ) {
406
				break;
407
			}
408
			$return[$key] = (string) esc_url_raw( $value );
409
			break;
410
		case 'string' :
411
			// Fallback string -> array, or for string -> object
412
			if ( is_array( $value ) || is_object( $value ) ) {
413 View Code Duplication
				if ( !empty( $types[0] ) ) {
414
					$next_type = array_shift( $types );
415
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
416
				}
417
			}
418
419
			// Fallback string -> false
420 View Code Duplication
			if ( !is_string( $value ) ) {
421
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
422
					$next_type = array_shift( $types );
423
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
424
				}
425
			}
426
			$return[$key] = (string) $value;
427
			break;
428
		case 'html' :
429
			$return[$key] = (string) $value;
430
			break;
431
		case 'safehtml' :
432
			$return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() );
433
			break;
434
		case 'zip' :
435
		case 'media' :
436
			if ( is_array( $value ) ) {
437
				if ( isset( $value['name'] ) && is_array( $value['name'] ) ) {
438
					// It's a $_FILES array
439
					// Reformat into array of $_FILES items
440
					$files = array();
441
442
					foreach ( $value['name'] as $k => $v ) {
443
						$files[$k] = array();
444
						foreach ( array_keys( $value ) as $file_key ) {
445
							$files[$k][$file_key] = $value[$file_key][$k];
446
						}
447
					}
448
449
					$return[$key] = $files;
450
					break;
451
				}
452
			} else {
453
				// no break - treat as 'array'
454
			}
455
			// nobreak
456
		case 'array' :
457
			// Fallback array -> string
458 View Code Duplication
			if ( is_string( $value ) ) {
459
				if ( !empty( $types[0] ) ) {
460
					$next_type = array_shift( $types );
461
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
462
				}
463
			}
464
465 View Code Duplication
			if ( isset( $type['children'] ) ) {
466
				$children = array();
467
				foreach ( (array) $value as $k => $child ) {
468
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
469
				}
470
				$return[$key] = (array) $children;
471
				break;
472
			}
473
474
			$return[$key] = (array) $value;
475
			break;
476
		case 'iso 8601 datetime' :
477
		case 'datetime' :
478
			// (string)s
479
			$dates = $this->parse_date( (string) $value );
480
			if ( $for_output ) {
481
				$return[$key] = $this->format_date( $dates[1], $dates[0] );
482
			} else {
483
				list( $return[$key], $return["{$key}_gmt"] ) = $dates;
484
			}
485
			break;
486
		case 'float' :
487
			$return[$key] = (float) $value;
488
			break;
489
		case 'int' :
490
		case 'integer' :
491
			$return[$key] = (int) $value;
492
			break;
493
		case 'bool' :
494
		case 'boolean' :
495
			$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value );
496
			break;
497
		case 'object' :
498
			// Fallback object -> false
499 View Code Duplication
			if ( is_scalar( $value ) || is_null( $value ) ) {
500
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
501
					return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
502
				}
503
			}
504
505 View Code Duplication
			if ( isset( $type['children'] ) ) {
506
				$children = array();
507
				foreach ( (array) $value as $k => $child ) {
508
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
509
				}
510
				$return[$key] = (object) $children;
511
				break;
512
			}
513
514
			if ( isset( $type['subtype'] ) ) {
515
				return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
516
			}
517
518
			$return[$key] = (object) $value;
519
			break;
520
		case 'post' :
521
			$return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
522
			break;
523
		case 'comment' :
524
			$return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
525
			break;
526
		case 'tag' :
527
		case 'category' :
528
			$docs = array(
529
				'ID'          => '(int)',
530
				'name'        => '(string)',
531
				'slug'        => '(string)',
532
				'description' => '(HTML)',
533
				'post_count'  => '(int)',
534
				'feed_url'    => '(string)',
535
				'meta'        => '(object)',
536
			);
537
			if ( 'category' === $type['type'] ) {
538
				$docs['parent'] = '(int)';
539
			}
540
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
541
			break;
542
		case 'post_reference' :
543 View Code Duplication
		case 'comment_reference' :
544
			$docs = array(
545
				'ID'    => '(int)',
546
				'type'  => '(string)',
547
				'title' => '(string)',
548
				'link'  => '(URL)',
549
			);
550
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
551
			break;
552 View Code Duplication
		case 'geo' :
553
			$docs = array(
554
				'latitude'  => '(float)',
555
				'longitude' => '(float)',
556
				'address'   => '(string)',
557
			);
558
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
559
			break;
560
		case 'author' :
561
			$docs = array(
562
				'ID'             => '(int)',
563
				'user_login'     => '(string)',
564
				'login'          => '(string)',
565
				'email'          => '(string|false)',
566
				'name'           => '(string)',
567
				'first_name'     => '(string)',
568
				'last_name'      => '(string)',
569
				'nice_name'      => '(string)',
570
				'URL'            => '(URL)',
571
				'avatar_URL'     => '(URL)',
572
				'profile_URL'    => '(URL)',
573
				'is_super_admin' => '(bool)',
574
				'roles'          => '(array:string)',
575
				'ip_address'     => '(string|false)',
576
			);
577
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
578
			break;
579 View Code Duplication
		case 'role' :
580
			$docs = array(
581
				'name'         => '(string)',
582
				'display_name' => '(string)',
583
				'capabilities' => '(object:boolean)',
584
			);
585
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
586
			break;
587
		case 'attachment' :
588
			$docs = array(
589
				'ID'        => '(int)',
590
				'URL'       => '(URL)',
591
				'guid'      => '(string)',
592
				'mime_type' => '(string)',
593
				'width'     => '(int)',
594
				'height'    => '(int)',
595
				'duration'  => '(int)',
596
			);
597
			$return[$key] = (object) $this->cast_and_filter(
598
				$value,
599
				/**
600
				 * Filter the documentation returned for a post attachment.
601
				 *
602
				 * @module json-api
603
				 *
604
				 * @since 1.9.0
605
				 *
606
				 * @param array $docs Array of documentation about a post attachment.
607
				 */
608
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
609
				false,
610
				$for_output
611
			);
612
			break;
613
		case 'metadata' :
614
			$docs = array(
615
				'id'       => '(int)',
616
				'key'       => '(string)',
617
				'value'     => '(string|false|float|int|array|object)',
618
				'previous_value' => '(string)',
619
				'operation'  => '(string)',
620
			);
621
			$return[$key] = (object) $this->cast_and_filter(
622
				$value,
623
				/** This filter is documented in class.json-api-endpoints.php */
624
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
625
				false,
626
				$for_output
627
			);
628
			break;
629
		case 'plugin' :
630
			$docs = array(
631
				'id'            => '(safehtml) The plugin\'s ID',
632
				'slug'          => '(safehtml) The plugin\'s Slug',
633
				'active'        => '(boolean)  The plugin status.',
634
				'update'        => '(object)   The plugin update info.',
635
				'name'          => '(safehtml) The name of the plugin.',
636
				'plugin_url'    => '(url)      Link to the plugin\'s web site.',
637
				'version'       => '(safehtml) The plugin version number.',
638
				'description'   => '(safehtml) Description of what the plugin does and/or notes from the author',
639
				'author'        => '(safehtml) The plugin author\'s name',
640
				'author_url'    => '(url)      The plugin author web site address',
641
				'network'       => '(boolean)  Whether the plugin can only be activated network wide.',
642
				'autoupdate'    => '(boolean)  Whether the plugin is auto updated',
643
				'log'           => '(array:safehtml) An array of update log strings.',
644
				'action_links'  => '(array) An array of action links that the plugin uses.',
645
			);
646
			$return[$key] = (object) $this->cast_and_filter(
647
				$value,
648
				/**
649
				 * Filter the documentation returned for a plugin.
650
				 *
651
				 * @module json-api
652
				 *
653
				 * @since 3.1.0
654
				 *
655
				 * @param array $docs Array of documentation about a plugin.
656
				 */
657
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
658
				false,
659
				$for_output
660
			);
661
			break;
662
		case 'plugin_v1_2' :
663
			$docs = class_exists( 'Jetpack_JSON_API_Get_Plugins_v1_2_Endpoint' )
664
				? Jetpack_JSON_API_Get_Plugins_v1_2_Endpoint::$_response_format
665
				: Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2;
666
			$return[$key] = (object) $this->cast_and_filter(
667
				$value,
668
				/**
669
				 * Filter the documentation returned for a plugin.
670
				 *
671
				 * @module json-api
672
				 *
673
				 * @since 3.1.0
674
				 *
675
				 * @param array $docs Array of documentation about a plugin.
676
				 */
677
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
678
				false,
679
				$for_output
680
			);
681
			break;
682
		case 'file_mod_capabilities':
683
			$docs           = array(
684
				'reasons_modify_files_unavailable' => '(array) The reasons why files can\'t be modified',
685
				'reasons_autoupdate_unavailable'   => '(array) The reasons why autoupdates aren\'t allowed',
686
				'modify_files'                     => '(boolean) true if files can be modified',
687
				'autoupdate_files'                 => '(boolean) true if autoupdates are allowed',
688
			);
689
			$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
690
			break;
691
		case 'jetpackmodule' :
692
			$docs = array(
693
				'id'          => '(string)   The module\'s ID',
694
				'active'      => '(boolean)  The module\'s status.',
695
				'name'        => '(string)   The module\'s name.',
696
				'description' => '(safehtml) The module\'s description.',
697
				'sort'        => '(int)      The module\'s display order.',
698
				'introduced'  => '(string)   The Jetpack version when the module was introduced.',
699
				'changed'     => '(string)   The Jetpack version when the module was changed.',
700
				'free'        => '(boolean)  The module\'s Free or Paid status.',
701
				'module_tags' => '(array)    The module\'s tags.',
702
				'override'    => '(string)   The module\'s override. Empty if no override, otherwise \'active\' or \'inactive\'',
703
			);
704
			$return[$key] = (object) $this->cast_and_filter(
705
				$value,
706
				/** This filter is documented in class.json-api-endpoints.php */
707
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
708
				false,
709
				$for_output
710
			);
711
			break;
712
		case 'sharing_button' :
713
			$docs = array(
714
				'ID'         => '(string)',
715
				'name'       => '(string)',
716
				'URL'        => '(string)',
717
				'icon'       => '(string)',
718
				'enabled'    => '(bool)',
719
				'visibility' => '(string)',
720
			);
721
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
722
			break;
723
		case 'sharing_button_service':
724
			$docs = array(
725
				'ID'               => '(string) The service identifier',
726
				'name'             => '(string) The service name',
727
				'class_name'       => '(string) Class name for custom style sharing button elements',
728
				'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
729
				'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
730
				'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview'
731
			);
732
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
733
			break;
734
		case 'site_keyring':
735
			$docs = array(
736
				'keyring_id'       => '(int) Keyring ID',
737
				'service'          => '(string) The service name',
738
				'external_user_id' => '(string) External user id for the service'
739
			);
740
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
741
			break;
742
		case 'taxonomy':
743
			$docs = array(
744
				'name'         => '(string) The taxonomy slug',
745
				'label'        => '(string) The taxonomy human-readable name',
746
				'labels'       => '(object) Mapping of labels for the taxonomy',
747
				'description'  => '(string) The taxonomy description',
748
				'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
749
				'public'       => '(bool) Whether the taxonomy is public',
750
				'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
751
			);
752
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
753
			break;
754
755
		default :
756
			$method_name = $type['type'] . '_docs';
757
			if ( method_exists( 'WPCOM_JSON_API_Jetpack_Overrides', $method_name ) ) {
758
				$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
759
			}
760
761
			if ( ! empty( $docs ) ) {
762
				$return[$key] = (object) $this->cast_and_filter(
763
					$value,
764
					/** This filter is documented in class.json-api-endpoints.php */
765
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
766
					false,
767
					$for_output
768
				);
769
			} else {
770
				trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
771
			}
772
		}
773
	}
774
775
	function parse_types( $text ) {
776
		if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
777
			return 'none';
778
		}
779
780
		$types = explode( '|', strtolower( $matches[1] ) );
781
		$return = array();
782
		foreach ( $types as $type ) {
783
			foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
784
				if ( false !== strpos( $type, $operator ) ) {
785
					$item = explode( $operator, $type, 2 );
786
					$return[] = array( 'type' => $item[0], $meaning => $item[1] );
787
					continue 2;
788
				}
789
			}
790
			$return[] = compact( 'type' );
791
		}
792
793
		return $return;
794
	}
795
796
	/**
797
	 * Checks if the endpoint is publicly displayable
798
	 */
799
	function is_publicly_documentable() {
800
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
801
	}
802
803
	/**
804
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
805
	 * Echoes HTML.
806
	 */
807
	function document( $show_description = true ) {
808
		global $wpdb;
809
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
810
		unset( $GLOBALS['post'] );
811
812
		$doc = $this->generate_documentation();
813
814
		if ( $show_description ) :
815
?>
816
<caption>
817
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
818
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
819
</caption>
820
821
<?php endif; ?>
822
823
<?php if ( true === $this->deprecated ) { ?>
824
<p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $this->new_version ); ?></strong></p>
825
<?php } ?>
826
827
<section class="resource-info">
828
	<h2 id="apidoc-resource-info">Resource Information</h2>
829
830
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
831
832
	<thead>
833
		<tr>
834
			<th class="api-index-title" scope="column">&nbsp;</th>
835
			<th class="api-index-title" scope="column">&nbsp;</th>
836
		</tr>
837
	</thead>
838
	<tbody>
839
840
		<tr class="api-index-item">
841
			<th scope="row" class="parameter api-index-item-title">Method</th>
842
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
843
		</tr>
844
845
		<tr class="api-index-item">
846
			<th scope="row" class="parameter api-index-item-title">URL</th>
847
			<?php
848
			$version = WPCOM_JSON_API__CURRENT_VERSION;
849
			if ( !empty( $this->max_version ) ) {
850
				$version = $this->max_version;
851
			}
852
			?>
853
			<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>
854
		</tr>
855
856
		<tr class="api-index-item">
857
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
858
			<?php
859
			$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'] ) );
860
			?>
861
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
862
		</tr>
863
864
	</tbody>
865
	</table>
866
867
</section>
868
869
<?php
870
871
		foreach ( array(
872
			'path'     => 'Method Parameters',
873
			'query'    => 'Query Parameters',
874
			'body'     => 'Request Parameters',
875
			'response' => 'Response Parameters',
876
		) as $doc_section_key => $label ) :
877
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
878
			if ( !$doc_section ) {
879
				continue;
880
			}
881
882
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
883
?>
884
885
<section class="<?php echo $param_label; ?>">
886
887
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
888
889
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
890
891
<thead>
892
	<tr>
893
		<th class="api-index-title" scope="column">Parameter</th>
894
		<th class="api-index-title" scope="column">Type</th>
895
		<th class="api-index-title" scope="column">Description</th>
896
	</tr>
897
</thead>
898
<tbody>
899
900
<?php foreach ( $doc_section as $key => $item ) : ?>
901
902
	<tr class="api-index-item">
903
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
904
		<td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
905
		<td class="description api-index-item-body"><?php
906
907
		$this->generate_doc_description( $item['description'] );
908
909
		?></td>
910
	</tr>
911
912
<?php endforeach; ?>
913
</tbody>
914
</table>
915
</section>
916
<?php endforeach; ?>
917
918
<?php
919
		if ( 'unset' !== $original_post ) {
920
			$GLOBALS['post'] = $original_post;
921
		}
922
	}
923
924
	function add_http_build_query_to_php_content_example( $matches ) {
925
		$trimmed_match = ltrim( $matches[0] );
926
		$pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
927
		$pad = ltrim( $pad, ' ' );
928
		$return = '  ' . str_replace( "\n", "\n  ", $matches[0] );
929
		return " http_build_query({$return}{$pad})";
930
	}
931
932
	/**
933
	 * Recursively generates the <dl>'s to document item descriptions.
934
	 * Echoes HTML.
935
	 */
936
	function generate_doc_description( $item ) {
937
		if ( is_array( $item ) ) : ?>
938
939
		<dl>
940
<?php			foreach ( $item as $description_key => $description_value ) : ?>
941
942
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
943
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
944
945
<?php			endforeach; ?>
946
947
		</dl>
948
949
<?php
950
		else :
951
			echo wp_kses_post( $item );
952
		endif;
953
	}
954
955
	/**
956
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
957
	 * Echoes HTML.
958
	 */
959
	function generate_documentation() {
960
		$format       = str_replace( '%d', '%s', $this->path );
961
		$path_labeled = $format;
962
		if ( ! empty( $this->path_labels ) ) {
963
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
964
		}
965
		$boolean_arg  = array( 'false', 'true' );
966
		$naeloob_arg  = array( 'true', 'false' );
967
968
		$doc = array(
969
			'description'  => $this->description,
970
			'method'       => $this->method,
971
			'path_format'  => $this->path,
972
			'path_labeled' => $path_labeled,
973
			'group'        => $this->group,
974
			'request' => array(
975
				'path'  => array(),
976
				'query' => array(),
977
				'body'  => array(),
978
			),
979
			'response' => array(
980
				'body' => array(),
981
			)
982
		);
983
984
		foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
985
			foreach ( (array) $this->$_property as $key => $description ) {
986
				if ( is_array( $description ) ) {
987
					$description_keys = array_keys( $description );
988
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
989
						$type = '(bool)';
990
					} else {
991
						$type = '(string)';
992
					}
993
994
					if ( 'response_format' !== $_property ) {
995
						// hack - don't show "(default)" in response format
996
						reset( $description );
997
						$description_key = key( $description );
998
						$description[$description_key] = "(default) {$description[$description_key]}";
999
					}
1000
				} else {
1001
					$types   = $this->parse_types( $description );
1002
					$type    = array();
1003
					$default = '';
1004
1005
					if ( 'none' == $types ) {
1006
						$types = array();
1007
						$types[]['type'] = 'none';
1008
					}
1009
1010
					foreach ( $types as $type_array ) {
1011
						$type[] = $type_array['type'];
1012
						if ( isset( $type_array['default'] ) ) {
1013
							$default = $type_array['default'];
1014
							if ( 'string' === $type_array['type'] ) {
1015
								$default = "'$default'";
1016
							}
1017
						}
1018
					}
1019
					$type = '(' . join( '|', $type ) . ')';
1020
					$noop = ''; // skip an index in list below
1021
					list( $noop, $description ) = explode( ')', $description, 2 );
1022
					$description = trim( $description );
1023
					if ( $default ) {
1024
						$description .= " Default: $default.";
1025
					}
1026
				}
1027
1028
				$item = compact( 'type', 'description' );
1029
1030
				if ( 'response_format' === $_property ) {
1031
					$doc['response'][$doc_item][$key] = $item;
1032
				} else {
1033
					$doc['request'][$doc_item][$key] = $item;
1034
				}
1035
			}
1036
		}
1037
1038
		return $doc;
1039
	}
1040
1041
	function user_can_view_post( $post_id ) {
1042
		$post = get_post( $post_id );
1043
		if ( !$post || is_wp_error( $post ) ) {
1044
			return false;
1045
		}
1046
1047 View Code Duplication
		if ( 'inherit' === $post->post_status ) {
1048
			$parent_post = get_post( $post->post_parent );
1049
			$post_status_obj = get_post_status_object( $parent_post->post_status );
1050
		} else {
1051
			$post_status_obj = get_post_status_object( $post->post_status );
1052
		}
1053
1054
		if ( !$post_status_obj->public ) {
1055
			if ( is_user_logged_in() ) {
1056
				if ( $post_status_obj->protected ) {
1057
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1058
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1059
					}
1060
				} elseif ( $post_status_obj->private ) {
1061
					if ( !current_user_can( 'read_post', $post->ID ) ) {
1062
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1063
					}
1064
				} elseif ( in_array( $post->post_status, array( 'inherit', 'trash' ) ) ) {
1065
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1066
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1067
					}
1068
				} elseif ( 'auto-draft' === $post->post_status ) {
1069
					//allow auto-drafts
1070
				} else {
1071
					return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1072
				}
1073
			} else {
1074
				return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1075
			}
1076
		}
1077
1078 View Code Duplication
		if (
1079
			-1 == get_option( 'blog_public' ) &&
1080
			/**
1081
			 * Filter access to a specific post.
1082
			 *
1083
			 * @module json-api
1084
			 *
1085
			 * @since 3.4.0
1086
			 *
1087
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1088
			 * @param WP_Post $post Post data.
1089
			 */
1090
			! apply_filters(
1091
				'wpcom_json_api_user_can_view_post',
1092
				current_user_can( 'read_post', $post->ID ),
1093
				$post
1094
			)
1095
		) {
1096
			return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

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

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

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

Loading history...
1097
		}
1098
1099 View Code Duplication
		if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
1100
			return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

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

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

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

Loading history...
1101
		}
1102
1103
		return true;
1104
	}
1105
1106
	/**
1107
	 * Returns author object.
1108
	 *
1109
	 * @param object $author user ID, user row, WP_User object, comment row, post row
1110
	 * @param bool $show_email_and_ip output the author's email address and IP address?
1111
	 *
1112
	 * @return object
1113
	 */
1114
	function get_author( $author, $show_email_and_ip = false ) {
1115
		$ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1116
1117
		if ( isset( $author->comment_author_email ) ) {
1118
			$ID          = 0;
1119
			$login       = '';
1120
			$email       = $author->comment_author_email;
1121
			$name        = $author->comment_author;
1122
			$first_name  = '';
1123
			$last_name   = '';
1124
			$URL         = $author->comment_author_url;
1125
			$avatar_URL  = $this->api->get_avatar_url( $author );
1126
			$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1127
			$nice        = '';
1128
			$site_id     = -1;
1129
1130
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1131
			// "&" is the only email/URL character altered by wp_kses()
1132
			foreach ( array( 'email', 'URL' ) as $field ) {
1133
				$$field = str_replace( '&amp;', '&', $$field );
1134
			}
1135
		} else {
1136
			if ( isset( $author->user_id ) && $author->user_id ) {
1137
				$author = $author->user_id;
1138
			} elseif ( isset( $author->user_email ) ) {
1139
				$author = $author->ID;
1140
			} elseif ( isset( $author->post_author ) ) {
1141
				// then $author is a Post Object.
1142
				if ( 0 == $author->post_author )
1143
					return null;
1144
				/**
1145
				 * Filter whether the current site is a Jetpack site.
1146
				 *
1147
				 * @module json-api
1148
				 *
1149
				 * @since 3.3.0
1150
				 *
1151
				 * @param bool false Is the current site a Jetpack site. Default to false.
1152
				 * @param int get_current_blog_id() Blog ID.
1153
				 */
1154
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1155
				$post_id = $author->ID;
1156
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1157
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1158
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1159
					$login      = '';
1160
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1161
					$first_name = '';
1162
					$last_name  = '';
1163
					$URL        = '';
1164
					$nice       = '';
1165
				} else {
1166
					$author = $author->post_author;
1167
				}
1168
			}
1169
1170
			if ( ! isset( $ID ) ) {
1171
				$user = get_user_by( 'id', $author );
1172
				if ( ! $user || is_wp_error( $user ) ) {
1173
					trigger_error( 'Unknown user', E_USER_WARNING );
1174
1175
					return null;
1176
				}
1177
				$ID         = $user->ID;
1178
				$email      = $user->user_email;
1179
				$login      = $user->user_login;
1180
				$name       = $user->display_name;
1181
				$first_name = $user->first_name;
1182
				$last_name  = $user->last_name;
1183
				$URL        = $user->user_url;
1184
				$nice       = $user->user_nicename;
1185
			}
1186
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1187
				$active_blog = get_active_blog_for_user( $ID );
1188
				$site_id     = $active_blog->blog_id;
1189
				if ( $site_id > -1 ) {
1190
					$site_visible = (
1191
						-1 != $active_blog->public ||
1192
						is_private_blog_user( $site_id, get_current_user_id() )
1193
					);
1194
				}
1195
				$profile_URL = "https://en.gravatar.com/{$login}";
1196
			} else {
1197
				$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1198
				$site_id     = -1;
1199
			}
1200
1201
			$avatar_URL = $this->api->get_avatar_url( $email );
1202
		}
1203
1204
		if ( $show_email_and_ip ) {
1205
			$email = (string) $email;
1206
			$ip_address = (string) $ip_address;
1207
		} else {
1208
			$email = false;
1209
			$ip_address = false;
1210
		}
1211
1212
		$author = array(
1213
			'ID'          => (int) $ID,
1214
			'login'       => (string) $login,
1215
			'email'       => $email, // (string|bool)
1216
			'name'        => (string) $name,
1217
			'first_name'  => (string) $first_name,
1218
			'last_name'   => (string) $last_name,
1219
			'nice_name'   => (string) $nice,
1220
			'URL'         => (string) esc_url_raw( $URL ),
1221
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1222
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1223
			'ip_address'  => $ip_address, // (string|bool)
1224
		);
1225
1226
		if ( $site_id > -1 ) {
1227
			$author['site_ID']      = (int) $site_id;
1228
			$author['site_visible'] = $site_visible;
1229
		}
1230
1231
		return (object) $author;
1232
	}
1233
1234
	function get_media_item( $media_id ) {
1235
		$media_item = get_post( $media_id );
1236
1237
		if ( !$media_item || is_wp_error( $media_item ) )
1238
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1239
1240
		$response = array(
1241
			'id'    => strval( $media_item->ID ),
1242
			'date' =>  (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1243
			'parent'           => $media_item->post_parent,
1244
			'link'             => wp_get_attachment_url( $media_item->ID ),
1245
			'title'            => $media_item->post_title,
1246
			'caption'          => $media_item->post_excerpt,
1247
			'description'      => $media_item->post_content,
1248
			'metadata'         => wp_get_attachment_metadata( $media_item->ID ),
1249
		);
1250
1251
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1252
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1253
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1254
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1255
		}
1256
1257
		$response['meta'] = (object) array(
1258
			'links' => (object) array(
1259
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1260
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1261
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1262
			),
1263
		);
1264
1265
		return (object) $response;
1266
	}
1267
1268
	function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1269
1270
		if ( ! $media_item ) {
1271
			$media_item = get_post( $media_id );
1272
		}
1273
1274
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1275
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1276
		}
1277
1278
		$attachment_file = get_attached_file( $media_item->ID );
1279
1280
		$file = basename( $attachment_file ? $attachment_file : $file );
1281
		$file_info = pathinfo( $file );
1282
		$ext  = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1283
1284
		$response = array(
1285
			'ID'           => $media_item->ID,
1286
			'URL'          => wp_get_attachment_url( $media_item->ID ),
1287
			'guid'         => $media_item->guid,
1288
			'date'         => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1289
			'post_ID'      => $media_item->post_parent,
1290
			'author_ID'    => (int) $media_item->post_author,
1291
			'file'         => $file,
1292
			'mime_type'    => $media_item->post_mime_type,
1293
			'extension'    => $ext,
1294
			'title'        => $media_item->post_title,
1295
			'caption'      => $media_item->post_excerpt,
1296
			'description'  => $media_item->post_content,
1297
			'alt'          => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1298
			'icon'         => wp_mime_type_icon( $media_item->ID ),
1299
			'thumbnails'   => array()
1300
		);
1301
1302
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1303
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1304 View Code Duplication
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1305
				$response['height'] = $metadata['height'];
1306
				$response['width'] = $metadata['width'];
1307
			}
1308
1309
			if ( isset( $metadata['sizes'] ) ) {
1310
				/**
1311
				 * Filter the thumbnail sizes available for each attachment ID.
1312
				 *
1313
				 * @module json-api
1314
				 *
1315
				 * @since 3.9.0
1316
				 *
1317
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1318
				 * @param string $media_id Attachment ID.
1319
				 */
1320
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
1321 View Code Duplication
				if ( is_array( $sizes ) ) {
1322
					foreach ( $sizes as $size => $size_details ) {
1323
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1324
					}
1325
					/**
1326
					 * Filter the thumbnail URLs for attachment files.
1327
					 *
1328
					 * @module json-api
1329
					 *
1330
					 * @since 7.1.0
1331
					 *
1332
					 * @param array $metadata['sizes'] Array with thumbnail sizes as keys and URLs as values.
1333
					 */
1334
					$response['thumbnails'] = apply_filters( 'rest_api_thumbnail_size_urls', $response['thumbnails'] );
1335
				}
1336
			}
1337
1338
			if ( isset( $metadata['image_meta'] ) ) {
1339
				$response['exif'] = $metadata['image_meta'];
1340
			}
1341
		}
1342
1343 View Code Duplication
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1344
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1345
			$response['length'] = $metadata['length'];
1346
			$response['exif']   = $metadata;
1347
		}
1348
1349
		$is_video = false;
1350
1351
		if (
1352
			in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1353
			||
1354
			$response['mime_type'] === 'video/videopress'
1355
		) {
1356
			$is_video = true;
1357
		}
1358
1359
1360
		if ( $is_video ) {
1361
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1362
1363 View Code Duplication
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1364
				$response['height'] = $metadata['height'];
1365
				$response['width']  = $metadata['width'];
1366
			}
1367
1368
			if ( isset( $metadata['length'] ) ) {
1369
				$response['length'] = $metadata['length'];
1370
			}
1371
1372
			// add VideoPress info
1373
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1374
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1375
1376
				// If we failed to get VideoPress info, but it exists in the meta data (for some reason)
1377
				// then let's use that.
1378
				if ( false === $info && isset( $metadata['videopress'] ) ) {
1379
				    $info = (object) $metadata['videopress'];
1380
				}
1381
1382
				// Thumbnails
1383 View Code Duplication
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1384
					$response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' );
1385
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1386
						if ( video_format_done( $info, $size ) ) {
1387
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1388
						} else {
1389
							unset( $response['thumbnails'][ $size ] );
1390
						}
1391
					}
1392
				}
1393
1394
				// If we didn't get VideoPress information (for some reason) then let's
1395
				// not try and include it in the response.
1396
				if ( isset( $info->guid ) ) {
1397
					$response['videopress_guid']            = $info->guid;
1398
					$response['videopress_processing_done'] = true;
1399
					if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1400
						$response['videopress_processing_done'] = false;
1401
					}
1402
				}
1403
			}
1404
		}
1405
1406
		$response['thumbnails'] = (object) $response['thumbnails'];
1407
1408
		$response['meta'] = (object) array(
1409
			'links' => (object) array(
1410
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1411
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1412
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1413
			),
1414
		);
1415
1416
		// add VideoPress link to the meta
1417
		if ( isset ( $response['videopress_guid'] ) ) {
1418 View Code Duplication
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1419
				$response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1420
			}
1421
		}
1422
1423 View Code Duplication
		if ( $media_item->post_parent > 0 ) {
1424
			$response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1425
		}
1426
1427
		return (object) $response;
1428
	}
1429
1430
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1431
1432
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1433
		/// keep updating this function
1434
		if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1435
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1436
		}
1437
1438
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1439
	}
1440
1441
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1442
		// Permissions
1443 View Code Duplication
		switch ( $context ) {
1444
		case 'edit' :
1445
			$tax = get_taxonomy( $taxonomy_type );
1446
			if ( !current_user_can( $tax->cap->edit_terms ) )
1447
				return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1448
			break;
1449
		case 'display' :
1450
			if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1451
				return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1452
			}
1453
			break;
1454
		default :
1455
			return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1456
		}
1457
1458
		$response                = array();
1459
		$response['ID']          = (int) $taxonomy->term_id;
1460
		$response['name']        = (string) $taxonomy->name;
1461
		$response['slug']        = (string) $taxonomy->slug;
1462
		$response['description'] = (string) $taxonomy->description;
1463
		$response['post_count']  = (int) $taxonomy->count;
1464
		$response['feed_url']    = get_term_feed_link( $taxonomy->term_id, $taxonomy_type );
1465
1466
		if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1467
			$response['parent'] = (int) $taxonomy->parent;
1468
		}
1469
1470
		$response['meta'] = (object) array(
1471
			'links' => (object) array(
1472
				'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1473
				'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1474
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1475
			),
1476
		);
1477
1478
		return (object) $response;
1479
	}
1480
1481
	/**
1482
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1483
	 *
1484
	 * @param $date_gmt (string) GMT datetime string.
1485
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1486
	 *
1487
	 * @return string
1488
	 */
1489
	function format_date( $date_gmt, $date = null ) {
1490
		return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1491
	}
1492
1493
	/**
1494
	 * Parses a date string and returns the local and GMT representations
1495
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1496
	 * timezones or offsets. If the parsed datetime was not localized to a
1497
	 * particular timezone or offset we will assume it was given in GMT
1498
	 * relative to now and will convert it to local time using either the
1499
	 * timezone set in the options table for the blog or the GMT offset.
1500
	 *
1501
	 * @param datetime string
1502
	 *
1503
	 * @return array( $local_time_string, $gmt_time_string )
1504
	 */
1505
	function parse_date( $date_string ) {
1506
		$date_string_info = date_parse( $date_string );
1507
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1508
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1509
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1510
				$dt_local = clone $dt_utc = new DateTime( $date_string );
1511
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1512
				return array(
1513
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1514
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1515
				);
1516
			}
1517
1518
			// It's parseable but no TZ info so assume UTC
1519
			$dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1520
		} else {
1521
			// Could not parse time, use now in UTC
1522
			$dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1523
		}
1524
1525
		// First try to use timezone as it's daylight savings aware.
1526
		$timezone_string = get_option( 'timezone_string' );
1527
		if ( $timezone_string ) {
1528
			$tz = timezone_open( $timezone_string );
1529
			if ( $tz ) {
1530
				$dt_local->setTimezone( $tz );
1531
				return array(
1532
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1533
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1534
				);
1535
			}
1536
		}
1537
1538
		// Fallback to GMT offset (in hours)
1539
		// NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1540
		$gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1541
		$dt_local->modify("+{$gmt_offset_seconds} seconds");
1542
		return array(
1543
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1544
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1545
		);
1546
	}
1547
1548
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1549
	function load_theme_functions() {
1550
		// bail if we've done this already (can happen when calling /batch endpoint)
1551
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) )
1552
			return;
1553
1554
		// VIP context loading is handled elsewhere, so bail to prevent
1555
		// duplicate loading. See `switch_to_blog_and_validate_user()`
1556
		if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1557
			return;
1558
		}
1559
1560
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1561
1562
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1563
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1564
1565
		$copy_dirs = array( get_template_directory() );
1566
1567
		// Is this a child theme? Load the child theme's functions file.
1568
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1569
			foreach ( $function_files as $function_file ) {
1570
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1571
					require_once(  get_stylesheet_directory() . $function_file );
1572
				}
1573
			}
1574
			$copy_dirs[] = get_stylesheet_directory();
1575
		}
1576
1577
		foreach ( $function_files as $function_file ) {
1578
			if ( file_exists( get_template_directory() . $function_file ) ) {
1579
				require_once(  get_template_directory() . $function_file );
1580
			}
1581
		}
1582
1583
		// add inc/wpcom.php and/or includes/wpcom.php
1584
		wpcom_load_theme_compat_file();
1585
1586
		// Enable including additional directories or files in actions to be copied
1587
		$copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1588
1589
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1590
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1591
1592
		/**
1593
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1594
		 *
1595
		 * The REST API does not load the theme when processing requests.
1596
		 * To enable theme-based functionality, the API will load the '/functions.php',
1597
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1598
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1599
		 *
1600
		 * @module json-api
1601
		 *
1602
		 * @since 3.2.0
1603
		 */
1604
		do_action( 'restapi_theme_after_setup_theme' );
1605
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1606
1607
		/**
1608
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1609
		 *
1610
		 * The REST API does not load the theme when processing requests.
1611
		 * To enable theme-based functionality, the API will load the '/functions.php',
1612
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1613
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1614
		 *
1615
		 * @module json-api
1616
		 *
1617
		 * @since 3.2.0
1618
		 */
1619
		do_action( 'restapi_theme_init' );
1620
	}
1621
1622
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1623
		global $wp_filter;
1624
		foreach ( $wp_filter as $hook => $actions ) {
1625
1626
			if ( $from_hook != $hook ) {
1627
				continue;
1628
			}
1629
			if ( ! has_action( $hook ) ) {
1630
				continue;
1631
			}
1632
1633
			foreach ( $actions as $priority => $callbacks ) {
1634
				foreach( $callbacks as $callback_key => $callback_data ) {
1635
					$callback = $callback_data['function'];
1636
1637
					// use reflection api to determine filename where function is defined
1638
					$reflection = $this->get_reflection( $callback );
1639
1640
					if ( false !== $reflection ) {
1641
						$file_name = $reflection->getFileName();
1642
						foreach( $base_paths as $base_path ) {
1643
1644
							// only copy hooks with functions which are part of the specified files
1645
							if ( 0 === strpos( $file_name, $base_path ) ) {
1646
								add_action(
1647
									$to_hook,
1648
									$callback_data['function'],
1649
									$priority,
1650
									$callback_data['accepted_args']
1651
								);
1652
							}
1653
						}
1654
					}
1655
				}
1656
			}
1657
		}
1658
	}
1659
1660
	function get_reflection( $callback ) {
1661
		if ( is_array( $callback ) ) {
1662
			list( $class, $method ) = $callback;
1663
			return new ReflectionMethod( $class, $method );
1664
		}
1665
1666
		if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) {
1667
			list( $class, $method ) = explode( "::", $callback );
1668
			return new ReflectionMethod( $class, $method );
1669
		}
1670
1671
		if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) {
1672
			return new ReflectionMethod( $callback, "__invoke" );
1673
		}
1674
1675
		if ( is_string( $callback ) && strpos( $callback, "::" ) == false && function_exists( $callback ) ) {
1676
			return new ReflectionFunction( $callback );
1677
		}
1678
1679
		return false;
1680
	}
1681
1682
	/**
1683
	* Check whether a user can view or edit a post type
1684
	* @param string $post_type              post type to check
1685
	* @param string $context                'display' or 'edit'
1686
	* @return bool
1687
	*/
1688 View Code Duplication
	function current_user_can_access_post_type( $post_type, $context='display' ) {
1689
		$post_type_object = get_post_type_object( $post_type );
1690
		if ( ! $post_type_object ) {
1691
			return false;
1692
		}
1693
1694
		switch( $context ) {
1695
			case 'edit':
1696
				return current_user_can( $post_type_object->cap->edit_posts );
1697
			case 'display':
1698
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1699
			default:
1700
				return false;
1701
		}
1702
	}
1703
1704 View Code Duplication
	function is_post_type_allowed( $post_type ) {
1705
		// if the post type is empty, that's fine, WordPress will default to post
1706
		if ( empty( $post_type ) ) {
1707
			return true;
1708
		}
1709
1710
		// allow special 'any' type
1711
		if ( 'any' == $post_type ) {
1712
			return true;
1713
		}
1714
1715
		// check for allowed types
1716
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1717
			return true;
1718
		}
1719
1720
		if ( $post_type_object = get_post_type_object( $post_type ) ) {
1721
			if ( ! empty( $post_type_object->show_in_rest ) ) {
1722
				return $post_type_object->show_in_rest;
1723
			}
1724
			if ( ! empty( $post_type_object->publicly_queryable ) ) {
1725
				return $post_type_object->publicly_queryable;
1726
			}
1727
		}
1728
1729
		return ! empty( $post_type_object->public );
1730
	}
1731
1732
	/**
1733
	 * Gets the whitelisted post types that JP should allow access to.
1734
	 *
1735
	 * @return array Whitelisted post types.
1736
	 */
1737 View Code Duplication
	protected function _get_whitelisted_post_types() {
1738
		$allowed_types = array( 'post', 'page', 'revision' );
1739
1740
		/**
1741
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1742
		 *
1743
		 * @module json-api
1744
		 *
1745
		 * @since 2.2.3
1746
		 *
1747
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1748
		 */
1749
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1750
1751
		return array_unique( $allowed_types );
1752
	}
1753
1754
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1755
1756
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1757
1758
		$media_ids = $errors = array();
1759
		$user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
1760
		$media_attrs = array_values( $media_attrs ); // reset the keys
1761
		$i = 0;
1762
1763
		if ( ! empty( $media_files ) ) {
1764
			$this->api->trap_wp_die( 'upload_error' );
1765
			foreach ( $media_files as $media_item ) {
1766
				$_FILES['.api.media.item.'] = $media_item;
1767 View Code Duplication
				if ( ! $user_can_upload_files ) {
1768
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1769
				} else {
1770
					if ( $force_parent_id ) {
1771
						$parent_id = absint( $force_parent_id );
1772
					} elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1773
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1774
					} else {
1775
						$parent_id = 0;
1776
					}
1777
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1778
				}
1779
				if ( is_wp_error( $media_id ) ) {
1780
					$errors[$i]['file']   = $media_item['name'];
1781
					$errors[$i]['error']   = $media_id->get_error_code();
1782
					$errors[$i]['message'] = $media_id->get_error_message();
1783
				} else {
1784
					$media_ids[$i] = $media_id;
1785
				}
1786
1787
				$i++;
1788
			}
1789
			$this->api->trap_wp_die( null );
1790
			unset( $_FILES['.api.media.item.'] );
1791
		}
1792
1793
		if ( ! empty( $media_urls ) ) {
1794
			foreach ( $media_urls as $url ) {
1795 View Code Duplication
				if ( ! $user_can_upload_files ) {
1796
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1797
				} else {
1798
					if ( $force_parent_id ) {
1799
						$parent_id = absint( $force_parent_id );
1800
					} else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1801
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1802
					} else {
1803
						$parent_id = 0;
1804
					}
1805
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1806
				}
1807
				if ( is_wp_error( $media_id ) ) {
1808
					$errors[$i] = array(
1809
						'file'    => $url,
1810
						'error'   => $media_id->get_error_code(),
1811
						'message' => $media_id->get_error_message(),
1812
					);
1813
				} elseif ( ! empty( $media_id ) ) {
1814
					$media_ids[$i] = $media_id;
1815
				}
1816
1817
				$i++;
1818
			}
1819
		}
1820
1821
		if ( ! empty( $media_attrs ) ) {
1822
			foreach ( $media_ids as $index => $media_id ) {
1823
				if ( empty( $media_attrs[$index] ) )
1824
					continue;
1825
1826
				$attrs = $media_attrs[$index];
1827
				$insert = array();
1828
1829
				// Attributes: Title, Caption, Description
1830
1831
				if ( isset( $attrs['title'] ) ) {
1832
					$insert['post_title'] = $attrs['title'];
1833
				}
1834
1835
				if ( isset( $attrs['caption'] ) ) {
1836
					$insert['post_excerpt'] = $attrs['caption'];
1837
				}
1838
1839
				if ( isset( $attrs['description'] ) ) {
1840
					$insert['post_content'] = $attrs['description'];
1841
				}
1842
1843
				if ( ! empty( $insert ) ) {
1844
					$insert['ID'] = $media_id;
1845
					wp_update_post( (object) $insert );
1846
				}
1847
1848
				// Attributes: Alt
1849
1850 View Code Duplication
				if ( isset( $attrs['alt'] ) ) {
1851
					$alt = wp_strip_all_tags( $attrs['alt'], true );
1852
					update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1853
				}
1854
1855
				// Attributes: Artist, Album
1856
1857
				$id3_meta = array();
1858
1859 View Code Duplication
				foreach ( array( 'artist', 'album' ) as $key ) {
1860
					if ( isset( $attrs[ $key ] ) ) {
1861
						$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1862
					}
1863
				}
1864
1865
				if ( ! empty( $id3_meta ) ) {
1866
					// Before updating metadata, ensure that the item is audio
1867
					$item = $this->get_media_item_v1_1( $media_id );
1868
					if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1869
						wp_update_attachment_metadata( $media_id, $id3_meta );
1870
					}
1871
				}
1872
			}
1873
		}
1874
1875
		return array( 'media_ids' => $media_ids, 'errors' => $errors );
1876
1877
	}
1878
1879
	function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1880
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1881
			return false;
1882
1883
		// if we didn't get a URL, let's bail
1884
		$parsed = @parse_url( $url );
1885
		if ( empty( $parsed ) )
1886
			return false;
1887
1888
		$tmp = download_url( $url );
1889
		if ( is_wp_error( $tmp ) ) {
1890
			return $tmp;
1891
		}
1892
1893
		// First check to see if we get a mime-type match by file, otherwise, check to
1894
		// see if WordPress supports this file as an image. If neither, then it is not supported.
1895 View Code Duplication
		if ( ! $this->is_file_supported_for_sideloading( $tmp ) || 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1896
			@unlink( $tmp );
1897
			return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
1898
		}
1899
1900
		// emulate a $_FILES entry
1901
		$file_array = array(
1902
			'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1903
			'tmp_name' => $tmp,
1904
		);
1905
1906
		$id = media_handle_sideload( $file_array, $parent_post_id );
1907
		if ( file_exists( $tmp ) ) {
1908
			@unlink( $tmp );
1909
		}
1910
1911
		if ( is_wp_error( $id ) ) {
1912
			return $id;
1913
		}
1914
1915
		if ( ! $id || ! is_int( $id ) ) {
1916
			return false;
1917
		}
1918
1919
		return $id;
1920
	}
1921
1922
	/**
1923
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1924
	 *
1925
	 * @param string $file Path to file to get its mime type.
1926
	 *
1927
	 * @return bool
1928
	 */
1929 View Code Duplication
	protected function is_file_supported_for_sideloading( $file ) {
1930
		if ( class_exists( 'finfo' ) ) { // php 5.3+
1931
			// phpcs:ignore PHPCompatibility.PHP.NewClasses.finfoFound
1932
			$finfo = new finfo( FILEINFO_MIME );
1933
			$mime = explode( '; ', $finfo->file( $file ) );
1934
			$type = $mime[0];
1935
1936
		} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
1937
			$type = mime_content_type( $file );
1938
1939
		} else {
1940
			return false;
1941
		}
1942
1943
		/**
1944
		 * Filter the list of supported mime types for media sideloading.
1945
		 *
1946
		 * @since 4.0.0
1947
		 *
1948
		 * @module json-api
1949
		 *
1950
		 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
1951
		 */
1952
		$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
1953
			'image/png',
1954
			'image/jpeg',
1955
			'image/gif',
1956
			'image/bmp',
1957
			'video/quicktime',
1958
			'video/mp4',
1959
			'video/mpeg',
1960
			'video/ogg',
1961
			'video/3gpp',
1962
			'video/3gpp2',
1963
			'video/h261',
1964
			'video/h262',
1965
			'video/h264',
1966
			'video/x-msvideo',
1967
			'video/x-ms-wmv',
1968
			'video/x-ms-asf',
1969
		) );
1970
1971
		// If the type returned was not an array as expected, then we know we don't have a match.
1972
		if ( ! is_array( $supported_mime_types ) ) {
1973
			return false;
1974
		}
1975
1976
		return in_array( $type, $supported_mime_types );
1977
	}
1978
1979
	function allow_video_uploads( $mimes ) {
1980
		// if we are on Jetpack, bail - Videos are already allowed
1981
		if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1982
			return $mimes;
1983
		}
1984
1985
		// extra check that this filter is only ever applied during REST API requests
1986
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1987
			return $mimes;
1988
		}
1989
1990
		// bail early if they already have the upgrade..
1991
		if ( get_option( 'video_upgrade' ) == '1' ) {
1992
			return $mimes;
1993
		}
1994
1995
		// lets whitelist to only specific clients right now
1996
		$clients_allowed_video_uploads = array();
1997
		/**
1998
		 * Filter the list of whitelisted video clients.
1999
		 *
2000
		 * @module json-api
2001
		 *
2002
		 * @since 3.2.0
2003
		 *
2004
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
2005
		 */
2006
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
2007
		if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
2008
			return $mimes;
2009
		}
2010
2011
		$mime_list = wp_get_mime_types();
2012
2013
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
2014
		/**
2015
		 * Filter the video filetypes allowed on the site.
2016
		 *
2017
		 * @module json-api
2018
		 *
2019
		 * @since 3.2.0
2020
		 *
2021
		 * @param array $video_exts Array of video filetypes allowed on the site.
2022
		 */
2023
		$video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
2024
		$video_mimes = array();
2025
2026
		if ( !empty( $video_exts ) ) {
2027
			foreach ( $video_exts as $ext ) {
2028
				foreach ( $mime_list as $ext_pattern => $mime ) {
2029
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
2030
						$video_mimes[$ext_pattern] = $mime;
2031
				}
2032
			}
2033
2034
			$mimes = array_merge( $mimes, $video_mimes );
2035
		}
2036
2037
		return $mimes;
2038
	}
2039
2040
	function is_current_site_multi_user() {
2041
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2042
		if ( false === $users ) {
2043
			$user_query = new WP_User_Query( array(
2044
				'blog_id' => get_current_blog_id(),
2045
				'fields'  => 'ID',
2046
			) );
2047
			$users = (int) $user_query->get_total();
2048
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2049
		}
2050
		return $users > 1;
2051
	}
2052
2053
	function allows_cross_origin_requests() {
2054
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2055
	}
2056
2057
	function allows_unauthorized_requests( $origin, $complete_access_origins  ) {
2058
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2059
	}
2060
2061
	function get_platform() {
2062
		return wpcom_get_sal_platform( $this->api->token_details );
2063
	}
2064
2065
	/**
2066
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2067
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
2068
	 *
2069
	 * Override this method if you want to do something different.
2070
	 *
2071
	 * @param  int  $blog_id
2072
	 * @return bool
2073
	 */
2074
	function force_wpcom_request( $blog_id ) {
2075
		return false;
2076
	}
2077
2078
	/**
2079
	 * Return endpoint response
2080
	 *
2081
	 * @param ... determined by ->$path
2082
	 *
2083
	 * @return
2084
	 * 	falsy: HTTP 500, no response body
2085
	 *	WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2086
	 *	$data: HTTP 200, json_encode( $data ) response body
2087
	 */
2088
	abstract function callback( $path = '' );
2089
2090
2091
}
2092
2093
require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
2094