Completed
Push — add/jetpack-start-reconnect-us... ( 7f1fbb )
by
unknown
10:36
created

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

Upgrade to new PHP Analysis Engine

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

1
<?php
2
3
require_once( dirname( __FILE__ ) . '/json-api-config.php' );
4
require_once( dirname( __FILE__ ) . '/sal/class.json-api-links.php' );
5
require_once( dirname( __FILE__ ) . '/sal/class.json-api-metadata.php' );
6
require_once( dirname( __FILE__ ) . '/sal/class.json-api-date.php' );
7
8
// Endpoint
9
abstract class WPCOM_JSON_API_Endpoint {
10
	// The API Object
11
	public $api;
12
13
	// The link-generating utility class
14
	public $links;
15
16
	public $pass_wpcom_user_details = false;
17
18
	// One liner.
19
	public $description;
20
21
	// Object Grouping For Documentation (Users, Posts, Comments)
22
	public $group;
23
24
	// Stats extra value to bump
25
	public $stat;
26
27
	// HTTP Method
28
	public $method = 'GET';
29
30
	// Minimum version of the api for which to serve this endpoint
31
	public $min_version = '0';
32
33
	// Maximum version of the api for which to serve this endpoint
34
	public $max_version = WPCOM_JSON_API__CURRENT_VERSION;
35
36
	// Path at which to serve this endpoint: sprintf() format.
37
	public $path = '';
38
39
	// Identifiers to fill sprintf() formatted $path
40
	public $path_labels = array();
41
42
	// Accepted query parameters
43
	public $query = array(
44
		// Parameter name
45
		'context' => array(
46
			// Default value => description
47
			'display' => 'Formats the output as HTML for display.  Shortcodes are parsed, paragraph tags are added, etc..',
48
			// Other possible values => description
49
			'edit'    => 'Formats the output for editing.  Shortcodes are left unparsed, significant whitespace is kept, etc..',
50
		),
51
		'http_envelope' => array(
52
			'false' => '',
53
			'true'  => 'Some environments (like in-browser JavaScript or Flash) block or divert responses with a non-200 HTTP status code.  Setting this parameter will force the HTTP status code to always be 200.  The JSON response is wrapped in an "envelope" containing the "real" HTTP status code and headers.',
54
		),
55
		'pretty' => array(
56
			'false' => '',
57
			'true'  => 'Output pretty JSON',
58
		),
59
		'meta' => "(string) Optional. Loads data from the endpoints found in the 'meta' part of the response. Comma-separated list. Example: meta=site,likes",
60
		'fields' => '(string) Optional. Returns specified fields only. Comma-separated list. Example: fields=ID,title',
61
		// Parameter name => description (default value is empty)
62
		'callback' => '(string) An optional JSONP callback function.',
63
	);
64
65
	// Response format
66
	public $response_format = array();
67
68
	// Request format
69
	public $request_format = array();
70
71
	// Is this endpoint still in testing phase?  If so, not available to the public.
72
	public $in_testing = false;
73
74
	// Is this endpoint still allowed if the site in question is flagged?
75
	public $allowed_if_flagged = false;
76
77
	// Is this endpoint allowed if the site is red flagged?
78
	public $allowed_if_red_flagged = false;
79
80
	/**
81
	 * @var string Version of the API
82
	 */
83
	public $version = '';
84
85
	/**
86
	 * @var string Example request to make
87
	 */
88
	public $example_request = '';
89
90
	/**
91
	 * @var string Example request data (for POST methods)
92
	 */
93
	public $example_request_data = '';
94
95
	/**
96
	 * @var string Example response from $example_request
97
	 */
98
	public $example_response = '';
99
100
	/**
101
	 * @var bool Set to true if the endpoint implements its own filtering instead of the standard `fields` query method
102
	 */
103
	public $custom_fields_filtering = false;
104
105
	/**
106
	 * @var bool Set to true if the endpoint accepts all cross origin requests. You probably should not set this flag.
107
	 */
108
	public $allow_cross_origin_request = false;
109
110
	/**
111
	 * @var bool Set to true if the endpoint can recieve unauthorized POST requests.
112
	 */
113
	public $allow_unauthorized_request = false;
114
115
	/**
116
	 * @var bool Set to true if the endpoint should accept site based (not user based) authentication.
117
	 */
118
	public $allow_jetpack_site_auth = false;
119
120
	/**
121
	 * @var bool Set to true if the endpoint should accept auth from an upload token.
122
	 */
123
	public $allow_upload_token_auth = false;
124
125
	function __construct( $args ) {
126
		$defaults = array(
127
			'in_testing'           => false,
128
			'allowed_if_flagged'   => false,
129
			'allowed_if_red_flagged' => false,
130
			'description'          => '',
131
			'group'	               => '',
132
			'method'               => 'GET',
133
			'path'                 => '/',
134
			'min_version'          => '0',
135
			'max_version'          => WPCOM_JSON_API__CURRENT_VERSION,
136
			'force'	               => '',
137
			'deprecated'           => false,
138
			'new_version'          => WPCOM_JSON_API__CURRENT_VERSION,
139
			'jp_disabled'          => false,
140
			'path_labels'          => array(),
141
			'request_format'       => array(),
142
			'response_format'      => array(),
143
			'query_parameters'     => array(),
144
			'version'              => 'v1',
145
			'example_request'      => '',
146
			'example_request_data' => '',
147
			'example_response'     => '',
148
			'required_scope'       => '',
149
			'pass_wpcom_user_details' => false,
150
			'custom_fields_filtering' => false,
151
			'allow_cross_origin_request' => false,
152
			'allow_unauthorized_request' => false,
153
			'allow_jetpack_site_auth'    => false,
154
			'allow_upload_token_auth'    => false,
155
		);
156
157
		$args = wp_parse_args( $args, $defaults );
158
159
		$this->in_testing  = $args['in_testing'];
160
161
		$this->allowed_if_flagged = $args['allowed_if_flagged'];
162
		$this->allowed_if_red_flagged = $args['allowed_if_red_flagged'];
163
164
		$this->description = $args['description'];
165
		$this->group       = $args['group'];
166
		$this->stat        = $args['stat'];
167
		$this->force	   = $args['force'];
168
		$this->jp_disabled = $args['jp_disabled'];
169
170
		$this->method      = $args['method'];
171
		$this->path        = $args['path'];
172
		$this->path_labels = $args['path_labels'];
173
		$this->min_version = $args['min_version'];
174
		$this->max_version = $args['max_version'];
175
		$this->deprecated  = $args['deprecated'];
176
		$this->new_version = $args['new_version'];
177
178
		// Ensure max version is not less than min version
179
		if ( version_compare( $this->min_version, $this->max_version, '>' ) ) {
180
			$this->max_version = $this->min_version;
181
		}
182
183
		$this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
184
		$this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
185
186
		$this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
187
		$this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
188
		$this->allow_jetpack_site_auth    = (bool) $args['allow_jetpack_site_auth'];
189
		$this->allow_upload_token_auth    = (bool) $args['allow_upload_token_auth'];
190
191
		$this->version     = $args['version'];
192
193
		$this->required_scope = $args['required_scope'];
194
195 View Code Duplication
		if ( $this->request_format ) {
196
			$this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
197
		} else {
198
			$this->request_format = $args['request_format'];
199
		}
200
201 View Code Duplication
		if ( $this->response_format ) {
202
			$this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
203
		} else {
204
			$this->response_format = $args['response_format'];
205
		}
206
207
		if ( false === $args['query_parameters'] ) {
208
			$this->query = array();
209
		} elseif ( is_array( $args['query_parameters'] ) ) {
210
			$this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
211
		}
212
213
		$this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
214
		$this->links = WPCOM_JSON_API_Links::getInstance();
215
216
		/** Example Request/Response ******************************************/
217
218
		// Examples for endpoint documentation request
219
		$this->example_request      = $args['example_request'];
220
		$this->example_request_data = $args['example_request_data'];
221
		$this->example_response     = $args['example_response'];
222
223
		$this->api->add( $this );
224
	}
225
226
	// Get all query args.  Prefill with defaults
227
	function query_args( $return_default_values = true, $cast_and_filter = true ) {
228
		$args = array_intersect_key( $this->api->query, $this->query );
229
230
		if ( !$cast_and_filter ) {
231
			return $args;
232
		}
233
234
		return $this->cast_and_filter( $args, $this->query, $return_default_values );
235
	}
236
237
	// Get POST body data
238
	function input( $return_default_values = true, $cast_and_filter = true ) {
239
		$input = trim( $this->api->post_body );
240
		$content_type = $this->api->content_type;
241
		if ( $content_type ) {
242
			list ( $content_type ) = explode( ';', $content_type );
243
		}
244
		$content_type = trim( $content_type );
245
		switch ( $content_type ) {
246
		case 'application/json' :
247
		case 'application/x-javascript' :
248
		case 'text/javascript' :
249
		case 'text/x-javascript' :
250
		case 'text/x-json' :
251
		case 'text/json' :
252
			$return = json_decode( $input, true );
253
254
			if ( function_exists( 'json_last_error' ) ) {
255
				if ( JSON_ERROR_NONE !== json_last_error() ) {
256
					return null;
257
				}
258
			} else {
259
				if ( is_null( $return ) && json_encode( null ) !== $input ) {
260
					return null;
261
				}
262
			}
263
264
			break;
265
		case 'multipart/form-data' :
266
			$return = array_merge( stripslashes_deep( $_POST ), $_FILES );
267
			break;
268
		case 'application/x-www-form-urlencoded' :
269
			//attempt JSON first, since probably a curl command
270
			$return = json_decode( $input, true );
271
272
			if ( is_null( $return ) ) {
273
				wp_parse_str( $input, $return );
274
			}
275
276
			break;
277
		default :
278
			wp_parse_str( $input, $return );
279
			break;
280
		}
281
282
		if ( isset( $this->api->query['force'] ) 
283
		    && 'secure' === $this->api->query['force']
284
		    && isset( $return['secure_key'] ) ) {
285
			$this->api->post_body = $this->get_secure_body( $return['secure_key'] );
286
			$this->api->query['force'] = false;
287
			return $this->input( $return_default_values, $cast_and_filter );
288
		}
289
290
		if ( $cast_and_filter ) {
291
			$return = $this->cast_and_filter( $return, $this->request_format, $return_default_values );
292
		}
293
		return $return;
294
	}
295
296
297
	protected function get_secure_body( $secure_key ) {
298
		$response =  Jetpack_Client::wpcom_json_api_request_as_blog( 
299
			sprintf( '/sites/%d/secure-request', Jetpack_Options::get_option('id' ) ), 
300
			'1.1', 
301
			array( 'method' => 'POST' ), 
302
			array( 'secure_key' => $secure_key ) 
303
		);
304
		if ( 200 !== $response['response']['code'] ) {
305
			return null;
306
		}
307
		return json_decode( $response['body'], true );
308
	}
309
310
	function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
311
		$return_as_object = false;
312
		if ( is_object( $data ) ) {
313
			// @todo this should probably be a deep copy if $data can ever have nested objects
314
			$data = (array) $data;
315
			$return_as_object = true;
316
		} elseif ( !is_array( $data ) ) {
317
			return $data;
318
		}
319
320
		$boolean_arg = array( 'false', 'true' );
321
		$naeloob_arg = array( 'true', 'false' );
322
323
		$return = array();
324
325
		foreach ( $documentation as $key => $description ) {
326
			if ( is_array( $description ) ) {
327
				// String or boolean array keys only
328
				$whitelist = array_keys( $description );
329
330
				if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
331
					// Truthiness
332
					if ( isset( $data[$key] ) ) {
333
						$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $data[$key] );
334
					} elseif ( $return_default_values ) {
335
						$return[$key] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg.
336
					}
337
				} elseif ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) {
338
					// String Key
339
					$return[$key] = (string) $data[$key];
340
				} elseif ( $return_default_values ) {
341
					// Default value
342
					$return[$key] = (string) current( $whitelist );
343
				}
344
345
				continue;
346
			}
347
348
			$types = $this->parse_types( $description );
349
			$type = array_shift( $types );
350
351
			// Explicit default - string and int only for now.  Always set these reguardless of $return_default_values
352
			if ( isset( $type['default'] ) ) {
353
				if ( !isset( $data[$key] ) ) {
354
					$data[$key] = $type['default'];
355
				}
356
			}
357
358
			if ( !isset( $data[$key] ) ) {
359
				continue;
360
			}
361
362
			$this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output );
363
		}
364
365
		if ( $return_as_object ) {
366
			return (object) $return;
367
		}
368
369
		return $return;
370
	}
371
372
	/**
373
	 * Casts $value according to $type.
374
	 * Handles fallbacks for certain values of $type when $value is not that $type
375
	 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way),
376
	 * and string -> object (one way)
377
	 *
378
	 * Handles "child types" - array:URL, object:category
379
	 * array:URL means an array of URLs
380
	 * object:category means a hash of categories
381
	 *
382
	 * Handles object typing - object>post means an object of type post
383
	 */
384
	function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
385
		if ( is_string( $type ) ) {
386
			$type = compact( 'type' );
387
		}
388
389
		switch ( $type['type'] ) {
390
		case 'false' :
391
			$return[$key] = false;
392
			break;
393
		case 'url' :
394
			$return[$key] = (string) esc_url_raw( $value );
395
			break;
396
		case 'string' :
397
			// Fallback string -> array, or for string -> object
398
			if ( is_array( $value ) || is_object( $value ) ) {
399 View Code Duplication
				if ( !empty( $types[0] ) ) {
400
					$next_type = array_shift( $types );
401
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
402
				}
403
			}
404
405
			// Fallback string -> false
406 View Code Duplication
			if ( !is_string( $value ) ) {
407
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
408
					$next_type = array_shift( $types );
409
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
410
				}
411
			}
412
			$return[$key] = (string) $value;
413
			break;
414
		case 'html' :
415
			$return[$key] = (string) $value;
416
			break;
417
		case 'safehtml' :
418
			$return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() );
419
			break;
420
		case 'zip' :
421
		case 'media' :
422
			if ( is_array( $value ) ) {
423
				if ( isset( $value['name'] ) && is_array( $value['name'] ) ) {
424
					// It's a $_FILES array
425
					// Reformat into array of $_FILES items
426
					$files = array();
427
428
					foreach ( $value['name'] as $k => $v ) {
429
						$files[$k] = array();
430
						foreach ( array_keys( $value ) as $file_key ) {
431
							$files[$k][$file_key] = $value[$file_key][$k];
432
						}
433
					}
434
435
					$return[$key] = $files;
436
					break;
437
				}
438
			} else {
439
				// no break - treat as 'array'
440
			}
441
			// nobreak
442
		case 'array' :
443
			// Fallback array -> string
444 View Code Duplication
			if ( is_string( $value ) ) {
445
				if ( !empty( $types[0] ) ) {
446
					$next_type = array_shift( $types );
447
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
448
				}
449
			}
450
451 View Code Duplication
			if ( isset( $type['children'] ) ) {
452
				$children = array();
453
				foreach ( (array) $value as $k => $child ) {
454
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
455
				}
456
				$return[$key] = (array) $children;
457
				break;
458
			}
459
460
			$return[$key] = (array) $value;
461
			break;
462
		case 'iso 8601 datetime' :
463
		case 'datetime' :
464
			// (string)s
465
			$dates = $this->parse_date( (string) $value );
466
			if ( $for_output ) {
467
				$return[$key] = $this->format_date( $dates[1], $dates[0] );
468
			} else {
469
				list( $return[$key], $return["{$key}_gmt"] ) = $dates;
470
			}
471
			break;
472
		case 'float' :
473
			$return[$key] = (float) $value;
474
			break;
475
		case 'int' :
476
		case 'integer' :
477
			$return[$key] = (int) $value;
478
			break;
479
		case 'bool' :
480
		case 'boolean' :
481
			$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value );
482
			break;
483
		case 'object' :
484
			// Fallback object -> false
485 View Code Duplication
			if ( is_scalar( $value ) || is_null( $value ) ) {
486
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
487
					return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
488
				}
489
			}
490
491 View Code Duplication
			if ( isset( $type['children'] ) ) {
492
				$children = array();
493
				foreach ( (array) $value as $k => $child ) {
494
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
495
				}
496
				$return[$key] = (object) $children;
497
				break;
498
			}
499
500
			if ( isset( $type['subtype'] ) ) {
501
				return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
502
			}
503
504
			$return[$key] = (object) $value;
505
			break;
506
		case 'post' :
507
			$return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
508
			break;
509
		case 'comment' :
510
			$return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
511
			break;
512
		case 'tag' :
513
		case 'category' :
514
			$docs = array(
515
				'ID'          => '(int)',
516
				'name'        => '(string)',
517
				'slug'        => '(string)',
518
				'description' => '(HTML)',
519
				'post_count'  => '(int)',
520
				'meta'        => '(object)',
521
			);
522
			if ( 'category' === $type['type'] ) {
523
				$docs['parent'] = '(int)';
524
			}
525
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
526
			break;
527
		case 'post_reference' :
528 View Code Duplication
		case 'comment_reference' :
529
			$docs = array(
530
				'ID'    => '(int)',
531
				'type'  => '(string)',
532
				'title' => '(string)',
533
				'link'  => '(URL)',
534
			);
535
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
536
			break;
537 View Code Duplication
		case 'geo' :
538
			$docs = array(
539
				'latitude'  => '(float)',
540
				'longitude' => '(float)',
541
				'address'   => '(string)',
542
			);
543
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
544
			break;
545
		case 'author' :
546
			$docs = array(
547
				'ID'             => '(int)',
548
				'user_login'     => '(string)',
549
				'login'          => '(string)',
550
				'email'          => '(string|false)',
551
				'name'           => '(string)',
552
				'first_name'     => '(string)',
553
				'last_name'      => '(string)',
554
				'nice_name'      => '(string)',
555
				'URL'            => '(URL)',
556
				'avatar_URL'     => '(URL)',
557
				'profile_URL'    => '(URL)',
558
				'is_super_admin' => '(bool)',
559
				'roles'          => '(array:string)'
560
			);
561
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
562
			break;
563 View Code Duplication
		case 'role' :
564
			$docs = array(
565
				'name'         => '(string)',
566
				'display_name' => '(string)',
567
				'capabilities' => '(object:boolean)',
568
			);
569
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
570
			break;
571
		case 'attachment' :
572
			$docs = array(
573
				'ID'        => '(int)',
574
				'URL'       => '(URL)',
575
				'guid'      => '(string)',
576
				'mime_type' => '(string)',
577
				'width'     => '(int)',
578
				'height'    => '(int)',
579
				'duration'  => '(int)',
580
			);
581
			$return[$key] = (object) $this->cast_and_filter(
582
				$value,
583
				/**
584
				 * Filter the documentation returned for a post attachment.
585
				 *
586
				 * @module json-api
587
				 *
588
				 * @since 1.9.0
589
				 *
590
				 * @param array $docs Array of documentation about a post attachment.
591
				 */
592
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
593
				false,
594
				$for_output
595
			);
596
			break;
597
		case 'metadata' :
598
			$docs = array(
599
				'id'       => '(int)',
600
				'key'       => '(string)',
601
				'value'     => '(string|false|float|int|array|object)',
602
				'previous_value' => '(string)',
603
				'operation'  => '(string)',
604
			);
605
			$return[$key] = (object) $this->cast_and_filter(
606
				$value,
607
				/** This filter is documented in class.json-api-endpoints.php */
608
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
609
				false,
610
				$for_output
611
			);
612
			break;
613
		case 'plugin' :
614
			$docs = array(
615
				'id'            => '(safehtml) The plugin\'s ID',
616
				'slug'          => '(safehtml) The plugin\'s Slug',
617
				'active'        => '(boolean)  The plugin status.',
618
				'update'        => '(object)   The plugin update info.',
619
				'name'          => '(safehtml) The name of the plugin.',
620
				'plugin_url'    => '(url)      Link to the plugin\'s web site.',
621
				'version'       => '(safehtml) The plugin version number.',
622
				'description'   => '(safehtml) Description of what the plugin does and/or notes from the author',
623
				'author'        => '(safehtml) The plugin author\'s name',
624
				'author_url'    => '(url)      The plugin author web site address',
625
				'network'       => '(boolean)  Whether the plugin can only be activated network wide.',
626
				'autoupdate'    => '(boolean)  Whether the plugin is auto updated',
627
				'log'           => '(array:safehtml) An array of update log strings.',
628
        'action_links'  => '(array) An array of action links that the plugin uses.',
629
			);
630
			$return[$key] = (object) $this->cast_and_filter(
631
				$value,
632
				/**
633
				 * Filter the documentation returned for a plugin.
634
				 *
635
				 * @module json-api
636
				 *
637
				 * @since 3.1.0
638
				 *
639
				 * @param array $docs Array of documentation about a plugin.
640
				 */
641
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
642
				false,
643
				$for_output
644
			);
645
			break;
646
		case 'jetpackmodule' :
647
			$docs = array(
648
				'id'          => '(string)   The module\'s ID',
649
				'active'      => '(boolean)  The module\'s status.',
650
				'name'        => '(string)   The module\'s name.',
651
				'description' => '(safehtml) The module\'s description.',
652
				'sort'        => '(int)      The module\'s display order.',
653
				'introduced'  => '(string)   The Jetpack version when the module was introduced.',
654
				'changed'     => '(string)   The Jetpack version when the module was changed.',
655
				'free'        => '(boolean)  The module\'s Free or Paid status.',
656
				'module_tags' => '(array)    The module\'s tags.'
657
			);
658
			$return[$key] = (object) $this->cast_and_filter(
659
				$value,
660
				/** This filter is documented in class.json-api-endpoints.php */
661
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
662
				false,
663
				$for_output
664
			);
665
			break;
666
		case 'sharing_button' :
667
			$docs = array(
668
				'ID'         => '(string)',
669
				'name'       => '(string)',
670
				'URL'        => '(string)',
671
				'icon'       => '(string)',
672
				'enabled'    => '(bool)',
673
				'visibility' => '(string)',
674
			);
675
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
676
			break;
677
		case 'sharing_button_service':
678
			$docs = array(
679
				'ID'               => '(string) The service identifier',
680
				'name'             => '(string) The service name',
681
				'class_name'       => '(string) Class name for custom style sharing button elements',
682
				'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
683
				'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
684
				'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview'
685
			);
686
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
687
			break;
688
		case 'taxonomy':
689
			$docs = array(
690
				'name'         => '(string) The taxonomy slug',
691
				'label'        => '(string) The taxonomy human-readable name',
692
				'labels'       => '(object) Mapping of labels for the taxonomy',
693
				'description'  => '(string) The taxonomy description',
694
				'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
695
				'public'       => '(bool) Whether the taxonomy is public',
696
				'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
697
			);
698
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
699
			break;
700
701
		default :
702
			$method_name = $type['type'] . '_docs';
703
			if ( method_exists( WPCOM_JSON_API_Jetpack_Overrides, $method_name ) ) {
704
				$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
705
			}
706
707
			if ( ! empty( $docs ) ) {
708
				$return[$key] = (object) $this->cast_and_filter(
709
					$value,
710
					/** This filter is documented in class.json-api-endpoints.php */
711
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
712
					false,
713
					$for_output
714
				);
715
			} else {
716
				trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
717
			}
718
		}
719
	}
720
721
	function parse_types( $text ) {
722
		if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
723
			return 'none';
724
		}
725
726
		$types = explode( '|', strtolower( $matches[1] ) );
727
		$return = array();
728
		foreach ( $types as $type ) {
729
			foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
730
				if ( false !== strpos( $type, $operator ) ) {
731
					$item = explode( $operator, $type, 2 );
732
					$return[] = array( 'type' => $item[0], $meaning => $item[1] );
733
					continue 2;
734
				}
735
			}
736
			$return[] = compact( 'type' );
737
		}
738
739
		return $return;
740
	}
741
742
	/**
743
	 * Checks if the endpoint is publicly displayable
744
	 */
745
	function is_publicly_documentable() {
746
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
747
	}
748
749
	/**
750
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
751
	 * Echoes HTML.
752
	 */
753
	function document( $show_description = true ) {
754
		global $wpdb;
755
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
756
		unset( $GLOBALS['post'] );
757
758
		$doc = $this->generate_documentation();
759
760
		if ( $show_description ) :
761
?>
762
<caption>
763
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
764
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
765
</caption>
766
767
<?php endif; ?>
768
769
<?php if ( true === $this->deprecated ) { ?>
770
<p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $this->new_version ); ?></strong></p>
771
<?php } ?>
772
773
<section class="resource-info">
774
	<h2 id="apidoc-resource-info">Resource Information</h2>
775
776
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
777
778
	<thead>
779
		<tr>
780
			<th class="api-index-title" scope="column">&nbsp;</th>
781
			<th class="api-index-title" scope="column">&nbsp;</th>
782
		</tr>
783
	</thead>
784
	<tbody>
785
786
		<tr class="api-index-item">
787
			<th scope="row" class="parameter api-index-item-title">Method</th>
788
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
789
		</tr>
790
791
		<tr class="api-index-item">
792
			<th scope="row" class="parameter api-index-item-title">URL</th>
793
			<?php
794
			$version = WPCOM_JSON_API__CURRENT_VERSION;
795
			if ( !empty( $this->max_version ) ) {
796
				$version = $this->max_version;
797
			}
798
			?>
799
			<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>
800
		</tr>
801
802
		<tr class="api-index-item">
803
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
804
			<?php
805
			$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'] ) );
806
			?>
807
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
808
		</tr>
809
810
	</tbody>
811
	</table>
812
813
</section>
814
815
<?php
816
817
		foreach ( array(
818
			'path'     => 'Method Parameters',
819
			'query'    => 'Query Parameters',
820
			'body'     => 'Request Parameters',
821
			'response' => 'Response Parameters',
822
		) as $doc_section_key => $label ) :
823
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
824
			if ( !$doc_section ) {
825
				continue;
826
			}
827
828
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
829
?>
830
831
<section class="<?php echo $param_label; ?>">
832
833
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
834
835
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
836
837
<thead>
838
	<tr>
839
		<th class="api-index-title" scope="column">Parameter</th>
840
		<th class="api-index-title" scope="column">Type</th>
841
		<th class="api-index-title" scope="column">Description</th>
842
	</tr>
843
</thead>
844
<tbody>
845
846
<?php foreach ( $doc_section as $key => $item ) : ?>
847
848
	<tr class="api-index-item">
849
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
850
		<td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
851
		<td class="description api-index-item-body"><?php
852
853
		$this->generate_doc_description( $item['description'] );
854
855
		?></td>
856
	</tr>
857
858
<?php endforeach; ?>
859
</tbody>
860
</table>
861
</section>
862
<?php endforeach; ?>
863
864
<?php
865
		if ( 'unset' !== $original_post ) {
866
			$GLOBALS['post'] = $original_post;
867
		}
868
	}
869
870
	function add_http_build_query_to_php_content_example( $matches ) {
871
		$trimmed_match = ltrim( $matches[0] );
872
		$pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
873
		$pad = ltrim( $pad, ' ' );
874
		$return = '  ' . str_replace( "\n", "\n  ", $matches[0] );
875
		return " http_build_query({$return}{$pad})";
876
	}
877
878
	/**
879
	 * Recursively generates the <dl>'s to document item descriptions.
880
	 * Echoes HTML.
881
	 */
882
	function generate_doc_description( $item ) {
883
		if ( is_array( $item ) ) : ?>
884
885
		<dl>
886
<?php			foreach ( $item as $description_key => $description_value ) : ?>
887
888
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
889
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
890
891
<?php			endforeach; ?>
892
893
		</dl>
894
895
<?php
896
		else :
897
			echo wp_kses_post( $item );
898
		endif;
899
	}
900
901
	/**
902
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
903
	 * Echoes HTML.
904
	 */
905
	function generate_documentation() {
906
		$format       = str_replace( '%d', '%s', $this->path );
907
		$path_labeled = $format;
908
		if ( ! empty( $this->path_labels ) ) {
909
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
910
		}
911
		$boolean_arg  = array( 'false', 'true' );
912
		$naeloob_arg  = array( 'true', 'false' );
913
914
		$doc = array(
915
			'description'  => $this->description,
916
			'method'       => $this->method,
917
			'path_format'  => $this->path,
918
			'path_labeled' => $path_labeled,
919
			'group'        => $this->group,
920
			'request' => array(
921
				'path'  => array(),
922
				'query' => array(),
923
				'body'  => array(),
924
			),
925
			'response' => array(
926
				'body' => array(),
927
			)
928
		);
929
930
		foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
931
			foreach ( (array) $this->$_property as $key => $description ) {
932
				if ( is_array( $description ) ) {
933
					$description_keys = array_keys( $description );
934
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
935
						$type = '(bool)';
936
					} else {
937
						$type = '(string)';
938
					}
939
940
					if ( 'response_format' !== $_property ) {
941
						// hack - don't show "(default)" in response format
942
						reset( $description );
943
						$description_key = key( $description );
944
						$description[$description_key] = "(default) {$description[$description_key]}";
945
					}
946
				} else {
947
					$types   = $this->parse_types( $description );
948
					$type    = array();
949
					$default = '';
950
951
					if ( 'none' == $types ) {
952
						$types = array();
953
						$types[]['type'] = 'none';
954
					}
955
956
					foreach ( $types as $type_array ) {
957
						$type[] = $type_array['type'];
958
						if ( isset( $type_array['default'] ) ) {
959
							$default = $type_array['default'];
960
							if ( 'string' === $type_array['type'] ) {
961
								$default = "'$default'";
962
							}
963
						}
964
					}
965
					$type = '(' . join( '|', $type ) . ')';
966
					$noop = ''; // skip an index in list below
967
					list( $noop, $description ) = explode( ')', $description, 2 );
968
					$description = trim( $description );
969
					if ( $default ) {
970
						$description .= " Default: $default.";
971
					}
972
				}
973
974
				$item = compact( 'type', 'description' );
975
976
				if ( 'response_format' === $_property ) {
977
					$doc['response'][$doc_item][$key] = $item;
978
				} else {
979
					$doc['request'][$doc_item][$key] = $item;
980
				}
981
			}
982
		}
983
984
		return $doc;
985
	}
986
987
	function user_can_view_post( $post_id ) {
988
		$post = get_post( $post_id );
989
		if ( !$post || is_wp_error( $post ) ) {
990
			return false;
991
		}
992
993 View Code Duplication
		if ( 'inherit' === $post->post_status ) {
994
			$parent_post = get_post( $post->post_parent );
995
			$post_status_obj = get_post_status_object( $parent_post->post_status );
996
		} else {
997
			$post_status_obj = get_post_status_object( $post->post_status );
998
		}
999
1000
		if ( !$post_status_obj->public ) {
1001
			if ( is_user_logged_in() ) {
1002
				if ( $post_status_obj->protected ) {
1003
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1004
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1005
					}
1006
				} elseif ( $post_status_obj->private ) {
1007
					if ( !current_user_can( 'read_post', $post->ID ) ) {
1008
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1009
					}
1010
				} elseif ( 'trash' === $post->post_status ) {
1011
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1012
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1013
					}
1014
				} elseif ( 'auto-draft' === $post->post_status ) {
1015
					//allow auto-drafts
1016
				} else {
1017
					return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1018
				}
1019
			} else {
1020
				return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1021
			}
1022
		}
1023
1024 View Code Duplication
		if (
1025
			-1 == get_option( 'blog_public' ) &&
1026
			/**
1027
			 * Filter access to a specific post.
1028
			 *
1029
			 * @module json-api
1030
			 *
1031
			 * @since 3.4.0
1032
			 *
1033
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1034
			 * @param WP_Post $post Post data.
1035
			 */
1036
			! apply_filters(
1037
				'wpcom_json_api_user_can_view_post',
1038
				current_user_can( 'read_post', $post->ID ),
1039
				$post
1040
			)
1041
		) {
1042
			return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
1043
		}
1044
1045 View Code Duplication
		if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
1046
			return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
1047
		}
1048
1049
		return true;
1050
	}
1051
1052
	/**
1053
	 * Returns author object.
1054
	 *
1055
	 * @param $author user ID, user row, WP_User object, comment row, post row
1056
	 * @param $show_email output the author's email address?
1057
	 *
1058
	 * @return (object)
1059
	 */
1060
	function get_author( $author, $show_email = false ) {
1061
		if ( isset( $author->comment_author_email ) && !$author->user_id ) {
1062
			$ID          = 0;
1063
			$login       = '';
1064
			$email       = $author->comment_author_email;
1065
			$name        = $author->comment_author;
1066
			$first_name  = '';
1067
			$last_name   = '';
1068
			$URL         = $author->comment_author_url;
1069
			$avatar_URL  = $this->api->get_avatar_url( $author );
1070
			$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1071
			$nice        = '';
1072
			$site_id     = -1;
1073
1074
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1075
			// "&" is the only email/URL character altered by wp_kses()
1076
			foreach ( array( 'email', 'URL' ) as $field ) {
1077
				$$field = str_replace( '&amp;', '&', $$field );
1078
			}
1079
		} else {
1080
			if ( isset( $author->user_id ) && $author->user_id ) {
1081
				$author = $author->user_id;
1082
			} elseif ( isset( $author->user_email ) ) {
1083
				$author = $author->ID;
1084
			} elseif ( isset( $author->post_author ) ) {
1085
				// then $author is a Post Object.
1086
				if ( 0 == $author->post_author )
1087
					return null;
1088
				/**
1089
				 * Filter whether the current site is a Jetpack site.
1090
				 *
1091
				 * @module json-api
1092
				 *
1093
				 * @since 3.3.0
1094
				 *
1095
				 * @param bool false Is the current site a Jetpack site. Default to false.
1096
				 * @param int get_current_blog_id() Blog ID.
1097
				 */
1098
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1099
				$post_id = $author->ID;
1100
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1101
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1102
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1103
					$login      = '';
1104
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1105
					$first_name = '';
1106
					$last_name  = '';
1107
					$URL        = '';
1108
					$nice       = '';
1109
				} else {
1110
					$author = $author->post_author;
1111
				}
1112
			}
1113
1114
			if ( ! isset( $ID ) ) {
1115
				$user = get_user_by( 'id', $author );
1116
				if ( ! $user || is_wp_error( $user ) ) {
1117
					trigger_error( 'Unknown user', E_USER_WARNING );
1118
1119
					return null;
1120
				}
1121
				$ID         = $user->ID;
1122
				$email      = $user->user_email;
1123
				$login      = $user->user_login;
1124
				$name       = $user->display_name;
1125
				$first_name = $user->first_name;
1126
				$last_name  = $user->last_name;
1127
				$URL        = $user->user_url;
1128
				$nice       = $user->user_nicename;
1129
			}
1130
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1131
				$active_blog = get_active_blog_for_user( $ID );
1132
				$site_id     = $active_blog->blog_id;
1133
				$profile_URL = "https://en.gravatar.com/{$login}";
1134
			} else {
1135
				$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1136
				$site_id     = -1;
1137
			}
1138
1139
			$avatar_URL = $this->api->get_avatar_url( $email );
1140
		}
1141
1142
		$email = $show_email ? (string) $email : false;
1143
1144
		$author = array(
1145
			'ID'          => (int) $ID,
1146
			'login'       => (string) $login,
1147
			'email'       => $email, // (string|bool)
1148
			'name'        => (string) $name,
1149
			'first_name'  => (string) $first_name,
1150
			'last_name'   => (string) $last_name,
1151
			'nice_name'   => (string) $nice,
1152
			'URL'         => (string) esc_url_raw( $URL ),
1153
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1154
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1155
		);
1156
1157
		if ($site_id > -1) {
1158
			$author['site_ID'] = (int) $site_id;
1159
		}
1160
1161
		return (object) $author;
1162
	}
1163
1164
	function get_media_item( $media_id ) {
1165
		$media_item = get_post( $media_id );
1166
1167
		if ( !$media_item || is_wp_error( $media_item ) )
1168
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1169
1170
		$response = array(
1171
			'id'    => strval( $media_item->ID ),
1172
			'date' =>  (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1173
			'parent'           => $media_item->post_parent,
1174
			'link'             => wp_get_attachment_url( $media_item->ID ),
1175
			'title'            => $media_item->post_title,
1176
			'caption'          => $media_item->post_excerpt,
1177
			'description'      => $media_item->post_content,
1178
			'metadata'         => wp_get_attachment_metadata( $media_item->ID ),
1179
		);
1180
1181
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1182
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1183
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1184
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1185
		}
1186
1187
		$response['meta'] = (object) array(
1188
			'links' => (object) array(
1189
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1190
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1191
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1192
			),
1193
		);
1194
1195
		return (object) $response;
1196
	}
1197
1198
	function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1199
1200
		if ( ! $media_item ) {
1201
			$media_item = get_post( $media_id );
1202
		}
1203
1204
		if ( ! $media_item || is_wp_error( $media_item ) )
1205
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1206
1207
		$attachment_file = get_attached_file( $media_item->ID );
1208
1209
		$file = basename( $attachment_file ? $attachment_file : $file );
1210
		$file_info = pathinfo( $file );
1211
		$ext  = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1212
1213
		$response = array(
1214
			'ID'           => $media_item->ID,
1215
			'URL'          => wp_get_attachment_url( $media_item->ID ),
1216
			'guid'         => $media_item->guid,
1217
			'date'         => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1218
			'post_ID'      => $media_item->post_parent,
1219
			'author_ID'    => (int) $media_item->post_author,
1220
			'file'         => $file,
1221
			'mime_type'    => $media_item->post_mime_type,
1222
			'extension'    => $ext,
1223
			'title'        => $media_item->post_title,
1224
			'caption'      => $media_item->post_excerpt,
1225
			'description'  => $media_item->post_content,
1226
			'alt'          => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1227
			'icon'         => wp_mime_type_icon( $media_item->ID ),
1228
			'thumbnails'   => array()
1229
		);
1230
1231
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1232
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1233
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1234
				$response['height'] = $metadata['height'];
1235
				$response['width'] = $metadata['width'];
1236
			}
1237
1238
			if ( isset( $metadata['sizes'] ) ) {
1239
				/**
1240
				 * Filter the thumbnail sizes available for each attachment ID.
1241
				 *
1242
				 * @module json-api
1243
				 *
1244
				 * @since 3.9.0
1245
				 *
1246
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1247
				 * @param string $media_id Attachment ID.
1248
				 */
1249
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_id );
1250
				if ( is_array( $sizes ) ) {
1251
					foreach ( $sizes as $size => $size_details ) {
1252
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1253
					}
1254
				}
1255
			}
1256
1257
			if ( isset( $metadata['image_meta'] ) ) {
1258
				$response['exif'] = $metadata['image_meta'];
1259
			}
1260
		}
1261
1262
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1263
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1264
			$response['length'] = $metadata['length'];
1265
			$response['exif']   = $metadata;
1266
		}
1267
1268
		if (
1269
		        in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1270
            ||
1271
                $response['mime_type'] === 'video/videopress'
1272
        ) {
1273
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1274
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1275
				$response['height'] = $metadata['height'];
1276
				$response['width']  = $metadata['width'];
1277
			}
1278
1279
			if ( isset( $metadata['length'] ) ) {
1280
				$response['length'] = $metadata['length'];
1281
			}
1282
1283
			// add VideoPress info
1284
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1285
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_id );
1286
1287
				// Thumbnails
1288
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1289
					$response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' );
1290
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1291
						if ( video_format_done( $info, $size ) ) {
1292
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1293
						} else {
1294
							unset( $response['thumbnails'][ $size ] );
1295
						}
1296
					}
1297
				}
1298
1299
				$response['videopress_guid'] = $info->guid;
1300
				$response['videopress_processing_done'] = true;
1301
				if ( '0000-00-00 00:00:00' == $info->finish_date_gmt ) {
1302
					$response['videopress_processing_done'] = false;
1303
				}
1304
			}
1305
		}
1306
1307
		$response['thumbnails'] = (object) $response['thumbnails'];
1308
1309
		$response['meta'] = (object) array(
1310
			'links' => (object) array(
1311
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1312
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1313
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1314
			),
1315
		);
1316
1317
		// add VideoPress link to the meta
1318
		if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) {
1319
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1320
				$response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1321
			}
1322
		}
1323
1324
		if ( $media_item->post_parent > 0 ) {
1325
			$response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1326
		}
1327
1328
		return (object) $response;
1329
	}
1330
1331
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1332
1333
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1334
		/// keep updating this function
1335
		if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1336
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1337
		}
1338
1339
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1340
	}
1341
1342 View Code Duplication
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1343
		// Permissions
1344
		switch ( $context ) {
1345
		case 'edit' :
1346
			$tax = get_taxonomy( $taxonomy_type );
1347
			if ( !current_user_can( $tax->cap->edit_terms ) )
1348
				return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1349
			break;
1350
		case 'display' :
1351
			if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1352
				return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1353
			}
1354
			break;
1355
		default :
0 ignored issues
show
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1356
			return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1357
		}
1358
1359
		$response                = array();
1360
		$response['ID']          = (int) $taxonomy->term_id;
1361
		$response['name']        = (string) $taxonomy->name;
1362
		$response['slug']        = (string) $taxonomy->slug;
1363
		$response['description'] = (string) $taxonomy->description;
1364
		$response['post_count']  = (int) $taxonomy->count;
1365
1366
		if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1367
			$response['parent'] = (int) $taxonomy->parent;
1368
		}
1369
1370
		$response['meta'] = (object) array(
1371
			'links' => (object) array(
1372
				'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1373
				'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1374
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1375
			),
1376
		);
1377
1378
		return (object) $response;
1379
	}
1380
1381
	/**
1382
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1383
	 *
1384
	 * @param $date_gmt (string) GMT datetime string.
1385
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1386
	 *
1387
	 * @return string
1388
	 */
1389
	function format_date( $date_gmt, $date = null ) {
1390
		return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1391
	}
1392
1393
	/**
1394
	 * Parses a date string and returns the local and GMT representations
1395
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1396
	 * timezones or offsets. If the parsed datetime was not localized to a
1397
	 * particular timezone or offset we will assume it was given in GMT
1398
	 * relative to now and will convert it to local time using either the
1399
	 * timezone set in the options table for the blog or the GMT offset.
1400
	 *
1401
	 * @param datetime string
1402
	 *
1403
	 * @return array( $local_time_string, $gmt_time_string )
1404
	 */
1405
	function parse_date( $date_string ) {
1406
		$date_string_info = date_parse( $date_string );
1407
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1408
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1409
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1410
				$dt_local = clone $dt_utc = new DateTime( $date_string );
1411
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1412
				return array(
1413
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1414
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1415
				);
1416
			}
1417
1418
			// It's parseable but no TZ info so assume UTC
1419
			$dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1420
		} else {
1421
			// Could not parse time, use now in UTC
1422
			$dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1423
		}
1424
1425
		// First try to use timezone as it's daylight savings aware.
1426
		$timezone_string = get_option( 'timezone_string' );
1427
		if ( $timezone_string ) {
1428
			$tz = timezone_open( $timezone_string );
1429
			if ( $tz ) {
1430
				$dt_local->setTimezone( $tz );
1431
				return array(
1432
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1433
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1434
				);
1435
			}
1436
		}
1437
1438
		// Fallback to GMT offset (in hours)
1439
		// NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1440
		$gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1441
		$dt_local->modify("+{$gmt_offset_seconds} seconds");
1442
		return array(
1443
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1444
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1445
		);
1446
	}
1447
1448
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1449
	function load_theme_functions() {
1450
		// bail if we've done this already (can happen when calling /batch endpoint)
1451
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) )
1452
			return;
1453
1454
		// VIP context loading is handled elsewhere, so bail to prevent
1455
		// duplicate loading. See `switch_to_blog_and_validate_user()`
1456
		if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1457
			return;
1458
		}
1459
1460
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1461
1462
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1463
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1464
1465
		$copy_dirs = array( get_template_directory() );
1466
1467
		// Is this a child theme? Load the child theme's functions file.
1468
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1469
			foreach ( $function_files as $function_file ) {
1470
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1471
					require_once(  get_stylesheet_directory() . $function_file );
1472
				}
1473
			}
1474
			$copy_dirs[] = get_stylesheet_directory();
1475
		}
1476
1477
		foreach ( $function_files as $function_file ) {
1478
			if ( file_exists( get_template_directory() . $function_file ) ) {
1479
				require_once(  get_template_directory() . $function_file );
1480
			}
1481
		}
1482
1483
		// add inc/wpcom.php and/or includes/wpcom.php
1484
		wpcom_load_theme_compat_file();
1485
1486
		// Enable including additional directories or files in actions to be copied
1487
		$copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1488
1489
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1490
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1491
1492
		/**
1493
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1494
		 *
1495
		 * The REST API does not load the theme when processing requests.
1496
		 * To enable theme-based functionality, the API will load the '/functions.php',
1497
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1498
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1499
		 *
1500
		 * @module json-api
1501
		 *
1502
		 * @since 3.2.0
1503
		 */
1504
		do_action( 'restapi_theme_after_setup_theme' );
1505
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1506
1507
		/**
1508
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1509
		 *
1510
		 * The REST API does not load the theme when processing requests.
1511
		 * To enable theme-based functionality, the API will load the '/functions.php',
1512
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1513
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1514
		 *
1515
		 * @module json-api
1516
		 *
1517
		 * @since 3.2.0
1518
		 */
1519
		do_action( 'restapi_theme_init' );
1520
	}
1521
1522
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1523
		global $wp_filter;
1524
		foreach ( $wp_filter as $hook => $actions ) {
1525
1526
			if ( $from_hook != $hook ) {
1527
				continue;
1528
			}
1529
			if ( ! has_action( $hook ) ) {
1530
				continue;
1531
			}
1532
1533
			foreach ( $actions as $priority => $callbacks ) {
1534
				foreach( $callbacks as $callback_key => $callback_data ) {
1535
					$callback = $callback_data['function'];
1536
1537
					// use reflection api to determine filename where function is defined
1538
					$reflection = $this->get_reflection( $callback );
1539
1540
					if ( false !== $reflection ) {
1541
						$file_name = $reflection->getFileName();
1542
						foreach( $base_paths as $base_path ) {
1543
1544
							// only copy hooks with functions which are part of the specified files
1545
							if ( 0 === strpos( $file_name, $base_path ) ) {
1546
								add_action(
1547
									$to_hook,
1548
									$callback_data['function'],
1549
									$priority,
1550
									$callback_data['accepted_args']
1551
								);
1552
							}
1553
						}
1554
					}
1555
				}
1556
			}
1557
		}
1558
	}
1559
1560
	function get_reflection( $callback ) {
1561
		if ( is_array( $callback ) ) {
1562
			list( $class, $method ) = $callback;
1563
			return new ReflectionMethod( $class, $method );
1564
		}
1565
1566
		if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) {
1567
			list( $class, $method ) = explode( "::", $callback );
1568
			return new ReflectionMethod( $class, $method );
1569
		}
1570
1571
		if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) {
1572
			return new ReflectionMethod( $callback, "__invoke" );
1573
		}
1574
1575
		if ( is_string( $callback ) && strpos( $callback, "::" ) == false && function_exists( $callback ) ) {
1576
			return new ReflectionFunction( $callback );
1577
		}
1578
1579
		return false;
1580
	}
1581
1582
	/**
1583
	* Check whether a user can view or edit a post type
1584
	* @param string $post_type              post type to check
1585
	* @param string $context                'display' or 'edit'
1586
	* @return bool
1587
	*/
1588 View Code Duplication
	function current_user_can_access_post_type( $post_type, $context='display' ) {
1589
		$post_type_object = get_post_type_object( $post_type );
1590
		if ( ! $post_type_object ) {
1591
			return false;
1592
		}
1593
1594
		switch( $context ) {
1595
			case 'edit':
1596
				return current_user_can( $post_type_object->cap->edit_posts );
1597
			case 'display':
1598
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1599
			default:
1600
				return false;
1601
		}
1602
	}
1603
1604
	function is_post_type_allowed( $post_type ) {
1605
		// if the post type is empty, that's fine, WordPress will default to post
1606
		if ( empty( $post_type ) ) {
1607
			return true;
1608
		}
1609
1610
		// allow special 'any' type
1611
		if ( 'any' == $post_type ) {
1612
			return true;
1613
		}
1614
1615
		// check for allowed types
1616
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1617
			return true;
1618
		}
1619
1620
		if ( $post_type_object = get_post_type_object( $post_type ) ) {
1621
			if ( ! empty( $post_type_object->show_in_rest ) ) {
1622
				return $post_type_object->show_in_rest;
1623
			}
1624
			if ( ! empty( $post_type_object->publicly_queryable ) ) {
1625
				return $post_type_object->publicly_queryable;
1626
			}
1627
		}
1628
1629
		return ! empty( $post_type_object->public );
1630
	}
1631
1632
	/**
1633
	 * Gets the whitelisted post types that JP should allow access to.
1634
	 *
1635
	 * @return array Whitelisted post types.
1636
	 */
1637 View Code Duplication
	protected function _get_whitelisted_post_types() {
1638
		$allowed_types = array( 'post', 'page', 'revision' );
1639
1640
		/**
1641
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1642
		 *
1643
		 * @module json-api
1644
		 *
1645
		 * @since 2.2.3
1646
		 *
1647
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1648
		 */
1649
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1650
1651
		return array_unique( $allowed_types );
1652
	}
1653
1654
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1655
1656
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1657
1658
		$media_ids = $errors = array();
1659
		$user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
1660
		$media_attrs = array_values( $media_attrs ); // reset the keys
1661
		$i = 0;
1662
1663
		if ( ! empty( $media_files ) ) {
1664
			$this->api->trap_wp_die( 'upload_error' );
1665
			foreach ( $media_files as $media_item ) {
1666
				$_FILES['.api.media.item.'] = $media_item;
1667 View Code Duplication
				if ( ! $user_can_upload_files ) {
1668
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1669
				} else {
1670
					if ( $force_parent_id ) {
1671
						$parent_id = absint( $force_parent_id );
1672
					} elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1673
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1674
					} else {
1675
						$parent_id = 0;
1676
					}
1677
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1678
				}
1679
				if ( is_wp_error( $media_id ) ) {
1680
					$errors[$i]['file']   = $media_item['name'];
1681
					$errors[$i]['error']   = $media_id->get_error_code();
1682
					$errors[$i]['message'] = $media_id->get_error_message();
1683
				} else {
1684
					$media_ids[$i] = $media_id;
1685
				}
1686
1687
				$i++;
1688
			}
1689
			$this->api->trap_wp_die( null );
1690
			unset( $_FILES['.api.media.item.'] );
1691
		}
1692
1693
		if ( ! empty( $media_urls ) ) {
1694
			foreach ( $media_urls as $url ) {
1695 View Code Duplication
				if ( ! $user_can_upload_files ) {
1696
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1697
				} else {
1698
					if ( $force_parent_id ) {
1699
						$parent_id = absint( $force_parent_id );
1700
					} else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1701
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1702
					} else {
1703
						$parent_id = 0;
1704
					}
1705
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1706
				}
1707
				if ( is_wp_error( $media_id ) ) {
1708
					$errors[$i] = array(
1709
						'file'    => $url,
1710
						'error'   => $media_id->get_error_code(),
1711
						'message' => $media_id->get_error_message(),
1712
					);
1713
				} elseif ( ! empty( $media_id ) ) {
1714
					$media_ids[$i] = $media_id;
1715
				}
1716
1717
				$i++;
1718
			}
1719
		}
1720
1721
		if ( ! empty( $media_attrs ) ) {
1722
			foreach ( $media_ids as $index => $media_id ) {
1723
				if ( empty( $media_attrs[$index] ) )
1724
					continue;
1725
1726
				$attrs = $media_attrs[$index];
1727
				$insert = array();
1728
1729
				// Attributes: Title, Caption, Description
1730
1731
				if ( isset( $attrs['title'] ) ) {
1732
					$insert['post_title'] = $attrs['title'];
1733
				}
1734
1735
				if ( isset( $attrs['caption'] ) ) {
1736
					$insert['post_excerpt'] = $attrs['caption'];
1737
				}
1738
1739
				if ( isset( $attrs['description'] ) ) {
1740
					$insert['post_content'] = $attrs['description'];
1741
				}
1742
1743
				if ( ! empty( $insert ) ) {
1744
					$insert['ID'] = $media_id;
1745
					wp_update_post( (object) $insert );
1746
				}
1747
1748
				// Attributes: Alt
1749
1750 View Code Duplication
				if ( isset( $attrs['alt'] ) ) {
1751
					$alt = wp_strip_all_tags( $attrs['alt'], true );
1752
					update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1753
				}
1754
1755
				// Attributes: Artist, Album
1756
1757
				$id3_meta = array();
1758
1759 View Code Duplication
				foreach ( array( 'artist', 'album' ) as $key ) {
1760
					if ( isset( $attrs[ $key ] ) ) {
1761
						$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1762
					}
1763
				}
1764
1765 View Code Duplication
				if ( ! empty( $id3_meta ) ) {
1766
					// Before updating metadata, ensure that the item is audio
1767
					$item = $this->get_media_item_v1_1( $media_id );
1768
					if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1769
						wp_update_attachment_metadata( $media_id, $id3_meta );
1770
					}
1771
				}
1772
			}
1773
		}
1774
1775
		return array( 'media_ids' => $media_ids, 'errors' => $errors );
1776
1777
	}
1778
1779
	function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1780
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1781
			return false;
1782
1783
		// if we didn't get a URL, let's bail
1784
		$parsed = @parse_url( $url );
1785
		if ( empty( $parsed ) )
1786
			return false;
1787
1788
		$tmp = download_url( $url );
1789
		if ( is_wp_error( $tmp ) ) {
1790
			return $tmp;
1791
		}
1792
1793
		// First check to see if we get a mime-type match by file, otherwise, check to
1794
		// see if WordPress supports this file as an image. If neither, then it is not supported.
1795
		if ( ! $this->is_file_supported_for_sideloading( $tmp ) && 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1796
			@unlink( $tmp );
1797
			return false;
1798
		}
1799
1800
		// emulate a $_FILES entry
1801
		$file_array = array(
1802
			'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1803
			'tmp_name' => $tmp,
1804
		);
1805
1806
		$id = media_handle_sideload( $file_array, $parent_post_id );
1807
		if ( file_exists( $tmp ) ) {
1808
			@unlink( $tmp );
1809
		}
1810
1811
		if ( is_wp_error( $id ) ) {
1812
			return $id;
1813
		}
1814
1815
		if ( ! $id || ! is_int( $id ) ) {
1816
			return false;
1817
		}
1818
1819
		return $id;
1820
	}
1821
1822
	/**
1823
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1824
	 *
1825
	 * @param string $file Path to file to get its mime type.
1826
	 *
1827
	 * @return bool
1828
	 */
1829 View Code Duplication
	protected function is_file_supported_for_sideloading( $file ) {
1830
		if ( class_exists( 'finfo' ) ) { // php 5.3+
1831
			$finfo = new finfo( FILEINFO_MIME );
1832
			$mime = explode( '; ', $finfo->file( $file ) );
1833
			$type = $mime[0];
1834
1835
		} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
1836
			$type = mime_content_type( $file );
1837
1838
		} else {
1839
			return false;
1840
		}
1841
1842
		/**
1843
		 * Filter the list of supported mime types for media sideloading.
1844
		 *
1845
		 * @since 4.0.0
1846
		 *
1847
		 * @module json-api
1848
		 *
1849
		 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
1850
		 */
1851
		$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
1852
			'image/png',
1853
			'image/jpeg',
1854
			'image/gif',
1855
			'image/bmp',
1856
			'video/quicktime',
1857
			'video/mp4',
1858
			'video/mpeg',
1859
			'video/ogg',
1860
			'video/3gpp',
1861
			'video/3gpp2',
1862
			'video/h261',
1863
			'video/h262',
1864
			'video/h264',
1865
			'video/x-msvideo',
1866
			'video/x-ms-wmv',
1867
			'video/x-ms-asf',
1868
		) );
1869
1870
		// If the type returned was not an array as expected, then we know we don't have a match.
1871
		if ( ! is_array( $supported_mime_types ) ) {
1872
			return false;
1873
		}
1874
1875
		return in_array( $type, $supported_mime_types );
1876
	}
1877
1878
	function allow_video_uploads( $mimes ) {
1879
		// if we are on Jetpack, bail - Videos are already allowed
1880
		if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1881
			return $mimes;
1882
		}
1883
1884
		// extra check that this filter is only ever applied during REST API requests
1885
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1886
			return $mimes;
1887
		}
1888
1889
		// bail early if they already have the upgrade..
1890
		if ( get_option( 'video_upgrade' ) == '1' ) {
1891
			return $mimes;
1892
		}
1893
1894
		// lets whitelist to only specific clients right now
1895
		$clients_allowed_video_uploads = array();
1896
		/**
1897
		 * Filter the list of whitelisted video clients.
1898
		 *
1899
		 * @module json-api
1900
		 *
1901
		 * @since 3.2.0
1902
		 *
1903
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1904
		 */
1905
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1906
		if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1907
			return $mimes;
1908
		}
1909
1910
		$mime_list = wp_get_mime_types();
1911
1912
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1913
		/**
1914
		 * Filter the video filetypes allowed on the site.
1915
		 *
1916
		 * @module json-api
1917
		 *
1918
		 * @since 3.2.0
1919
		 *
1920
		 * @param array $video_exts Array of video filetypes allowed on the site.
1921
		 */
1922
		$video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
1923
		$video_mimes = array();
1924
1925
		if ( !empty( $video_exts ) ) {
1926
			foreach ( $video_exts as $ext ) {
1927
				foreach ( $mime_list as $ext_pattern => $mime ) {
1928
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
1929
						$video_mimes[$ext_pattern] = $mime;
1930
				}
1931
			}
1932
1933
			$mimes = array_merge( $mimes, $video_mimes );
1934
		}
1935
1936
		return $mimes;
1937
	}
1938
1939
	function is_current_site_multi_user() {
1940
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
1941
		if ( false === $users ) {
1942
			$user_query = new WP_User_Query( array(
1943
				'blog_id' => get_current_blog_id(),
1944
				'fields'  => 'ID',
1945
			) );
1946
			$users = (int) $user_query->get_total();
1947
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
1948
		}
1949
		return $users > 1;
1950
	}
1951
1952
	function allows_cross_origin_requests() {
1953
		return 'GET' == $this->method || $this->allow_cross_origin_request;
1954
	}
1955
1956
	function allows_unauthorized_requests( $origin, $complete_access_origins  ) {
1957
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
1958
	}
1959
1960
	function get_platform() {
1961
		return wpcom_get_sal_platform( $this->api->token_details );
1962
	}
1963
1964
	/**
1965
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
1966
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
1967
	 *
1968
	 * Override this method if you want to do something different.
1969
	 *
1970
	 * @param  int  $blog_id
1971
	 * @return bool
1972
	 */
1973
	function force_wpcom_request( $blog_id ) {
1974
		return false;
1975
	}
1976
1977
	/**
1978
	 * Return endpoint response
1979
	 *
1980
	 * @param ... determined by ->$path
1981
	 *
1982
	 * @return
1983
	 * 	falsy: HTTP 500, no response body
1984
	 *	WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
1985
	 *	$data: HTTP 200, json_encode( $data ) response body
1986
	 */
1987
	abstract function callback( $path = '' );
1988
1989
1990
}
1991
1992
require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
1993