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