Completed
Push — content-options-blog-display-7... ( 930de1 )
by
unknown
12:25
created

class.json-api-endpoints.php (3 issues)

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
	function __construct( $args ) {
121
		$defaults = array(
122
			'in_testing'           => false,
123
			'allowed_if_flagged'   => false,
124
			'allowed_if_red_flagged' => false,
125
			'description'          => '',
126
			'group'	               => '',
127
			'method'               => 'GET',
128
			'path'                 => '/',
129
			'min_version'          => '0',
130
			'max_version'          => WPCOM_JSON_API__CURRENT_VERSION,
131
			'force'	               => '',
132
			'deprecated'           => false,
133
			'new_version'          => WPCOM_JSON_API__CURRENT_VERSION,
134
			'jp_disabled'          => false,
135
			'path_labels'          => array(),
136
			'request_format'       => array(),
137
			'response_format'      => array(),
138
			'query_parameters'     => array(),
139
			'version'              => 'v1',
140
			'example_request'      => '',
141
			'example_request_data' => '',
142
			'example_response'     => '',
143
			'required_scope'       => '',
144
			'pass_wpcom_user_details' => false,
145
			'custom_fields_filtering' => false,
146
			'allow_cross_origin_request' => false,
147
			'allow_unauthorized_request' => false,
148
			'allow_jetpack_site_auth'    => false,
149
		);
150
151
		$args = wp_parse_args( $args, $defaults );
152
153
		$this->in_testing  = $args['in_testing'];
154
155
		$this->allowed_if_flagged = $args['allowed_if_flagged'];
156
		$this->allowed_if_red_flagged = $args['allowed_if_red_flagged'];
157
158
		$this->description = $args['description'];
159
		$this->group       = $args['group'];
160
		$this->stat        = $args['stat'];
161
		$this->force	   = $args['force'];
162
		$this->jp_disabled = $args['jp_disabled'];
163
164
		$this->method      = $args['method'];
165
		$this->path        = $args['path'];
166
		$this->path_labels = $args['path_labels'];
167
		$this->min_version = $args['min_version'];
168
		$this->max_version = $args['max_version'];
169
		$this->deprecated  = $args['deprecated'];
170
		$this->new_version = $args['new_version'];
171
172
		$this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
173
		$this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
174
175
		$this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
176
		$this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
177
		$this->allow_jetpack_site_auth    = (bool) $args['allow_jetpack_site_auth'];
178
179
		$this->version     = $args['version'];
180
181
		$this->required_scope = $args['required_scope'];
182
183 View Code Duplication
		if ( $this->request_format ) {
184
			$this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
185
		} else {
186
			$this->request_format = $args['request_format'];
187
		}
188
189 View Code Duplication
		if ( $this->response_format ) {
190
			$this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
191
		} else {
192
			$this->response_format = $args['response_format'];
193
		}
194
195
		if ( false === $args['query_parameters'] ) {
196
			$this->query = array();
197
		} elseif ( is_array( $args['query_parameters'] ) ) {
198
			$this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
199
		}
200
201
		$this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
202
		$this->links = WPCOM_JSON_API_Links::getInstance();
203
204
		/** Example Request/Response ******************************************/
205
206
		// Examples for endpoint documentation request
207
		$this->example_request      = $args['example_request'];
208
		$this->example_request_data = $args['example_request_data'];
209
		$this->example_response     = $args['example_response'];
210
211
		$this->api->add( $this );
212
	}
213
214
	// Get all query args.  Prefill with defaults
215
	function query_args( $return_default_values = true, $cast_and_filter = true ) {
216
		$args = array_intersect_key( $this->api->query, $this->query );
217
218
		if ( !$cast_and_filter ) {
219
			return $args;
220
		}
221
222
		return $this->cast_and_filter( $args, $this->query, $return_default_values );
223
	}
224
225
	// Get POST body data
226
	function input( $return_default_values = true, $cast_and_filter = true ) {
227
		$input = trim( $this->api->post_body );
228
		$content_type = $this->api->content_type;
229
		if ( $content_type ) {
230
			list ( $content_type ) = explode( ';', $content_type );
231
		}
232
		$content_type = trim( $content_type );
233
		switch ( $content_type ) {
234
		case 'application/json' :
235
		case 'application/x-javascript' :
236
		case 'text/javascript' :
237
		case 'text/x-javascript' :
238
		case 'text/x-json' :
239
		case 'text/json' :
240
			$return = json_decode( $input, true );
241
242
			if ( function_exists( 'json_last_error' ) ) {
243
				if ( JSON_ERROR_NONE !== json_last_error() ) {
244
					return null;
245
				}
246
			} else {
247
				if ( is_null( $return ) && json_encode( null ) !== $input ) {
248
					return null;
249
				}
250
			}
251
252
			break;
253
		case 'multipart/form-data' :
254
			$return = array_merge( stripslashes_deep( $_POST ), $_FILES );
255
			break;
256
		case 'application/x-www-form-urlencoded' :
257
			//attempt JSON first, since probably a curl command
258
			$return = json_decode( $input, true );
259
260
			if ( is_null( $return ) ) {
261
				wp_parse_str( $input, $return );
262
			}
263
264
			break;
265
		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...
266
			wp_parse_str( $input, $return );
267
			break;
268
		}
269
270
		if ( !$cast_and_filter ) {
271
			return $return;
0 ignored issues
show
The variable $return does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
272
		}
273
274
		return $this->cast_and_filter( $return, $this->request_format, $return_default_values );
275
	}
276
277
	function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
278
		$return_as_object = false;
279
		if ( is_object( $data ) ) {
280
			// @todo this should probably be a deep copy if $data can ever have nested objects
281
			$data = (array) $data;
282
			$return_as_object = true;
283
		} elseif ( !is_array( $data ) ) {
284
			return $data;
285
		}
286
287
		$boolean_arg = array( 'false', 'true' );
288
		$naeloob_arg = array( 'true', 'false' );
289
290
		$return = array();
291
292
		foreach ( $documentation as $key => $description ) {
293
			if ( is_array( $description ) ) {
294
				// String or boolean array keys only
295
				$whitelist = array_keys( $description );
296
297
				if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
298
					// Truthiness
299
					if ( isset( $data[$key] ) ) {
300
						$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $data[$key] );
301
					} elseif ( $return_default_values ) {
302
						$return[$key] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg.
303
					}
304
				} elseif ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) {
305
					// String Key
306
					$return[$key] = (string) $data[$key];
307
				} elseif ( $return_default_values ) {
308
					// Default value
309
					$return[$key] = (string) current( $whitelist );
310
				}
311
312
				continue;
313
			}
314
315
			$types = $this->parse_types( $description );
316
			$type = array_shift( $types );
317
318
			// Explicit default - string and int only for now.  Always set these reguardless of $return_default_values
319
			if ( isset( $type['default'] ) ) {
320
				if ( !isset( $data[$key] ) ) {
321
					$data[$key] = $type['default'];
322
				}
323
			}
324
325
			if ( !isset( $data[$key] ) ) {
326
				continue;
327
			}
328
329
			$this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output );
330
		}
331
332
		if ( $return_as_object ) {
333
			return (object) $return;
334
		}
335
336
		return $return;
337
	}
338
339
	/**
340
	 * Casts $value according to $type.
341
	 * Handles fallbacks for certain values of $type when $value is not that $type
342
	 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way),
343
	 * and string -> object (one way)
344
	 *
345
	 * Handles "child types" - array:URL, object:category
346
	 * array:URL means an array of URLs
347
	 * object:category means a hash of categories
348
	 *
349
	 * Handles object typing - object>post means an object of type post
350
	 */
351
	function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
352
		if ( is_string( $type ) ) {
353
			$type = compact( 'type' );
354
		}
355
356
		switch ( $type['type'] ) {
357
		case 'false' :
358
			$return[$key] = false;
359
			break;
360
		case 'url' :
361
			$return[$key] = (string) esc_url_raw( $value );
362
			break;
363
		case 'string' :
364
			// Fallback string -> array, or for string -> object
365
			if ( is_array( $value ) || is_object( $value ) ) {
366 View Code Duplication
				if ( !empty( $types[0] ) ) {
367
					$next_type = array_shift( $types );
368
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
369
				}
370
			}
371
372
			// Fallback string -> false
373 View Code Duplication
			if ( !is_string( $value ) ) {
374
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
375
					$next_type = array_shift( $types );
376
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
377
				}
378
			}
379
			$return[$key] = (string) $value;
380
			break;
381
		case 'html' :
382
			$return[$key] = (string) $value;
383
			break;
384
		case 'safehtml' :
385
			$return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() );
386
			break;
387
		case 'zip' :
388
		case 'media' :
389
			if ( is_array( $value ) ) {
390
				if ( isset( $value['name'] ) && is_array( $value['name'] ) ) {
391
					// It's a $_FILES array
392
					// Reformat into array of $_FILES items
393
					$files = array();
394
395
					foreach ( $value['name'] as $k => $v ) {
396
						$files[$k] = array();
397
						foreach ( array_keys( $value ) as $file_key ) {
398
							$files[$k][$file_key] = $value[$file_key][$k];
399
						}
400
					}
401
402
					$return[$key] = $files;
403
					break;
404
				}
405
			} else {
406
				// no break - treat as 'array'
407
			}
408
			// nobreak
409
		case 'array' :
410
			// Fallback array -> string
411 View Code Duplication
			if ( is_string( $value ) ) {
412
				if ( !empty( $types[0] ) ) {
413
					$next_type = array_shift( $types );
414
					return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
415
				}
416
			}
417
418 View Code Duplication
			if ( isset( $type['children'] ) ) {
419
				$children = array();
420
				foreach ( (array) $value as $k => $child ) {
421
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
422
				}
423
				$return[$key] = (array) $children;
424
				break;
425
			}
426
427
			$return[$key] = (array) $value;
428
			break;
429
		case 'iso 8601 datetime' :
430
		case 'datetime' :
431
			// (string)s
432
			$dates = $this->parse_date( (string) $value );
433
			if ( $for_output ) {
434
				$return[$key] = $this->format_date( $dates[1], $dates[0] );
435
			} else {
436
				list( $return[$key], $return["{$key}_gmt"] ) = $dates;
437
			}
438
			break;
439
		case 'float' :
440
			$return[$key] = (float) $value;
441
			break;
442
		case 'int' :
443
		case 'integer' :
444
			$return[$key] = (int) $value;
445
			break;
446
		case 'bool' :
447
		case 'boolean' :
448
			$return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value );
449
			break;
450
		case 'object' :
451
			// Fallback object -> false
452 View Code Duplication
			if ( is_scalar( $value ) || is_null( $value ) ) {
453
				if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
454
					return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
455
				}
456
			}
457
458 View Code Duplication
			if ( isset( $type['children'] ) ) {
459
				$children = array();
460
				foreach ( (array) $value as $k => $child ) {
461
					$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
462
				}
463
				$return[$key] = (object) $children;
464
				break;
465
			}
466
467
			if ( isset( $type['subtype'] ) ) {
468
				return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
469
			}
470
471
			$return[$key] = (object) $value;
472
			break;
473
		case 'post' :
474
			$return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
475
			break;
476
		case 'comment' :
477
			$return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
478
			break;
479
		case 'tag' :
480
		case 'category' :
481
			$docs = array(
482
				'ID'          => '(int)',
483
				'name'        => '(string)',
484
				'slug'        => '(string)',
485
				'description' => '(HTML)',
486
				'post_count'  => '(int)',
487
				'meta'        => '(object)',
488
			);
489
			if ( 'category' === $type['type'] ) {
490
				$docs['parent'] = '(int)';
491
			}
492
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
493
			break;
494
		case 'post_reference' :
495 View Code Duplication
		case 'comment_reference' :
496
			$docs = array(
497
				'ID'    => '(int)',
498
				'type'  => '(string)',
499
				'title' => '(string)',
500
				'link'  => '(URL)',
501
			);
502
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
503
			break;
504 View Code Duplication
		case 'geo' :
505
			$docs = array(
506
				'latitude'  => '(float)',
507
				'longitude' => '(float)',
508
				'address'   => '(string)',
509
			);
510
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
511
			break;
512
		case 'author' :
513
			$docs = array(
514
				'ID'             => '(int)',
515
				'user_login'     => '(string)',
516
				'login'          => '(string)',
517
				'email'          => '(string|false)',
518
				'name'           => '(string)',
519
				'first_name'     => '(string)',
520
				'last_name'      => '(string)',
521
				'nice_name'      => '(string)',
522
				'URL'            => '(URL)',
523
				'avatar_URL'     => '(URL)',
524
				'profile_URL'    => '(URL)',
525
				'is_super_admin' => '(bool)',
526
				'roles'          => '(array:string)'
527
			);
528
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
529
			break;
530 View Code Duplication
		case 'role' :
531
			$docs = array(
532
				'name'         => '(string)',
533
				'display_name' => '(string)',
534
				'capabilities' => '(object:boolean)',
535
			);
536
			$return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
537
			break;
538
		case 'attachment' :
539
			$docs = array(
540
				'ID'        => '(int)',
541
				'URL'       => '(URL)',
542
				'guid'      => '(string)',
543
				'mime_type' => '(string)',
544
				'width'     => '(int)',
545
				'height'    => '(int)',
546
				'duration'  => '(int)',
547
			);
548
			$return[$key] = (object) $this->cast_and_filter(
549
				$value,
550
				/**
551
				 * Filter the documentation returned for a post attachment.
552
				 *
553
				 * @module json-api
554
				 *
555
				 * @since 1.9.0
556
				 *
557
				 * @param array $docs Array of documentation about a post attachment.
558
				 */
559
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
560
				false,
561
				$for_output
562
			);
563
			break;
564
		case 'metadata' :
565
			$docs = array(
566
				'id'       => '(int)',
567
				'key'       => '(string)',
568
				'value'     => '(string|false|float|int|array|object)',
569
				'previous_value' => '(string)',
570
				'operation'  => '(string)',
571
			);
572
			$return[$key] = (object) $this->cast_and_filter(
573
				$value,
574
				/** This filter is documented in class.json-api-endpoints.php */
575
				apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
576
				false,
577
				$for_output
578
			);
579
			break;
580
		case 'plugin' :
581
			$docs = array(
582
				'id'          => '(safehtml) The plugin\'s ID',
583
				'slug'        => '(safehtml) The plugin\'s Slug',
584
				'active'      => '(boolean)  The plugin status.',
585
				'update'      => '(object)   The plugin update info.',
586
				'name'        => '(safehtml) The name of the plugin.',
587
				'plugin_url'  => '(url)      Link to the plugin\'s web site.',
588
				'version'     => '(safehtml) The plugin version number.',
589
				'description' => '(safehtml) Description of what the plugin does and/or notes from the author',
590
				'author'      => '(safehtml) The plugin author\'s name',
591
				'author_url'  => '(url)      The plugin author web site address',
592
				'network'     => '(boolean)  Whether the plugin can only be activated network wide.',
593
				'autoupdate'  => '(boolean)  Whether the plugin is auto updated',
594
				'log'         => '(array:safehtml) An array of update log strings.',
595
			);
596
			$return[$key] = (object) $this->cast_and_filter(
597
				$value,
598
				/**
599
				 * Filter the documentation returned for a plugin.
600
				 *
601
				 * @module json-api
602
				 *
603
				 * @since 3.1.0
604
				 *
605
				 * @param array $docs Array of documentation about a plugin.
606
				 */
607
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
608
				false,
609
				$for_output
610
			);
611
			break;
612
		case 'jetpackmodule' :
613
			$docs = array(
614
				'id'          => '(string)   The module\'s ID',
615
				'active'      => '(boolean)  The module\'s status.',
616
				'name'        => '(string)   The module\'s name.',
617
				'description' => '(safehtml) The module\'s description.',
618
				'sort'        => '(int)      The module\'s display order.',
619
				'introduced'  => '(string)   The Jetpack version when the module was introduced.',
620
				'changed'     => '(string)   The Jetpack version when the module was changed.',
621
				'free'        => '(boolean)  The module\'s Free or Paid status.',
622
				'module_tags' => '(array)    The module\'s tags.'
623
			);
624
			$return[$key] = (object) $this->cast_and_filter(
625
				$value,
626
				/** This filter is documented in class.json-api-endpoints.php */
627
				apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
628
				false,
629
				$for_output
630
			);
631
			break;
632
		case 'sharing_button' :
633
			$docs = array(
634
				'ID'         => '(string)',
635
				'name'       => '(string)',
636
				'URL'        => '(string)',
637
				'icon'       => '(string)',
638
				'enabled'    => '(bool)',
639
				'visibility' => '(string)',
640
			);
641
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
642
			break;
643
		case 'sharing_button_service':
644
			$docs = array(
645
				'ID'               => '(string) The service identifier',
646
				'name'             => '(string) The service name',
647
				'class_name'       => '(string) Class name for custom style sharing button elements',
648
				'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
649
				'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
650
				'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview'
651
			);
652
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
653
			break;
654
		case 'taxonomy':
655
			$docs = array(
656
				'name'         => '(string) The taxonomy slug',
657
				'label'        => '(string) The taxonomy human-readable name',
658
				'labels'       => '(object) Mapping of labels for the taxonomy',
659
				'description'  => '(string) The taxonomy description',
660
				'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
661
				'public'       => '(bool) Whether the taxonomy is public',
662
				'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
663
			);
664
			$return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
665
			break;
666
667
		default :
668
			$method_name = $type['type'] . '_docs';
669
			if ( method_exists( WPCOM_JSON_API_Jetpack_Overrides, $method_name ) ) {
670
				$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
671
			}
672
673
			if ( ! empty( $docs ) ) {
674
				$return[$key] = (object) $this->cast_and_filter(
675
					$value,
676
					/** This filter is documented in class.json-api-endpoints.php */
677
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
678
					false,
679
					$for_output
680
				);
681
			} else {
682
				trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
683
			}
684
		}
685
	}
686
687
	function parse_types( $text ) {
688
		if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
689
			return 'none';
690
		}
691
692
		$types = explode( '|', strtolower( $matches[1] ) );
693
		$return = array();
694
		foreach ( $types as $type ) {
695
			foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
696
				if ( false !== strpos( $type, $operator ) ) {
697
					$item = explode( $operator, $type, 2 );
698
					$return[] = array( 'type' => $item[0], $meaning => $item[1] );
699
					continue 2;
700
				}
701
			}
702
			$return[] = compact( 'type' );
703
		}
704
705
		return $return;
706
	}
707
708
	/**
709
	 * Checks if the endpoint is publicly displayable
710
	 */
711
	function is_publicly_documentable() {
712
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
713
	}
714
715
	/**
716
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
717
	 * Echoes HTML.
718
	 */
719
	function document( $show_description = true ) {
720
		global $wpdb;
721
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
722
		unset( $GLOBALS['post'] );
723
724
		$doc = $this->generate_documentation();
725
726
		if ( $show_description ) :
727
?>
728
<caption>
729
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
730
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
731
</caption>
732
733
<?php endif; ?>
734
735
<?php if ( true === $this->deprecated ) { ?>
736
<p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $this->new_version ); ?></strong></p>
737
<?php } ?>
738
739
<section class="resource-info">
740
	<h2 id="apidoc-resource-info">Resource Information</h2>
741
742
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
743
744
	<thead>
745
		<tr>
746
			<th class="api-index-title" scope="column">&nbsp;</th>
747
			<th class="api-index-title" scope="column">&nbsp;</th>
748
		</tr>
749
	</thead>
750
	<tbody>
751
752
		<tr class="api-index-item">
753
			<th scope="row" class="parameter api-index-item-title">Method</th>
754
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
755
		</tr>
756
757
		<tr class="api-index-item">
758
			<th scope="row" class="parameter api-index-item-title">URL</th>
759
			<?php
760
			$version = WPCOM_JSON_API__CURRENT_VERSION;
761
			if ( !empty( $this->max_version ) ) {
762
				$version = $this->max_version;
763
			}
764
			?>
765
			<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>
766
		</tr>
767
768
		<tr class="api-index-item">
769
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
770
			<?php
771
			$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'] ) );
772
			?>
773
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
774
		</tr>
775
776
	</tbody>
777
	</table>
778
779
</section>
780
781
<?php
782
783
		foreach ( array(
784
			'path'     => 'Method Parameters',
785
			'query'    => 'Query Parameters',
786
			'body'     => 'Request Parameters',
787
			'response' => 'Response Parameters',
788
		) as $doc_section_key => $label ) :
789
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
790
			if ( !$doc_section ) {
791
				continue;
792
			}
793
794
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
795
?>
796
797
<section class="<?php echo $param_label; ?>">
798
799
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
800
801
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
802
803
<thead>
804
	<tr>
805
		<th class="api-index-title" scope="column">Parameter</th>
806
		<th class="api-index-title" scope="column">Type</th>
807
		<th class="api-index-title" scope="column">Description</th>
808
	</tr>
809
</thead>
810
<tbody>
811
812
<?php foreach ( $doc_section as $key => $item ) : ?>
813
814
	<tr class="api-index-item">
815
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
816
		<td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
817
		<td class="description api-index-item-body"><?php
818
819
		$this->generate_doc_description( $item['description'] );
820
821
		?></td>
822
	</tr>
823
824
<?php endforeach; ?>
825
</tbody>
826
</table>
827
</section>
828
<?php endforeach; ?>
829
830
<?php
831
		if ( 'unset' !== $original_post ) {
832
			$GLOBALS['post'] = $original_post;
833
		}
834
	}
835
836
	function add_http_build_query_to_php_content_example( $matches ) {
837
		$trimmed_match = ltrim( $matches[0] );
838
		$pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
839
		$pad = ltrim( $pad, ' ' );
840
		$return = '  ' . str_replace( "\n", "\n  ", $matches[0] );
841
		return " http_build_query({$return}{$pad})";
842
	}
843
844
	/**
845
	 * Recursively generates the <dl>'s to document item descriptions.
846
	 * Echoes HTML.
847
	 */
848
	function generate_doc_description( $item ) {
849
		if ( is_array( $item ) ) : ?>
850
851
		<dl>
852
<?php			foreach ( $item as $description_key => $description_value ) : ?>
853
854
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
855
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
856
857
<?php			endforeach; ?>
858
859
		</dl>
860
861
<?php
862
		else :
863
			echo wp_kses_post( $item );
864
		endif;
865
	}
866
867
	/**
868
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
869
	 * Echoes HTML.
870
	 */
871
	function generate_documentation() {
872
		$format       = str_replace( '%d', '%s', $this->path );
873
		$path_labeled = $format;
874
		if ( ! empty( $this->path_labels ) ) {
875
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
876
		}
877
		$boolean_arg  = array( 'false', 'true' );
878
		$naeloob_arg  = array( 'true', 'false' );
879
880
		$doc = array(
881
			'description'  => $this->description,
882
			'method'       => $this->method,
883
			'path_format'  => $this->path,
884
			'path_labeled' => $path_labeled,
885
			'group'        => $this->group,
886
			'request' => array(
887
				'path'  => array(),
888
				'query' => array(),
889
				'body'  => array(),
890
			),
891
			'response' => array(
892
				'body' => array(),
893
			)
894
		);
895
896
		foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
897
			foreach ( (array) $this->$_property as $key => $description ) {
898
				if ( is_array( $description ) ) {
899
					$description_keys = array_keys( $description );
900
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
901
						$type = '(bool)';
902
					} else {
903
						$type = '(string)';
904
					}
905
906
					if ( 'response_format' !== $_property ) {
907
						// hack - don't show "(default)" in response format
908
						reset( $description );
909
						$description_key = key( $description );
910
						$description[$description_key] = "(default) {$description[$description_key]}";
911
					}
912
				} else {
913
					$types   = $this->parse_types( $description );
914
					$type    = array();
915
					$default = '';
916
917
					if ( 'none' == $types ) {
918
						$types = array();
919
						$types[]['type'] = 'none';
920
					}
921
922
					foreach ( $types as $type_array ) {
923
						$type[] = $type_array['type'];
924
						if ( isset( $type_array['default'] ) ) {
925
							$default = $type_array['default'];
926
							if ( 'string' === $type_array['type'] ) {
927
								$default = "'$default'";
928
							}
929
						}
930
					}
931
					$type = '(' . join( '|', $type ) . ')';
932
					$noop = ''; // skip an index in list below
933
					list( $noop, $description ) = explode( ')', $description, 2 );
934
					$description = trim( $description );
935
					if ( $default ) {
936
						$description .= " Default: $default.";
937
					}
938
				}
939
940
				$item = compact( 'type', 'description' );
941
942
				if ( 'response_format' === $_property ) {
943
					$doc['response'][$doc_item][$key] = $item;
944
				} else {
945
					$doc['request'][$doc_item][$key] = $item;
946
				}
947
			}
948
		}
949
950
		return $doc;
951
	}
952
953
	function user_can_view_post( $post_id ) {
954
		$post = get_post( $post_id );
955
		if ( !$post || is_wp_error( $post ) ) {
956
			return false;
957
		}
958
959 View Code Duplication
		if ( 'inherit' === $post->post_status ) {
960
			$parent_post = get_post( $post->post_parent );
961
			$post_status_obj = get_post_status_object( $parent_post->post_status );
962
		} else {
963
			$post_status_obj = get_post_status_object( $post->post_status );
964
		}
965
966
		if ( !$post_status_obj->public ) {
967
			if ( is_user_logged_in() ) {
968
				if ( $post_status_obj->protected ) {
969
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
970
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
971
					}
972
				} elseif ( $post_status_obj->private ) {
973
					if ( !current_user_can( 'read_post', $post->ID ) ) {
974
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
975
					}
976
				} elseif ( 'trash' === $post->post_status ) {
977
					if ( !current_user_can( 'edit_post', $post->ID ) ) {
978
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
979
					}
980
				} elseif ( 'auto-draft' === $post->post_status ) {
981
					//allow auto-drafts
982
				} else {
983
					return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
984
				}
985
			} else {
986
				return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
987
			}
988
		}
989
990 View Code Duplication
		if (
991
			-1 == get_option( 'blog_public' ) &&
992
			/**
993
			 * Filter access to a specific post.
994
			 *
995
			 * @module json-api
996
			 *
997
			 * @since 3.4.0
998
			 *
999
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1000
			 * @param WP_Post $post Post data.
1001
			 */
1002
			! apply_filters(
1003
				'wpcom_json_api_user_can_view_post',
1004
				current_user_can( 'read_post', $post->ID ),
1005
				$post
1006
			)
1007
		) {
1008
			return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
1009
		}
1010
1011 View Code Duplication
		if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
1012
			return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
1013
		}
1014
1015
		return true;
1016
	}
1017
1018
	/**
1019
	 * Returns author object.
1020
	 *
1021
	 * @param $author user ID, user row, WP_User object, comment row, post row
1022
	 * @param $show_email output the author's email address?
1023
	 *
1024
	 * @return (object)
1025
	 */
1026
	function get_author( $author, $show_email = false ) {
1027
		if ( isset( $author->comment_author_email ) && !$author->user_id ) {
1028
			$ID          = 0;
1029
			$login       = '';
1030
			$email       = $author->comment_author_email;
1031
			$name        = $author->comment_author;
1032
			$first_name  = '';
1033
			$last_name   = '';
1034
			$URL         = $author->comment_author_url;
1035
			$avatar_URL  = get_avatar_url( $author );
1036
			$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1037
			$nice        = '';
1038
			$site_id     = -1;
1039
1040
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1041
			// "&" is the only email/URL character altered by wp_kses()
1042
			foreach ( array( 'email', 'URL' ) as $field ) {
1043
				$$field = str_replace( '&amp;', '&', $$field );
1044
			}
1045
		} else {
1046
			if ( isset( $author->user_id ) && $author->user_id ) {
1047
				$author = $author->user_id;
1048
			} elseif ( isset( $author->user_email ) ) {
1049
				$author = $author->ID;
1050
			} elseif ( isset( $author->post_author ) ) {
1051
				// then $author is a Post Object.
1052
				if ( 0 == $author->post_author )
1053
					return null;
1054
				/**
1055
				 * Filter whether the current site is a Jetpack site.
1056
				 *
1057
				 * @module json-api
1058
				 *
1059
				 * @since 3.3.0
1060
				 *
1061
				 * @param bool false Is the current site a Jetpack site. Default to false.
1062
				 * @param int get_current_blog_id() Blog ID.
1063
				 */
1064
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1065
				$post_id = $author->ID;
1066
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1067
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1068
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1069
					$login      = '';
1070
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1071
					$first_name = '';
1072
					$last_name  = '';
1073
					$URL        = '';
1074
					$nice       = '';
1075
				} else {
1076
					$author = $author->post_author;
1077
				}
1078
			}
1079
1080
			if ( ! isset( $ID ) ) {
1081
				$user = get_user_by( 'id', $author );
1082
				if ( ! $user || is_wp_error( $user ) ) {
1083
					trigger_error( 'Unknown user', E_USER_WARNING );
1084
1085
					return null;
1086
				}
1087
				$ID         = $user->ID;
1088
				$email      = $user->user_email;
1089
				$login      = $user->user_login;
1090
				$name       = $user->display_name;
1091
				$first_name = $user->first_name;
1092
				$last_name  = $user->last_name;
1093
				$URL        = $user->user_url;
1094
				$nice       = $user->user_nicename;
1095
			}
1096 View Code Duplication
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1097
				$active_blog = get_active_blog_for_user( $ID );
1098
				$site_id     = $active_blog->blog_id;
1099
				$profile_URL = "https://en.gravatar.com/{$login}";
1100
			} else {
1101
				$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1102
				$site_id     = -1;
1103
			}
1104
1105
			$avatar_URL = get_avatar_url( $email );
1106
		}
1107
1108
		$email = $show_email ? (string) $email : false;
1109
1110
		$author = array(
1111
			'ID'          => (int) $ID,
1112
			'login'       => (string) $login,
1113
			'email'       => $email, // (string|bool)
1114
			'name'        => (string) $name,
1115
			'first_name'  => (string) $first_name,
1116
			'last_name'   => (string) $last_name,
1117
			'nice_name'   => (string) $nice,
1118
			'URL'         => (string) esc_url_raw( $URL ),
1119
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1120
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1121
		);
1122
1123
		if ($site_id > -1) {
1124
			$author['site_ID'] = (int) $site_id;
1125
		}
1126
1127
		return (object) $author;
1128
	}
1129
1130
	function get_media_item( $media_id ) {
1131
		$media_item = get_post( $media_id );
1132
1133
		if ( !$media_item || is_wp_error( $media_item ) )
1134
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1135
1136
		$response = array(
1137
			'id'    => strval( $media_item->ID ),
1138
			'date' =>  (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1139
			'parent'           => $media_item->post_parent,
1140
			'link'             => wp_get_attachment_url( $media_item->ID ),
1141
			'title'            => $media_item->post_title,
1142
			'caption'          => $media_item->post_excerpt,
1143
			'description'      => $media_item->post_content,
1144
			'metadata'         => wp_get_attachment_metadata( $media_item->ID ),
1145
		);
1146
1147
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1148
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1149
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1150
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1151
		}
1152
1153
		$response['meta'] = (object) array(
1154
			'links' => (object) array(
1155
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1156
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1157
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1158
			),
1159
		);
1160
1161
		return (object) $response;
1162
	}
1163
1164 View Code Duplication
	function get_media_item_v1_1( $media_id ) {
1165
		$media_item = get_post( $media_id );
1166
1167
		if ( ! $media_item || is_wp_error( $media_item ) )
1168
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1169
1170
		$file = basename( wp_get_attachment_url( $media_item->ID ) );
1171
		$file_info = pathinfo( $file );
1172
		$ext  = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1173
1174
		$response = array(
1175
			'ID'           => $media_item->ID,
1176
			'URL'          => wp_get_attachment_url( $media_item->ID ),
1177
			'guid'         => $media_item->guid,
1178
			'date'         => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1179
			'post_ID'      => $media_item->post_parent,
1180
			'author_ID'    => (int) $media_item->post_author,
1181
			'file'         => $file,
1182
			'mime_type'    => $media_item->post_mime_type,
1183
			'extension'    => $ext,
1184
			'title'        => $media_item->post_title,
1185
			'caption'      => $media_item->post_excerpt,
1186
			'description'  => $media_item->post_content,
1187
			'alt'          => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1188
			'thumbnails'   => array()
1189
		);
1190
1191
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1192
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1193
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1194
				$response['height'] = $metadata['height'];
1195
				$response['width'] = $metadata['width'];
1196
			}
1197
1198
			if ( isset( $metadata['sizes'] ) ) {
1199
				/**
1200
				 * Filter the thumbnail sizes available for each attachment ID.
1201
				 *
1202
				 * @module json-api
1203
				 *
1204
				 * @since 3.9.0
1205
				 *
1206
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1207
				 * @param string $media_id Attachment ID.
1208
				 */
1209
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_id );
1210
				if ( is_array( $sizes ) ) {
1211
					foreach ( $sizes as $size => $size_details ) {
1212
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1213
					}
1214
				}
1215
			}
1216
1217
			if ( isset( $metadata['image_meta'] ) ) {
1218
				$response['exif'] = $metadata['image_meta'];
1219
			}
1220
		}
1221
1222
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1223
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1224
			$response['length'] = $metadata['length'];
1225
			$response['exif']   = $metadata;
1226
		}
1227
1228
		if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) {
1229
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1230
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1231
				$response['height'] = $metadata['height'];
1232
				$response['width']  = $metadata['width'];
1233
			}
1234
1235
			if ( isset( $metadata['length'] ) ) {
1236
				$response['length'] = $metadata['length'];
1237
			}
1238
1239
			// add VideoPress info
1240
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1241
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_id );
1242
1243
				// Thumbnails
1244
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1245
					$response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' );
1246
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1247
						if ( video_format_done( $info, $size ) ) {
1248
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1249
						} else {
1250
							unset( $response['thumbnails'][ $size ] );
1251
						}
1252
					}
1253
				}
1254
1255
				$response['videopress_guid'] = $info->guid;
1256
				$response['videopress_processing_done'] = true;
1257
				if ( '0000-00-00 00:00:00' == $info->finish_date_gmt ) {
1258
					$response['videopress_processing_done'] = false;
1259
				}
1260
			}
1261
		}
1262
1263
		$response['thumbnails'] = (object) $response['thumbnails'];
1264
1265
		$response['meta'] = (object) array(
1266
			'links' => (object) array(
1267
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1268
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1269
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1270
			),
1271
		);
1272
1273
		// add VideoPress link to the meta
1274
		if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) {
1275
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1276
				$response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1277
			}
1278
		}
1279
1280
		if ( $media_item->post_parent > 0 ) {
1281
			$response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1282
		}
1283
1284
		return (object) $response;
1285
	}
1286
1287
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1288
1289
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1290
		/// keep updating this function
1291
		if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1292
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1293
		}
1294
1295
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1296
	}
1297
1298 View Code Duplication
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1299
		// Permissions
1300
		switch ( $context ) {
1301
		case 'edit' :
1302
			$tax = get_taxonomy( $taxonomy_type );
1303
			if ( !current_user_can( $tax->cap->edit_terms ) )
1304
				return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1305
			break;
1306
		case 'display' :
1307
			if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1308
				return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1309
			}
1310
			break;
1311
		default :
1312
			return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1313
		}
1314
1315
		$response                = array();
1316
		$response['ID']          = (int) $taxonomy->term_id;
1317
		$response['name']        = (string) $taxonomy->name;
1318
		$response['slug']        = (string) $taxonomy->slug;
1319
		$response['description'] = (string) $taxonomy->description;
1320
		$response['post_count']  = (int) $taxonomy->count;
1321
1322
		if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1323
			$response['parent'] = (int) $taxonomy->parent;
1324
		}
1325
1326
		$response['meta'] = (object) array(
1327
			'links' => (object) array(
1328
				'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1329
				'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1330
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1331
			),
1332
		);
1333
1334
		return (object) $response;
1335
	}
1336
1337
	/**
1338
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1339
	 *
1340
	 * @param $date_gmt (string) GMT datetime string.
1341
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1342
	 *
1343
	 * @return string
1344
	 */
1345
	function format_date( $date_gmt, $date = null ) {
1346
		return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1347
	}
1348
1349
	/**
1350
	 * Parses a date string and returns the local and GMT representations
1351
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1352
	 * timezones or offsets. If the parsed datetime was not localized to a
1353
	 * particular timezone or offset we will assume it was given in GMT
1354
	 * relative to now and will convert it to local time using either the
1355
	 * timezone set in the options table for the blog or the GMT offset.
1356
	 *
1357
	 * @param datetime string
1358
	 *
1359
	 * @return array( $local_time_string, $gmt_time_string )
1360
	 */
1361
	function parse_date( $date_string ) {
1362
		$date_string_info = date_parse( $date_string );
1363
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1364
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1365
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1366
				$dt_local = clone $dt_utc = new DateTime( $date_string );
1367
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1368
				return array(
1369
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1370
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1371
				);
1372
			}
1373
1374
			// It's parseable but no TZ info so assume UTC
1375
			$dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1376
		} else {
1377
			// Could not parse time, use now in UTC
1378
			$dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1379
		}
1380
1381
		// First try to use timezone as it's daylight savings aware.
1382
		$timezone_string = get_option( 'timezone_string' );
1383
		if ( $timezone_string ) {
1384
			$tz = timezone_open( $timezone_string );
1385
			if ( $tz ) {
1386
				$dt_local->setTimezone( $tz );
1387
				return array(
1388
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1389
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1390
				);
1391
			}
1392
		}
1393
1394
		// Fallback to GMT offset (in hours)
1395
		// NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1396
		$gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1397
		$dt_local->modify("+{$gmt_offset_seconds} seconds");
1398
		return array(
1399
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1400
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1401
		);
1402
	}
1403
1404
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1405
	function load_theme_functions() {
1406
		// bail if we've done this already (can happen when calling /batch endpoint)
1407
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) )
1408
			return;
1409
1410
		// VIP context loading is handled elsewhere, so bail to prevent
1411
		// duplicate loading. See `switch_to_blog_and_validate_user()`
1412
		if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1413
			return;
1414
		}
1415
1416
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1417
1418
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1419
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1420
1421
		$copy_dirs = array( get_template_directory() );
1422
1423
		// Is this a child theme? Load the child theme's functions file.
1424
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1425
			foreach ( $function_files as $function_file ) {
1426
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1427
					require_once(  get_stylesheet_directory() . $function_file );
1428
				}
1429
			}
1430
			$copy_dirs[] = get_stylesheet_directory();
1431
		}
1432
1433
		foreach ( $function_files as $function_file ) {
1434
			if ( file_exists( get_template_directory() . $function_file ) ) {
1435
				require_once(  get_template_directory() . $function_file );
1436
			}
1437
		}
1438
1439
		// add inc/wpcom.php and/or includes/wpcom.php
1440
		wpcom_load_theme_compat_file();
1441
1442
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1443
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1444
1445
		/**
1446
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1447
		 *
1448
		 * The REST API does not load the theme when processing requests.
1449
		 * To enable theme-based functionality, the API will load the '/functions.php',
1450
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1451
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1452
		 *
1453
		 * @module json-api
1454
		 *
1455
		 * @since 3.2.0
1456
		 */
1457
		do_action( 'restapi_theme_after_setup_theme' );
1458
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1459
1460
		/**
1461
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1462
		 *
1463
		 * The REST API does not load the theme when processing requests.
1464
		 * To enable theme-based functionality, the API will load the '/functions.php',
1465
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1466
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1467
		 *
1468
		 * @module json-api
1469
		 *
1470
		 * @since 3.2.0
1471
		 */
1472
		do_action( 'restapi_theme_init' );
1473
	}
1474
1475
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1476
		global $wp_filter;
1477
		foreach ( $wp_filter as $hook => $actions ) {
1478
1479
			if ( $from_hook != $hook ) {
1480
				continue;
1481
			}
1482
1483
			foreach ( (array) $actions as $priority => $callbacks ) {
1484
				foreach( $callbacks as $callback_key => $callback_data ) {
1485
					$callback = $callback_data['function'];
1486
1487
					// use reflection api to determine filename where function is defined
1488
					$reflection = $this->get_reflection( $callback );
1489
1490
					if ( false !== $reflection ) {
1491
						$file_name = $reflection->getFileName();
1492
						foreach( $base_paths as $base_path ) {
1493
1494
							// only copy hooks with functions which are part of the specified files
1495
							if ( 0 === strpos( $file_name, $base_path ) ) {
1496
								add_action(
1497
									$to_hook,
1498
									$callback_data['function'],
1499
									$priority,
1500
									$callback_data['accepted_args']
1501
								);
1502
							}
1503
						}
1504
					}
1505
				}
1506
			}
1507
		}
1508
	}
1509
1510
	function get_reflection( $callback ) {
1511
		if ( is_array( $callback ) ) {
1512
			list( $class, $method ) = $callback;
1513
			return new ReflectionMethod( $class, $method );
1514
		}
1515
1516
		if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) {
1517
			list( $class, $method ) = explode( "::", $callback );
1518
			return new ReflectionMethod( $class, $method );
1519
		}
1520
1521
		if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) {
1522
			return new ReflectionMethod( $callback, "__invoke" );
1523
		}
1524
1525
		if ( is_string( $callback ) && strpos( $callback, "::" ) == false && function_exists( $callback ) ) {
1526
			return new ReflectionFunction( $callback );
1527
		}
1528
1529
		return false;
1530
	}
1531
1532
	/**
1533
	* Check whether a user can view or edit a post type
1534
	* @param string $post_type              post type to check
1535
	* @param string $context                'display' or 'edit'
1536
	* @return bool
1537
	*/
1538
	function current_user_can_access_post_type( $post_type, $context='display' ) {
1539
		$post_type_object = get_post_type_object( $post_type );
1540
		if ( ! $post_type_object ) {
1541
			return false;
1542
		}
1543
1544
		switch( $context ) {
1545
			case 'edit':
1546
				return current_user_can( $post_type_object->cap->edit_posts );
1547
			case 'display':
1548
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1549
			default:
1550
				return false;
1551
		}
1552
	}
1553
1554
	function is_post_type_allowed( $post_type ) {
1555
		// if the post type is empty, that's fine, WordPress will default to post
1556
		if ( empty( $post_type ) ) {
1557
			return true;
1558
		}
1559
1560
		// allow special 'any' type
1561
		if ( 'any' == $post_type ) {
1562
			return true;
1563
		}
1564
1565
		// check for allowed types
1566
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1567
			return true;
1568
		}
1569
1570
		if ( $post_type_object = get_post_type_object( $post_type ) ) {
1571
			if ( ! empty( $post_type_object->show_in_rest ) ) {
1572
				return $post_type_object->show_in_rest;
1573
			}
1574
			if ( ! empty( $post_type_object->publicly_queryable ) ) {
1575
				return $post_type_object->publicly_queryable;
1576
			}
1577
		}
1578
1579
		return ! empty( $post_type_object->public );
1580
	}
1581
1582
	/**
1583
	 * Gets the whitelisted post types that JP should allow access to.
1584
	 *
1585
	 * @return array Whitelisted post types.
1586
	 */
1587 View Code Duplication
	protected function _get_whitelisted_post_types() {
1588
		$allowed_types = array( 'post', 'page', 'revision' );
1589
1590
		/**
1591
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1592
		 *
1593
		 * @module json-api
1594
		 *
1595
		 * @since 2.2.3
1596
		 *
1597
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1598
		 */
1599
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1600
1601
		return array_unique( $allowed_types );
1602
	}
1603
1604
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1605
1606
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1607
1608
		$media_ids = $errors = array();
1609
		$user_can_upload_files = current_user_can( 'upload_files' );
1610
		$media_attrs = array_values( $media_attrs ); // reset the keys
1611
		$i = 0;
1612
1613
		if ( ! empty( $media_files ) ) {
1614
			$this->api->trap_wp_die( 'upload_error' );
1615
			foreach ( $media_files as $media_item ) {
1616
				$_FILES['.api.media.item.'] = $media_item;
1617 View Code Duplication
				if ( ! $user_can_upload_files ) {
1618
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1619
				} else {
1620
					if ( $force_parent_id ) {
1621
						$parent_id = absint( $force_parent_id );
1622
					} elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1623
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1624
					} else {
1625
						$parent_id = 0;
1626
					}
1627
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1628
				}
1629
				if ( is_wp_error( $media_id ) ) {
1630
					$errors[$i]['file']   = $media_item['name'];
1631
					$errors[$i]['error']   = $media_id->get_error_code();
1632
					$errors[$i]['message'] = $media_id->get_error_message();
1633
				} else {
1634
					$media_ids[$i] = $media_id;
1635
				}
1636
1637
				$i++;
1638
			}
1639
			$this->api->trap_wp_die( null );
1640
			unset( $_FILES['.api.media.item.'] );
1641
		}
1642
1643
		if ( ! empty( $media_urls ) ) {
1644
			foreach ( $media_urls as $url ) {
1645 View Code Duplication
				if ( ! $user_can_upload_files ) {
1646
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1647
				} else {
1648
					if ( $force_parent_id ) {
1649
						$parent_id = absint( $force_parent_id );
1650
					} else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1651
						$parent_id = absint( $media_attrs[$i]['parent_id'] );
1652
					} else {
1653
						$parent_id = 0;
1654
					}
1655
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1656
				}
1657
				if ( is_wp_error( $media_id ) ) {
1658
					$errors[$i] = array(
1659
						'file'    => $url,
1660
						'error'   => $media_id->get_error_code(),
1661
						'message' => $media_id->get_error_message(),
1662
					);
1663
				} elseif ( ! empty( $media_id ) ) {
1664
					$media_ids[$i] = $media_id;
1665
				}
1666
1667
				$i++;
1668
			}
1669
		}
1670
1671
		if ( ! empty( $media_attrs ) ) {
1672
			foreach ( $media_ids as $index => $media_id ) {
1673
				if ( empty( $media_attrs[$index] ) )
1674
					continue;
1675
1676
				$attrs = $media_attrs[$index];
1677
				$insert = array();
1678
1679
				// Attributes: Title, Caption, Description
1680
1681
				if ( isset( $attrs['title'] ) ) {
1682
					$insert['post_title'] = $attrs['title'];
1683
				}
1684
1685
				if ( isset( $attrs['caption'] ) ) {
1686
					$insert['post_excerpt'] = $attrs['caption'];
1687
				}
1688
1689
				if ( isset( $attrs['description'] ) ) {
1690
					$insert['post_content'] = $attrs['description'];
1691
				}
1692
1693
				if ( ! empty( $insert ) ) {
1694
					$insert['ID'] = $media_id;
1695
					wp_update_post( (object) $insert );
1696
				}
1697
1698
				// Attributes: Alt
1699
1700 View Code Duplication
				if ( isset( $attrs['alt'] ) ) {
1701
					$alt = wp_strip_all_tags( $attrs['alt'], true );
1702
					update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1703
				}
1704
1705
				// Attributes: Artist, Album
1706
1707
				$id3_meta = array();
1708
1709 View Code Duplication
				foreach ( array( 'artist', 'album' ) as $key ) {
1710
					if ( isset( $attrs[ $key ] ) ) {
1711
						$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1712
					}
1713
				}
1714
1715 View Code Duplication
				if ( ! empty( $id3_meta ) ) {
1716
					// Before updating metadata, ensure that the item is audio
1717
					$item = $this->get_media_item_v1_1( $media_id );
1718
					if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1719
						wp_update_attachment_metadata( $media_id, $id3_meta );
1720
					}
1721
				}
1722
			}
1723
		}
1724
1725
		return array( 'media_ids' => $media_ids, 'errors' => $errors );
1726
1727
	}
1728
1729
	function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1730
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1731
			return false;
1732
1733
		// if we didn't get a URL, let's bail
1734
		$parsed = @parse_url( $url );
1735
		if ( empty( $parsed ) )
1736
			return false;
1737
1738
		$tmp = download_url( $url );
1739
		if ( is_wp_error( $tmp ) ) {
1740
			return $tmp;
1741
		}
1742
1743
		// First check to see if we get a mime-type match by file, otherwise, check to
1744
		// see if WordPress supports this file as an image. If neither, then it is not supported.
1745
		if ( ! $this->is_file_supported_for_sideloading( $tmp ) && 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1746
			@unlink( $tmp );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1747
			return false;
1748
		}
1749
1750
		// emulate a $_FILES entry
1751
		$file_array = array(
1752
			'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1753
			'tmp_name' => $tmp,
1754
		);
1755
1756
		$id = media_handle_sideload( $file_array, $parent_post_id );
1757
		@unlink( $tmp );
1758
1759
		if ( is_wp_error( $id ) ) {
1760
			return $id;
1761
		}
1762
1763
		if ( ! $id || ! is_int( $id ) ) {
1764
			return false;
1765
		}
1766
1767
		return $id;
1768
	}
1769
1770
	/**
1771
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1772
	 *
1773
	 * @param string $file Path to file to get its mime type.
1774
	 *
1775
	 * @return bool
1776
	 */
1777 View Code Duplication
	protected function is_file_supported_for_sideloading( $file ) {
1778
		if ( class_exists( 'finfo' ) ) { // php 5.3+
1779
			$finfo = new finfo( FILEINFO_MIME );
1780
			$mime = explode( '; ', $finfo->file( $file ) );
1781
			$type = $mime[0];
1782
1783
		} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
1784
			$type = mime_content_type( $file );
1785
1786
		} else {
1787
			return false;
1788
		}
1789
1790
		/**
1791
		 * Filter the list of supported mime types for media sideloading.
1792
		 *
1793
		 * @since 4.0.0
1794
		 *
1795
		 * @module json-api
1796
		 *
1797
		 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
1798
		 */
1799
		$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
1800
			'image/png',
1801
			'image/jpeg',
1802
			'image/gif',
1803
			'image/bmp',
1804
			'video/quicktime',
1805
			'video/mp4',
1806
			'video/mpeg',
1807
			'video/ogg',
1808
			'video/3gpp',
1809
			'video/3gpp2',
1810
			'video/h261',
1811
			'video/h262',
1812
			'video/h264',
1813
			'video/x-msvideo',
1814
			'video/x-ms-wmv',
1815
			'video/x-ms-asf',
1816
		) );
1817
1818
		// If the type returned was not an array as expected, then we know we don't have a match.
1819
		if ( ! is_array( $supported_mime_types ) ) {
1820
			return false;
1821
		}
1822
1823
		return in_array( $type, $supported_mime_types );
1824
	}
1825
1826
	function allow_video_uploads( $mimes ) {
1827
		// if we are on Jetpack, bail - Videos are already allowed
1828
		if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1829
			return $mimes;
1830
		}
1831
1832
		// extra check that this filter is only ever applied during REST API requests
1833
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1834
			return $mimes;
1835
		}
1836
1837
		// bail early if they already have the upgrade..
1838
		if ( get_option( 'video_upgrade' ) == '1' ) {
1839
			return $mimes;
1840
		}
1841
1842
		// lets whitelist to only specific clients right now
1843
		$clients_allowed_video_uploads = array();
1844
		/**
1845
		 * Filter the list of whitelisted video clients.
1846
		 *
1847
		 * @module json-api
1848
		 *
1849
		 * @since 3.2.0
1850
		 *
1851
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1852
		 */
1853
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1854
		if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1855
			return $mimes;
1856
		}
1857
1858
		$mime_list = wp_get_mime_types();
1859
1860
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1861
		/**
1862
		 * Filter the video filetypes allowed on the site.
1863
		 *
1864
		 * @module json-api
1865
		 *
1866
		 * @since 3.2.0
1867
		 *
1868
		 * @param array $video_exts Array of video filetypes allowed on the site.
1869
		 */
1870
		$video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
1871
		$video_mimes = array();
1872
1873
		if ( !empty( $video_exts ) ) {
1874
			foreach ( $video_exts as $ext ) {
1875
				foreach ( $mime_list as $ext_pattern => $mime ) {
1876
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
1877
						$video_mimes[$ext_pattern] = $mime;
1878
				}
1879
			}
1880
1881
			$mimes = array_merge( $mimes, $video_mimes );
1882
		}
1883
1884
		return $mimes;
1885
	}
1886
1887
	function is_current_site_multi_user() {
1888
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
1889
		if ( false === $users ) {
1890
			$user_query = new WP_User_Query( array(
1891
				'blog_id' => get_current_blog_id(),
1892
				'fields'  => 'ID',
1893
			) );
1894
			$users = (int) $user_query->get_total();
1895
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
1896
		}
1897
		return $users > 1;
1898
	}
1899
1900
	function allows_cross_origin_requests() {
1901
		return 'GET' == $this->method || $this->allow_cross_origin_request;
1902
	}
1903
1904
	function allows_unauthorized_requests( $origin, $complete_access_origins  ) {
1905
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
1906
	}
1907
1908
	function get_platform() {
1909
		return wpcom_get_sal_platform( $this->api->token_details );
1910
	}
1911
1912
	/**
1913
	 * Return endpoint response
1914
	 *
1915
	 * @param ... determined by ->$path
1916
	 *
1917
	 * @return
1918
	 * 	falsy: HTTP 500, no response body
1919
	 *	WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
1920
	 *	$data: HTTP 200, json_encode( $data ) response body
1921
	 */
1922
	abstract function callback( $path = '' );
1923
1924
1925
}
1926
1927
require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
1928