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