Completed
Pull Request — feature/sync-json-endpoints (#7169)
by
unknown
11:30
created

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

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

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1784
		}
1785
1786
		if ( is_wp_error( $id ) ) {
1787
			return $id;
1788
		}
1789
1790
		if ( ! $id || ! is_int( $id ) ) {
1791
			return false;
1792
		}
1793
1794
		return $id;
1795
	}
1796
1797
	/**
1798
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1799
	 *
1800
	 * @param string $file Path to file to get its mime type.
1801
	 *
1802
	 * @return bool
1803
	 */
1804 View Code Duplication
	protected function is_file_supported_for_sideloading( $file ) {
1805
		if ( class_exists( 'finfo' ) ) { // php 5.3+
1806
			$finfo = new finfo( FILEINFO_MIME );
1807
			$mime = explode( '; ', $finfo->file( $file ) );
1808
			$type = $mime[0];
1809
1810
		} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
1811
			$type = mime_content_type( $file );
1812
1813
		} else {
1814
			return false;
1815
		}
1816
1817
		/**
1818
		 * Filter the list of supported mime types for media sideloading.
1819
		 *
1820
		 * @since 4.0.0
1821
		 *
1822
		 * @module json-api
1823
		 *
1824
		 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
1825
		 */
1826
		$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
1827
			'image/png',
1828
			'image/jpeg',
1829
			'image/gif',
1830
			'image/bmp',
1831
			'video/quicktime',
1832
			'video/mp4',
1833
			'video/mpeg',
1834
			'video/ogg',
1835
			'video/3gpp',
1836
			'video/3gpp2',
1837
			'video/h261',
1838
			'video/h262',
1839
			'video/h264',
1840
			'video/x-msvideo',
1841
			'video/x-ms-wmv',
1842
			'video/x-ms-asf',
1843
		) );
1844
1845
		// If the type returned was not an array as expected, then we know we don't have a match.
1846
		if ( ! is_array( $supported_mime_types ) ) {
1847
			return false;
1848
		}
1849
1850
		return in_array( $type, $supported_mime_types );
1851
	}
1852
1853
	function allow_video_uploads( $mimes ) {
1854
		// if we are on Jetpack, bail - Videos are already allowed
1855
		if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1856
			return $mimes;
1857
		}
1858
1859
		// extra check that this filter is only ever applied during REST API requests
1860
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1861
			return $mimes;
1862
		}
1863
1864
		// bail early if they already have the upgrade..
1865
		if ( get_option( 'video_upgrade' ) == '1' ) {
1866
			return $mimes;
1867
		}
1868
1869
		// lets whitelist to only specific clients right now
1870
		$clients_allowed_video_uploads = array();
1871
		/**
1872
		 * Filter the list of whitelisted video clients.
1873
		 *
1874
		 * @module json-api
1875
		 *
1876
		 * @since 3.2.0
1877
		 *
1878
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1879
		 */
1880
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1881
		if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1882
			return $mimes;
1883
		}
1884
1885
		$mime_list = wp_get_mime_types();
1886
1887
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1888
		/**
1889
		 * Filter the video filetypes allowed on the site.
1890
		 *
1891
		 * @module json-api
1892
		 *
1893
		 * @since 3.2.0
1894
		 *
1895
		 * @param array $video_exts Array of video filetypes allowed on the site.
1896
		 */
1897
		$video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
1898
		$video_mimes = array();
1899
1900
		if ( !empty( $video_exts ) ) {
1901
			foreach ( $video_exts as $ext ) {
1902
				foreach ( $mime_list as $ext_pattern => $mime ) {
1903
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
1904
						$video_mimes[$ext_pattern] = $mime;
1905
				}
1906
			}
1907
1908
			$mimes = array_merge( $mimes, $video_mimes );
1909
		}
1910
1911
		return $mimes;
1912
	}
1913
1914
	function is_current_site_multi_user() {
1915
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
1916
		if ( false === $users ) {
1917
			$user_query = new WP_User_Query( array(
1918
				'blog_id' => get_current_blog_id(),
1919
				'fields'  => 'ID',
1920
			) );
1921
			$users = (int) $user_query->get_total();
1922
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
1923
		}
1924
		return $users > 1;
1925
	}
1926
1927
	function allows_cross_origin_requests() {
1928
		return 'GET' == $this->method || $this->allow_cross_origin_request;
1929
	}
1930
1931
	function allows_unauthorized_requests( $origin, $complete_access_origins  ) {
1932
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
1933
	}
1934
1935
	function get_platform() {
1936
		return wpcom_get_sal_platform( $this->api->token_details );
1937
	}
1938
1939
	/**
1940
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
1941
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
1942
	 *
1943
	 * Override this method if you want to do something different.
1944
	 *
1945
	 * @param  int  $blog_id
1946
	 * @return bool
1947
	 */
1948
	function force_wpcom_request( $blog_id ) {
1949
		return false;
1950
	}
1951
1952
	/**
1953
	 * Return endpoint response
1954
	 *
1955
	 * @param ... determined by ->$path
1956
	 *
1957
	 * @return
1958
	 * 	falsy: HTTP 500, no response body
1959
	 *	WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
1960
	 *	$data: HTTP 200, json_encode( $data ) response body
1961
	 */
1962
	abstract function callback( $path = '' );
1963
1964
1965
}
1966
1967
require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
1968