Completed
Push — jetpack-fusion-mock-files ( e51750...3b1561 )
by
unknown
13:31
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 :
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...
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 = Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2;
654
			$return[$key] = (object) $this->cast_and_filter(
655
				$value,
656
				/**
657
				 * Filter the documentation returned for a plugin.
658
				 *
659
				 * @module json-api
660
				 *
661
				 * @since 3.1.0
662
				 *
663
				 * @param array $docs Array of documentation about a plugin.
664
				 */
665
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
666
				false,
667
				$for_output
668
			);
669
			break;
670
		case 'file_mod_capabilities':
671
			$docs           = array(
672
				'reasons_modify_files_unavailable' => '(array) The reasons why files can\'t be modified',
673
				'reasons_autoupdate_unavailable'   => '(array) The reasons why autoupdates aren\'t allowed',
674
				'modify_files'                     => '(boolean) true if files can be modified',
675
				'autoupdate_files'                 => '(boolean) true if autoupdates are allowed',
676
			);
677
			$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
678
			break;
679
		case 'jetpackmodule' :
680
			$docs = array(
681
				'id'          => '(string)   The module\'s ID',
682
				'active'      => '(boolean)  The module\'s status.',
683
				'name'        => '(string)   The module\'s name.',
684
				'description' => '(safehtml) The module\'s description.',
685
				'sort'        => '(int)      The module\'s display order.',
686
				'introduced'  => '(string)   The Jetpack version when the module was introduced.',
687
				'changed'     => '(string)   The Jetpack version when the module was changed.',
688
				'free'        => '(boolean)  The module\'s Free or Paid status.',
689
				'module_tags' => '(array)    The module\'s tags.'
690
			);
691
			$return[$key] = (object) $this->cast_and_filter(
692
				$value,
693
				/** This filter is documented in class.json-api-endpoints.php */
694
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
695
				false,
696
				$for_output
697
			);
698
			break;
699
		case 'sharing_button' :
700
			$docs = array(
701
				'ID'         => '(string)',
702
				'name'       => '(string)',
703
				'URL'        => '(string)',
704
				'icon'       => '(string)',
705
				'enabled'    => '(bool)',
706
				'visibility' => '(string)',
707
			);
708
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
709
			break;
710
		case 'sharing_button_service':
711
			$docs = array(
712
				'ID'               => '(string) The service identifier',
713
				'name'             => '(string) The service name',
714
				'class_name'       => '(string) Class name for custom style sharing button elements',
715
				'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
716
				'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
717
				'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview'
718
			);
719
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
720
			break;
721
		case 'taxonomy':
722
			$docs = array(
723
				'name'         => '(string) The taxonomy slug',
724
				'label'        => '(string) The taxonomy human-readable name',
725
				'labels'       => '(object) Mapping of labels for the taxonomy',
726
				'description'  => '(string) The taxonomy description',
727
				'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
728
				'public'       => '(bool) Whether the taxonomy is public',
729
				'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
730
			);
731
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
732
			break;
733
734
		default :
735
			$method_name = $type['type'] . '_docs';
736
			if ( method_exists( WPCOM_JSON_API_Jetpack_Overrides, $method_name ) ) {
737
				$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
738
			}
739
740
			if ( ! empty( $docs ) ) {
741
				$return[$key] = (object) $this->cast_and_filter(
742
					$value,
743
					/** This filter is documented in class.json-api-endpoints.php */
744
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
745
					false,
746
					$for_output
747
				);
748
			} else {
749
				trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
750
			}
751
		}
752
	}
753
754
	function parse_types( $text ) {
755
		if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
756
			return 'none';
757
		}
758
759
		$types = explode( '|', strtolower( $matches[1] ) );
760
		$return = array();
761
		foreach ( $types as $type ) {
762
			foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
763
				if ( false !== strpos( $type, $operator ) ) {
764
					$item = explode( $operator, $type, 2 );
765
					$return[] = array( 'type' => $item[0], $meaning => $item[1] );
766
					continue 2;
767
				}
768
			}
769
			$return[] = compact( 'type' );
770
		}
771
772
		return $return;
773
	}
774
775
	/**
776
	 * Checks if the endpoint is publicly displayable
777
	 */
778
	function is_publicly_documentable() {
779
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
780
	}
781
782
	/**
783
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
784
	 * Echoes HTML.
785
	 */
786
	function document( $show_description = true ) {
787
		global $wpdb;
788
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
789
		unset( $GLOBALS['post'] );
790
791
		$doc = $this->generate_documentation();
792
793
		if ( $show_description ) :
794
?>
795
<caption>
796
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
797
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
798
</caption>
799
800
<?php endif; ?>
801
802
<?php if ( true === $this->deprecated ) { ?>
803
<p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $this->new_version ); ?></strong></p>
804
<?php } ?>
805
806
<section class="resource-info">
807
	<h2 id="apidoc-resource-info">Resource Information</h2>
808
809
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
810
811
	<thead>
812
		<tr>
813
			<th class="api-index-title" scope="column">&nbsp;</th>
814
			<th class="api-index-title" scope="column">&nbsp;</th>
815
		</tr>
816
	</thead>
817
	<tbody>
818
819
		<tr class="api-index-item">
820
			<th scope="row" class="parameter api-index-item-title">Method</th>
821
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
822
		</tr>
823
824
		<tr class="api-index-item">
825
			<th scope="row" class="parameter api-index-item-title">URL</th>
826
			<?php
827
			$version = WPCOM_JSON_API__CURRENT_VERSION;
828
			if ( !empty( $this->max_version ) ) {
829
				$version = $this->max_version;
830
			}
831
			?>
832
			<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>
833
		</tr>
834
835
		<tr class="api-index-item">
836
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
837
			<?php
838
			$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'] ) );
839
			?>
840
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
841
		</tr>
842
843
	</tbody>
844
	</table>
845
846
</section>
847
848
<?php
849
850
		foreach ( array(
851
			'path'     => 'Method Parameters',
852
			'query'    => 'Query Parameters',
853
			'body'     => 'Request Parameters',
854
			'response' => 'Response Parameters',
855
		) as $doc_section_key => $label ) :
856
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
857
			if ( !$doc_section ) {
858
				continue;
859
			}
860
861
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
862
?>
863
864
<section class="<?php echo $param_label; ?>">
865
866
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
867
868
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
869
870
<thead>
871
	<tr>
872
		<th class="api-index-title" scope="column">Parameter</th>
873
		<th class="api-index-title" scope="column">Type</th>
874
		<th class="api-index-title" scope="column">Description</th>
875
	</tr>
876
</thead>
877
<tbody>
878
879
<?php foreach ( $doc_section as $key => $item ) : ?>
880
881
	<tr class="api-index-item">
882
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
883
		<td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
884
		<td class="description api-index-item-body"><?php
885
886
		$this->generate_doc_description( $item['description'] );
887
888
		?></td>
889
	</tr>
890
891
<?php endforeach; ?>
892
</tbody>
893
</table>
894
</section>
895
<?php endforeach; ?>
896
897
<?php
898
		if ( 'unset' !== $original_post ) {
899
			$GLOBALS['post'] = $original_post;
900
		}
901
	}
902
903
	function add_http_build_query_to_php_content_example( $matches ) {
904
		$trimmed_match = ltrim( $matches[0] );
905
		$pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
906
		$pad = ltrim( $pad, ' ' );
907
		$return = '  ' . str_replace( "\n", "\n  ", $matches[0] );
908
		return " http_build_query({$return}{$pad})";
909
	}
910
911
	/**
912
	 * Recursively generates the <dl>'s to document item descriptions.
913
	 * Echoes HTML.
914
	 */
915
	function generate_doc_description( $item ) {
916
		if ( is_array( $item ) ) : ?>
917
918
		<dl>
919
<?php			foreach ( $item as $description_key => $description_value ) : ?>
920
921
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
922
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
923
924
<?php			endforeach; ?>
925
926
		</dl>
927
928
<?php
929
		else :
930
			echo wp_kses_post( $item );
931
		endif;
932
	}
933
934
	/**
935
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
936
	 * Echoes HTML.
937
	 */
938
	function generate_documentation() {
939
		$format       = str_replace( '%d', '%s', $this->path );
940
		$path_labeled = $format;
941
		if ( ! empty( $this->path_labels ) ) {
942
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
943
		}
944
		$boolean_arg  = array( 'false', 'true' );
945
		$naeloob_arg  = array( 'true', 'false' );
946
947
		$doc = array(
948
			'description'  => $this->description,
949
			'method'       => $this->method,
950
			'path_format'  => $this->path,
951
			'path_labeled' => $path_labeled,
952
			'group'        => $this->group,
953
			'request' => array(
954
				'path'  => array(),
955
				'query' => array(),
956
				'body'  => array(),
957
			),
958
			'response' => array(
959
				'body' => array(),
960
			)
961
		);
962
963
		foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
964
			foreach ( (array) $this->$_property as $key => $description ) {
965
				if ( is_array( $description ) ) {
966
					$description_keys = array_keys( $description );
967
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
968
						$type = '(bool)';
969
					} else {
970
						$type = '(string)';
971
					}
972
973
					if ( 'response_format' !== $_property ) {
974
						// hack - don't show "(default)" in response format
975
						reset( $description );
976
						$description_key = key( $description );
977
						$description[$description_key] = "(default) {$description[$description_key]}";
978
					}
979
				} else {
980
					$types   = $this->parse_types( $description );
981
					$type    = array();
982
					$default = '';
983
984
					if ( 'none' == $types ) {
985
						$types = array();
986
						$types[]['type'] = 'none';
987
					}
988
989
					foreach ( $types as $type_array ) {
990
						$type[] = $type_array['type'];
991
						if ( isset( $type_array['default'] ) ) {
992
							$default = $type_array['default'];
993
							if ( 'string' === $type_array['type'] ) {
994
								$default = "'$default'";
995
							}
996
						}
997
					}
998
					$type = '(' . join( '|', $type ) . ')';
999
					$noop = ''; // skip an index in list below
1000
					list( $noop, $description ) = explode( ')', $description, 2 );
1001
					$description = trim( $description );
1002
					if ( $default ) {
1003
						$description .= " Default: $default.";
1004
					}
1005
				}
1006
1007
				$item = compact( 'type', 'description' );
1008
1009
				if ( 'response_format' === $_property ) {
1010
					$doc['response'][$doc_item][$key] = $item;
1011
				} else {
1012
					$doc['request'][$doc_item][$key] = $item;
1013
				}
1014
			}
1015
		}
1016
1017
		return $doc;
1018
	}
1019
1020
	function user_can_view_post( $post_id ) {
1021
		$post = get_post( $post_id );
1022
		if ( !$post || is_wp_error( $post ) ) {
1023
			return false;
1024
		}
1025
1026 View Code Duplication
		if ( 'inherit' === $post->post_status ) {
1027
			$parent_post = get_post( $post->post_parent );
1028
			$post_status_obj = get_post_status_object( $parent_post->post_status );
1029
		} else {
1030
			$post_status_obj = get_post_status_object( $post->post_status );
1031
		}
1032
1033
		if ( !$post_status_obj->public ) {
1034
			if ( is_user_logged_in() ) {
1035
				if ( $post_status_obj->protected ) {
1036
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1037
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1038
					}
1039
				} elseif ( $post_status_obj->private ) {
1040
					if ( !current_user_can( 'read_post', $post->ID ) ) {
1041
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1042
					}
1043
				} elseif ( in_array( $post->post_status, array( 'inherit', 'trash' ) ) ) {
1044
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
1045
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1046
					}
1047
				} elseif ( 'auto-draft' === $post->post_status ) {
1048
					//allow auto-drafts
1049
				} else {
1050
					return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1051
				}
1052
			} else {
1053
				return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1054
			}
1055
		}
1056
1057 View Code Duplication
		if (
1058
			-1 == get_option( 'blog_public' ) &&
1059
			/**
1060
			 * Filter access to a specific post.
1061
			 *
1062
			 * @module json-api
1063
			 *
1064
			 * @since 3.4.0
1065
			 *
1066
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1067
			 * @param WP_Post $post Post data.
1068
			 */
1069
			! apply_filters(
1070
				'wpcom_json_api_user_can_view_post',
1071
				current_user_can( 'read_post', $post->ID ),
1072
				$post
1073
			)
1074
		) {
1075
			return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
1076
		}
1077
1078 View Code Duplication
		if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
1079
			return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
1080
		}
1081
1082
		return true;
1083
	}
1084
1085
	/**
1086
	 * Returns author object.
1087
	 *
1088
	 * @param object $author user ID, user row, WP_User object, comment row, post row
1089
	 * @param bool $show_email_and_ip output the author's email address and IP address?
1090
	 *
1091
	 * @return object
1092
	 */
1093
	function get_author( $author, $show_email_and_ip = false ) {
1094
		$ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1095
1096
		if ( isset( $author->comment_author_email ) ) {
1097
			$ID          = 0;
1098
			$login       = '';
1099
			$email       = $author->comment_author_email;
1100
			$name        = $author->comment_author;
1101
			$first_name  = '';
1102
			$last_name   = '';
1103
			$URL         = $author->comment_author_url;
1104
			$avatar_URL  = $this->api->get_avatar_url( $author );
1105
			$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1106
			$nice        = '';
1107
			$site_id     = -1;
1108
1109
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1110
			// "&" is the only email/URL character altered by wp_kses()
1111
			foreach ( array( 'email', 'URL' ) as $field ) {
1112
				$$field = str_replace( '&amp;', '&', $$field );
1113
			}
1114
		} else {
1115
			if ( isset( $author->user_id ) && $author->user_id ) {
1116
				$author = $author->user_id;
1117
			} elseif ( isset( $author->user_email ) ) {
1118
				$author = $author->ID;
1119
			} elseif ( isset( $author->post_author ) ) {
1120
				// then $author is a Post Object.
1121
				if ( 0 == $author->post_author )
1122
					return null;
1123
				/**
1124
				 * Filter whether the current site is a Jetpack site.
1125
				 *
1126
				 * @module json-api
1127
				 *
1128
				 * @since 3.3.0
1129
				 *
1130
				 * @param bool false Is the current site a Jetpack site. Default to false.
1131
				 * @param int get_current_blog_id() Blog ID.
1132
				 */
1133
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1134
				$post_id = $author->ID;
1135
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1136
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1137
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1138
					$login      = '';
1139
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1140
					$first_name = '';
1141
					$last_name  = '';
1142
					$URL        = '';
1143
					$nice       = '';
1144
				} else {
1145
					$author = $author->post_author;
1146
				}
1147
			}
1148
1149
			if ( ! isset( $ID ) ) {
1150
				$user = get_user_by( 'id', $author );
1151
				if ( ! $user || is_wp_error( $user ) ) {
1152
					trigger_error( 'Unknown user', E_USER_WARNING );
1153
1154
					return null;
1155
				}
1156
				$ID         = $user->ID;
1157
				$email      = $user->user_email;
1158
				$login      = $user->user_login;
1159
				$name       = $user->display_name;
1160
				$first_name = $user->first_name;
1161
				$last_name  = $user->last_name;
1162
				$URL        = $user->user_url;
1163
				$nice       = $user->user_nicename;
1164
			}
1165
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1166
				$active_blog = get_active_blog_for_user( $ID );
1167
				$site_id     = $active_blog->blog_id;
1168
				$profile_URL = "https://en.gravatar.com/{$login}";
1169
			} else {
1170
				$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1171
				$site_id     = -1;
1172
			}
1173
1174
			$avatar_URL = $this->api->get_avatar_url( $email );
1175
		}
1176
1177
		if ( $show_email_and_ip ) {
1178
			$email = (string) $email;
1179
			$ip_address = (string) $ip_address;
1180
		} else {
1181
			$email = false;
1182
			$ip_address = false;
1183
		}
1184
1185
		$author = array(
1186
			'ID'          => (int) $ID,
1187
			'login'       => (string) $login,
1188
			'email'       => $email, // (string|bool)
1189
			'name'        => (string) $name,
1190
			'first_name'  => (string) $first_name,
1191
			'last_name'   => (string) $last_name,
1192
			'nice_name'   => (string) $nice,
1193
			'URL'         => (string) esc_url_raw( $URL ),
1194
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1195
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1196
			'ip_address'  => $ip_address, // (string|bool)
1197
		);
1198
1199
		if ($site_id > -1) {
1200
			$author['site_ID'] = (int) $site_id;
1201
		}
1202
1203
		return (object) $author;
1204
	}
1205
1206
	function get_media_item( $media_id ) {
1207
		$media_item = get_post( $media_id );
1208
1209
		if ( !$media_item || is_wp_error( $media_item ) )
1210
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1211
1212
		$response = array(
1213
			'id'    => strval( $media_item->ID ),
1214
			'date' =>  (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1215
			'parent'           => $media_item->post_parent,
1216
			'link'             => wp_get_attachment_url( $media_item->ID ),
1217
			'title'            => $media_item->post_title,
1218
			'caption'          => $media_item->post_excerpt,
1219
			'description'      => $media_item->post_content,
1220
			'metadata'         => wp_get_attachment_metadata( $media_item->ID ),
1221
		);
1222
1223
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1224
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1225
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1226
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1227
		}
1228
1229
		$response['meta'] = (object) array(
1230
			'links' => (object) array(
1231
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1232
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1233
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1234
			),
1235
		);
1236
1237
		return (object) $response;
1238
	}
1239
1240
	function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1241
1242
		if ( ! $media_item ) {
1243
			$media_item = get_post( $media_id );
1244
		}
1245
1246
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1247
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1248
		}
1249
1250
		$attachment_file = get_attached_file( $media_item->ID );
1251
1252
		$file = basename( $attachment_file ? $attachment_file : $file );
1253
		$file_info = pathinfo( $file );
1254
		$ext  = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1255
1256
		$response = array(
1257
			'ID'           => $media_item->ID,
1258
			'URL'          => wp_get_attachment_url( $media_item->ID ),
1259
			'guid'         => $media_item->guid,
1260
			'date'         => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1261
			'post_ID'      => $media_item->post_parent,
1262
			'author_ID'    => (int) $media_item->post_author,
1263
			'file'         => $file,
1264
			'mime_type'    => $media_item->post_mime_type,
1265
			'extension'    => $ext,
1266
			'title'        => $media_item->post_title,
1267
			'caption'      => $media_item->post_excerpt,
1268
			'description'  => $media_item->post_content,
1269
			'alt'          => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1270
			'icon'         => wp_mime_type_icon( $media_item->ID ),
1271
			'thumbnails'   => array()
1272
		);
1273
1274 View Code Duplication
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1275
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1276
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1277
				$response['height'] = $metadata['height'];
1278
				$response['width'] = $metadata['width'];
1279
			}
1280
1281
			if ( isset( $metadata['sizes'] ) ) {
1282
				/**
1283
				 * Filter the thumbnail sizes available for each attachment ID.
1284
				 *
1285
				 * @module json-api
1286
				 *
1287
				 * @since 3.9.0
1288
				 *
1289
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1290
				 * @param string $media_id Attachment ID.
1291
				 */
1292
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
1293
				if ( is_array( $sizes ) ) {
1294
					foreach ( $sizes as $size => $size_details ) {
1295
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1296
					}
1297
				}
1298
			}
1299
1300
			if ( isset( $metadata['image_meta'] ) ) {
1301
				$response['exif'] = $metadata['image_meta'];
1302
			}
1303
		}
1304
1305
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1306
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1307
			$response['length'] = $metadata['length'];
1308
			$response['exif']   = $metadata;
1309
		}
1310
1311
		$is_video = false;
1312
1313
		if (
1314
			in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1315
			||
1316
			$response['mime_type'] === 'video/videopress'
1317
		) {
1318
			$is_video = true;
1319
		}
1320
1321
1322
		if ( $is_video ) {
1323
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1324
1325
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1326
				$response['height'] = $metadata['height'];
1327
				$response['width']  = $metadata['width'];
1328
			}
1329
1330
			if ( isset( $metadata['length'] ) ) {
1331
				$response['length'] = $metadata['length'];
1332
			}
1333
1334
			// add VideoPress info
1335
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1336
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1337
1338
				// If we failed to get VideoPress info, but it exists in the meta data (for some reason)
1339
				// then let's use that.
1340
				if ( false === $info && isset( $metadata['videopress'] ) ) {
1341
				    $info = (object) $metadata['videopress'];
1342
				}
1343
1344
				// Thumbnails
1345
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1346
					$response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' );
1347
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1348
						if ( video_format_done( $info, $size ) ) {
1349
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1350
						} else {
1351
							unset( $response['thumbnails'][ $size ] );
1352
						}
1353
					}
1354
				}
1355
1356
				// If we didn't get VideoPress information (for some reason) then let's
1357
				// not try and include it in the response.
1358
				if ( isset( $info->guid ) ) {
1359
					$response['videopress_guid']            = $info->guid;
1360
					$response['videopress_processing_done'] = true;
1361
					if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1362
						$response['videopress_processing_done'] = false;
1363
					}
1364
				}
1365
			}
1366
		}
1367
1368
		$response['thumbnails'] = (object) $response['thumbnails'];
1369
1370
		$response['meta'] = (object) array(
1371
			'links' => (object) array(
1372
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1373
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1374
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1375
			),
1376
		);
1377
1378
		// add VideoPress link to the meta
1379
		if ( isset ( $response['videopress_guid'] ) ) {
1380
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1381
				$response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1382
			}
1383
		}
1384
1385
		if ( $media_item->post_parent > 0 ) {
1386
			$response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1387
		}
1388
1389
		return (object) $response;
1390
	}
1391
1392
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1393
1394
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1395
		/// keep updating this function
1396
		if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1397
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1398
		}
1399
1400
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1401
	}
1402
1403
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1404
		// Permissions
1405
		switch ( $context ) {
1406
		case 'edit' :
1407
			$tax = get_taxonomy( $taxonomy_type );
1408
			if ( !current_user_can( $tax->cap->edit_terms ) )
1409
				return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1410
			break;
1411
		case 'display' :
1412
			if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1413
				return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1414
			}
1415
			break;
1416
		default :
1417
			return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1418
		}
1419
1420
		$response                = array();
1421
		$response['ID']          = (int) $taxonomy->term_id;
1422
		$response['name']        = (string) $taxonomy->name;
1423
		$response['slug']        = (string) $taxonomy->slug;
1424
		$response['description'] = (string) $taxonomy->description;
1425
		$response['post_count']  = (int) $taxonomy->count;
1426
1427
		if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1428
			$response['parent'] = (int) $taxonomy->parent;
1429
		}
1430
1431
		$response['meta'] = (object) array(
1432
			'links' => (object) array(
1433
				'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1434
				'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1435
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1436
			),
1437
		);
1438
1439
		return (object) $response;
1440
	}
1441
1442
	/**
1443
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1444
	 *
1445
	 * @param $date_gmt (string) GMT datetime string.
1446
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1447
	 *
1448
	 * @return string
1449
	 */
1450
	function format_date( $date_gmt, $date = null ) {
1451
		return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1452
	}
1453
1454
	/**
1455
	 * Parses a date string and returns the local and GMT representations
1456
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1457
	 * timezones or offsets. If the parsed datetime was not localized to a
1458
	 * particular timezone or offset we will assume it was given in GMT
1459
	 * relative to now and will convert it to local time using either the
1460
	 * timezone set in the options table for the blog or the GMT offset.
1461
	 *
1462
	 * @param datetime string
1463
	 *
1464
	 * @return array( $local_time_string, $gmt_time_string )
1465
	 */
1466
	function parse_date( $date_string ) {
1467
		$date_string_info = date_parse( $date_string );
1468
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1469
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1470
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1471
				$dt_local = clone $dt_utc = new DateTime( $date_string );
1472
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1473
				return array(
1474
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1475
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1476
				);
1477
			}
1478
1479
			// It's parseable but no TZ info so assume UTC
1480
			$dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1481
		} else {
1482
			// Could not parse time, use now in UTC
1483
			$dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1484
		}
1485
1486
		// First try to use timezone as it's daylight savings aware.
1487
		$timezone_string = get_option( 'timezone_string' );
1488
		if ( $timezone_string ) {
1489
			$tz = timezone_open( $timezone_string );
1490
			if ( $tz ) {
1491
				$dt_local->setTimezone( $tz );
1492
				return array(
1493
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1494
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1495
				);
1496
			}
1497
		}
1498
1499
		// Fallback to GMT offset (in hours)
1500
		// NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1501
		$gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1502
		$dt_local->modify("+{$gmt_offset_seconds} seconds");
1503
		return array(
1504
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1505
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1506
		);
1507
	}
1508
1509
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1510
	function load_theme_functions() {
1511
		// bail if we've done this already (can happen when calling /batch endpoint)
1512
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) )
1513
			return;
1514
1515
		// VIP context loading is handled elsewhere, so bail to prevent
1516
		// duplicate loading. See `switch_to_blog_and_validate_user()`
1517
		if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1518
			return;
1519
		}
1520
1521
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1522
1523
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1524
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1525
1526
		$copy_dirs = array( get_template_directory() );
1527
1528
		// Is this a child theme? Load the child theme's functions file.
1529
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1530
			foreach ( $function_files as $function_file ) {
1531
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1532
					require_once(  get_stylesheet_directory() . $function_file );
1533
				}
1534
			}
1535
			$copy_dirs[] = get_stylesheet_directory();
1536
		}
1537
1538
		foreach ( $function_files as $function_file ) {
1539
			if ( file_exists( get_template_directory() . $function_file ) ) {
1540
				require_once(  get_template_directory() . $function_file );
1541
			}
1542
		}
1543
1544
		// add inc/wpcom.php and/or includes/wpcom.php
1545
		wpcom_load_theme_compat_file();
1546
1547
		// Enable including additional directories or files in actions to be copied
1548
		$copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1549
1550
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1551
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1552
1553
		/**
1554
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1555
		 *
1556
		 * The REST API does not load the theme when processing requests.
1557
		 * To enable theme-based functionality, the API will load the '/functions.php',
1558
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1559
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1560
		 *
1561
		 * @module json-api
1562
		 *
1563
		 * @since 3.2.0
1564
		 */
1565
		do_action( 'restapi_theme_after_setup_theme' );
1566
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1567
1568
		/**
1569
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1570
		 *
1571
		 * The REST API does not load the theme when processing requests.
1572
		 * To enable theme-based functionality, the API will load the '/functions.php',
1573
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1574
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1575
		 *
1576
		 * @module json-api
1577
		 *
1578
		 * @since 3.2.0
1579
		 */
1580
		do_action( 'restapi_theme_init' );
1581
	}
1582
1583
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1584
		global $wp_filter;
1585
		foreach ( $wp_filter as $hook => $actions ) {
1586
1587
			if ( $from_hook != $hook ) {
1588
				continue;
1589
			}
1590
			if ( ! has_action( $hook ) ) {
1591
				continue;
1592
			}
1593
1594
			foreach ( $actions as $priority => $callbacks ) {
1595
				foreach( $callbacks as $callback_key => $callback_data ) {
1596
					$callback = $callback_data['function'];
1597
1598
					// use reflection api to determine filename where function is defined
1599
					$reflection = $this->get_reflection( $callback );
1600
1601
					if ( false !== $reflection ) {
1602
						$file_name = $reflection->getFileName();
1603
						foreach( $base_paths as $base_path ) {
1604
1605
							// only copy hooks with functions which are part of the specified files
1606
							if ( 0 === strpos( $file_name, $base_path ) ) {
1607
								add_action(
1608
									$to_hook,
1609
									$callback_data['function'],
1610
									$priority,
1611
									$callback_data['accepted_args']
1612
								);
1613
							}
1614
						}
1615
					}
1616
				}
1617
			}
1618
		}
1619
	}
1620
1621
	function get_reflection( $callback ) {
1622
		if ( is_array( $callback ) ) {
1623
			list( $class, $method ) = $callback;
1624
			return new ReflectionMethod( $class, $method );
1625
		}
1626
1627
		if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) {
1628
			list( $class, $method ) = explode( "::", $callback );
1629
			return new ReflectionMethod( $class, $method );
1630
		}
1631
1632
		if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) {
1633
			return new ReflectionMethod( $callback, "__invoke" );
1634
		}
1635
1636
		if ( is_string( $callback ) && strpos( $callback, "::" ) == false && function_exists( $callback ) ) {
1637
			return new ReflectionFunction( $callback );
1638
		}
1639
1640
		return false;
1641
	}
1642
1643
	/**
1644
	* Check whether a user can view or edit a post type
1645
	* @param string $post_type              post type to check
1646
	* @param string $context                'display' or 'edit'
1647
	* @return bool
1648
	*/
1649 View Code Duplication
	function current_user_can_access_post_type( $post_type, $context='display' ) {
1650
		$post_type_object = get_post_type_object( $post_type );
1651
		if ( ! $post_type_object ) {
1652
			return false;
1653
		}
1654
1655
		switch( $context ) {
1656
			case 'edit':
1657
				return current_user_can( $post_type_object->cap->edit_posts );
1658
			case 'display':
1659
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1660
			default:
1661
				return false;
1662
		}
1663
	}
1664
1665
	function is_post_type_allowed( $post_type ) {
1666
		// if the post type is empty, that's fine, WordPress will default to post
1667
		if ( empty( $post_type ) ) {
1668
			return true;
1669
		}
1670
1671
		// allow special 'any' type
1672
		if ( 'any' == $post_type ) {
1673
			return true;
1674
		}
1675
1676
		// check for allowed types
1677
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1678
			return true;
1679
		}
1680
1681
		if ( $post_type_object = get_post_type_object( $post_type ) ) {
1682
			if ( ! empty( $post_type_object->show_in_rest ) ) {
1683
				return $post_type_object->show_in_rest;
1684
			}
1685
			if ( ! empty( $post_type_object->publicly_queryable ) ) {
1686
				return $post_type_object->publicly_queryable;
1687
			}
1688
		}
1689
1690
		return ! empty( $post_type_object->public );
1691
	}
1692
1693
	/**
1694
	 * Gets the whitelisted post types that JP should allow access to.
1695
	 *
1696
	 * @return array Whitelisted post types.
1697
	 */
1698 View Code Duplication
	protected function _get_whitelisted_post_types() {
1699
		$allowed_types = array( 'post', 'page', 'revision' );
1700
1701
		/**
1702
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1703
		 *
1704
		 * @module json-api
1705
		 *
1706
		 * @since 2.2.3
1707
		 *
1708
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1709
		 */
1710
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1711
1712
		return array_unique( $allowed_types );
1713
	}
1714
1715
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1716
1717
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1718
1719
		$media_ids = $errors = array();
1720
		$user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
1721
		$media_attrs = array_values( $media_attrs ); // reset the keys
1722
		$i = 0;
1723
1724
		if ( ! empty( $media_files ) ) {
1725
			$this->api->trap_wp_die( 'upload_error' );
1726
			foreach ( $media_files as $media_item ) {
1727
				$_FILES['.api.media.item.'] = $media_item;
1728 View Code Duplication
				if ( ! $user_can_upload_files ) {
1729
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1730
				} else {
1731
					if ( $force_parent_id ) {
1732
						$parent_id = absint( $force_parent_id );
1733
					} elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1734
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1735
					} else {
1736
						$parent_id = 0;
1737
					}
1738
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1739
				}
1740
				if ( is_wp_error( $media_id ) ) {
1741
					$errors[$i]['file']   = $media_item['name'];
1742
					$errors[$i]['error']   = $media_id->get_error_code();
1743
					$errors[$i]['message'] = $media_id->get_error_message();
1744
				} else {
1745
					$media_ids[$i] = $media_id;
1746
				}
1747
1748
				$i++;
1749
			}
1750
			$this->api->trap_wp_die( null );
1751
			unset( $_FILES['.api.media.item.'] );
1752
		}
1753
1754
		if ( ! empty( $media_urls ) ) {
1755
			foreach ( $media_urls as $url ) {
1756 View Code Duplication
				if ( ! $user_can_upload_files ) {
1757
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1758
				} else {
1759
					if ( $force_parent_id ) {
1760
						$parent_id = absint( $force_parent_id );
1761
					} else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1762
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1763
					} else {
1764
						$parent_id = 0;
1765
					}
1766
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1767
				}
1768
				if ( is_wp_error( $media_id ) ) {
1769
					$errors[$i] = array(
1770
						'file'    => $url,
1771
						'error'   => $media_id->get_error_code(),
1772
						'message' => $media_id->get_error_message(),
1773
					);
1774
				} elseif ( ! empty( $media_id ) ) {
1775
					$media_ids[$i] = $media_id;
1776
				}
1777
1778
				$i++;
1779
			}
1780
		}
1781
1782
		if ( ! empty( $media_attrs ) ) {
1783
			foreach ( $media_ids as $index => $media_id ) {
1784
				if ( empty( $media_attrs[$index] ) )
1785
					continue;
1786
1787
				$attrs = $media_attrs[$index];
1788
				$insert = array();
1789
1790
				// Attributes: Title, Caption, Description
1791
1792
				if ( isset( $attrs['title'] ) ) {
1793
					$insert['post_title'] = $attrs['title'];
1794
				}
1795
1796
				if ( isset( $attrs['caption'] ) ) {
1797
					$insert['post_excerpt'] = $attrs['caption'];
1798
				}
1799
1800
				if ( isset( $attrs['description'] ) ) {
1801
					$insert['post_content'] = $attrs['description'];
1802
				}
1803
1804
				if ( ! empty( $insert ) ) {
1805
					$insert['ID'] = $media_id;
1806
					wp_update_post( (object) $insert );
1807
				}
1808
1809
				// Attributes: Alt
1810
1811 View Code Duplication
				if ( isset( $attrs['alt'] ) ) {
1812
					$alt = wp_strip_all_tags( $attrs['alt'], true );
1813
					update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1814
				}
1815
1816
				// Attributes: Artist, Album
1817
1818
				$id3_meta = array();
1819
1820 View Code Duplication
				foreach ( array( 'artist', 'album' ) as $key ) {
1821
					if ( isset( $attrs[ $key ] ) ) {
1822
						$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1823
					}
1824
				}
1825
1826 View Code Duplication
				if ( ! empty( $id3_meta ) ) {
1827
					// Before updating metadata, ensure that the item is audio
1828
					$item = $this->get_media_item_v1_1( $media_id );
1829
					if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1830
						wp_update_attachment_metadata( $media_id, $id3_meta );
1831
					}
1832
				}
1833
			}
1834
		}
1835
1836
		return array( 'media_ids' => $media_ids, 'errors' => $errors );
1837
1838
	}
1839
1840
	function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1841
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1842
			return false;
1843
1844
		// if we didn't get a URL, let's bail
1845
		$parsed = @parse_url( $url );
1846
		if ( empty( $parsed ) )
1847
			return false;
1848
1849
		$tmp = download_url( $url );
1850
		if ( is_wp_error( $tmp ) ) {
1851
			return $tmp;
1852
		}
1853
1854
		// First check to see if we get a mime-type match by file, otherwise, check to
1855
		// see if WordPress supports this file as an image. If neither, then it is not supported.
1856
		if ( ! $this->is_file_supported_for_sideloading( $tmp ) && 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1857
			@unlink( $tmp );
1858
			return false;
1859
		}
1860
1861
		// emulate a $_FILES entry
1862
		$file_array = array(
1863
			'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1864
			'tmp_name' => $tmp,
1865
		);
1866
1867
		$id = media_handle_sideload( $file_array, $parent_post_id );
1868
		if ( file_exists( $tmp ) ) {
1869
			@unlink( $tmp );
1870
		}
1871
1872
		if ( is_wp_error( $id ) ) {
1873
			return $id;
1874
		}
1875
1876
		if ( ! $id || ! is_int( $id ) ) {
1877
			return false;
1878
		}
1879
1880
		return $id;
1881
	}
1882
1883
	/**
1884
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1885
	 *
1886
	 * @param string $file Path to file to get its mime type.
1887
	 *
1888
	 * @return bool
1889
	 */
1890 View Code Duplication
	protected function is_file_supported_for_sideloading( $file ) {
1891
		if ( class_exists( 'finfo' ) ) { // php 5.3+
1892
			$finfo = new finfo( FILEINFO_MIME );
1893
			$mime = explode( '; ', $finfo->file( $file ) );
1894
			$type = $mime[0];
1895
1896
		} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
1897
			$type = mime_content_type( $file );
1898
1899
		} else {
1900
			return false;
1901
		}
1902
1903
		/**
1904
		 * Filter the list of supported mime types for media sideloading.
1905
		 *
1906
		 * @since 4.0.0
1907
		 *
1908
		 * @module json-api
1909
		 *
1910
		 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
1911
		 */
1912
		$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
1913
			'image/png',
1914
			'image/jpeg',
1915
			'image/gif',
1916
			'image/bmp',
1917
			'video/quicktime',
1918
			'video/mp4',
1919
			'video/mpeg',
1920
			'video/ogg',
1921
			'video/3gpp',
1922
			'video/3gpp2',
1923
			'video/h261',
1924
			'video/h262',
1925
			'video/h264',
1926
			'video/x-msvideo',
1927
			'video/x-ms-wmv',
1928
			'video/x-ms-asf',
1929
		) );
1930
1931
		// If the type returned was not an array as expected, then we know we don't have a match.
1932
		if ( ! is_array( $supported_mime_types ) ) {
1933
			return false;
1934
		}
1935
1936
		return in_array( $type, $supported_mime_types );
1937
	}
1938
1939
	function allow_video_uploads( $mimes ) {
1940
		// if we are on Jetpack, bail - Videos are already allowed
1941
		if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1942
			return $mimes;
1943
		}
1944
1945
		// extra check that this filter is only ever applied during REST API requests
1946
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1947
			return $mimes;
1948
		}
1949
1950
		// bail early if they already have the upgrade..
1951
		if ( get_option( 'video_upgrade' ) == '1' ) {
1952
			return $mimes;
1953
		}
1954
1955
		// lets whitelist to only specific clients right now
1956
		$clients_allowed_video_uploads = array();
1957
		/**
1958
		 * Filter the list of whitelisted video clients.
1959
		 *
1960
		 * @module json-api
1961
		 *
1962
		 * @since 3.2.0
1963
		 *
1964
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1965
		 */
1966
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1967
		if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1968
			return $mimes;
1969
		}
1970
1971
		$mime_list = wp_get_mime_types();
1972
1973
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1974
		/**
1975
		 * Filter the video filetypes allowed on the site.
1976
		 *
1977
		 * @module json-api
1978
		 *
1979
		 * @since 3.2.0
1980
		 *
1981
		 * @param array $video_exts Array of video filetypes allowed on the site.
1982
		 */
1983
		$video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
1984
		$video_mimes = array();
1985
1986
		if ( !empty( $video_exts ) ) {
1987
			foreach ( $video_exts as $ext ) {
1988
				foreach ( $mime_list as $ext_pattern => $mime ) {
1989
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
1990
						$video_mimes[$ext_pattern] = $mime;
1991
				}
1992
			}
1993
1994
			$mimes = array_merge( $mimes, $video_mimes );
1995
		}
1996
1997
		return $mimes;
1998
	}
1999
2000
	function is_current_site_multi_user() {
2001
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2002
		if ( false === $users ) {
2003
			$user_query = new WP_User_Query( array(
2004
				'blog_id' => get_current_blog_id(),
2005
				'fields'  => 'ID',
2006
			) );
2007
			$users = (int) $user_query->get_total();
2008
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2009
		}
2010
		return $users > 1;
2011
	}
2012
2013
	function allows_cross_origin_requests() {
2014
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2015
	}
2016
2017
	function allows_unauthorized_requests( $origin, $complete_access_origins  ) {
2018
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2019
	}
2020
2021
	function get_platform() {
2022
		return wpcom_get_sal_platform( $this->api->token_details );
2023
	}
2024
2025
	/**
2026
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2027
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
2028
	 *
2029
	 * Override this method if you want to do something different.
2030
	 *
2031
	 * @param  int  $blog_id
2032
	 * @return bool
2033
	 */
2034
	function force_wpcom_request( $blog_id ) {
2035
		return false;
2036
	}
2037
2038
	/**
2039
	 * Return endpoint response
2040
	 *
2041
	 * @param ... determined by ->$path
2042
	 *
2043
	 * @return
2044
	 * 	falsy: HTTP 500, no response body
2045
	 *	WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2046
	 *	$data: HTTP 200, json_encode( $data ) response body
2047
	 */
2048
	abstract function callback( $path = '' );
2049
2050
2051
}
2052
2053
require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
2054