Completed
Push — update/grunion-personal-data ( beacb7...22114a )
by
unknown
14:02
created

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

Upgrade to new PHP Analysis Engine

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

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

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

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

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

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

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