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