Completed
Push — gm-17/payment-widget ( cb2702...55e70e )
by
unknown
13:32 queued 03:19
created

class.json-api-endpoints.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

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

1
<?php
2
3
require_once( dirname( __FILE__ ) . '/json-api-config.php' );
4
require_once( dirname( __FILE__ ) . '/sal/class.json-api-links.php' );
5
require_once( dirname( __FILE__ ) . '/sal/class.json-api-metadata.php' );
6
require_once( dirname( __FILE__ ) . '/sal/class.json-api-date.php' );
7
8
// Endpoint
9
abstract class WPCOM_JSON_API_Endpoint {
10
	// The API Object
11
	public $api;
12
13
	// The link-generating utility class
14
	public $links;
15
16
	public $pass_wpcom_user_details = false;
17
18
	// One liner.
19
	public $description;
20
21
	// Object Grouping For Documentation (Users, Posts, Comments)
22
	public $group;
23
24
	// Stats extra value to bump
25
	public $stat;
26
27
	// HTTP Method
28
	public $method = 'GET';
29
30
	// Minimum version of the api for which to serve this endpoint
31
	public $min_version = '0';
32
33
	// Maximum version of the api for which to serve this endpoint
34
	public $max_version = WPCOM_JSON_API__CURRENT_VERSION;
35
36
	// Path at which to serve this endpoint: sprintf() format.
37
	public $path = '';
38
39
	// Identifiers to fill sprintf() formatted $path
40
	public $path_labels = array();
41
42
	// Accepted query parameters
43
	public $query = array(
44
		// Parameter name
45
		'context' => array(
46
			// Default value => description
47
			'display' => 'Formats the output as HTML for display.  Shortcodes are parsed, paragraph tags are added, etc..',
48
			// Other possible values => description
49
			'edit'    => 'Formats the output for editing.  Shortcodes are left unparsed, significant whitespace is kept, etc..',
50
		),
51
		'http_envelope' => array(
52
			'false' => '',
53
			'true'  => 'Some environments (like in-browser JavaScript or Flash) block or divert responses with a non-200 HTTP status code.  Setting this parameter will force the HTTP status code to always be 200.  The JSON response is wrapped in an "envelope" containing the "real" HTTP status code and headers.',
54
		),
55
		'pretty' => array(
56
			'false' => '',
57
			'true'  => 'Output pretty JSON',
58
		),
59
		'meta' => "(string) Optional. Loads data from the endpoints found in the 'meta' part of the response. Comma-separated list. Example: meta=site,likes",
60
		'fields' => '(string) Optional. Returns specified fields only. Comma-separated list. Example: fields=ID,title',
61
		// Parameter name => description (default value is empty)
62
		'callback' => '(string) An optional JSONP callback function.',
63
	);
64
65
	// Response format
66
	public $response_format = array();
67
68
	// Request format
69
	public $request_format = array();
70
71
	// Is this endpoint still in testing phase?  If so, not available to the public.
72
	public $in_testing = false;
73
74
	// Is this endpoint still allowed if the site in question is flagged?
75
	public $allowed_if_flagged = false;
76
77
	// Is this endpoint allowed if the site is red flagged?
78
	public $allowed_if_red_flagged = false;
79
80
	/**
81
	 * @var string Version of the API
82
	 */
83
	public $version = '';
84
85
	/**
86
	 * @var string Example request to make
87
	 */
88
	public $example_request = '';
89
90
	/**
91
	 * @var string Example request data (for POST methods)
92
	 */
93
	public $example_request_data = '';
94
95
	/**
96
	 * @var string Example response from $example_request
97
	 */
98
	public $example_response = '';
99
100
	/**
101
	 * @var bool Set to true if the endpoint implements its own filtering instead of the standard `fields` query method
102
	 */
103
	public $custom_fields_filtering = false;
104
105
	/**
106
	 * @var bool Set to true if the endpoint accepts all cross origin requests. You probably should not set this flag.
107
	 */
108
	public $allow_cross_origin_request = false;
109
110
	/**
111
	 * @var bool Set to true if the endpoint can recieve unauthorized POST requests.
112
	 */
113
	public $allow_unauthorized_request = false;
114
115
	/**
116
	 * @var bool Set to true if the endpoint should accept site based (not user based) authentication.
117
	 */
118
	public $allow_jetpack_site_auth = false;
119
120
	/**
121
	 * @var bool Set to true if the endpoint should accept auth from an upload token.
122
	 */
123
	public $allow_upload_token_auth = false;
124
125
	function __construct( $args ) {
126
		$defaults = array(
127
			'in_testing'           => false,
128
			'allowed_if_flagged'   => false,
129
			'allowed_if_red_flagged' => false,
130
			'description'          => '',
131
			'group'	               => '',
132
			'method'               => 'GET',
133
			'path'                 => '/',
134
			'min_version'          => '0',
135
			'max_version'          => WPCOM_JSON_API__CURRENT_VERSION,
136
			'force'	               => '',
137
			'deprecated'           => false,
138
			'new_version'          => WPCOM_JSON_API__CURRENT_VERSION,
139
			'jp_disabled'          => false,
140
			'path_labels'          => array(),
141
			'request_format'       => array(),
142
			'response_format'      => array(),
143
			'query_parameters'     => array(),
144
			'version'              => 'v1',
145
			'example_request'      => '',
146
			'example_request_data' => '',
147
			'example_response'     => '',
148
			'required_scope'       => '',
149
			'pass_wpcom_user_details' => false,
150
			'custom_fields_filtering' => false,
151
			'allow_cross_origin_request' => false,
152
			'allow_unauthorized_request' => false,
153
			'allow_jetpack_site_auth'    => false,
154
			'allow_upload_token_auth'    => false,
155
		);
156
157
		$args = wp_parse_args( $args, $defaults );
158
159
		$this->in_testing  = $args['in_testing'];
160
161
		$this->allowed_if_flagged = $args['allowed_if_flagged'];
162
		$this->allowed_if_red_flagged = $args['allowed_if_red_flagged'];
163
164
		$this->description = $args['description'];
165
		$this->group       = $args['group'];
166
		$this->stat        = $args['stat'];
167
		$this->force	   = $args['force'];
168
		$this->jp_disabled = $args['jp_disabled'];
169
170
		$this->method      = $args['method'];
171
		$this->path        = $args['path'];
172
		$this->path_labels = $args['path_labels'];
173
		$this->min_version = $args['min_version'];
174
		$this->max_version = $args['max_version'];
175
		$this->deprecated  = $args['deprecated'];
176
		$this->new_version = $args['new_version'];
177
178
		// Ensure max version is not less than min version
179
		if ( version_compare( $this->min_version, $this->max_version, '>' ) ) {
180
			$this->max_version = $this->min_version;
181
		}
182
183
		$this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
184
		$this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
185
186
		$this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
187
		$this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
188
		$this->allow_jetpack_site_auth    = (bool) $args['allow_jetpack_site_auth'];
189
		$this->allow_upload_token_auth    = (bool) $args['allow_upload_token_auth'];
190
191
		$this->version     = $args['version'];
192
193
		$this->required_scope = $args['required_scope'];
194
195 View Code Duplication
		if ( $this->request_format ) {
196
			$this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
197
		} else {
198
			$this->request_format = $args['request_format'];
199
		}
200
201 View Code Duplication
		if ( $this->response_format ) {
202
			$this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
203
		} else {
204
			$this->response_format = $args['response_format'];
205
		}
206
207
		if ( false === $args['query_parameters'] ) {
208
			$this->query = array();
209
		} elseif ( is_array( $args['query_parameters'] ) ) {
210
			$this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
211
		}
212
213
		$this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
214
		$this->links = WPCOM_JSON_API_Links::getInstance();
215
216
		/** Example Request/Response ******************************************/
217
218
		// Examples for endpoint documentation request
219
		$this->example_request      = $args['example_request'];
220
		$this->example_request_data = $args['example_request_data'];
221
		$this->example_response     = $args['example_response'];
222
223
		$this->api->add( $this );
224
	}
225
226
	// Get all query args.  Prefill with defaults
227
	function query_args( $return_default_values = true, $cast_and_filter = true ) {
228
		$args = array_intersect_key( $this->api->query, $this->query );
229
230
		if ( !$cast_and_filter ) {
231
			return $args;
232
		}
233
234
		return $this->cast_and_filter( $args, $this->query, $return_default_values );
235
	}
236
237
	// Get POST body data
238
	function input( $return_default_values = true, $cast_and_filter = true ) {
239
		$input = trim( $this->api->post_body );
240
		$content_type = $this->api->content_type;
241
		if ( $content_type ) {
242
			list ( $content_type ) = explode( ';', $content_type );
243
		}
244
		$content_type = trim( $content_type );
245
		switch ( $content_type ) {
246
		case 'application/json' :
247
		case 'application/x-javascript' :
248
		case 'text/javascript' :
249
		case 'text/x-javascript' :
250
		case 'text/x-json' :
251
		case 'text/json' :
252
			$return = json_decode( $input, true );
253
254
			if ( function_exists( 'json_last_error' ) ) {
255
				if ( JSON_ERROR_NONE !== json_last_error() ) {
256
					return null;
257
				}
258
			} else {
259
				if ( is_null( $return ) && json_encode( null ) !== $input ) {
260
					return null;
261
				}
262
			}
263
264
			break;
265
		case 'multipart/form-data' :
266
			$return = array_merge( stripslashes_deep( $_POST ), $_FILES );
267
			break;
268
		case 'application/x-www-form-urlencoded' :
269
			//attempt JSON first, since probably a curl command
270
			$return = json_decode( $input, true );
271
272
			if ( is_null( $return ) ) {
273
				wp_parse_str( $input, $return );
274
			}
275
276
			break;
277
		default :
278
			wp_parse_str( $input, $return );
279
			break;
280
		}
281
282
		if ( isset( $this->api->query['force'] )
283
		    && 'secure' === $this->api->query['force']
284
		    && isset( $return['secure_key'] ) ) {
285
			$this->api->post_body = $this->get_secure_body( $return['secure_key'] );
286
			$this->api->query['force'] = false;
287
			return $this->input( $return_default_values, $cast_and_filter );
288
		}
289
290
		if ( $cast_and_filter ) {
291
			$return = $this->cast_and_filter( $return, $this->request_format, $return_default_values );
292
		}
293
		return $return;
294
	}
295
296
297
	protected function get_secure_body( $secure_key ) {
298
		$response =  Jetpack_Client::wpcom_json_api_request_as_blog(
299
			sprintf( '/sites/%d/secure-request', Jetpack_Options::get_option('id' ) ),
300
			'1.1',
301
			array( 'method' => 'POST' ),
302
			array( 'secure_key' => $secure_key )
303
		);
304
		if ( 200 !== $response['response']['code'] ) {
305
			return null;
306
		}
307
		return json_decode( $response['body'], true );
308
	}
309
310
	function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
311
		$return_as_object = false;
312
		if ( is_object( $data ) ) {
313
			// @todo this should probably be a deep copy if $data can ever have nested objects
314
			$data = (array) $data;
315
			$return_as_object = true;
316
		} elseif ( !is_array( $data ) ) {
317
			return $data;
318
		}
319
320
		$boolean_arg = array( 'false', 'true' );
321
		$naeloob_arg = array( 'true', 'false' );
322
323
		$return = array();
324
325
		foreach ( $documentation as $key => $description ) {
326
			if ( is_array( $description ) ) {
327
				// String or boolean array keys only
328
				$whitelist = array_keys( $description );
329
330
				if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
331
					// Truthiness
332
					if ( isset( $data[$key] ) ) {
333
						$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $data[$key] );
334
					} elseif ( $return_default_values ) {
335
						$return[$key] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg.
336
					}
337
				} elseif ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) {
338
					// String Key
339
					$return[$key] = (string) $data[$key];
340
				} elseif ( $return_default_values ) {
341
					// Default value
342
					$return[$key] = (string) current( $whitelist );
343
				}
344
345
				continue;
346
			}
347
348
			$types = $this->parse_types( $description );
349
			$type = array_shift( $types );
350
351
			// Explicit default - string and int only for now.  Always set these reguardless of $return_default_values
352
			if ( isset( $type['default'] ) ) {
353
				if ( !isset( $data[$key] ) ) {
354
					$data[$key] = $type['default'];
355
				}
356
			}
357
358
			if ( !isset( $data[$key] ) ) {
359
				continue;
360
			}
361
362
			$this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output );
363
		}
364
365
		if ( $return_as_object ) {
366
			return (object) $return;
367
		}
368
369
		return $return;
370
	}
371
372
	/**
373
	 * Casts $value according to $type.
374
	 * Handles fallbacks for certain values of $type when $value is not that $type
375
	 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way),
376
	 * and string -> object (one way)
377
	 *
378
	 * Handles "child types" - array:URL, object:category
379
	 * array:URL means an array of URLs
380
	 * object:category means a hash of categories
381
	 *
382
	 * Handles object typing - object>post means an object of type post
383
	 */
384
	function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
385
		if ( is_string( $type ) ) {
386
			$type = compact( 'type' );
387
		}
388
389
		switch ( $type['type'] ) {
390
		case 'false' :
391
			$return[$key] = false;
392
			break;
393
		case 'url' :
394
			$return[$key] = (string) esc_url_raw( $value );
395
			break;
396
		case 'string' :
397
			// Fallback string -> array, or for string -> object
398
			if ( is_array( $value ) || is_object( $value ) ) {
399
				if ( !empty( $types[0] ) ) {
400
					$next_type = array_shift( $types );
401
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
402
				}
403
			}
404
405
			// Fallback string -> false
406 View Code Duplication
			if ( !is_string( $value ) ) {
407
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
408
					$next_type = array_shift( $types );
409
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
410
				}
411
			}
412
			$return[$key] = (string) $value;
413
			break;
414
		case 'html' :
415
			$return[$key] = (string) $value;
416
			break;
417
		case 'safehtml' :
418
			$return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() );
419
			break;
420
		case 'zip' :
421
		case 'media' :
422
			if ( is_array( $value ) ) {
423
				if ( isset( $value['name'] ) && is_array( $value['name'] ) ) {
424
					// It's a $_FILES array
425
					// Reformat into array of $_FILES items
426
					$files = array();
427
428
					foreach ( $value['name'] as $k => $v ) {
429
						$files[$k] = array();
430
						foreach ( array_keys( $value ) as $file_key ) {
431
							$files[$k][$file_key] = $value[$file_key][$k];
432
						}
433
					}
434
435
					$return[$key] = $files;
436
					break;
437
				}
438
			} else {
439
				// no break - treat as 'array'
440
			}
441
			// nobreak
442
		case 'array' :
443
			// Fallback array -> string
444
			if ( is_string( $value ) ) {
445
				if ( !empty( $types[0] ) ) {
446
					$next_type = array_shift( $types );
447
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
448
				}
449
			}
450
451 View Code Duplication
			if ( isset( $type['children'] ) ) {
452
				$children = array();
453
				foreach ( (array) $value as $k => $child ) {
454
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
455
				}
456
				$return[$key] = (array) $children;
457
				break;
458
			}
459
460
			$return[$key] = (array) $value;
461
			break;
462
		case 'iso 8601 datetime' :
463
		case 'datetime' :
464
			// (string)s
465
			$dates = $this->parse_date( (string) $value );
466
			if ( $for_output ) {
467
				$return[$key] = $this->format_date( $dates[1], $dates[0] );
468
			} else {
469
				list( $return[$key], $return["{$key}_gmt"] ) = $dates;
470
			}
471
			break;
472
		case 'float' :
473
			$return[$key] = (float) $value;
474
			break;
475
		case 'int' :
476
		case 'integer' :
477
			$return[$key] = (int) $value;
478
			break;
479
		case 'bool' :
480
		case 'boolean' :
481
			$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value );
482
			break;
483
		case 'object' :
484
			// Fallback object -> false
485 View Code Duplication
			if ( is_scalar( $value ) || is_null( $value ) ) {
486
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
487
					return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
488
				}
489
			}
490
491 View Code Duplication
			if ( isset( $type['children'] ) ) {
492
				$children = array();
493
				foreach ( (array) $value as $k => $child ) {
494
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
495
				}
496
				$return[$key] = (object) $children;
497
				break;
498
			}
499
500
			if ( isset( $type['subtype'] ) ) {
501
				return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
502
			}
503
504
			$return[$key] = (object) $value;
505
			break;
506
		case 'post' :
507
			$return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
508
			break;
509
		case 'comment' :
510
			$return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
511
			break;
512
		case 'tag' :
513
		case 'category' :
514
			$docs = array(
515
				'ID'          => '(int)',
516
				'name'        => '(string)',
517
				'slug'        => '(string)',
518
				'description' => '(HTML)',
519
				'post_count'  => '(int)',
520
				'meta'        => '(object)',
521
			);
522
			if ( 'category' === $type['type'] ) {
523
				$docs['parent'] = '(int)';
524
			}
525
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
526
			break;
527
		case 'post_reference' :
528 View Code Duplication
		case 'comment_reference' :
529
			$docs = array(
530
				'ID'    => '(int)',
531
				'type'  => '(string)',
532
				'title' => '(string)',
533
				'link'  => '(URL)',
534
			);
535
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
536
			break;
537 View Code Duplication
		case 'geo' :
538
			$docs = array(
539
				'latitude'  => '(float)',
540
				'longitude' => '(float)',
541
				'address'   => '(string)',
542
			);
543
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
544
			break;
545
		case 'author' :
546
			$docs = array(
547
				'ID'             => '(int)',
548
				'user_login'     => '(string)',
549
				'login'          => '(string)',
550
				'email'          => '(string|false)',
551
				'name'           => '(string)',
552
				'first_name'     => '(string)',
553
				'last_name'      => '(string)',
554
				'nice_name'      => '(string)',
555
				'URL'            => '(URL)',
556
				'avatar_URL'     => '(URL)',
557
				'profile_URL'    => '(URL)',
558
				'is_super_admin' => '(bool)',
559
				'roles'          => '(array:string)',
560
				'ip_address'     => '(string|false)',
561
			);
562
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
563
			break;
564 View Code Duplication
		case 'role' :
565
			$docs = array(
566
				'name'         => '(string)',
567
				'display_name' => '(string)',
568
				'capabilities' => '(object:boolean)',
569
			);
570
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
571
			break;
572
		case 'attachment' :
573
			$docs = array(
574
				'ID'        => '(int)',
575
				'URL'       => '(URL)',
576
				'guid'      => '(string)',
577
				'mime_type' => '(string)',
578
				'width'     => '(int)',
579
				'height'    => '(int)',
580
				'duration'  => '(int)',
581
			);
582
			$return[$key] = (object) $this->cast_and_filter(
583
				$value,
584
				/**
585
				 * Filter the documentation returned for a post attachment.
586
				 *
587
				 * @module json-api
588
				 *
589
				 * @since 1.9.0
590
				 *
591
				 * @param array $docs Array of documentation about a post attachment.
592
				 */
593
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
594
				false,
595
				$for_output
596
			);
597
			break;
598
		case 'metadata' :
599
			$docs = array(
600
				'id'       => '(int)',
601
				'key'       => '(string)',
602
				'value'     => '(string|false|float|int|array|object)',
603
				'previous_value' => '(string)',
604
				'operation'  => '(string)',
605
			);
606
			$return[$key] = (object) $this->cast_and_filter(
607
				$value,
608
				/** This filter is documented in class.json-api-endpoints.php */
609
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
610
				false,
611
				$for_output
612
			);
613
			break;
614
		case 'plugin' :
615
			$docs = array(
616
				'id'            => '(safehtml) The plugin\'s ID',
617
				'slug'          => '(safehtml) The plugin\'s Slug',
618
				'active'        => '(boolean)  The plugin status.',
619
				'update'        => '(object)   The plugin update info.',
620
				'name'          => '(safehtml) The name of the plugin.',
621
				'plugin_url'    => '(url)      Link to the plugin\'s web site.',
622
				'version'       => '(safehtml) The plugin version number.',
623
				'description'   => '(safehtml) Description of what the plugin does and/or notes from the author',
624
				'author'        => '(safehtml) The plugin author\'s name',
625
				'author_url'    => '(url)      The plugin author web site address',
626
				'network'       => '(boolean)  Whether the plugin can only be activated network wide.',
627
				'autoupdate'    => '(boolean)  Whether the plugin is auto updated',
628
				'log'           => '(array:safehtml) An array of update log strings.',
629
        		'action_links'  => '(array) An array of action links that the plugin uses.',
630
			);
631
			$return[$key] = (object) $this->cast_and_filter(
632
				$value,
633
				/**
634
				 * Filter the documentation returned for a plugin.
635
				 *
636
				 * @module json-api
637
				 *
638
				 * @since 3.1.0
639
				 *
640
				 * @param array $docs Array of documentation about a plugin.
641
				 */
642
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
643
				false,
644
				$for_output
645
			);
646
			break;
647
		case 'plugin_v1_2' :
648
			$docs = Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2;
0 ignored issues
show
The property _response_format_v1_2 cannot be accessed from this context as it is declared private in class Jetpack_JSON_API_Plugins_Endpoint.

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

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

Loading history...
649
			$return[$key] = (object) $this->cast_and_filter(
650
				$value,
651
				/**
652
				 * Filter the documentation returned for a plugin.
653
				 *
654
				 * @module json-api
655
				 *
656
				 * @since 3.1.0
657
				 *
658
				 * @param array $docs Array of documentation about a plugin.
659
				 */
660
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
661
				false,
662
				$for_output
663
			);
664
			break;
665
		case 'file_mod_capabilities':
666
			$docs           = array(
667
				'reasons_modify_files_disabled' => '(array|string) The reasons why files can\'t be modified',
668
				'reasons_autoupdate_disabled'   => '(array|string) The reasons why autoupdates aren\'t allowed',
669
				'modify_files'                  => '(boolean) true if files can be modified',
670
				'autoupdate_files'              => '(boolean) true if autoupdates are allowed',
671
			);
672
			$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
673
			break;
674
		case 'jetpackmodule' :
675
			$docs = array(
676
				'id'          => '(string)   The module\'s ID',
677
				'active'      => '(boolean)  The module\'s status.',
678
				'name'        => '(string)   The module\'s name.',
679
				'description' => '(safehtml) The module\'s description.',
680
				'sort'        => '(int)      The module\'s display order.',
681
				'introduced'  => '(string)   The Jetpack version when the module was introduced.',
682
				'changed'     => '(string)   The Jetpack version when the module was changed.',
683
				'free'        => '(boolean)  The module\'s Free or Paid status.',
684
				'module_tags' => '(array)    The module\'s tags.'
685
			);
686
			$return[$key] = (object) $this->cast_and_filter(
687
				$value,
688
				/** This filter is documented in class.json-api-endpoints.php */
689
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
690
				false,
691
				$for_output
692
			);
693
			break;
694
		case 'sharing_button' :
695
			$docs = array(
696
				'ID'         => '(string)',
697
				'name'       => '(string)',
698
				'URL'        => '(string)',
699
				'icon'       => '(string)',
700
				'enabled'    => '(bool)',
701
				'visibility' => '(string)',
702
			);
703
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
704
			break;
705
		case 'sharing_button_service':
706
			$docs = array(
707
				'ID'               => '(string) The service identifier',
708
				'name'             => '(string) The service name',
709
				'class_name'       => '(string) Class name for custom style sharing button elements',
710
				'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
711
				'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
712
				'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview'
713
			);
714
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
715
			break;
716
		case 'taxonomy':
717
			$docs = array(
718
				'name'         => '(string) The taxonomy slug',
719
				'label'        => '(string) The taxonomy human-readable name',
720
				'labels'       => '(object) Mapping of labels for the taxonomy',
721
				'description'  => '(string) The taxonomy description',
722
				'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
723
				'public'       => '(bool) Whether the taxonomy is public',
724
				'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
725
			);
726
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
727
			break;
728
729
		default :
730
			$method_name = $type['type'] . '_docs';
731
			if ( method_exists( WPCOM_JSON_API_Jetpack_Overrides, $method_name ) ) {
732
				$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
733
			}
734
735
			if ( ! empty( $docs ) ) {
736
				$return[$key] = (object) $this->cast_and_filter(
737
					$value,
738
					/** This filter is documented in class.json-api-endpoints.php */
739
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
740
					false,
741
					$for_output
742
				);
743
			} else {
744
				trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
745
			}
746
		}
747
	}
748
749
	function parse_types( $text ) {
750
		if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
751
			return 'none';
752
		}
753
754
		$types = explode( '|', strtolower( $matches[1] ) );
755
		$return = array();
756
		foreach ( $types as $type ) {
757
			foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
758
				if ( false !== strpos( $type, $operator ) ) {
759
					$item = explode( $operator, $type, 2 );
760
					$return[] = array( 'type' => $item[0], $meaning => $item[1] );
761
					continue 2;
762
				}
763
			}
764
			$return[] = compact( 'type' );
765
		}
766
767
		return $return;
768
	}
769
770
	/**
771
	 * Checks if the endpoint is publicly displayable
772
	 */
773
	function is_publicly_documentable() {
774
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
775
	}
776
777
	/**
778
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
779
	 * Echoes HTML.
780
	 */
781
	function document( $show_description = true ) {
782
		global $wpdb;
783
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
784
		unset( $GLOBALS['post'] );
785
786
		$doc = $this->generate_documentation();
787
788
		if ( $show_description ) :
789
?>
790
<caption>
791
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
792
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
793
</caption>
794
795
<?php endif; ?>
796
797
<?php if ( true === $this->deprecated ) { ?>
798
<p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $this->new_version ); ?></strong></p>
799
<?php } ?>
800
801
<section class="resource-info">
802
	<h2 id="apidoc-resource-info">Resource Information</h2>
803
804
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
805
806
	<thead>
807
		<tr>
808
			<th class="api-index-title" scope="column">&nbsp;</th>
809
			<th class="api-index-title" scope="column">&nbsp;</th>
810
		</tr>
811
	</thead>
812
	<tbody>
813
814
		<tr class="api-index-item">
815
			<th scope="row" class="parameter api-index-item-title">Method</th>
816
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
817
		</tr>
818
819
		<tr class="api-index-item">
820
			<th scope="row" class="parameter api-index-item-title">URL</th>
821
			<?php
822
			$version = WPCOM_JSON_API__CURRENT_VERSION;
823
			if ( !empty( $this->max_version ) ) {
824
				$version = $this->max_version;
825
			}
826
			?>
827
			<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>
828
		</tr>
829
830
		<tr class="api-index-item">
831
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
832
			<?php
833
			$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'] ) );
834
			?>
835
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
836
		</tr>
837
838
	</tbody>
839
	</table>
840
841
</section>
842
843
<?php
844
845
		foreach ( array(
846
			'path'     => 'Method Parameters',
847
			'query'    => 'Query Parameters',
848
			'body'     => 'Request Parameters',
849
			'response' => 'Response Parameters',
850
		) as $doc_section_key => $label ) :
851
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
852
			if ( !$doc_section ) {
853
				continue;
854
			}
855
856
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
857
?>
858
859
<section class="<?php echo $param_label; ?>">
860
861
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
862
863
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
864
865
<thead>
866
	<tr>
867
		<th class="api-index-title" scope="column">Parameter</th>
868
		<th class="api-index-title" scope="column">Type</th>
869
		<th class="api-index-title" scope="column">Description</th>
870
	</tr>
871
</thead>
872
<tbody>
873
874
<?php foreach ( $doc_section as $key => $item ) : ?>
875
876
	<tr class="api-index-item">
877
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
878
		<td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
879
		<td class="description api-index-item-body"><?php
880
881
		$this->generate_doc_description( $item['description'] );
882
883
		?></td>
884
	</tr>
885
886
<?php endforeach; ?>
887
</tbody>
888
</table>
889
</section>
890
<?php endforeach; ?>
891
892
<?php
893
		if ( 'unset' !== $original_post ) {
894
			$GLOBALS['post'] = $original_post;
895
		}
896
	}
897
898
	function add_http_build_query_to_php_content_example( $matches ) {
899
		$trimmed_match = ltrim( $matches[0] );
900
		$pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
901
		$pad = ltrim( $pad, ' ' );
902
		$return = '  ' . str_replace( "\n", "\n  ", $matches[0] );
903
		return " http_build_query({$return}{$pad})";
904
	}
905
906
	/**
907
	 * Recursively generates the <dl>'s to document item descriptions.
908
	 * Echoes HTML.
909
	 */
910
	function generate_doc_description( $item ) {
911
		if ( is_array( $item ) ) : ?>
912
913
		<dl>
914
<?php			foreach ( $item as $description_key => $description_value ) : ?>
915
916
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
917
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
918
919
<?php			endforeach; ?>
920
921
		</dl>
922
923
<?php
924
		else :
925
			echo wp_kses_post( $item );
926
		endif;
927
	}
928
929
	/**
930
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
931
	 * Echoes HTML.
932
	 */
933
	function generate_documentation() {
934
		$format       = str_replace( '%d', '%s', $this->path );
935
		$path_labeled = $format;
936
		if ( ! empty( $this->path_labels ) ) {
937
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
938
		}
939
		$boolean_arg  = array( 'false', 'true' );
940
		$naeloob_arg  = array( 'true', 'false' );
941
942
		$doc = array(
943
			'description'  => $this->description,
944
			'method'       => $this->method,
945
			'path_format'  => $this->path,
946
			'path_labeled' => $path_labeled,
947
			'group'        => $this->group,
948
			'request' => array(
949
				'path'  => array(),
950
				'query' => array(),
951
				'body'  => array(),
952
			),
953
			'response' => array(
954
				'body' => array(),
955
			)
956
		);
957
958
		foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
959
			foreach ( (array) $this->$_property as $key => $description ) {
960
				if ( is_array( $description ) ) {
961
					$description_keys = array_keys( $description );
962
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
963
						$type = '(bool)';
964
					} else {
965
						$type = '(string)';
966
					}
967
968
					if ( 'response_format' !== $_property ) {
969
						// hack - don't show "(default)" in response format
970
						reset( $description );
971
						$description_key = key( $description );
972
						$description[$description_key] = "(default) {$description[$description_key]}";
973
					}
974
				} else {
975
					$types   = $this->parse_types( $description );
976
					$type    = array();
977
					$default = '';
978
979
					if ( 'none' == $types ) {
980
						$types = array();
981
						$types[]['type'] = 'none';
982
					}
983
984
					foreach ( $types as $type_array ) {
985
						$type[] = $type_array['type'];
986
						if ( isset( $type_array['default'] ) ) {
987
							$default = $type_array['default'];
988
							if ( 'string' === $type_array['type'] ) {
989
								$default = "'$default'";
990
							}
991
						}
992
					}
993
					$type = '(' . join( '|', $type ) . ')';
994
					$noop = ''; // skip an index in list below
995
					list( $noop, $description ) = explode( ')', $description, 2 );
996
					$description = trim( $description );
997
					if ( $default ) {
998
						$description .= " Default: $default.";
999
					}
1000
				}
1001
1002
				$item = compact( 'type', 'description' );
1003
1004
				if ( 'response_format' === $_property ) {
1005
					$doc['response'][$doc_item][$key] = $item;
1006
				} else {
1007
					$doc['request'][$doc_item][$key] = $item;
1008
				}
1009
			}
1010
		}
1011
1012
		return $doc;
1013
	}
1014
1015
	function user_can_view_post( $post_id ) {
1016
		$post = get_post( $post_id );
1017
		if ( !$post || is_wp_error( $post ) ) {
1018
			return false;
1019
		}
1020
1021 View Code Duplication
		if ( 'inherit' === $post->post_status ) {
1022
			$parent_post = get_post( $post->post_parent );
1023
			$post_status_obj = get_post_status_object( $parent_post->post_status );
1024
		} else {
1025
			$post_status_obj = get_post_status_object( $post->post_status );
1026
		}
1027
1028
		if ( !$post_status_obj->public ) {
1029
			if ( is_user_logged_in() ) {
1030
				if ( $post_status_obj->protected ) {
1031
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1032
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1033
					}
1034
				} elseif ( $post_status_obj->private ) {
1035
					if ( !current_user_can( 'read_post', $post->ID ) ) {
1036
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1037
					}
1038
				} elseif ( in_array( $post->post_status, array( 'inherit', 'trash' ) ) ) {
1039
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1040
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1041
					}
1042
				} elseif ( 'auto-draft' === $post->post_status ) {
1043
					//allow auto-drafts
1044
				} else {
1045
					return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1046
				}
1047
			} else {
1048
				return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1049
			}
1050
		}
1051
1052 View Code Duplication
		if (
1053
			-1 == get_option( 'blog_public' ) &&
1054
			/**
1055
			 * Filter access to a specific post.
1056
			 *
1057
			 * @module json-api
1058
			 *
1059
			 * @since 3.4.0
1060
			 *
1061
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1062
			 * @param WP_Post $post Post data.
1063
			 */
1064
			! apply_filters(
1065
				'wpcom_json_api_user_can_view_post',
1066
				current_user_can( 'read_post', $post->ID ),
1067
				$post
1068
			)
1069
		) {
1070
			return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
1071
		}
1072
1073 View Code Duplication
		if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
1074
			return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
1075
		}
1076
1077
		return true;
1078
	}
1079
1080
	/**
1081
	 * Returns author object.
1082
	 *
1083
	 * @param object $author user ID, user row, WP_User object, comment row, post row
1084
	 * @param bool $show_email_and_ip output the author's email address and IP address?
1085
	 *
1086
	 * @return object
1087
	 */
1088
	function get_author( $author, $show_email_and_ip = false ) {
1089
		$ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1090
1091
		if ( isset( $author->comment_author_email ) ) {
1092
			$ID          = 0;
1093
			$login       = '';
1094
			$email       = $author->comment_author_email;
1095
			$name        = $author->comment_author;
1096
			$first_name  = '';
1097
			$last_name   = '';
1098
			$URL         = $author->comment_author_url;
1099
			$avatar_URL  = $this->api->get_avatar_url( $author );
1100
			$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1101
			$nice        = '';
1102
			$site_id     = -1;
1103
1104
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1105
			// "&" is the only email/URL character altered by wp_kses()
1106
			foreach ( array( 'email', 'URL' ) as $field ) {
1107
				$$field = str_replace( '&amp;', '&', $$field );
1108
			}
1109
		} else {
1110
			if ( isset( $author->user_id ) && $author->user_id ) {
1111
				$author = $author->user_id;
1112
			} elseif ( isset( $author->user_email ) ) {
1113
				$author = $author->ID;
1114
			} elseif ( isset( $author->post_author ) ) {
1115
				// then $author is a Post Object.
1116
				if ( 0 == $author->post_author )
1117
					return null;
1118
				/**
1119
				 * Filter whether the current site is a Jetpack site.
1120
				 *
1121
				 * @module json-api
1122
				 *
1123
				 * @since 3.3.0
1124
				 *
1125
				 * @param bool false Is the current site a Jetpack site. Default to false.
1126
				 * @param int get_current_blog_id() Blog ID.
1127
				 */
1128
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1129
				$post_id = $author->ID;
1130
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1131
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1132
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1133
					$login      = '';
1134
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1135
					$first_name = '';
1136
					$last_name  = '';
1137
					$URL        = '';
1138
					$nice       = '';
1139
				} else {
1140
					$author = $author->post_author;
1141
				}
1142
			}
1143
1144
			if ( ! isset( $ID ) ) {
1145
				$user = get_user_by( 'id', $author );
1146
				if ( ! $user || is_wp_error( $user ) ) {
1147
					trigger_error( 'Unknown user', E_USER_WARNING );
1148
1149
					return null;
1150
				}
1151
				$ID         = $user->ID;
1152
				$email      = $user->user_email;
1153
				$login      = $user->user_login;
1154
				$name       = $user->display_name;
1155
				$first_name = $user->first_name;
1156
				$last_name  = $user->last_name;
1157
				$URL        = $user->user_url;
1158
				$nice       = $user->user_nicename;
1159
			}
1160
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1161
				$active_blog = get_active_blog_for_user( $ID );
1162
				$site_id     = $active_blog->blog_id;
1163
				$profile_URL = "https://en.gravatar.com/{$login}";
1164
			} else {
1165
				$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1166
				$site_id     = -1;
1167
			}
1168
1169
			$avatar_URL = $this->api->get_avatar_url( $email );
1170
		}
1171
1172
		if ( $show_email_and_ip ) {
1173
			$email = (string) $email;
1174
			$ip_address = (string) $ip_address;
1175
		} else {
1176
			$email = false;
1177
			$ip_address = false;
1178
		}
1179
1180
		$author = array(
1181
			'ID'          => (int) $ID,
1182
			'login'       => (string) $login,
1183
			'email'       => $email, // (string|bool)
1184
			'name'        => (string) $name,
1185
			'first_name'  => (string) $first_name,
1186
			'last_name'   => (string) $last_name,
1187
			'nice_name'   => (string) $nice,
1188
			'URL'         => (string) esc_url_raw( $URL ),
1189
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1190
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1191
			'ip_address'  => $ip_address, // (string|bool)
1192
		);
1193
1194
		if ($site_id > -1) {
1195
			$author['site_ID'] = (int) $site_id;
1196
		}
1197
1198
		return (object) $author;
1199
	}
1200
1201
	function get_media_item( $media_id ) {
1202
		$media_item = get_post( $media_id );
1203
1204
		if ( !$media_item || is_wp_error( $media_item ) )
1205
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1206
1207
		$response = array(
1208
			'id'    => strval( $media_item->ID ),
1209
			'date' =>  (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1210
			'parent'           => $media_item->post_parent,
1211
			'link'             => wp_get_attachment_url( $media_item->ID ),
1212
			'title'            => $media_item->post_title,
1213
			'caption'          => $media_item->post_excerpt,
1214
			'description'      => $media_item->post_content,
1215
			'metadata'         => wp_get_attachment_metadata( $media_item->ID ),
1216
		);
1217
1218
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1219
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1220
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1221
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1222
		}
1223
1224
		$response['meta'] = (object) array(
1225
			'links' => (object) array(
1226
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1227
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1228
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1229
			),
1230
		);
1231
1232
		return (object) $response;
1233
	}
1234
1235
	function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1236
1237
		if ( ! $media_item ) {
1238
			$media_item = get_post( $media_id );
1239
		}
1240
1241
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1242
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1243
		}
1244
1245
		$attachment_file = get_attached_file( $media_item->ID );
1246
1247
		$file = basename( $attachment_file ? $attachment_file : $file );
1248
		$file_info = pathinfo( $file );
1249
		$ext  = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1250
1251
		$response = array(
1252
			'ID'           => $media_item->ID,
1253
			'URL'          => wp_get_attachment_url( $media_item->ID ),
1254
			'guid'         => $media_item->guid,
1255
			'date'         => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1256
			'post_ID'      => $media_item->post_parent,
1257
			'author_ID'    => (int) $media_item->post_author,
1258
			'file'         => $file,
1259
			'mime_type'    => $media_item->post_mime_type,
1260
			'extension'    => $ext,
1261
			'title'        => $media_item->post_title,
1262
			'caption'      => $media_item->post_excerpt,
1263
			'description'  => $media_item->post_content,
1264
			'alt'          => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1265
			'icon'         => wp_mime_type_icon( $media_item->ID ),
1266
			'thumbnails'   => array()
1267
		);
1268
1269 View Code Duplication
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1270
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1271
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1272
				$response['height'] = $metadata['height'];
1273
				$response['width'] = $metadata['width'];
1274
			}
1275
1276
			if ( isset( $metadata['sizes'] ) ) {
1277
				/**
1278
				 * Filter the thumbnail sizes available for each attachment ID.
1279
				 *
1280
				 * @module json-api
1281
				 *
1282
				 * @since 3.9.0
1283
				 *
1284
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1285
				 * @param string $media_id Attachment ID.
1286
				 */
1287
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
1288
				if ( is_array( $sizes ) ) {
1289
					foreach ( $sizes as $size => $size_details ) {
1290
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1291
					}
1292
				}
1293
			}
1294
1295
			if ( isset( $metadata['image_meta'] ) ) {
1296
				$response['exif'] = $metadata['image_meta'];
1297
			}
1298
		}
1299
1300
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1301
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1302
			$response['length'] = $metadata['length'];
1303
			$response['exif']   = $metadata;
1304
		}
1305
1306
		$is_video = false;
1307
1308
		if (
1309
			in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1310
			||
1311
			$response['mime_type'] === 'video/videopress'
1312
		) {
1313
			$is_video = true;
1314
		}
1315
1316
1317
		if ( $is_video ) {
1318
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1319
1320
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1321
				$response['height'] = $metadata['height'];
1322
				$response['width']  = $metadata['width'];
1323
			}
1324
1325
			if ( isset( $metadata['length'] ) ) {
1326
				$response['length'] = $metadata['length'];
1327
			}
1328
1329
			// add VideoPress info
1330
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1331
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1332
1333
				// If we failed to get VideoPress info, but it exists in the meta data (for some reason)
1334
				// then let's use that.
1335
				if ( false === $info && isset( $metadata['videopress'] ) ) {
1336
				    $info = (object) $metadata['videopress'];
1337
				}
1338
1339
				// Thumbnails
1340
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1341
					$response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' );
1342
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1343
						if ( video_format_done( $info, $size ) ) {
1344
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1345
						} else {
1346
							unset( $response['thumbnails'][ $size ] );
1347
						}
1348
					}
1349
				}
1350
1351
				// If we didn't get VideoPress information (for some reason) then let's
1352
				// not try and include it in the response.
1353
				if ( isset( $info->guid ) ) {
1354
					$response['videopress_guid']            = $info->guid;
1355
					$response['videopress_processing_done'] = true;
1356
					if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1357
						$response['videopress_processing_done'] = false;
1358
					}
1359
				}
1360
			}
1361
		}
1362
1363
		$response['thumbnails'] = (object) $response['thumbnails'];
1364
1365
		$response['meta'] = (object) array(
1366
			'links' => (object) array(
1367
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1368
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1369
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1370
			),
1371
		);
1372
1373
		// add VideoPress link to the meta
1374
		if ( isset ( $response['videopress_guid'] ) ) {
1375
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1376
				$response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1377
			}
1378
		}
1379
1380
		if ( $media_item->post_parent > 0 ) {
1381
			$response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1382
		}
1383
1384
		return (object) $response;
1385
	}
1386
1387
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1388
1389
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1390
		/// keep updating this function
1391
		if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1392
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1393
		}
1394
1395
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1396
	}
1397
1398
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1399
		// Permissions
1400
		switch ( $context ) {
1401
		case 'edit' :
1402
			$tax = get_taxonomy( $taxonomy_type );
1403
			if ( !current_user_can( $tax->cap->edit_terms ) )
1404
				return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1405
			break;
1406
		case 'display' :
1407
			if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1408
				return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1409
			}
1410
			break;
1411
		default :
1412
			return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1413
		}
1414
1415
		$response                = array();
1416
		$response['ID']          = (int) $taxonomy->term_id;
1417
		$response['name']        = (string) $taxonomy->name;
1418
		$response['slug']        = (string) $taxonomy->slug;
1419
		$response['description'] = (string) $taxonomy->description;
1420
		$response['post_count']  = (int) $taxonomy->count;
1421
1422
		if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1423
			$response['parent'] = (int) $taxonomy->parent;
1424
		}
1425
1426
		$response['meta'] = (object) array(
1427
			'links' => (object) array(
1428
				'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1429
				'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1430
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1431
			),
1432
		);
1433
1434
		return (object) $response;
1435
	}
1436
1437
	/**
1438
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1439
	 *
1440
	 * @param $date_gmt (string) GMT datetime string.
1441
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1442
	 *
1443
	 * @return string
1444
	 */
1445
	function format_date( $date_gmt, $date = null ) {
1446
		return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1447
	}
1448
1449
	/**
1450
	 * Parses a date string and returns the local and GMT representations
1451
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1452
	 * timezones or offsets. If the parsed datetime was not localized to a
1453
	 * particular timezone or offset we will assume it was given in GMT
1454
	 * relative to now and will convert it to local time using either the
1455
	 * timezone set in the options table for the blog or the GMT offset.
1456
	 *
1457
	 * @param datetime string
1458
	 *
1459
	 * @return array( $local_time_string, $gmt_time_string )
1460
	 */
1461
	function parse_date( $date_string ) {
1462
		$date_string_info = date_parse( $date_string );
1463
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1464
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1465
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1466
				$dt_local = clone $dt_utc = new DateTime( $date_string );
1467
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1468
				return array(
1469
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1470
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1471
				);
1472
			}
1473
1474
			// It's parseable but no TZ info so assume UTC
1475
			$dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1476
		} else {
1477
			// Could not parse time, use now in UTC
1478
			$dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1479
		}
1480
1481
		// First try to use timezone as it's daylight savings aware.
1482
		$timezone_string = get_option( 'timezone_string' );
1483
		if ( $timezone_string ) {
1484
			$tz = timezone_open( $timezone_string );
1485
			if ( $tz ) {
1486
				$dt_local->setTimezone( $tz );
1487
				return array(
1488
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1489
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1490
				);
1491
			}
1492
		}
1493
1494
		// Fallback to GMT offset (in hours)
1495
		// NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1496
		$gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1497
		$dt_local->modify("+{$gmt_offset_seconds} seconds");
1498
		return array(
1499
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1500
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1501
		);
1502
	}
1503
1504
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1505
	function load_theme_functions() {
1506
		// bail if we've done this already (can happen when calling /batch endpoint)
1507
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) )
1508
			return;
1509
1510
		// VIP context loading is handled elsewhere, so bail to prevent
1511
		// duplicate loading. See `switch_to_blog_and_validate_user()`
1512
		if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1513
			return;
1514
		}
1515
1516
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1517
1518
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1519
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1520
1521
		$copy_dirs = array( get_template_directory() );
1522
1523
		// Is this a child theme? Load the child theme's functions file.
1524
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1525
			foreach ( $function_files as $function_file ) {
1526
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1527
					require_once(  get_stylesheet_directory() . $function_file );
1528
				}
1529
			}
1530
			$copy_dirs[] = get_stylesheet_directory();
1531
		}
1532
1533
		foreach ( $function_files as $function_file ) {
1534
			if ( file_exists( get_template_directory() . $function_file ) ) {
1535
				require_once(  get_template_directory() . $function_file );
1536
			}
1537
		}
1538
1539
		// add inc/wpcom.php and/or includes/wpcom.php
1540
		wpcom_load_theme_compat_file();
1541
1542
		// Enable including additional directories or files in actions to be copied
1543
		$copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1544
1545
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1546
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1547
1548
		/**
1549
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1550
		 *
1551
		 * The REST API does not load the theme when processing requests.
1552
		 * To enable theme-based functionality, the API will load the '/functions.php',
1553
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1554
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1555
		 *
1556
		 * @module json-api
1557
		 *
1558
		 * @since 3.2.0
1559
		 */
1560
		do_action( 'restapi_theme_after_setup_theme' );
1561
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1562
1563
		/**
1564
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1565
		 *
1566
		 * The REST API does not load the theme when processing requests.
1567
		 * To enable theme-based functionality, the API will load the '/functions.php',
1568
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1569
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1570
		 *
1571
		 * @module json-api
1572
		 *
1573
		 * @since 3.2.0
1574
		 */
1575
		do_action( 'restapi_theme_init' );
1576
	}
1577
1578
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1579
		global $wp_filter;
1580
		foreach ( $wp_filter as $hook => $actions ) {
1581
1582
			if ( $from_hook != $hook ) {
1583
				continue;
1584
			}
1585
			if ( ! has_action( $hook ) ) {
1586
				continue;
1587
			}
1588
1589
			foreach ( $actions as $priority => $callbacks ) {
1590
				foreach( $callbacks as $callback_key => $callback_data ) {
1591
					$callback = $callback_data['function'];
1592
1593
					// use reflection api to determine filename where function is defined
1594
					$reflection = $this->get_reflection( $callback );
1595
1596
					if ( false !== $reflection ) {
1597
						$file_name = $reflection->getFileName();
1598
						foreach( $base_paths as $base_path ) {
1599
1600
							// only copy hooks with functions which are part of the specified files
1601
							if ( 0 === strpos( $file_name, $base_path ) ) {
1602
								add_action(
1603
									$to_hook,
1604
									$callback_data['function'],
1605
									$priority,
1606
									$callback_data['accepted_args']
1607
								);
1608
							}
1609
						}
1610
					}
1611
				}
1612
			}
1613
		}
1614
	}
1615
1616
	function get_reflection( $callback ) {
1617
		if ( is_array( $callback ) ) {
1618
			list( $class, $method ) = $callback;
1619
			return new ReflectionMethod( $class, $method );
1620
		}
1621
1622
		if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) {
1623
			list( $class, $method ) = explode( "::", $callback );
1624
			return new ReflectionMethod( $class, $method );
1625
		}
1626
1627
		if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) {
1628
			return new ReflectionMethod( $callback, "__invoke" );
1629
		}
1630
1631
		if ( is_string( $callback ) && strpos( $callback, "::" ) == false && function_exists( $callback ) ) {
1632
			return new ReflectionFunction( $callback );
1633
		}
1634
1635
		return false;
1636
	}
1637
1638
	/**
1639
	* Check whether a user can view or edit a post type
1640
	* @param string $post_type              post type to check
1641
	* @param string $context                'display' or 'edit'
1642
	* @return bool
1643
	*/
1644 View Code Duplication
	function current_user_can_access_post_type( $post_type, $context='display' ) {
1645
		$post_type_object = get_post_type_object( $post_type );
1646
		if ( ! $post_type_object ) {
1647
			return false;
1648
		}
1649
1650
		switch( $context ) {
1651
			case 'edit':
1652
				return current_user_can( $post_type_object->cap->edit_posts );
1653
			case 'display':
1654
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1655
			default:
1656
				return false;
1657
		}
1658
	}
1659
1660
	function is_post_type_allowed( $post_type ) {
1661
		// if the post type is empty, that's fine, WordPress will default to post
1662
		if ( empty( $post_type ) ) {
1663
			return true;
1664
		}
1665
1666
		// allow special 'any' type
1667
		if ( 'any' == $post_type ) {
1668
			return true;
1669
		}
1670
1671
		// check for allowed types
1672
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1673
			return true;
1674
		}
1675
1676
		if ( $post_type_object = get_post_type_object( $post_type ) ) {
1677
			if ( ! empty( $post_type_object->show_in_rest ) ) {
1678
				return $post_type_object->show_in_rest;
1679
			}
1680
			if ( ! empty( $post_type_object->publicly_queryable ) ) {
1681
				return $post_type_object->publicly_queryable;
1682
			}
1683
		}
1684
1685
		return ! empty( $post_type_object->public );
1686
	}
1687
1688
	/**
1689
	 * Gets the whitelisted post types that JP should allow access to.
1690
	 *
1691
	 * @return array Whitelisted post types.
1692
	 */
1693 View Code Duplication
	protected function _get_whitelisted_post_types() {
1694
		$allowed_types = array( 'post', 'page', 'revision' );
1695
1696
		/**
1697
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1698
		 *
1699
		 * @module json-api
1700
		 *
1701
		 * @since 2.2.3
1702
		 *
1703
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1704
		 */
1705
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1706
1707
		return array_unique( $allowed_types );
1708
	}
1709
1710
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1711
1712
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1713
1714
		$media_ids = $errors = array();
1715
		$user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
1716
		$media_attrs = array_values( $media_attrs ); // reset the keys
1717
		$i = 0;
1718
1719
		if ( ! empty( $media_files ) ) {
1720
			$this->api->trap_wp_die( 'upload_error' );
1721
			foreach ( $media_files as $media_item ) {
1722
				$_FILES['.api.media.item.'] = $media_item;
1723 View Code Duplication
				if ( ! $user_can_upload_files ) {
1724
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1725
				} else {
1726
					if ( $force_parent_id ) {
1727
						$parent_id = absint( $force_parent_id );
1728
					} elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1729
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1730
					} else {
1731
						$parent_id = 0;
1732
					}
1733
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1734
				}
1735
				if ( is_wp_error( $media_id ) ) {
1736
					$errors[$i]['file']   = $media_item['name'];
1737
					$errors[$i]['error']   = $media_id->get_error_code();
1738
					$errors[$i]['message'] = $media_id->get_error_message();
1739
				} else {
1740
					$media_ids[$i] = $media_id;
1741
				}
1742
1743
				$i++;
1744
			}
1745
			$this->api->trap_wp_die( null );
1746
			unset( $_FILES['.api.media.item.'] );
1747
		}
1748
1749
		if ( ! empty( $media_urls ) ) {
1750
			foreach ( $media_urls as $url ) {
1751 View Code Duplication
				if ( ! $user_can_upload_files ) {
1752
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1753
				} else {
1754
					if ( $force_parent_id ) {
1755
						$parent_id = absint( $force_parent_id );
1756
					} else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1757
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1758
					} else {
1759
						$parent_id = 0;
1760
					}
1761
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1762
				}
1763
				if ( is_wp_error( $media_id ) ) {
1764
					$errors[$i] = array(
1765
						'file'    => $url,
1766
						'error'   => $media_id->get_error_code(),
1767
						'message' => $media_id->get_error_message(),
1768
					);
1769
				} elseif ( ! empty( $media_id ) ) {
1770
					$media_ids[$i] = $media_id;
1771
				}
1772
1773
				$i++;
1774
			}
1775
		}
1776
1777
		if ( ! empty( $media_attrs ) ) {
1778
			foreach ( $media_ids as $index => $media_id ) {
1779
				if ( empty( $media_attrs[$index] ) )
1780
					continue;
1781
1782
				$attrs = $media_attrs[$index];
1783
				$insert = array();
1784
1785
				// Attributes: Title, Caption, Description
1786
1787
				if ( isset( $attrs['title'] ) ) {
1788
					$insert['post_title'] = $attrs['title'];
1789
				}
1790
1791
				if ( isset( $attrs['caption'] ) ) {
1792
					$insert['post_excerpt'] = $attrs['caption'];
1793
				}
1794
1795
				if ( isset( $attrs['description'] ) ) {
1796
					$insert['post_content'] = $attrs['description'];
1797
				}
1798
1799
				if ( ! empty( $insert ) ) {
1800
					$insert['ID'] = $media_id;
1801
					wp_update_post( (object) $insert );
1802
				}
1803
1804
				// Attributes: Alt
1805
1806 View Code Duplication
				if ( isset( $attrs['alt'] ) ) {
1807
					$alt = wp_strip_all_tags( $attrs['alt'], true );
1808
					update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1809
				}
1810
1811
				// Attributes: Artist, Album
1812
1813
				$id3_meta = array();
1814
1815 View Code Duplication
				foreach ( array( 'artist', 'album' ) as $key ) {
1816
					if ( isset( $attrs[ $key ] ) ) {
1817
						$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1818
					}
1819
				}
1820
1821 View Code Duplication
				if ( ! empty( $id3_meta ) ) {
1822
					// Before updating metadata, ensure that the item is audio
1823
					$item = $this->get_media_item_v1_1( $media_id );
1824
					if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1825
						wp_update_attachment_metadata( $media_id, $id3_meta );
1826
					}
1827
				}
1828
			}
1829
		}
1830
1831
		return array( 'media_ids' => $media_ids, 'errors' => $errors );
1832
1833
	}
1834
1835
	function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1836
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1837
			return false;
1838
1839
		// if we didn't get a URL, let's bail
1840
		$parsed = @parse_url( $url );
1841
		if ( empty( $parsed ) )
1842
			return false;
1843
1844
		$tmp = download_url( $url );
1845
		if ( is_wp_error( $tmp ) ) {
1846
			return $tmp;
1847
		}
1848
1849
		// First check to see if we get a mime-type match by file, otherwise, check to
1850
		// see if WordPress supports this file as an image. If neither, then it is not supported.
1851
		if ( ! $this->is_file_supported_for_sideloading( $tmp ) && 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1852
			@unlink( $tmp );
1853
			return false;
1854
		}
1855
1856
		// emulate a $_FILES entry
1857
		$file_array = array(
1858
			'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1859
			'tmp_name' => $tmp,
1860
		);
1861
1862
		$id = media_handle_sideload( $file_array, $parent_post_id );
1863
		if ( file_exists( $tmp ) ) {
1864
			@unlink( $tmp );
1865
		}
1866
1867
		if ( is_wp_error( $id ) ) {
1868
			return $id;
1869
		}
1870
1871
		if ( ! $id || ! is_int( $id ) ) {
1872
			return false;
1873
		}
1874
1875
		return $id;
1876
	}
1877
1878
	/**
1879
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1880
	 *
1881
	 * @param string $file Path to file to get its mime type.
1882
	 *
1883
	 * @return bool
1884
	 */
1885 View Code Duplication
	protected function is_file_supported_for_sideloading( $file ) {
1886
		if ( class_exists( 'finfo' ) ) { // php 5.3+
1887
			$finfo = new finfo( FILEINFO_MIME );
1888
			$mime = explode( '; ', $finfo->file( $file ) );
1889
			$type = $mime[0];
1890
1891
		} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
1892
			$type = mime_content_type( $file );
1893
1894
		} else {
1895
			return false;
1896
		}
1897
1898
		/**
1899
		 * Filter the list of supported mime types for media sideloading.
1900
		 *
1901
		 * @since 4.0.0
1902
		 *
1903
		 * @module json-api
1904
		 *
1905
		 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
1906
		 */
1907
		$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
1908
			'image/png',
1909
			'image/jpeg',
1910
			'image/gif',
1911
			'image/bmp',
1912
			'video/quicktime',
1913
			'video/mp4',
1914
			'video/mpeg',
1915
			'video/ogg',
1916
			'video/3gpp',
1917
			'video/3gpp2',
1918
			'video/h261',
1919
			'video/h262',
1920
			'video/h264',
1921
			'video/x-msvideo',
1922
			'video/x-ms-wmv',
1923
			'video/x-ms-asf',
1924
		) );
1925
1926
		// If the type returned was not an array as expected, then we know we don't have a match.
1927
		if ( ! is_array( $supported_mime_types ) ) {
1928
			return false;
1929
		}
1930
1931
		return in_array( $type, $supported_mime_types );
1932
	}
1933
1934
	function allow_video_uploads( $mimes ) {
1935
		// if we are on Jetpack, bail - Videos are already allowed
1936
		if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1937
			return $mimes;
1938
		}
1939
1940
		// extra check that this filter is only ever applied during REST API requests
1941
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1942
			return $mimes;
1943
		}
1944
1945
		// bail early if they already have the upgrade..
1946
		if ( get_option( 'video_upgrade' ) == '1' ) {
1947
			return $mimes;
1948
		}
1949
1950
		// lets whitelist to only specific clients right now
1951
		$clients_allowed_video_uploads = array();
1952
		/**
1953
		 * Filter the list of whitelisted video clients.
1954
		 *
1955
		 * @module json-api
1956
		 *
1957
		 * @since 3.2.0
1958
		 *
1959
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1960
		 */
1961
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1962
		if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1963
			return $mimes;
1964
		}
1965
1966
		$mime_list = wp_get_mime_types();
1967
1968
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1969
		/**
1970
		 * Filter the video filetypes allowed on the site.
1971
		 *
1972
		 * @module json-api
1973
		 *
1974
		 * @since 3.2.0
1975
		 *
1976
		 * @param array $video_exts Array of video filetypes allowed on the site.
1977
		 */
1978
		$video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
1979
		$video_mimes = array();
1980
1981
		if ( !empty( $video_exts ) ) {
1982
			foreach ( $video_exts as $ext ) {
1983
				foreach ( $mime_list as $ext_pattern => $mime ) {
1984
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
1985
						$video_mimes[$ext_pattern] = $mime;
1986
				}
1987
			}
1988
1989
			$mimes = array_merge( $mimes, $video_mimes );
1990
		}
1991
1992
		return $mimes;
1993
	}
1994
1995
	function is_current_site_multi_user() {
1996
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
1997
		if ( false === $users ) {
1998
			$user_query = new WP_User_Query( array(
1999
				'blog_id' => get_current_blog_id(),
2000
				'fields'  => 'ID',
2001
			) );
2002
			$users = (int) $user_query->get_total();
2003
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2004
		}
2005
		return $users > 1;
2006
	}
2007
2008
	function allows_cross_origin_requests() {
2009
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2010
	}
2011
2012
	function allows_unauthorized_requests( $origin, $complete_access_origins  ) {
2013
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2014
	}
2015
2016
	function get_platform() {
2017
		return wpcom_get_sal_platform( $this->api->token_details );
2018
	}
2019
2020
	/**
2021
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2022
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
2023
	 *
2024
	 * Override this method if you want to do something different.
2025
	 *
2026
	 * @param  int  $blog_id
2027
	 * @return bool
2028
	 */
2029
	function force_wpcom_request( $blog_id ) {
2030
		return false;
2031
	}
2032
2033
	/**
2034
	 * Return endpoint response
2035
	 *
2036
	 * @param ... determined by ->$path
2037
	 *
2038
	 * @return
2039
	 * 	falsy: HTTP 500, no response body
2040
	 *	WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2041
	 *	$data: HTTP 200, json_encode( $data ) response body
2042
	 */
2043
	abstract function callback( $path = '' );
2044
2045
2046
}
2047
2048
require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
2049