Completed
Push — remove/wp-admin-publicize-conn... ( d708bd...e84ae6 )
by
unknown
64:00 queued 52:22
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
			if ( is_array( $metadata['sizes'] ) ) {
1173
			      	foreach ( $metadata['sizes'] as $size => $size_details ) {
1174
			      	      	$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1175
			      	}
1176
			}
1177
			$response['exif']   = $metadata['image_meta'];
0 ignored issues
show
Equals sign not aligned correctly; expected 1 space but found 3 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
1178
		}
1179
1180
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1181
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1182
			$response['length'] = $metadata['length'];
1183
			$response['exif']   = $metadata;
1184
		}
1185
1186
		if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) {
1187
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1188 View Code Duplication
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1189
				$response['height'] = $metadata['height'];
1190
				$response['width']  = $metadata['width'];
1191
			}
1192
1193
			if ( isset( $metadata['length'] ) ) {
1194
				$response['length'] = $metadata['length'];
1195
			}
1196
1197
			// add VideoPress info
1198
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1199
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_id );
1200
1201
				// Thumbnails
1202
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1203
					$response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' );
1204
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1205
						if ( video_format_done( $info, $size ) ) {
1206
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1207
						} else {
1208
							unset( $response['thumbnails'][ $size ] );
1209
						}
1210
					}
1211
				}
1212
1213
				$response['videopress_guid'] = $info->guid;
1214
				$response['videopress_processing_done'] = true;
1215
				if ( '0000-00-00 00:00:00' == $info->finish_date_gmt ) {
1216
					$response['videopress_processing_done'] = false;
1217
				}
1218
			}
1219
		}
1220
1221
		$response['thumbnails'] = (object) $response['thumbnails'];
1222
1223
		$response['meta'] = (object) array(
1224
			'links' => (object) array(
1225
				'self' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1226
				'help' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1227
				'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
1228
			),
1229
		);
1230
1231
		// add VideoPress link to the meta
1232
		if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) {
1233
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1234
				$response['meta']->links->videopress = (string) $this->get_link( '/videos/%s', $response['videopress_guid'], '' );
1235
			}
1236
		}
1237
1238
		if ( $media_item->post_parent > 0 ) {
1239
			$response['meta']->links->parent = (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1240
		}
1241
1242
		return (object) $response;
1243
	}
1244
1245
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1246
1247
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1248
		/// keep updating this function
1249
		if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1250
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1251
		}
1252
1253
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1254
	}
1255
1256
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1257
		// Permissions
1258
		switch ( $context ) {
1259
		case 'edit' :
1260
			$tax = get_taxonomy( $taxonomy_type );
1261
			if ( !current_user_can( $tax->cap->edit_terms ) )
1262
				return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1263
			break;
1264
		case 'display' :
1265 View Code Duplication
			if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1266
				return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1267
			}
1268
			break;
1269
		default :
1270
			return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1271
		}
1272
1273
		$response                = array();
1274
		$response['ID']          = (int) $taxonomy->term_id;
1275
		$response['name']        = (string) $taxonomy->name;
1276
		$response['slug']        = (string) $taxonomy->slug;
1277
		$response['description'] = (string) $taxonomy->description;
1278
		$response['post_count']  = (int) $taxonomy->count;
1279
1280
		if ( 'category' === $taxonomy_type )
1281
			$response['parent'] = (int) $taxonomy->parent;
1282
1283
		$response['meta'] = (object) array(
1284
			'links' => (object) array(
1285
				'self' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1286
				'help' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1287
				'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
1288
			),
1289
		);
1290
1291
		return (object) $response;
1292
	}
1293
1294
	/**
1295
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1296
	 *
1297
	 * @param $date_gmt (string) GMT datetime string.
1298
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1299
	 *
1300
	 * @return string
1301
	 */
1302
	function format_date( $date_gmt, $date = null ) {
1303
		$timestamp_gmt = strtotime( "$date_gmt+0000" );
1304
1305
		if ( null === $date ) {
1306
			$timestamp = $timestamp_gmt;
1307
			$hours     = $minutes = $west = 0;
1308
		} else {
1309
			$date_time = date_create( "$date+0000" );
1310
			if ( $date_time ) {
1311
				$timestamp = date_format(  $date_time, 'U' );
1312
			} else {
1313
				$timestamp = 0;
1314
			}
1315
1316
			// "0000-00-00 00:00:00" == -62169984000
1317
			if ( -62169984000 == $timestamp_gmt ) {
1318
				// WordPress sets post_date=now, post_date_gmt="0000-00-00 00:00:00" for all drafts
1319
				// WordPress sets post_modified=now, post_modified_gmt="0000-00-00 00:00:00" for new drafts
1320
1321
				// Try to guess the correct offset from the blog's options.
1322
				$timezone_string = get_option( 'timezone_string' );
1323
1324
				if ( $timezone_string && $date_time ) {
1325
					$timezone = timezone_open( $timezone_string );
1326
					if ( $timezone ) {
1327
						$offset = $timezone->getOffset( $date_time );
1328
					}
1329
				} else {
1330
					$offset = 3600 * get_option( 'gmt_offset' );
1331
				}
1332
			} else {
1333
				$offset = $timestamp - $timestamp_gmt;
1334
			}
1335
1336
			$west      = $offset < 0;
1337
			$offset    = abs( $offset );
1338
			$hours     = (int) floor( $offset / 3600 );
1339
			$offset   -= $hours * 3600;
1340
			$minutes   = (int) floor( $offset / 60 );
1341
		}
1342
1343
		return (string) gmdate( 'Y-m-d\\TH:i:s', $timestamp ) . sprintf( '%s%02d:%02d', $west ? '-' : '+', $hours, $minutes );
1344
	}
1345
1346
	/**
1347
	 * Parses a date string and returns the local and GMT representations
1348
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1349
	 * timezones or offsets. If the parsed datetime was not localized to a
1350
	 * particular timezone or offset we will assume it was given in GMT
1351
	 * relative to now and will convert it to local time using either the
1352
	 * timezone set in the options table for the blog or the GMT offset.
1353
	 *
1354
	 * @param datetime string
1355
	 *
1356
	 * @return array( $local_time_string, $gmt_time_string )
1357
	 */
1358
	function parse_date( $date_string ) {
1359
		$date_string_info = date_parse( $date_string );
1360
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1361
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1362
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1363
				$dt_local = clone $dt_utc = new DateTime( $date_string );
1364
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1365
				return array(
1366
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1367
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1368
				);
1369
			}
1370
1371
			// It's parseable but no TZ info so assume UTC
1372
			$dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1373
		} else {
1374
			// Could not parse time, use now in UTC
1375
			$dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1376
		}
1377
1378
		// First try to use timezone as it's daylight savings aware.
1379
		$timezone_string = get_option( 'timezone_string' );
1380
		if ( $timezone_string ) {
1381
			$tz = timezone_open( $timezone_string );
1382
			if ( $tz ) {
1383
				$dt_local->setTimezone( $tz );
1384
				return array(
1385
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1386
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1387
				);
1388
			}
1389
		}
1390
1391
		// Fallback to GMT offset (in hours)
1392
		// NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1393
		$gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1394
		$dt_local->modify("+{$gmt_offset_seconds} seconds");
1395
		return array(
1396
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1397
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1398
		);
1399
	}
1400
1401
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1402
	function load_theme_functions() {
1403
		// bail if we've done this already (can happen when calling /batch endpoint)
1404
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) )
1405
			return;
1406
1407
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1408
1409
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1410
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1411
1412
		$copy_dirs = array( get_template_directory() );
1413
		if ( wpcom_is_vip() ) {
1414
			$copy_dirs[] = WP_CONTENT_DIR . '/themes/vip/plugins/';
1415
		}
1416
1417
		// Is this a child theme? Load the child theme's functions file.
1418
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1419
			foreach ( $function_files as $function_file ) {
1420
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1421
					require_once(  get_stylesheet_directory() . $function_file );
1422
				}
1423
			}
1424
			$copy_dirs[] = get_stylesheet_directory();
1425
		}
1426
1427
		foreach ( $function_files as $function_file ) {
1428
			if ( file_exists( get_template_directory() . $function_file ) ) {
1429
				require_once(  get_template_directory() . $function_file );
1430
			}
1431
		}
1432
1433
		// add inc/wpcom.php and/or includes/wpcom.php
1434
		wpcom_load_theme_compat_file();
1435
1436
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1437
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1438
1439
		/**
1440
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1441
		 *
1442
		 * The REST API does not load the theme when processing requests.
1443
		 * To enable theme-based functionality, the API will load the '/functions.php',
1444
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1445
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1446
		 *
1447
		 * @module json-api
1448
		 *
1449
		 * @since 3.2.0
1450
		 */
1451
		do_action( 'restapi_theme_after_setup_theme' );
1452
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1453
1454
		/**
1455
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1456
		 *
1457
		 * The REST API does not load the theme when processing requests.
1458
		 * To enable theme-based functionality, the API will load the '/functions.php',
1459
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1460
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1461
		 *
1462
		 * @module json-api
1463
		 *
1464
		 * @since 3.2.0
1465
		 */
1466
		do_action( 'restapi_theme_init' );
1467
	}
1468
1469
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1470
		global $wp_filter;
1471
		foreach ( $wp_filter as $hook => $actions ) {
1472
			if ( $from_hook <> $hook )
1473
				continue;
1474
			foreach ( (array) $actions as $priority => $callbacks ) {
1475
				foreach( $callbacks as $callback_key => $callback_data ) {
1476
					$callback = $callback_data['function'];
1477
					$reflection = $this->get_reflection( $callback ); // use reflection api to determine filename where function is defined
1478
					if ( false !== $reflection ) {
1479
						$file_name = $reflection->getFileName();
1480
						foreach( $base_paths as $base_path ) {
1481
							if ( 0 === strpos( $file_name, $base_path ) ) { // only copy hooks with functions which are part of the specified files
1482
								$wp_filter[ $to_hook ][ $priority ][ 'cph' . $callback_key ] = $callback_data;
1483
							}
1484
						}
1485
					}
1486
				}
1487
			}
1488
		}
1489
	}
1490
1491
	function get_reflection( $callback ) {
1492
		if ( is_array( $callback ) ) {
1493
			list( $class, $method ) = $callback;
1494
			return new ReflectionMethod( $class, $method );
1495
		}
1496
1497
		if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) {
1498
			list( $class, $method ) = explode( "::", $callback );
1499
			return new ReflectionMethod( $class, $method );
1500
		}
1501
1502
		if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) {
1503
			return new ReflectionMethod( $callback, "__invoke" );
1504
		}
1505
1506
		if ( is_string( $callback ) && strpos( $callback, "::" ) == false && function_exists( $callback ) ) {
1507
			return new ReflectionFunction( $callback );
1508
		}
1509
1510
		return false;
1511
	}
1512
1513
	/**
1514
	 * Try to find the closest supported version of an endpoint to the current endpoint
1515
	 *
1516
	 * For example, if we were looking at the path /animals/panda:
1517
	 * - if the current endpoint is v1.3 and there is a v1.3 of /animals/%s available, we return 1.3
1518
	 * - if the current endpoint is v1.3 and there is no v1.3 of /animals/%s known, we fall back to the
1519
	 *   maximum available version of /animals/%s, e.g. 1.1
1520
	 *
1521
	 * This method is used in get_link() to construct meta links for API responses.
1522
	 *
1523
	 * @param $path string The current endpoint path, relative to the version
1524
	 * @param $method string Request method used to access the endpoint path
1525
	 * @return string The current version, or otherwise the maximum version available
1526
	 */
1527
	function get_closest_version_of_endpoint( $path, $request_method = 'GET' ) {
1528
1529
		$path = untrailingslashit( $path );
1530
1531
		// /help is a special case - always use the current request version
1532
		if ( wp_endswith( $path, '/help' ) ) {
1533
			return $this->api->version;
1534
		}
1535
1536
		$endpoint_path_versions = $this->get_endpoint_path_versions();
1537
		$last_path_segment = $this->get_last_segment_of_relative_path( $path );
1538
		$max_version_found = null;
1539
1540
		foreach ( $endpoint_path_versions as $endpoint_last_path_segment => $endpoints ) {
1541
1542
			// Does the last part of the path match the path key? (e.g. 'posts')
1543
			// If the last part contains a placeholder (e.g. %s), we want to carry on
1544
			if ( $last_path_segment != $endpoint_last_path_segment && ! strstr( $endpoint_last_path_segment, '%' ) ) {
1545
				continue;
1546
			}
1547
1548
			foreach ( $endpoints as $endpoint ) {
1549
				// Does the request method match?
1550
				if ( ! in_array( $request_method, $endpoint['request_methods'] ) ) {
1551
					continue;
1552
				}
1553
1554
				$endpoint_path = untrailingslashit( $endpoint['path'] );
1555
				$endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
1556
1557
				if ( ! preg_match( "#^$endpoint_path_regex\$#", $path, $matches ) ) {
1558
					continue;
1559
				}
1560
1561
				// Make sure the endpoint exists at the same version
1562
				if ( version_compare( $this->api->version, $endpoint['min_version'], '>=') &&
1563
					 version_compare( $this->api->version, $endpoint['max_version'], '<=') ) {
1564
					return $this->api->version;
1565
				}
1566
1567
				// If the endpoint doesn't exist at the same version, record the max version we found
1568
				if ( empty( $max_version_found ) || version_compare( $max_version_found, $endpoint['max_version'], '<' ) ) {
1569
					$max_version_found = $endpoint['max_version'];
1570
				}
1571
			}
1572
		}
1573
1574
		// If the endpoint version is less than the requested endpoint version, return the max version found
1575
		if ( ! empty( $max_version_found ) ) {
1576
			return $max_version_found;
1577
		}
1578
1579
		// Otherwise, use the API version of the current request
1580
		return $this->api->version;
1581
	}
1582
1583
	/**
1584
	 * Get an array of endpoint paths with their associated versions
1585
	 *
1586
	 * The result is cached for 30 minutes.
1587
	 *
1588
	 * @return array Array of endpoint paths, min_versions and max_versions, keyed by last segment of path
1589
	 **/
1590
	protected function get_endpoint_path_versions() {
1591
1592
		// Do we already have the result of this method in the cache?
1593
		$cache_result = get_transient( 'endpoint_path_versions' );
1594
1595
		if ( ! empty ( $cache_result ) ) {
1596
			return $cache_result;
1597
		}
1598
1599
		/*
1600
		 * Create a map of endpoints and their min/max versions keyed by the last segment of the path (e.g. 'posts')
1601
		 * This reduces the search space when finding endpoint matches in get_closest_version_of_endpoint()
1602
		 */
1603
		$endpoint_path_versions = array();
1604
1605
		foreach ( $this->api->endpoints as $key => $endpoint_objects ) {
1606
1607
			// The key contains a serialized path, min_version and max_version
1608
			list( $path, $min_version, $max_version ) = unserialize( $key );
1609
1610
			// Grab the last component of the relative path to use as the top-level key
1611
			$last_path_segment = $this->get_last_segment_of_relative_path( $path );
1612
1613
			$endpoint_path_versions[ $last_path_segment ][] = array(
1614
				'path' => $path,
1615
				'min_version' => $min_version,
1616
				'max_version' => $max_version,
1617
				'request_methods' => array_keys( $endpoint_objects )
1618
			);
1619
		}
1620
1621
		set_transient(
1622
			'endpoint_path_versions',
1623
			$endpoint_path_versions,
1624
			(HOUR_IN_SECONDS / 2)
1625
		);
1626
1627
		return $endpoint_path_versions;
1628
	}
1629
1630
	/**
1631
	 * Grab the last segment of a relative path
1632
	 *
1633
	 * @param string $path Path
1634
	 * @return string Last path segment
1635
	 */
1636
	protected function get_last_segment_of_relative_path( $path) {
1637
		$path_parts = array_filter( explode( '/', $path ) );
1638
1639
		if ( empty( $path_parts ) ) {
1640
			return null;
1641
		}
1642
1643
		return end( $path_parts );
1644
	}
1645
1646
	/**
1647
	 * Generate a URL to an endpoint
1648
	 *
1649
	 * Used to construct meta links in API responses
1650
	 *
1651
	 * @param mixed $args Optional arguments to be appended to URL
1652
	 * @return string Endpoint URL
1653
	 **/
1654
	function get_link() {
1655
		$args   = func_get_args();
1656
		$format = array_shift( $args );
1657
		$base = WPCOM_JSON_API__BASE;
1658
1659
		$path = array_pop( $args );
1660
1661
		if ( $path ) {
1662
			$path = '/' . ltrim( $path, '/' );
1663
		}
1664
1665
		$args[] = $path;
1666
1667
		// Escape any % in args before using sprintf
1668
		$escaped_args = array();
1669
		foreach ( $args as $arg_key => $arg_value ) {
1670
			$escaped_args[ $arg_key ] = str_replace( '%', '%%', $arg_value );
1671
		}
1672
1673
		$relative_path = vsprintf( "$format%s", $escaped_args );
1674
1675
		if ( ! wp_startswith( $relative_path, '.' ) ) {
1676
			// Generic version. Match the requested version as best we can
1677
			$api_version = $this->get_closest_version_of_endpoint( $relative_path );
1678
			$base        = substr( $base, 0, - 1 ) . $api_version;
1679
		}
1680
1681
		// escape any % in the relative path before running it through sprintf again
1682
		$relative_path = str_replace( '%', '%%', $relative_path );
1683
		// http, WPCOM_JSON_API__BASE, ...    , path
1684
		// %s  , %s                  , $format, %s
1685
		return esc_url_raw( sprintf( "%s://%s$relative_path", $this->api->public_api_scheme, $base ) );
1686
	}
1687
1688
	function get_me_link( $path = '' ) {
1689
		return $this->get_link( '/me', $path );
1690
	}
1691
1692
	function get_taxonomy_link( $blog_id, $taxonomy_id, $taxonomy_type, $path = '' ) {
1693
		if ( 'category' === $taxonomy_type )
1694
			return $this->get_link( '/sites/%d/categories/slug:%s', $blog_id, $taxonomy_id, $path );
1695
		else
1696
			return $this->get_link( '/sites/%d/tags/slug:%s', $blog_id, $taxonomy_id, $path );
1697
	}
1698
1699
	function get_media_link( $blog_id, $media_id, $path = '' ) {
1700
		return $this->get_link( '/sites/%d/media/%d', $blog_id, $media_id, $path );
1701
	}
1702
1703
	function get_site_link( $blog_id, $path = '' ) {
1704
		return $this->get_link( '/sites/%d', $blog_id, $path );
1705
	}
1706
1707
	function get_post_link( $blog_id, $post_id, $path = '' ) {
1708
		return $this->get_link( '/sites/%d/posts/%d', $blog_id, $post_id, $path );
1709
	}
1710
1711
	function get_comment_link( $blog_id, $comment_id, $path = '' ) {
1712
		return $this->get_link( '/sites/%d/comments/%d', $blog_id, $comment_id, $path );
1713
	}
1714
1715
	function get_publicize_connection_link( $blog_id, $publicize_connection_id, $path = '' ) {
1716
		return $this->get_link( '.1/sites/%d/publicize-connections/%d', $blog_id, $publicize_connection_id, $path );
1717
	}
1718
1719
	function get_publicize_connections_link( $keyring_token_id, $path = '' ) {
1720
		return $this->get_link( '.1/me/publicize-connections/?keyring_connection_ID=%d', $keyring_token_id, $path );
1721
	}
1722
1723
	function get_keyring_connection_link( $keyring_token_id, $path = '' ) {
1724
		return $this->get_link( '.1/me/keyring-connections/%d', $keyring_token_id, $path );
1725
	}
1726
1727
	function get_external_service_link( $external_service, $path = '' ) {
1728
		return $this->get_link( '.1/meta/external-services/%s', $external_service, $path );
1729
	}
1730
1731
1732
	/**
1733
	* Check whether a user can view or edit a post type
1734
	* @param string $post_type              post type to check
1735
	* @param string $context                'display' or 'edit'
1736
	* @return bool
1737
	*/
1738
	function current_user_can_access_post_type( $post_type, $context='display' ) {
1739
		$post_type_object = get_post_type_object( $post_type );
1740
		if ( ! $post_type_object ) {
1741
			return false;
1742
		}
1743
1744
		switch( $context ) {
1745
			case 'edit':
1746
				return current_user_can( $post_type_object->cap->edit_posts );
1747
			case 'display':
1748
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1749
			default:
1750
				return false;
1751
		}
1752
	}
1753
1754
	function is_post_type_allowed( $post_type ) {
1755
		// if the post type is empty, that's fine, WordPress will default to post
1756
		if ( empty( $post_type ) )
1757
			return true;
1758
1759
		// allow special 'any' type
1760
		if ( 'any' == $post_type )
1761
			return true;
1762
1763
		// check for allowed types
1764
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) )
1765
			return true;
1766
1767
		return false;
1768
	}
1769
1770
	/**
1771
	 * Gets the whitelisted post types that JP should allow access to.
1772
	 *
1773
	 * @return array Whitelisted post types.
1774
	 */
1775
	protected function _get_whitelisted_post_types() {
1776
		$allowed_types = array( 'post', 'page', 'revision' );
1777
1778
		/**
1779
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1780
		 *
1781
		 * @module json-api
1782
		 *
1783
		 * @since 2.2.3
1784
		 *
1785
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1786
		 */
1787
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1788
1789
		return array_unique( $allowed_types );
1790
	}
1791
1792
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1793
1794
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1795
1796
		$media_ids = $errors = array();
1797
		$user_can_upload_files = current_user_can( 'upload_files' );
1798
		$media_attrs = array_values( $media_attrs ); // reset the keys
1799
		$i = 0;
1800
1801
		if ( ! empty( $media_files ) ) {
1802
			$this->api->trap_wp_die( 'upload_error' );
1803
			foreach ( $media_files as $media_item ) {
1804
				$_FILES['.api.media.item.'] = $media_item;
1805 View Code Duplication
				if ( ! $user_can_upload_files ) {
1806
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1807
				} else {
1808
					if ( $force_parent_id ) {
1809
						$parent_id = absint( $force_parent_id );
1810
					} elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1811
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1812
					} else {
1813
						$parent_id = 0;
1814
					}
1815
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1816
				}
1817
				if ( is_wp_error( $media_id ) ) {
1818
					$errors[$i]['file']   = $media_item['name'];
1819
					$errors[$i]['error']   = $media_id->get_error_code();
1820
					$errors[$i]['message'] = $media_id->get_error_message();
1821
				} else {
1822
					$media_ids[$i] = $media_id;
1823
				}
1824
1825
				$i++;
1826
			}
1827
			$this->api->trap_wp_die( null );
1828
			unset( $_FILES['.api.media.item.'] );
1829
		}
1830
1831
		if ( ! empty( $media_urls ) ) {
1832
			foreach ( $media_urls as $url ) {
1833 View Code Duplication
				if ( ! $user_can_upload_files ) {
1834
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1835
				} else {
1836
					if ( $force_parent_id ) {
1837
						$parent_id = absint( $force_parent_id );
1838
					} else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1839
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1840
					} else {
1841
						$parent_id = 0;
1842
					}
1843
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1844
				}
1845
				if ( is_wp_error( $media_id ) ) {
1846
					$errors[$i] = array(
1847
						'file'    => $url,
1848
						'error'   => $media_id->get_error_code(),
1849
						'message' => $media_id->get_error_message(),
1850
					);
1851
				} elseif ( ! empty( $media_id ) ) {
1852
					$media_ids[$i] = $media_id;
1853
				}
1854
1855
				$i++;
1856
			}
1857
		}
1858
1859
		if ( ! empty( $media_attrs ) ) {
1860
			foreach ( $media_ids as $index => $media_id ) {
1861
				if ( empty( $media_attrs[$index] ) )
1862
					continue;
1863
1864
				$attrs = $media_attrs[$index];
1865
				$insert = array();
1866
1867
				if ( ! empty( $attrs['title'] ) ) {
1868
					$insert['post_title'] = $attrs['title'];
1869
				}
1870
1871
				if ( ! empty( $attrs['caption'] ) )
1872
					$insert['post_excerpt'] = $attrs['caption'];
1873
1874
				if ( ! empty( $attrs['description'] ) )
1875
					$insert['post_content'] = $attrs['description'];
1876
1877
				if ( empty( $insert ) )
1878
					continue;
1879
1880
				$insert['ID'] = $media_id;
1881
				wp_update_post( (object) $insert );
1882
			}
1883
		}
1884
1885
		return array( 'media_ids' => $media_ids, 'errors' => $errors );
1886
1887
	}
1888
1889
	function handle_media_sideload( $url, $parent_post_id = 0 ) {
1890
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1891
			return false;
1892
1893
		// if we didn't get a URL, let's bail
1894
		$parsed = @parse_url( $url );
1895
		if ( empty( $parsed ) )
1896
			return false;
1897
1898
		$tmp = download_url( $url );
1899
		if ( is_wp_error( $tmp ) ) {
1900
			return $tmp;
1901
		}
1902
1903
		if ( ! file_is_displayable_image( $tmp ) ) {
1904
			@unlink( $tmp );
1905
			return false;
1906
		}
1907
1908
		// emulate a $_FILES entry
1909
		$file_array = array(
1910
			'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1911
			'tmp_name' => $tmp,
1912
		);
1913
1914
		$id = media_handle_sideload( $file_array, $parent_post_id );
1915
		@unlink( $tmp );
1916
1917
		if ( is_wp_error( $id ) ) {
1918
			return $id;
1919
		}
1920
1921
		if ( ! $id || ! is_int( $id ) ) {
1922
			return false;
1923
		}
1924
1925
		return $id;
1926
	}
1927
1928
	function allow_video_uploads( $mimes ) {
1929
		// if we are on Jetpack, bail - Videos are already allowed
1930
		if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1931
			return $mimes;
1932
		}
1933
1934
		// extra check that this filter is only ever applied during REST API requests
1935
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1936
			return $mimes;
1937
		}
1938
1939
		// bail early if they already have the upgrade..
1940
		if ( get_option( 'video_upgrade' ) == '1' ) {
1941
			return $mimes;
1942
		}
1943
1944
		// lets whitelist to only specific clients right now
1945
		$clients_allowed_video_uploads = array();
1946
		/**
1947
		 * Filter the list of whitelisted video clients.
1948
		 *
1949
		 * @module json-api
1950
		 *
1951
		 * @since 3.2.0
1952
		 *
1953
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1954
		 */
1955
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1956
		if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1957
			return $mimes;
1958
		}
1959
1960
		$mime_list = wp_get_mime_types();
1961
1962
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1963
		/**
1964
		 * Filter the video filetypes allowed on the site.
1965
		 *
1966
		 * @module json-api
1967
		 *
1968
		 * @since 3.2.0
1969
		 *
1970
		 * @param array $video_exts Array of video filetypes allowed on the site.
1971
		 */
1972
		$video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
1973
		$video_mimes = array();
1974
1975
		if ( !empty( $video_exts ) ) {
1976
			foreach ( $video_exts as $ext ) {
1977
				foreach ( $mime_list as $ext_pattern => $mime ) {
1978
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
1979
						$video_mimes[$ext_pattern] = $mime;
1980
				}
1981
			}
1982
1983
			$mimes = array_merge( $mimes, $video_mimes );
1984
		}
1985
1986
		return $mimes;
1987
	}
1988
1989
	function is_current_site_multi_user() {
1990
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
1991
		if ( false === $users ) {
1992
			$user_query = new WP_User_Query( array(
1993
				'blog_id' => get_current_blog_id(),
1994
				'fields'  => 'ID',
1995
			) );
1996
			$users = (int) $user_query->get_total();
1997
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
1998
		}
1999
		return $users > 1;
2000
	}
2001
2002
	function allows_cross_origin_requests() {
2003
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2004
	}
2005
2006
	function allows_unauthorized_requests( $origin, $complete_access_origins  ) {
2007
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2008
	}
2009
2010
	/**
2011
	 * Return endpoint response
2012
	 *
2013
	 * @param ... determined by ->$path
2014
	 *
2015
	 * @return
2016
	 * 	falsy: HTTP 500, no response body
2017
	 *	WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2018
	 *	$data: HTTP 200, json_encode( $data ) response body
2019
	 */
2020
	abstract function callback( $path = '' );
2021
2022
2023
}
2024
2025
require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
2026