Completed
Push — fix/pay-with-paypal-require-li... ( ad14ad )
by
unknown
29:23 queued 21:29
created

class.json-api-endpoints.php (21 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
use Automattic\Jetpack\Connection\Client;
4
5
require_once dirname( __FILE__ ) . '/json-api-config.php';
6
require_once dirname( __FILE__ ) . '/sal/class.json-api-links.php';
7
require_once dirname( __FILE__ ) . '/sal/class.json-api-metadata.php';
8
require_once dirname( __FILE__ ) . '/sal/class.json-api-date.php';
9
10
// Endpoint
11
abstract class WPCOM_JSON_API_Endpoint {
12
	// The API Object
13
	public $api;
14
15
	// The link-generating utility class
16
	public $links;
17
18
	public $pass_wpcom_user_details = false;
19
20
	// One liner.
21
	public $description;
22
23
	// Object Grouping For Documentation (Users, Posts, Comments)
24
	public $group;
25
26
	// Stats extra value to bump
27
	public $stat;
28
29
	// HTTP Method
30
	public $method = 'GET';
31
32
	// Minimum version of the api for which to serve this endpoint
33
	public $min_version = '0';
34
35
	// Maximum version of the api for which to serve this endpoint
36
	public $max_version = WPCOM_JSON_API__CURRENT_VERSION;
37
38
	// Path at which to serve this endpoint: sprintf() format.
39
	public $path = '';
40
41
	// Identifiers to fill sprintf() formatted $path
42
	public $path_labels = array();
43
44
	// Accepted query parameters
45
	public $query = array(
46
		// Parameter name
47
		'context'       => array(
48
			// Default value => description
49
			'display' => 'Formats the output as HTML for display.  Shortcodes are parsed, paragraph tags are added, etc..',
50
			// Other possible values => description
51
			'edit'    => 'Formats the output for editing.  Shortcodes are left unparsed, significant whitespace is kept, etc..',
52
		),
53
		'http_envelope' => array(
54
			'false' => '',
55
			'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.',
56
		),
57
		'pretty'        => array(
58
			'false' => '',
59
			'true'  => 'Output pretty JSON',
60
		),
61
		'meta'          => "(string) Optional. Loads data from the endpoints found in the 'meta' part of the response. Comma-separated list. Example: meta=site,likes",
62
		'fields'        => '(string) Optional. Returns specified fields only. Comma-separated list. Example: fields=ID,title',
63
		// Parameter name => description (default value is empty)
64
		'callback'      => '(string) An optional JSONP callback function.',
65
	);
66
67
	// Response format
68
	public $response_format = array();
69
70
	// Request format
71
	public $request_format = array();
72
73
	// Is this endpoint still in testing phase?  If so, not available to the public.
74
	public $in_testing = false;
75
76
	// Is this endpoint still allowed if the site in question is flagged?
77
	public $allowed_if_flagged = false;
78
79
	// Is this endpoint allowed if the site is red flagged?
80
	public $allowed_if_red_flagged = false;
81
82
	// Is this endpoint allowed if the site is deleted?
83
	public $allowed_if_deleted = false;
84
85
	/**
86
	 * @var string Version of the API
87
	 */
88
	public $version = '';
89
90
	/**
91
	 * @var string Example request to make
92
	 */
93
	public $example_request = '';
94
95
	/**
96
	 * @var string Example request data (for POST methods)
97
	 */
98
	public $example_request_data = '';
99
100
	/**
101
	 * @var string Example response from $example_request
102
	 */
103
	public $example_response = '';
104
105
	/**
106
	 * @var bool Set to true if the endpoint implements its own filtering instead of the standard `fields` query method
107
	 */
108
	public $custom_fields_filtering = false;
109
110
	/**
111
	 * @var bool Set to true if the endpoint accepts all cross origin requests. You probably should not set this flag.
112
	 */
113
	public $allow_cross_origin_request = false;
114
115
	/**
116
	 * @var bool Set to true if the endpoint can recieve unauthorized POST requests.
117
	 */
118
	public $allow_unauthorized_request = false;
119
120
	/**
121
	 * @var bool Set to true if the endpoint should accept site based (not user based) authentication.
122
	 */
123
	public $allow_jetpack_site_auth = false;
124
125
	/**
126
	 * @var bool Set to true if the endpoint should accept auth from an upload token.
127
	 */
128
	public $allow_upload_token_auth = false;
129
130
	/**
131
	 * @var bool Set to true if the endpoint should require auth from a Rewind auth token.
132
	 */
133
	public $require_rewind_auth = false;
134
135
	function __construct( $args ) {
136
		$defaults = array(
137
			'in_testing'                 => false,
138
			'allowed_if_flagged'         => false,
139
			'allowed_if_red_flagged'     => false,
140
			'allowed_if_deleted'         => false,
141
			'description'                => '',
142
			'group'                      => '',
143
			'method'                     => 'GET',
144
			'path'                       => '/',
145
			'min_version'                => '0',
146
			'max_version'                => WPCOM_JSON_API__CURRENT_VERSION,
147
			'force'                      => '',
148
			'deprecated'                 => false,
149
			'new_version'                => WPCOM_JSON_API__CURRENT_VERSION,
150
			'jp_disabled'                => false,
151
			'path_labels'                => array(),
152
			'request_format'             => array(),
153
			'response_format'            => array(),
154
			'query_parameters'           => array(),
155
			'version'                    => 'v1',
156
			'example_request'            => '',
157
			'example_request_data'       => '',
158
			'example_response'           => '',
159
			'required_scope'             => '',
160
			'pass_wpcom_user_details'    => false,
161
			'custom_fields_filtering'    => false,
162
			'allow_cross_origin_request' => false,
163
			'allow_unauthorized_request' => false,
164
			'allow_jetpack_site_auth'    => false,
165
			'allow_upload_token_auth'    => false,
166
		);
167
168
		$args = wp_parse_args( $args, $defaults );
169
170
		$this->in_testing = $args['in_testing'];
171
172
		$this->allowed_if_flagged     = $args['allowed_if_flagged'];
173
		$this->allowed_if_red_flagged = $args['allowed_if_red_flagged'];
174
		$this->allowed_if_deleted     = $args['allowed_if_deleted'];
175
176
		$this->description = $args['description'];
177
		$this->group       = $args['group'];
178
		$this->stat        = $args['stat'];
179
		$this->force       = $args['force'];
180
		$this->jp_disabled = $args['jp_disabled'];
181
182
		$this->method      = $args['method'];
183
		$this->path        = $args['path'];
184
		$this->path_labels = $args['path_labels'];
185
		$this->min_version = $args['min_version'];
186
		$this->max_version = $args['max_version'];
187
		$this->deprecated  = $args['deprecated'];
188
		$this->new_version = $args['new_version'];
189
190
		// Ensure max version is not less than min version
191
		if ( version_compare( $this->min_version, $this->max_version, '>' ) ) {
192
			$this->max_version = $this->min_version;
193
		}
194
195
		$this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
196
		$this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
197
198
		$this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
199
		$this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
200
		$this->allow_jetpack_site_auth    = (bool) $args['allow_jetpack_site_auth'];
201
		$this->allow_upload_token_auth    = (bool) $args['allow_upload_token_auth'];
202
		$this->require_rewind_auth        = isset( $args['require_rewind_auth'] ) ? (bool) $args['require_rewind_auth'] : false;
203
204
		$this->version = $args['version'];
205
206
		$this->required_scope = $args['required_scope'];
207
208 View Code Duplication
		if ( $this->request_format ) {
209
			$this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
210
		} else {
211
			$this->request_format = $args['request_format'];
212
		}
213
214 View Code Duplication
		if ( $this->response_format ) {
215
			$this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
216
		} else {
217
			$this->response_format = $args['response_format'];
218
		}
219
220
		if ( false === $args['query_parameters'] ) {
221
			$this->query = array();
222
		} elseif ( is_array( $args['query_parameters'] ) ) {
223
			$this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
224
		}
225
226
		$this->api   = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
227
		$this->links = WPCOM_JSON_API_Links::getInstance();
228
229
		/** Example Request/Response */
230
231
		// Examples for endpoint documentation request
232
		$this->example_request      = $args['example_request'];
233
		$this->example_request_data = $args['example_request_data'];
234
		$this->example_response     = $args['example_response'];
235
236
		$this->api->add( $this );
237
	}
238
239
	// Get all query args.  Prefill with defaults
240
	function query_args( $return_default_values = true, $cast_and_filter = true ) {
241
		$args = array_intersect_key( $this->api->query, $this->query );
242
243
		if ( ! $cast_and_filter ) {
244
			return $args;
245
		}
246
247
		return $this->cast_and_filter( $args, $this->query, $return_default_values );
248
	}
249
250
	// Get POST body data
251
	function input( $return_default_values = true, $cast_and_filter = true ) {
252
		$input        = trim( $this->api->post_body );
253
		$content_type = $this->api->content_type;
254
		if ( $content_type ) {
255
			list ( $content_type ) = explode( ';', $content_type );
256
		}
257
		$content_type = trim( $content_type );
258
		switch ( $content_type ) {
259
			case 'application/json':
260
			case 'application/x-javascript':
261
			case 'text/javascript':
262
			case 'text/x-javascript':
263
			case 'text/x-json':
264
			case 'text/json':
265
				$return = json_decode( $input, true );
266
267
				if ( function_exists( 'json_last_error' ) ) {
268
					if ( JSON_ERROR_NONE !== json_last_error() ) { // phpcs:ignore PHPCompatibility
269
						return null;
270
					}
271
				} else {
272
					if ( is_null( $return ) && json_encode( null ) !== $input ) {
273
						return null;
274
					}
275
				}
276
277
				break;
278
			case 'multipart/form-data':
279
				$return = array_merge( stripslashes_deep( $_POST ), $_FILES );
280
				break;
281
			case 'application/x-www-form-urlencoded':
282
				// attempt JSON first, since probably a curl command
283
				$return = json_decode( $input, true );
284
285
				if ( is_null( $return ) ) {
286
					wp_parse_str( $input, $return );
287
				}
288
289
				break;
290
			default:
291
				wp_parse_str( $input, $return );
292
				break;
293
		}
294
295
		if ( isset( $this->api->query['force'] )
296
			&& 'secure' === $this->api->query['force']
297
			&& isset( $return['secure_key'] ) ) {
298
			$this->api->post_body      = $this->get_secure_body( $return['secure_key'] );
299
			$this->api->query['force'] = false;
300
			return $this->input( $return_default_values, $cast_and_filter );
301
		}
302
303
		if ( $cast_and_filter ) {
304
			$return = $this->cast_and_filter( $return, $this->request_format, $return_default_values );
305
		}
306
		return $return;
307
	}
308
309
310
	protected function get_secure_body( $secure_key ) {
311
		$response = Client::wpcom_json_api_request_as_blog(
312
			sprintf( '/sites/%d/secure-request', Jetpack_Options::get_option( 'id' ) ),
313
			'1.1',
314
			array( 'method' => 'POST' ),
315
			array( 'secure_key' => $secure_key )
316
		);
317
		if ( 200 !== $response['response']['code'] ) {
318
			return null;
319
		}
320
		return json_decode( $response['body'], true );
321
	}
322
323
	function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
324
		$return_as_object = false;
325
		if ( is_object( $data ) ) {
326
			// @todo this should probably be a deep copy if $data can ever have nested objects
327
			$data             = (array) $data;
328
			$return_as_object = true;
329
		} elseif ( ! is_array( $data ) ) {
330
			return $data;
331
		}
332
333
		$boolean_arg = array( 'false', 'true' );
334
		$naeloob_arg = array( 'true', 'false' );
335
336
		$return = array();
337
338
		foreach ( $documentation as $key => $description ) {
339
			if ( is_array( $description ) ) {
340
				// String or boolean array keys only
341
				$whitelist = array_keys( $description );
342
343
				if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
344
					// Truthiness
345
					if ( isset( $data[ $key ] ) ) {
346
						$return[ $key ] = (bool) WPCOM_JSON_API::is_truthy( $data[ $key ] );
347
					} elseif ( $return_default_values ) {
348
						$return[ $key ] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg.
349
					}
350
				} elseif ( isset( $data[ $key ] ) && isset( $description[ $data[ $key ] ] ) ) {
351
					// String Key
352
					$return[ $key ] = (string) $data[ $key ];
353
				} elseif ( $return_default_values ) {
354
					// Default value
355
					$return[ $key ] = (string) current( $whitelist );
356
				}
357
358
				continue;
359
			}
360
361
			$types = $this->parse_types( $description );
362
			$type  = array_shift( $types );
363
364
			// Explicit default - string and int only for now.  Always set these reguardless of $return_default_values
365
			if ( isset( $type['default'] ) ) {
366
				if ( ! isset( $data[ $key ] ) ) {
367
					$data[ $key ] = $type['default'];
368
				}
369
			}
370
371
			if ( ! isset( $data[ $key ] ) ) {
372
				continue;
373
			}
374
375
			$this->cast_and_filter_item( $return, $type, $key, $data[ $key ], $types, $for_output );
376
		}
377
378
		if ( $return_as_object ) {
379
			return (object) $return;
380
		}
381
382
		return $return;
383
	}
384
385
	/**
386
	 * Casts $value according to $type.
387
	 * Handles fallbacks for certain values of $type when $value is not that $type
388
	 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way),
389
	 * and string -> object (one way)
390
	 *
391
	 * Handles "child types" - array:URL, object:category
392
	 * array:URL means an array of URLs
393
	 * object:category means a hash of categories
394
	 *
395
	 * Handles object typing - object>post means an object of type post
396
	 */
397
	function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
398
		if ( is_string( $type ) ) {
399
			$type = compact( 'type' );
400
		}
401
402
		switch ( $type['type'] ) {
403
			case 'false':
404
				$return[ $key ] = false;
405
				break;
406
			case 'url':
407
				if ( is_object( $value ) && isset( $value->url ) && false !== strpos( $value->url, 'https://videos.files.wordpress.com/' ) ) {
408
					$value = $value->url;
409
				}
410
				// Check for string since esc_url_raw() expects one.
411
				if ( ! is_string( $value ) ) {
412
					break;
413
				}
414
				$return[ $key ] = (string) esc_url_raw( $value );
415
				break;
416
			case 'string':
417
				// Fallback string -> array, or for string -> object
418
				if ( is_array( $value ) || is_object( $value ) ) {
419 View Code Duplication
					if ( ! empty( $types[0] ) ) {
420
						$next_type = array_shift( $types );
421
						return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
422
					}
423
				}
424
425
				// Fallback string -> false
426 View Code Duplication
				if ( ! is_string( $value ) ) {
427
					if ( ! empty( $types[0] ) && 'false' === $types[0]['type'] ) {
428
						$next_type = array_shift( $types );
429
						return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
430
					}
431
				}
432
				$return[ $key ] = (string) $value;
433
				break;
434
			case 'html':
435
				$return[ $key ] = (string) $value;
436
				break;
437
			case 'safehtml':
438
				$return[ $key ] = wp_kses( (string) $value, wp_kses_allowed_html() );
439
				break;
440
			case 'zip':
441
			case 'media':
442
				if ( is_array( $value ) ) {
443
					if ( isset( $value['name'] ) && is_array( $value['name'] ) ) {
444
						// It's a $_FILES array
445
						// Reformat into array of $_FILES items
446
						$files = array();
447
448
						foreach ( $value['name'] as $k => $v ) {
449
							$files[ $k ] = array();
450
							foreach ( array_keys( $value ) as $file_key ) {
451
								$files[ $k ][ $file_key ] = $value[ $file_key ][ $k ];
452
							}
453
						}
454
455
						$return[ $key ] = $files;
456
						break;
457
					}
458
				} else {
459
					// no break - treat as 'array'
460
				}
461
				// nobreak
462
			case 'array':
463
				// Fallback array -> string
464 View Code Duplication
				if ( is_string( $value ) ) {
465
					if ( ! empty( $types[0] ) ) {
466
						$next_type = array_shift( $types );
467
						return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
468
					}
469
				}
470
471 View Code Duplication
				if ( isset( $type['children'] ) ) {
472
					$children = array();
473
					foreach ( (array) $value as $k => $child ) {
474
						$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
475
					}
476
					$return[ $key ] = (array) $children;
477
					break;
478
				}
479
480
				$return[ $key ] = (array) $value;
481
				break;
482
			case 'iso 8601 datetime':
483
			case 'datetime':
484
				// (string)s
485
				$dates = $this->parse_date( (string) $value );
486
				if ( $for_output ) {
487
					$return[ $key ] = $this->format_date( $dates[1], $dates[0] );
488
				} else {
489
					list( $return[ $key ], $return[ "{$key}_gmt" ] ) = $dates;
490
				}
491
				break;
492
			case 'float':
493
				$return[ $key ] = (float) $value;
494
				break;
495
			case 'int':
496
			case 'integer':
497
				$return[ $key ] = (int) $value;
498
				break;
499
			case 'bool':
500
			case 'boolean':
501
				$return[ $key ] = (bool) WPCOM_JSON_API::is_truthy( $value );
502
				break;
503
			case 'object':
504
				// Fallback object -> false
505 View Code Duplication
				if ( is_scalar( $value ) || is_null( $value ) ) {
506
					if ( ! empty( $types[0] ) && 'false' === $types[0]['type'] ) {
507
						return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
508
					}
509
				}
510
511 View Code Duplication
				if ( isset( $type['children'] ) ) {
512
					$children = array();
513
					foreach ( (array) $value as $k => $child ) {
514
						$this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
515
					}
516
					$return[ $key ] = (object) $children;
517
					break;
518
				}
519
520
				if ( isset( $type['subtype'] ) ) {
521
					return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
522
				}
523
524
				$return[ $key ] = (object) $value;
525
				break;
526
			case 'post':
527
				$return[ $key ] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
528
				break;
529
			case 'comment':
530
				$return[ $key ] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
531
				break;
532
			case 'tag':
533
			case 'category':
534
				$docs = array(
535
					'ID'          => '(int)',
536
					'name'        => '(string)',
537
					'slug'        => '(string)',
538
					'description' => '(HTML)',
539
					'post_count'  => '(int)',
540
					'feed_url'    => '(string)',
541
					'meta'        => '(object)',
542
				);
543
				if ( 'category' === $type['type'] ) {
544
					$docs['parent'] = '(int)';
545
				}
546
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
547
				break;
548
			case 'post_reference':
549 View Code Duplication
			case 'comment_reference':
550
				$docs           = array(
551
					'ID'    => '(int)',
552
					'type'  => '(string)',
553
					'title' => '(string)',
554
					'link'  => '(URL)',
555
				);
556
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
557
				break;
558 View Code Duplication
			case 'geo':
559
				$docs           = array(
560
					'latitude'  => '(float)',
561
					'longitude' => '(float)',
562
					'address'   => '(string)',
563
				);
564
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
565
				break;
566
			case 'author':
567
				$docs           = array(
568
					'ID'             => '(int)',
569
					'user_login'     => '(string)',
570
					'login'          => '(string)',
571
					'email'          => '(string|false)',
572
					'name'           => '(string)',
573
					'first_name'     => '(string)',
574
					'last_name'      => '(string)',
575
					'nice_name'      => '(string)',
576
					'URL'            => '(URL)',
577
					'avatar_URL'     => '(URL)',
578
					'profile_URL'    => '(URL)',
579
					'is_super_admin' => '(bool)',
580
					'roles'          => '(array:string)',
581
					'ip_address'     => '(string|false)',
582
				);
583
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
584
				break;
585 View Code Duplication
			case 'role':
586
				$docs           = array(
587
					'name'         => '(string)',
588
					'display_name' => '(string)',
589
					'capabilities' => '(object:boolean)',
590
				);
591
				$return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
592
				break;
593
			case 'attachment':
594
				$docs           = array(
595
					'ID'        => '(int)',
596
					'URL'       => '(URL)',
597
					'guid'      => '(string)',
598
					'mime_type' => '(string)',
599
					'width'     => '(int)',
600
					'height'    => '(int)',
601
					'duration'  => '(int)',
602
				);
603
				$return[ $key ] = (object) $this->cast_and_filter(
604
					$value,
605
					/**
606
					* Filter the documentation returned for a post attachment.
607
					*
608
					* @module json-api
609
					*
610
					* @since 1.9.0
611
					*
612
					* @param array $docs Array of documentation about a post attachment.
613
					*/
614
					apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
615
					false,
616
					$for_output
617
				);
618
				break;
619
			case 'metadata':
620
				$docs           = array(
621
					'id'             => '(int)',
622
					'key'            => '(string)',
623
					'value'          => '(string|false|float|int|array|object)',
624
					'previous_value' => '(string)',
625
					'operation'      => '(string)',
626
				);
627
				$return[ $key ] = (object) $this->cast_and_filter(
628
					$value,
629
					/** This filter is documented in class.json-api-endpoints.php */
630
					apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
631
					false,
632
					$for_output
633
				);
634
				break;
635
			case 'plugin':
636
				$docs           = array(
637
					'id'           => '(safehtml) The plugin\'s ID',
638
					'slug'         => '(safehtml) The plugin\'s Slug',
639
					'active'       => '(boolean)  The plugin status.',
640
					'update'       => '(object)   The plugin update info.',
641
					'name'         => '(safehtml) The name of the plugin.',
642
					'plugin_url'   => '(url)      Link to the plugin\'s web site.',
643
					'version'      => '(safehtml) The plugin version number.',
644
					'description'  => '(safehtml) Description of what the plugin does and/or notes from the author',
645
					'author'       => '(safehtml) The plugin author\'s name',
646
					'author_url'   => '(url)      The plugin author web site address',
647
					'network'      => '(boolean)  Whether the plugin can only be activated network wide.',
648
					'autoupdate'   => '(boolean)  Whether the plugin is auto updated',
649
					'log'          => '(array:safehtml) An array of update log strings.',
650
					'action_links' => '(array) An array of action links that the plugin uses.',
651
				);
652
				$return[ $key ] = (object) $this->cast_and_filter(
653
					$value,
654
					/**
655
					* Filter the documentation returned for a plugin.
656
					*
657
					* @module json-api
658
					*
659
					* @since 3.1.0
660
					*
661
					* @param array $docs Array of documentation about a plugin.
662
					*/
663
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
664
					false,
665
					$for_output
666
				);
667
				break;
668
			case 'plugin_v1_2':
669
				$docs           = class_exists( 'Jetpack_JSON_API_Get_Plugins_v1_2_Endpoint' )
670
				? Jetpack_JSON_API_Get_Plugins_v1_2_Endpoint::$_response_format
671
				: Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2;
672
				$return[ $key ] = (object) $this->cast_and_filter(
673
					$value,
674
					/**
675
					* Filter the documentation returned for a plugin.
676
					*
677
					* @module json-api
678
					*
679
					* @since 3.1.0
680
					*
681
					* @param array $docs Array of documentation about a plugin.
682
					*/
683
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
684
					false,
685
					$for_output
686
				);
687
				break;
688
			case 'file_mod_capabilities':
689
				$docs           = array(
690
					'reasons_modify_files_unavailable' => '(array) The reasons why files can\'t be modified',
691
					'reasons_autoupdate_unavailable'   => '(array) The reasons why autoupdates aren\'t allowed',
692
					'modify_files'                     => '(boolean) true if files can be modified',
693
					'autoupdate_files'                 => '(boolean) true if autoupdates are allowed',
694
				);
695
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
696
				break;
697
			case 'jetpackmodule':
698
				$docs           = array(
699
					'id'          => '(string)   The module\'s ID',
700
					'active'      => '(boolean)  The module\'s status.',
701
					'name'        => '(string)   The module\'s name.',
702
					'description' => '(safehtml) The module\'s description.',
703
					'sort'        => '(int)      The module\'s display order.',
704
					'introduced'  => '(string)   The Jetpack version when the module was introduced.',
705
					'changed'     => '(string)   The Jetpack version when the module was changed.',
706
					'free'        => '(boolean)  The module\'s Free or Paid status.',
707
					'module_tags' => '(array)    The module\'s tags.',
708
					'override'    => '(string)   The module\'s override. Empty if no override, otherwise \'active\' or \'inactive\'',
709
				);
710
				$return[ $key ] = (object) $this->cast_and_filter(
711
					$value,
712
					/** This filter is documented in class.json-api-endpoints.php */
713
					apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
714
					false,
715
					$for_output
716
				);
717
				break;
718
			case 'sharing_button':
719
				$docs           = array(
720
					'ID'         => '(string)',
721
					'name'       => '(string)',
722
					'URL'        => '(string)',
723
					'icon'       => '(string)',
724
					'enabled'    => '(bool)',
725
					'visibility' => '(string)',
726
				);
727
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
728
				break;
729
			case 'sharing_button_service':
730
				$docs           = array(
731
					'ID'               => '(string) The service identifier',
732
					'name'             => '(string) The service name',
733
					'class_name'       => '(string) Class name for custom style sharing button elements',
734
					'genericon'        => '(string) The Genericon unicode character for the custom style sharing button icon',
735
					'preview_smart'    => '(string) An HTML snippet of a rendered sharing button smart preview',
736
					'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview',
737
				);
738
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
739
				break;
740
			case 'site_keyring':
741
				$docs           = array(
742
					'keyring_id'       => '(int) Keyring ID',
743
					'service'          => '(string) The service name',
744
					'external_user_id' => '(string) External user id for the service',
745
				);
746
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
747
				break;
748
			case 'taxonomy':
749
				$docs           = array(
750
					'name'         => '(string) The taxonomy slug',
751
					'label'        => '(string) The taxonomy human-readable name',
752
					'labels'       => '(object) Mapping of labels for the taxonomy',
753
					'description'  => '(string) The taxonomy description',
754
					'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
755
					'public'       => '(bool) Whether the taxonomy is public',
756
					'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
757
				);
758
				$return[ $key ] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
759
				break;
760
761
			case 'visibility':
762
				// This is needed to fix a bug in WPAndroid where `public: "PUBLIC"` is sent in place of `public: 1`
763
				if ( 'public' === strtolower( $value ) ) {
764
					$return[ $key ] = 1;
765
				} else if ( 'private' === strtolower( $value ) ) {
766
					$return[ $key ] = -1;
767
				} else {
768
					$return[ $key ] = (int) $value;
769
				}
770
				break;
771
772
			default:
773
				$method_name = $type['type'] . '_docs';
774
				if ( method_exists( 'WPCOM_JSON_API_Jetpack_Overrides', $method_name ) ) {
775
					$docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
776
				}
777
778
				if ( ! empty( $docs ) ) {
779
					$return[ $key ] = (object) $this->cast_and_filter(
780
						$value,
781
						/** This filter is documented in class.json-api-endpoints.php */
782
						apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
783
						false,
784
						$for_output
785
					);
786
				} else {
787
					trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
788
				}
789
		}
790
	}
791
792
	function parse_types( $text ) {
793
		if ( ! preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
794
			return 'none';
795
		}
796
797
		$types  = explode( '|', strtolower( $matches[1] ) );
798
		$return = array();
799
		foreach ( $types as $type ) {
800
			foreach ( array(
801
				':' => 'children',
802
				'>' => 'subtype',
803
				'=' => 'default',
804
			) as $operator => $meaning ) {
805
				if ( false !== strpos( $type, $operator ) ) {
806
					$item     = explode( $operator, $type, 2 );
807
					$return[] = array(
808
						'type'   => $item[0],
809
						$meaning => $item[1],
810
					);
811
					continue 2;
812
				}
813
			}
814
			$return[] = compact( 'type' );
815
		}
816
817
		return $return;
818
	}
819
820
	/**
821
	 * Checks if the endpoint is publicly displayable
822
	 */
823
	function is_publicly_documentable() {
824
		return '__do_not_document' !== $this->group && true !== $this->in_testing;
825
	}
826
827
	/**
828
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
829
	 * Echoes HTML.
830
	 */
831
	function document( $show_description = true ) {
832
		global $wpdb;
833
		$original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
834
		unset( $GLOBALS['post'] );
835
836
		$doc = $this->generate_documentation();
837
838
		if ( $show_description ) :
839
			?>
840
<caption>
841
	<h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
842
	<p><?php echo wp_kses_post( $doc['description'] ); ?></p>
843
</caption>
844
845
<?php endif; ?>
846
847
		<?php if ( true === $this->deprecated ) { ?>
848
<p><strong>This endpoint is deprecated in favor of version <?php echo (float) $this->new_version; ?></strong></p>
849
<?php } ?>
850
851
<section class="resource-info">
852
	<h2 id="apidoc-resource-info">Resource Information</h2>
853
854
	<table class="api-doc api-doc-resource-parameters api-doc-resource">
855
856
	<thead>
857
		<tr>
858
			<th class="api-index-title" scope="column">&nbsp;</th>
859
			<th class="api-index-title" scope="column">&nbsp;</th>
860
		</tr>
861
	</thead>
862
	<tbody>
863
864
		<tr class="api-index-item">
865
			<th scope="row" class="parameter api-index-item-title">Method</th>
866
			<td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
867
		</tr>
868
869
		<tr class="api-index-item">
870
			<th scope="row" class="parameter api-index-item-title">URL</th>
871
			<?php
872
			$version = WPCOM_JSON_API__CURRENT_VERSION;
873
			if ( ! empty( $this->max_version ) ) {
874
				$version = $this->max_version;
875
			}
876
			?>
877
			<td class="type api-index-item-title">https://public-api.wordpress.com/rest/v<?php echo (float) $version; ?><?php echo wp_kses_post( $doc['path_labeled'] ); ?></td>
878
		</tr>
879
880
		<tr class="api-index-item">
881
			<th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
882
			<?php
883
			$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'] ) );
884
			?>
885
			<td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
886
		</tr>
887
888
	</tbody>
889
	</table>
890
891
</section>
892
893
		<?php
894
895
		foreach ( array(
896
			'path'     => 'Method Parameters',
897
			'query'    => 'Query Parameters',
898
			'body'     => 'Request Parameters',
899
			'response' => 'Response Parameters',
900
		) as $doc_section_key => $label ) :
901
			$doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][ $doc_section_key ];
902
			if ( ! $doc_section ) {
903
				continue;
904
			}
905
906
			$param_label = strtolower( str_replace( ' ', '-', $label ) );
907
			?>
908
909
<section class="<?php echo $param_label; ?>">
910
911
<h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
912
913
<table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
914
915
<thead>
916
	<tr>
917
		<th class="api-index-title" scope="column">Parameter</th>
918
		<th class="api-index-title" scope="column">Type</th>
919
		<th class="api-index-title" scope="column">Description</th>
920
	</tr>
921
</thead>
922
<tbody>
923
924
			<?php foreach ( $doc_section as $key => $item ) : ?>
925
926
	<tr class="api-index-item">
927
		<th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
928
		<td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
929
		<td class="description api-index-item-body">
930
				<?php
931
932
				$this->generate_doc_description( $item['description'] );
933
934
				?>
935
		</td>
936
	</tr>
937
938
			<?php endforeach; ?>
939
</tbody>
940
</table>
941
</section>
942
<?php endforeach; ?>
943
944
		<?php
945
		if ( 'unset' !== $original_post ) {
946
			$GLOBALS['post'] = $original_post;
947
		}
948
	}
949
950
	function add_http_build_query_to_php_content_example( $matches ) {
951
		$trimmed_match = ltrim( $matches[0] );
952
		$pad           = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
953
		$pad           = ltrim( $pad, ' ' );
954
		$return        = '  ' . str_replace( "\n", "\n  ", $matches[0] );
955
		return " http_build_query({$return}{$pad})";
956
	}
957
958
	/**
959
	 * Recursively generates the <dl>'s to document item descriptions.
960
	 * Echoes HTML.
961
	 */
962
	function generate_doc_description( $item ) {
963
		if ( is_array( $item ) ) :
964
			?>
965
966
		<dl>
967
			<?php	foreach ( $item as $description_key => $description_value ) : ?>
968
969
			<dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
970
			<dd><?php $this->generate_doc_description( $description_value ); ?></dd>
971
972
			<?php	endforeach; ?>
973
974
		</dl>
975
976
			<?php
977
		else :
978
			echo wp_kses_post( $item );
979
		endif;
980
	}
981
982
	/**
983
	 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
984
	 * Echoes HTML.
985
	 */
986
	function generate_documentation() {
987
		$format       = str_replace( '%d', '%s', $this->path );
988
		$path_labeled = $format;
989
		if ( ! empty( $this->path_labels ) ) {
990
			$path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
991
		}
992
		$boolean_arg = array( 'false', 'true' );
993
		$naeloob_arg = array( 'true', 'false' );
994
995
		$doc = array(
996
			'description'  => $this->description,
997
			'method'       => $this->method,
998
			'path_format'  => $this->path,
999
			'path_labeled' => $path_labeled,
1000
			'group'        => $this->group,
1001
			'request'      => array(
1002
				'path'  => array(),
1003
				'query' => array(),
1004
				'body'  => array(),
1005
			),
1006
			'response'     => array(
1007
				'body' => array(),
1008
			),
1009
		);
1010
1011
		foreach ( array(
1012
			'path_labels'     => 'path',
1013
			'query'           => 'query',
1014
			'request_format'  => 'body',
1015
			'response_format' => 'body',
1016
		) as $_property => $doc_item ) {
1017
			foreach ( (array) $this->$_property as $key => $description ) {
1018
				if ( is_array( $description ) ) {
1019
					$description_keys = array_keys( $description );
1020
					if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
1021
						$type = '(bool)';
1022
					} else {
1023
						$type = '(string)';
1024
					}
1025
1026
					if ( 'response_format' !== $_property ) {
1027
						// hack - don't show "(default)" in response format
1028
						reset( $description );
1029
						$description_key                 = key( $description );
1030
						$description[ $description_key ] = "(default) {$description[$description_key]}";
1031
					}
1032
				} else {
1033
					$types   = $this->parse_types( $description );
1034
					$type    = array();
1035
					$default = '';
1036
1037
					if ( 'none' == $types ) {
1038
						$types           = array();
1039
						$types[]['type'] = 'none';
1040
					}
1041
1042
					foreach ( $types as $type_array ) {
1043
						$type[] = $type_array['type'];
1044
						if ( isset( $type_array['default'] ) ) {
1045
							$default = $type_array['default'];
1046
							if ( 'string' === $type_array['type'] ) {
1047
								$default = "'$default'";
1048
							}
1049
						}
1050
					}
1051
					$type                       = '(' . join( '|', $type ) . ')';
1052
					$noop                       = ''; // skip an index in list below
1053
					list( $noop, $description ) = explode( ')', $description, 2 );
1054
					$description                = trim( $description );
1055
					if ( $default ) {
1056
						$description .= " Default: $default.";
1057
					}
1058
				}
1059
1060
				$item = compact( 'type', 'description' );
1061
1062
				if ( 'response_format' === $_property ) {
1063
					$doc['response'][ $doc_item ][ $key ] = $item;
1064
				} else {
1065
					$doc['request'][ $doc_item ][ $key ] = $item;
1066
				}
1067
			}
1068
		}
1069
1070
		return $doc;
1071
	}
1072
1073
	function user_can_view_post( $post_id ) {
1074
		$post = get_post( $post_id );
1075
		if ( ! $post || is_wp_error( $post ) ) {
1076
			return false;
1077
		}
1078
1079 View Code Duplication
		if ( 'inherit' === $post->post_status ) {
1080
			$parent_post     = get_post( $post->post_parent );
1081
			$post_status_obj = get_post_status_object( $parent_post->post_status );
1082
		} else {
1083
			$post_status_obj = get_post_status_object( $post->post_status );
1084
		}
1085
1086
		if ( ! $post_status_obj->public ) {
1087
			if ( is_user_logged_in() ) {
1088
				if ( $post_status_obj->protected ) {
1089
					if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1090
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1091
					}
1092
				} elseif ( $post_status_obj->private ) {
1093
					if ( ! current_user_can( 'read_post', $post->ID ) ) {
1094
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1095
					}
1096
				} elseif ( in_array( $post->post_status, array( 'inherit', 'trash' ) ) ) {
1097
					if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1098
						return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1099
					}
1100
				} elseif ( 'auto-draft' === $post->post_status ) {
1101
					// allow auto-drafts
1102
				} else {
1103
					return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1104
				}
1105
			} else {
1106
				return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1107
			}
1108
		}
1109
1110 View Code Duplication
		if (
1111
			-1 == get_option( 'blog_public' ) &&
1112
			/**
1113
			 * Filter access to a specific post.
1114
			 *
1115
			 * @module json-api
1116
			 *
1117
			 * @since 3.4.0
1118
			 *
1119
			 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1120
			 * @param WP_Post $post Post data.
1121
			 */
1122
			! apply_filters(
1123
				'wpcom_json_api_user_can_view_post',
1124
				current_user_can( 'read_post', $post->ID ),
1125
				$post
0 ignored issues
show
The call to apply_filters() has too many arguments starting with $post.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1126
			)
1127
		) {
1128
			return new WP_Error(
1129
				'unauthorized',
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1130
				'User cannot view post',
1131
				array(
1132
					'status_code' => 403,
1133
					'error'       => 'private_blog',
1134
				)
1135
			);
1136
		}
1137
1138 View Code Duplication
		if ( strlen( $post->post_password ) && ! current_user_can( 'edit_post', $post->ID ) ) {
1139
			return new WP_Error(
1140
				'unauthorized',
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1141
				'User cannot view password protected post',
1142
				array(
1143
					'status_code' => 403,
1144
					'error'       => 'password_protected',
1145
				)
1146
			);
1147
		}
1148
1149
		return true;
1150
	}
1151
1152
	/**
1153
	 * Returns author object.
1154
	 *
1155
	 * @param object $author user ID, user row, WP_User object, comment row, post row
1156
	 * @param bool   $show_email_and_ip output the author's email address and IP address?
1157
	 *
1158
	 * @return object
1159
	 */
1160
	function get_author( $author, $show_email_and_ip = false ) {
1161
		$ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1162
1163
		if ( isset( $author->comment_author_email ) ) {
1164
			$ID          = 0;
1165
			$login       = '';
1166
			$email       = $author->comment_author_email;
1167
			$name        = $author->comment_author;
1168
			$first_name  = '';
1169
			$last_name   = '';
1170
			$URL         = $author->comment_author_url;
1171
			$avatar_URL  = $this->api->get_avatar_url( $author );
1172
			$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1173
			$nice        = '';
1174
			$site_id     = -1;
1175
1176
			// Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1177
			// "&" is the only email/URL character altered by wp_kses()
1178
			foreach ( array( 'email', 'URL' ) as $field ) {
1179
				$$field = str_replace( '&amp;', '&', $$field );
1180
			}
1181
		} else {
1182
			if ( isset( $author->user_id ) && $author->user_id ) {
1183
				$author = $author->user_id;
1184
			} elseif ( isset( $author->user_email ) ) {
1185
				$author = $author->ID;
1186
			} elseif ( isset( $author->post_author ) ) {
1187
				// then $author is a Post Object.
1188
				if ( 0 == $author->post_author ) {
1189
					return null;
1190
				}
1191
				/**
1192
				 * Filter whether the current site is a Jetpack site.
1193
				 *
1194
				 * @module json-api
1195
				 *
1196
				 * @since 3.3.0
1197
				 *
1198
				 * @param bool false Is the current site a Jetpack site. Default to false.
1199
				 * @param int get_current_blog_id() Blog ID.
1200
				 */
1201
				$is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
0 ignored issues
show
The call to apply_filters() has too many arguments starting with get_current_blog_id().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1202
				$post_id    = $author->ID;
1203
				if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1204
					$ID         = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1205
					$email      = get_post_meta( $post_id, '_jetpack_author_email', true );
1206
					$login      = '';
1207
					$name       = get_post_meta( $post_id, '_jetpack_author', true );
1208
					$first_name = '';
1209
					$last_name  = '';
1210
					$URL        = '';
1211
					$nice       = '';
1212
				} else {
1213
					$author = $author->post_author;
1214
				}
1215
			}
1216
1217
			if ( ! isset( $ID ) ) {
1218
				$user = get_user_by( 'id', $author );
1219
				if ( ! $user || is_wp_error( $user ) ) {
1220
					trigger_error( 'Unknown user', E_USER_WARNING );
1221
1222
					return null;
1223
				}
1224
				$ID         = $user->ID;
1225
				$email      = $user->user_email;
1226
				$login      = $user->user_login;
1227
				$name       = $user->display_name;
1228
				$first_name = $user->first_name;
1229
				$last_name  = $user->last_name;
1230
				$URL        = $user->user_url;
1231
				$nice       = $user->user_nicename;
1232
			}
1233
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1234
				$active_blog = get_active_blog_for_user( $ID );
1235
				$site_id     = $active_blog->blog_id;
1236
				if ( $site_id > -1 ) {
1237
					$site_visible = (
1238
						-1 != $active_blog->public ||
1239
						is_private_blog_user( $site_id, get_current_user_id() )
1240
					);
1241
				}
1242
				$profile_URL = "https://en.gravatar.com/{$login}";
1243
			} else {
1244
				$profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1245
				$site_id     = -1;
1246
			}
1247
1248
			$avatar_URL = $this->api->get_avatar_url( $email );
1249
		}
1250
1251
		if ( $show_email_and_ip ) {
1252
			$email      = (string) $email;
1253
			$ip_address = (string) $ip_address;
1254
		} else {
1255
			$email      = false;
1256
			$ip_address = false;
1257
		}
1258
1259
		$author = array(
1260
			'ID'          => (int) $ID,
1261
			'login'       => (string) $login,
1262
			'email'       => $email, // (string|bool)
1263
			'name'        => (string) $name,
1264
			'first_name'  => (string) $first_name,
1265
			'last_name'   => (string) $last_name,
1266
			'nice_name'   => (string) $nice,
1267
			'URL'         => (string) esc_url_raw( $URL ),
1268
			'avatar_URL'  => (string) esc_url_raw( $avatar_URL ),
1269
			'profile_URL' => (string) esc_url_raw( $profile_URL ),
1270
			'ip_address'  => $ip_address, // (string|bool)
1271
		);
1272
1273
		if ( $site_id > -1 ) {
1274
			$author['site_ID']      = (int) $site_id;
1275
			$author['site_visible'] = $site_visible;
1276
		}
1277
1278
		return (object) $author;
1279
	}
1280
1281
	function get_media_item( $media_id ) {
1282
		$media_item = get_post( $media_id );
1283
1284
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1285
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unknown_media'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1286
		}
1287
1288
		$response = array(
1289
			'id'          => (string) $media_item->ID,
1290
			'date'        => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1291
			'parent'      => $media_item->post_parent,
1292
			'link'        => wp_get_attachment_url( $media_item->ID ),
1293
			'title'       => $media_item->post_title,
1294
			'caption'     => $media_item->post_excerpt,
1295
			'description' => $media_item->post_content,
1296
			'metadata'    => wp_get_attachment_metadata( $media_item->ID ),
1297
		);
1298
1299
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1300
			remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1301
			$response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1302
			add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1303
		}
1304
1305
		$response['meta'] = (object) array(
1306
			'links' => (object) array(
1307
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1308
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1309
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1310
			),
1311
		);
1312
1313
		return (object) $response;
1314
	}
1315
1316
	function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1317
1318
		if ( ! $media_item ) {
1319
			$media_item = get_post( $media_id );
1320
		}
1321
1322
		if ( ! $media_item || is_wp_error( $media_item ) ) {
1323
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unknown_media'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1324
		}
1325
1326
		$attachment_file = get_attached_file( $media_item->ID );
1327
1328
		$file      = basename( $attachment_file ? $attachment_file : $file );
1329
		$file_info = pathinfo( $file );
1330
		$ext       = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1331
1332
		$response = array(
1333
			'ID'          => $media_item->ID,
1334
			'URL'         => wp_get_attachment_url( $media_item->ID ),
1335
			'guid'        => $media_item->guid,
1336
			'date'        => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1337
			'post_ID'     => $media_item->post_parent,
1338
			'author_ID'   => (int) $media_item->post_author,
1339
			'file'        => $file,
1340
			'mime_type'   => $media_item->post_mime_type,
1341
			'extension'   => $ext,
1342
			'title'       => $media_item->post_title,
1343
			'caption'     => $media_item->post_excerpt,
1344
			'description' => $media_item->post_content,
1345
			'alt'         => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1346
			'icon'        => wp_mime_type_icon( $media_item->ID ),
1347
			'thumbnails'  => array(),
1348
		);
1349
1350
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1351
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1352
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1353
				$response['height'] = $metadata['height'];
1354
				$response['width']  = $metadata['width'];
1355
			}
1356
1357
			if ( isset( $metadata['sizes'] ) ) {
1358
				/**
1359
				 * Filter the thumbnail sizes available for each attachment ID.
1360
				 *
1361
				 * @module json-api
1362
				 *
1363
				 * @since 3.9.0
1364
				 *
1365
				 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1366
				 * @param string $media_id Attachment ID.
1367
				 */
1368
				$sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
0 ignored issues
show
The call to apply_filters() has too many arguments starting with $media_item->ID.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1369 View Code Duplication
				if ( is_array( $sizes ) ) {
1370
					foreach ( $sizes as $size => $size_details ) {
1371
						$response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1372
					}
1373
					/**
1374
					 * Filter the thumbnail URLs for attachment files.
1375
					 *
1376
					 * @module json-api
1377
					 *
1378
					 * @since 7.1.0
1379
					 *
1380
					 * @param array $metadata['sizes'] Array with thumbnail sizes as keys and URLs as values.
1381
					 */
1382
					$response['thumbnails'] = apply_filters( 'rest_api_thumbnail_size_urls', $response['thumbnails'] );
1383
				}
1384
			}
1385
1386
			if ( isset( $metadata['image_meta'] ) ) {
1387
				$response['exif'] = $metadata['image_meta'];
1388
			}
1389
		}
1390
1391
		if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1392
			$metadata           = wp_get_attachment_metadata( $media_item->ID );
1393
			$response['length'] = $metadata['length'];
1394
			$response['exif']   = $metadata;
1395
		}
1396
1397
		$is_video = false;
1398
1399
		if (
1400
			in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1401
			||
1402
			$response['mime_type'] === 'video/videopress'
1403
		) {
1404
			$is_video = true;
1405
		}
1406
1407
		if ( $is_video ) {
1408
			$metadata = wp_get_attachment_metadata( $media_item->ID );
1409
1410
			if ( isset( $metadata['height'], $metadata['width'] ) ) {
1411
				$response['height'] = $metadata['height'];
1412
				$response['width']  = $metadata['width'];
1413
			}
1414
1415
			if ( isset( $metadata['length'] ) ) {
1416
				$response['length'] = $metadata['length'];
1417
			}
1418
1419
			// add VideoPress info
1420
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1421
				$info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1422
1423
				// If we failed to get VideoPress info, but it exists in the meta data (for some reason)
1424
				// then let's use that.
1425
				if ( false === $info && isset( $metadata['videopress'] ) ) {
1426
					$info = (object) $metadata['videopress'];
1427
				}
1428
1429
				// Thumbnails
1430 View Code Duplication
				if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1431
					$response['thumbnails'] = array(
1432
						'fmt_hd'  => '',
1433
						'fmt_dvd' => '',
1434
						'fmt_std' => '',
1435
					);
1436
					foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1437
						if ( video_format_done( $info, $size ) ) {
1438
							$response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1439
						} else {
1440
							unset( $response['thumbnails'][ $size ] );
1441
						}
1442
					}
1443
				}
1444
1445
				// If we didn't get VideoPress information (for some reason) then let's
1446
				// not try and include it in the response.
1447
				if ( isset( $info->guid ) ) {
1448
					$response['videopress_guid']            = $info->guid;
1449
					$response['videopress_processing_done'] = true;
1450
					if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1451
						$response['videopress_processing_done'] = false;
1452
					}
1453
				}
1454
			}
1455
		}
1456
1457
		$response['thumbnails'] = (object) $response['thumbnails'];
1458
1459
		$response['meta'] = (object) array(
1460
			'links' => (object) array(
1461
				'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1462
				'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1463
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1464
			),
1465
		);
1466
1467
		// add VideoPress link to the meta
1468
		if ( isset( $response['videopress_guid'] ) ) {
1469 View Code Duplication
			if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1470
				$response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1471
			}
1472
		}
1473
1474 View Code Duplication
		if ( $media_item->post_parent > 0 ) {
1475
			$response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1476
		}
1477
1478
		return (object) $response;
1479
	}
1480
1481
	function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1482
1483
		$taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1484
		// keep updating this function
1485
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1486
			return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unknown_taxonomy'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1487
		}
1488
1489
		return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1490
	}
1491
1492
	function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1493
		// Permissions
1494
		switch ( $context ) {
1495
			case 'edit':
1496
				$tax = get_taxonomy( $taxonomy_type );
1497
				if ( ! current_user_can( $tax->cap->edit_terms ) ) {
1498
					return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1499
				}
1500
				break;
1501
			case 'display':
1502 View Code Duplication
				if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1503
					return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1504
				}
1505
				break;
1506
			default:
1507
				return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'invalid_context'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1508
		}
1509
1510
		$response                = array();
1511
		$response['ID']          = (int) $taxonomy->term_id;
1512
		$response['name']        = (string) $taxonomy->name;
1513
		$response['slug']        = (string) $taxonomy->slug;
1514
		$response['description'] = (string) $taxonomy->description;
1515
		$response['post_count']  = (int) $taxonomy->count;
1516
		$response['feed_url']    = get_term_feed_link( $taxonomy->term_id, $taxonomy_type );
1517
1518
		if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1519
			$response['parent'] = (int) $taxonomy->parent;
1520
		}
1521
1522
		$response['meta'] = (object) array(
1523
			'links' => (object) array(
1524
				'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1525
				'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1526
				'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1527
			),
1528
		);
1529
1530
		return (object) $response;
1531
	}
1532
1533
	/**
1534
	 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1535
	 *
1536
	 * @param $date_gmt (string) GMT datetime string.
1537
	 * @param $date (string) Optional.  Used to calculate the offset from GMT.
1538
	 *
1539
	 * @return string
1540
	 */
1541
	function format_date( $date_gmt, $date = null ) {
1542
		return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1543
	}
1544
1545
	/**
1546
	 * Parses a date string and returns the local and GMT representations
1547
	 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1548
	 * timezones or offsets. If the parsed datetime was not localized to a
1549
	 * particular timezone or offset we will assume it was given in GMT
1550
	 * relative to now and will convert it to local time using either the
1551
	 * timezone set in the options table for the blog or the GMT offset.
1552
	 *
1553
	 * @param datetime string $date_string Date to parse.
1554
	 *
1555
	 * @return array( $local_time_string, $gmt_time_string )
1556
	 */
1557
	public function parse_date( $date_string ) {
1558
		$date_string_info = date_parse( $date_string );
1559
		if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1560
			// Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1561
			if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1562
				$dt_utc   = new DateTime( $date_string );
1563
				$dt_local = clone $dt_utc;
1564
				$dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1565
				return array(
1566
					(string) $dt_local->format( 'Y-m-d H:i:s' ),
1567
					(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1568
				);
1569
			}
1570
1571
			// It's parseable but no TZ info so assume UTC.
1572
			$dt_utc   = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1573
			$dt_local = clone $dt_utc;
1574
		} else {
1575
			// Could not parse time, use now in UTC.
1576
			$dt_utc   = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1577
			$dt_local = clone $dt_utc;
1578
		}
1579
1580
		$dt_local->setTimezone( wp_timezone() );
1581
1582
		return array(
1583
			(string) $dt_local->format( 'Y-m-d H:i:s' ),
1584
			(string) $dt_utc->format( 'Y-m-d H:i:s' ),
1585
		);
1586
	}
1587
1588
	// Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1589
	function load_theme_functions() {
1590
		if ( false === defined( 'STYLESHEETPATH' ) ) {
1591
			wp_templating_constants();
1592
		}
1593
1594
		// bail if we've done this already (can happen when calling /batch endpoint)
1595
		if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) ) {
1596
			return;
1597
		}
1598
1599
		// VIP context loading is handled elsewhere, so bail to prevent
1600
		// duplicate loading. See `switch_to_blog_and_validate_user()`
1601
		if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1602
			return;
1603
		}
1604
1605
		define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1606
1607
		// the theme info we care about is found either within functions.php or one of the jetpack files.
1608
		$function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1609
1610
		$copy_dirs = array( get_template_directory() );
1611
1612
		// Is this a child theme? Load the child theme's functions file.
1613
		if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1614
			foreach ( $function_files as $function_file ) {
1615
				if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1616
					require_once get_stylesheet_directory() . $function_file;
1617
				}
1618
			}
1619
			$copy_dirs[] = get_stylesheet_directory();
1620
		}
1621
1622
		foreach ( $function_files as $function_file ) {
1623
			if ( file_exists( get_template_directory() . $function_file ) ) {
1624
				require_once get_template_directory() . $function_file;
1625
			}
1626
		}
1627
1628
		// add inc/wpcom.php and/or includes/wpcom.php
1629
		wpcom_load_theme_compat_file();
1630
1631
		// Enable including additional directories or files in actions to be copied
1632
		$copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1633
1634
		// since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1635
		$this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1636
1637
		/**
1638
		 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1639
		 *
1640
		 * The REST API does not load the theme when processing requests.
1641
		 * To enable theme-based functionality, the API will load the '/functions.php',
1642
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1643
		 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1644
		 *
1645
		 * @module json-api
1646
		 *
1647
		 * @since 3.2.0
1648
		 */
1649
		do_action( 'restapi_theme_after_setup_theme' );
1650
		$this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1651
1652
		/**
1653
		 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1654
		 *
1655
		 * The REST API does not load the theme when processing requests.
1656
		 * To enable theme-based functionality, the API will load the '/functions.php',
1657
		 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1658
		 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1659
		 *
1660
		 * @module json-api
1661
		 *
1662
		 * @since 3.2.0
1663
		 */
1664
		do_action( 'restapi_theme_init' );
1665
	}
1666
1667
	function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1668
		global $wp_filter;
1669
		foreach ( $wp_filter as $hook => $actions ) {
1670
1671
			if ( $from_hook != $hook ) {
1672
				continue;
1673
			}
1674
			if ( ! has_action( $hook ) ) {
1675
				continue;
1676
			}
1677
1678
			foreach ( $actions as $priority => $callbacks ) {
1679
				foreach ( $callbacks as $callback_key => $callback_data ) {
1680
					$callback = $callback_data['function'];
1681
1682
					// use reflection api to determine filename where function is defined
1683
					$reflection = $this->get_reflection( $callback );
1684
1685
					if ( false !== $reflection ) {
1686
						$file_name = $reflection->getFileName();
1687
						foreach ( $base_paths as $base_path ) {
1688
1689
							// only copy hooks with functions which are part of the specified files
1690
							if ( 0 === strpos( $file_name, $base_path ) ) {
1691
								add_action(
1692
									$to_hook,
1693
									$callback_data['function'],
1694
									$priority,
1695
									$callback_data['accepted_args']
1696
								);
1697
							}
1698
						}
1699
					}
1700
				}
1701
			}
1702
		}
1703
	}
1704
1705
	function get_reflection( $callback ) {
1706
		if ( is_array( $callback ) ) {
1707
			list( $class, $method ) = $callback;
1708
			return new ReflectionMethod( $class, $method );
1709
		}
1710
1711
		if ( is_string( $callback ) && strpos( $callback, '::' ) !== false ) {
1712
			list( $class, $method ) = explode( '::', $callback );
1713
			return new ReflectionMethod( $class, $method );
1714
		}
1715
1716
		if ( method_exists( $callback, "__invoke" ) ) {
1717
			return new ReflectionMethod( $callback, "__invoke" );
1718
		}
1719
1720
		if ( is_string( $callback ) && strpos( $callback, '::' ) == false && function_exists( $callback ) ) {
1721
			return new ReflectionFunction( $callback );
1722
		}
1723
1724
		return false;
1725
	}
1726
1727
	/**
1728
	 * Check whether a user can view or edit a post type
1729
	 *
1730
	 * @param string $post_type              post type to check
1731
	 * @param string $context                'display' or 'edit'
1732
	 * @return bool
1733
	 */
1734 View Code Duplication
	function current_user_can_access_post_type( $post_type, $context = 'display' ) {
1735
		$post_type_object = get_post_type_object( $post_type );
1736
		if ( ! $post_type_object ) {
1737
			return false;
1738
		}
1739
1740
		switch ( $context ) {
1741
			case 'edit':
1742
				return current_user_can( $post_type_object->cap->edit_posts );
1743
			case 'display':
1744
				return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1745
			default:
1746
				return false;
1747
		}
1748
	}
1749
1750 View Code Duplication
	function is_post_type_allowed( $post_type ) {
1751
		// if the post type is empty, that's fine, WordPress will default to post
1752
		if ( empty( $post_type ) ) {
1753
			return true;
1754
		}
1755
1756
		// allow special 'any' type
1757
		if ( 'any' == $post_type ) {
1758
			return true;
1759
		}
1760
1761
		// check for allowed types
1762
		if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1763
			return true;
1764
		}
1765
1766
		if ( $post_type_object = get_post_type_object( $post_type ) ) {
1767
			if ( ! empty( $post_type_object->show_in_rest ) ) {
1768
				return $post_type_object->show_in_rest;
1769
			}
1770
			if ( ! empty( $post_type_object->publicly_queryable ) ) {
1771
				return $post_type_object->publicly_queryable;
1772
			}
1773
		}
1774
1775
		return ! empty( $post_type_object->public );
1776
	}
1777
1778
	/**
1779
	 * Gets the whitelisted post types that JP should allow access to.
1780
	 *
1781
	 * @return array Whitelisted post types.
1782
	 */
1783 View Code Duplication
	protected function _get_whitelisted_post_types() {
1784
		$allowed_types = array( 'post', 'page', 'revision' );
1785
1786
		/**
1787
		 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1788
		 *
1789
		 * @module json-api
1790
		 *
1791
		 * @since 2.2.3
1792
		 *
1793
		 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1794
		 */
1795
		$allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1796
1797
		return array_unique( $allowed_types );
1798
	}
1799
1800
	function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1801
1802
		add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1803
1804
		$media_ids             = $errors = array();
1805
		$user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
0 ignored issues
show
The method is_authorized_with_upload_token() does not seem to exist on object<WPCOM_JSON_API>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1806
		$media_attrs           = array_values( $media_attrs ); // reset the keys
1807
		$i                     = 0;
1808
1809
		if ( ! empty( $media_files ) ) {
1810
			$this->api->trap_wp_die( 'upload_error' );
1811
			foreach ( $media_files as $media_item ) {
1812
				$_FILES['.api.media.item.'] = $media_item;
1813 View Code Duplication
				if ( ! $user_can_upload_files ) {
1814
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1815
				} else {
1816
					if ( $force_parent_id ) {
1817
						$parent_id = absint( $force_parent_id );
1818
					} elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1819
						$parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1820
					} else {
1821
						$parent_id = 0;
1822
					}
1823
					$media_id = media_handle_upload( '.api.media.item.', $parent_id );
1824
				}
1825
				if ( is_wp_error( $media_id ) ) {
1826
					$errors[ $i ]['file']    = $media_item['name'];
1827
					$errors[ $i ]['error']   = $media_id->get_error_code();
1828
					$errors[ $i ]['message'] = $media_id->get_error_message();
1829
				} else {
1830
					$media_ids[ $i ] = $media_id;
1831
				}
1832
1833
				$i++;
1834
			}
1835
			$this->api->trap_wp_die( null );
1836
			unset( $_FILES['.api.media.item.'] );
1837
		}
1838
1839
		if ( ! empty( $media_urls ) ) {
1840
			foreach ( $media_urls as $url ) {
1841 View Code Duplication
				if ( ! $user_can_upload_files ) {
1842
					$media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1843
				} else {
1844
					if ( $force_parent_id ) {
1845
						$parent_id = absint( $force_parent_id );
1846
					} elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1847
						$parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1848
					} else {
1849
						$parent_id = 0;
1850
					}
1851
					$media_id = $this->handle_media_sideload( $url, $parent_id );
1852
				}
1853
				if ( is_wp_error( $media_id ) ) {
1854
					$errors[ $i ] = array(
1855
						'file'    => $url,
1856
						'error'   => $media_id->get_error_code(),
1857
						'message' => $media_id->get_error_message(),
1858
					);
1859
				} elseif ( ! empty( $media_id ) ) {
1860
					$media_ids[ $i ] = $media_id;
1861
				}
1862
1863
				$i++;
1864
			}
1865
		}
1866
1867
		if ( ! empty( $media_attrs ) ) {
1868
			foreach ( $media_ids as $index => $media_id ) {
1869
				if ( empty( $media_attrs[ $index ] ) ) {
1870
					continue;
1871
				}
1872
1873
				$attrs  = $media_attrs[ $index ];
1874
				$insert = array();
1875
1876
				// Attributes: Title, Caption, Description
1877
1878
				if ( isset( $attrs['title'] ) ) {
1879
					$insert['post_title'] = $attrs['title'];
1880
				}
1881
1882
				if ( isset( $attrs['caption'] ) ) {
1883
					$insert['post_excerpt'] = $attrs['caption'];
1884
				}
1885
1886
				if ( isset( $attrs['description'] ) ) {
1887
					$insert['post_content'] = $attrs['description'];
1888
				}
1889
1890
				if ( ! empty( $insert ) ) {
1891
					$insert['ID'] = $media_id;
1892
					wp_update_post( (object) $insert );
1893
				}
1894
1895
				// Attributes: Alt
1896
1897 View Code Duplication
				if ( isset( $attrs['alt'] ) ) {
1898
					$alt = wp_strip_all_tags( $attrs['alt'], true );
1899
					update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1900
				}
1901
1902
				// Attributes: Artist, Album
1903
1904
				$id3_meta = array();
1905
1906 View Code Duplication
				foreach ( array( 'artist', 'album' ) as $key ) {
1907
					if ( isset( $attrs[ $key ] ) ) {
1908
						$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1909
					}
1910
				}
1911
1912
				if ( ! empty( $id3_meta ) ) {
1913
					// Before updating metadata, ensure that the item is audio
1914
					$item = $this->get_media_item_v1_1( $media_id );
1915
					if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1916
						wp_update_attachment_metadata( $media_id, $id3_meta );
1917
					}
1918
				}
1919
			}
1920
		}
1921
1922
		return array(
1923
			'media_ids' => $media_ids,
1924
			'errors'    => $errors,
1925
		);
1926
1927
	}
1928
1929
	function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1930
		if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) ) {
1931
			return false;
1932
		}
1933
1934
		// if we didn't get a URL, let's bail
1935
		$parsed = wp_parse_url( $url );
1936
		if ( empty( $parsed ) ) {
1937
			return false;
1938
		}
1939
1940
		$tmp = download_url( $url );
1941
		if ( is_wp_error( $tmp ) ) {
1942
			return $tmp;
1943
		}
1944
1945
		// First check to see if we get a mime-type match by file, otherwise, check to
1946
		// see if WordPress supports this file as an image. If neither, then it is not supported.
1947 View Code Duplication
		if ( ! $this->is_file_supported_for_sideloading( $tmp ) || 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1948
			@unlink( $tmp );
1949
			return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'invalid_input'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1950
		}
1951
1952
		// emulate a $_FILES entry
1953
		$file_array = array(
1954
			'name'     => basename( wp_parse_url( $url, PHP_URL_PATH ) ),
0 ignored issues
show
The call to wp_parse_url() has too many arguments starting with PHP_URL_PATH.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1955
			'tmp_name' => $tmp,
1956
		);
1957
1958
		$id = media_handle_sideload( $file_array, $parent_post_id );
1959
		if ( file_exists( $tmp ) ) {
1960
			@unlink( $tmp );
1961
		}
1962
1963
		if ( is_wp_error( $id ) ) {
1964
			return $id;
1965
		}
1966
1967
		if ( ! $id || ! is_int( $id ) ) {
1968
			return false;
1969
		}
1970
1971
		return $id;
1972
	}
1973
1974
	/**
1975
	 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1976
	 *
1977
	 * @param string $file Path to file to get its mime type.
1978
	 *
1979
	 * @return bool
1980
	 */
1981
	protected function is_file_supported_for_sideloading( $file ) {
1982
		return jetpack_is_file_supported_for_sideloading( $file );
1983
	}
1984
1985
	function allow_video_uploads( $mimes ) {
1986
		// if we are on Jetpack, bail - Videos are already allowed
1987
		if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
1988
			return $mimes;
1989
		}
1990
1991
		// extra check that this filter is only ever applied during REST API requests
1992
		if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1993
			return $mimes;
1994
		}
1995
1996
		// bail early if they already have the upgrade..
1997
		if ( get_option( 'video_upgrade' ) == '1' ) {
1998
			return $mimes;
1999
		}
2000
2001
		// lets whitelist to only specific clients right now
2002
		$clients_allowed_video_uploads = array();
2003
		/**
2004
		 * Filter the list of whitelisted video clients.
2005
		 *
2006
		 * @module json-api
2007
		 *
2008
		 * @since 3.2.0
2009
		 *
2010
		 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
2011
		 */
2012
		$clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
2013
		if ( ! in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
2014
			return $mimes;
2015
		}
2016
2017
		$mime_list = wp_get_mime_types();
2018
2019
		$video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
2020
		/**
2021
		 * Filter the video filetypes allowed on the site.
2022
		 *
2023
		 * @module json-api
2024
		 *
2025
		 * @since 3.2.0
2026
		 *
2027
		 * @param array $video_exts Array of video filetypes allowed on the site.
2028
		 */
2029
		$video_exts  = apply_filters( 'video_upload_filetypes', $video_exts );
2030
		$video_mimes = array();
2031
2032
		if ( ! empty( $video_exts ) ) {
2033
			foreach ( $video_exts as $ext ) {
2034
				foreach ( $mime_list as $ext_pattern => $mime ) {
2035
					if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false ) {
2036
						$video_mimes[ $ext_pattern ] = $mime;
2037
					}
2038
				}
2039
			}
2040
2041
			$mimes = array_merge( $mimes, $video_mimes );
2042
		}
2043
2044
		return $mimes;
2045
	}
2046
2047
	function is_current_site_multi_user() {
2048
		$users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2049
		if ( false === $users ) {
2050
			$user_query = new WP_User_Query(
2051
				array(
2052
					'blog_id' => get_current_blog_id(),
2053
					'fields'  => 'ID',
2054
				)
2055
			);
2056
			$users      = (int) $user_query->get_total();
2057
			wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2058
		}
2059
		return $users > 1;
2060
	}
2061
2062
	function allows_cross_origin_requests() {
2063
		return 'GET' == $this->method || $this->allow_cross_origin_request;
2064
	}
2065
2066
	function allows_unauthorized_requests( $origin, $complete_access_origins ) {
2067
		return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2068
	}
2069
2070
	/**
2071
	 * Whether this endpoint accepts site based authentication for the current request.
2072
	 *
2073
	 * @since 9.1.0
2074
	 *
2075
	 * @return bool true, if Jetpack blog token is used and `allow_jetpack_site_auth` is true,
2076
	 * false otherwise.
2077
	 */
2078
	public function accepts_site_based_authentication() {
2079
		return $this->allow_jetpack_site_auth &&
2080
			$this->api->is_jetpack_authorized_for_site();
2081
	}
2082
2083
	function get_platform() {
2084
		return wpcom_get_sal_platform( $this->api->token_details );
2085
	}
2086
2087
	/**
2088
	 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2089
	 * response from the WPCOM API, or potentially go to the Jetpack blog.
2090
	 *
2091
	 * Override this method if you want to do something different.
2092
	 *
2093
	 * @param  int $blog_id
2094
	 * @return bool
2095
	 */
2096
	function force_wpcom_request( $blog_id ) {
2097
		return false;
2098
	}
2099
2100
	/**
2101
	 * Get an array of all valid AMP origins for a blog's siteurl.
2102
	 *
2103
	 * @param string $siteurl Origin url of the API request.
2104
	 * @return array
2105
	 */
2106
	public function get_amp_cache_origins( $siteurl ) {
2107
		$host = parse_url( $siteurl, PHP_URL_HOST );
2108
2109
		/*
2110
		 * From AMP docs:
2111
		 * "When possible, the Google AMP Cache will create a subdomain for each AMP document's domain by first converting it
2112
		 * from IDN (punycode) to UTF-8. The caches replaces every - (dash) with -- (2 dashes) and replace every . (dot) with
2113
		 * - (dash). For example, pub.com will map to pub-com.cdn.ampproject.org."
2114
		 */
2115
		if ( function_exists( 'idn_to_utf8' ) ) {
2116
			// The third parameter is set explicitly to prevent issues with newer PHP versions compiled with an old ICU version.
2117
			// phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated
2118
			$host = idn_to_utf8( $host, IDNA_DEFAULT, defined( 'INTL_IDNA_VARIANT_UTS46' ) ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003 );
2119
		}
2120
		$subdomain = str_replace( array( '-', '.' ), array( '--', '-' ), $host );
2121
		return array(
2122
			$siteurl,
2123
			// Google AMP Cache (legacy).
2124
			'https://cdn.ampproject.org',
2125
			// Google AMP Cache subdomain.
2126
			sprintf( 'https://%s.cdn.ampproject.org', $subdomain ),
2127
			// Cloudflare AMP Cache.
2128
			sprintf( 'https://%s.amp.cloudflare.com', $subdomain ),
2129
			// Bing AMP Cache.
2130
			sprintf( 'https://%s.bing-amp.com', $subdomain ),
2131
		);
2132
	}
2133
2134
	/**
2135
	 * Return endpoint response
2136
	 *
2137
	 * @param string $path ... determined by ->$path.
2138
	 *
2139
	 * @return array|WP_Error
2140
	 *  falsy: HTTP 500, no response body
2141
	 *  WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2142
	 *  $data: HTTP 200, json_encode( $data ) response body
2143
	 */
2144
	abstract public function callback( $path = '' );
2145
2146
2147
}
2148
2149
require_once dirname( __FILE__ ) . '/json-endpoints.php';
2150