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