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