PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 8.2.0.1
Jetpack – WP Security, Backup, Speed, & Growth v8.2.0.1
15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / class.json-api-endpoints.php
jetpack Last commit date
3rd-party 6 years ago _inc 6 years ago bin 6 years ago css 6 years ago extensions 6 years ago images 6 years ago json-endpoints 6 years ago languages 6 years ago modules 6 years ago sal 6 years ago src 6 years ago vendor 6 years ago views 7 years ago .svnignore 12 years ago CODE-OF-CONDUCT.md 9 years ago changelog.txt 6 years ago class.frame-nonce-preview.php 6 years ago class.jetpack-admin.php 6 years ago class.jetpack-affiliate.php 6 years ago class.jetpack-autoupdate.php 6 years ago class.jetpack-bbpress-json-api-compat.php 6 years ago class.jetpack-cli.php 6 years ago class.jetpack-client-server.php 6 years ago class.jetpack-connection-banner.php 6 years ago class.jetpack-data.php 6 years ago class.jetpack-debugger.php 7 years ago class.jetpack-error.php 10 years ago class.jetpack-gutenberg.php 6 years ago class.jetpack-heartbeat.php 6 years ago class.jetpack-idc.php 6 years ago class.jetpack-ixr-client.php 6 years ago class.jetpack-modules-list-table.php 6 years ago class.jetpack-network-sites-list-table.php 6 years ago class.jetpack-network.php 6 years ago class.jetpack-plan.php 6 years ago class.jetpack-post-images.php 6 years ago class.jetpack-twitter-cards.php 6 years ago class.jetpack-user-agent.php 6 years ago class.jetpack-xmlrpc-server.php 6 years ago class.jetpack.php 6 years ago class.json-api-endpoints.php 6 years ago class.json-api.php 6 years ago class.photon.php 6 years ago composer.json 6 years ago functions.compat.php 6 years ago functions.cookies.php 6 years ago functions.gallery.php 6 years ago functions.global.php 6 years ago functions.opengraph.php 6 years ago functions.photon.php 6 years ago jest.config.js 6 years ago jetpack.php 6 years ago json-api-config.php 10 years ago json-endpoints.php 7 years ago load-jetpack.php 6 years ago locales.php 7 years ago readme.txt 6 years ago require-lib.php 6 years ago uninstall.php 6 years ago wpml-config.xml 10 years ago
class.json-api-endpoints.php
2088 lines
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 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 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 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 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 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 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 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 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 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 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 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 default:
762 $method_name = $type['type'] . '_docs';
763 if ( method_exists( 'WPCOM_JSON_API_Jetpack_Overrides', $method_name ) ) {
764 $docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
765 }
766
767 if ( ! empty( $docs ) ) {
768 $return[ $key ] = (object) $this->cast_and_filter(
769 $value,
770 /** This filter is documented in class.json-api-endpoints.php */
771 apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
772 false,
773 $for_output
774 );
775 } else {
776 trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
777 }
778 }
779 }
780
781 function parse_types( $text ) {
782 if ( ! preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
783 return 'none';
784 }
785
786 $types = explode( '|', strtolower( $matches[1] ) );
787 $return = array();
788 foreach ( $types as $type ) {
789 foreach ( array(
790 ':' => 'children',
791 '>' => 'subtype',
792 '=' => 'default',
793 ) as $operator => $meaning ) {
794 if ( false !== strpos( $type, $operator ) ) {
795 $item = explode( $operator, $type, 2 );
796 $return[] = array(
797 'type' => $item[0],
798 $meaning => $item[1],
799 );
800 continue 2;
801 }
802 }
803 $return[] = compact( 'type' );
804 }
805
806 return $return;
807 }
808
809 /**
810 * Checks if the endpoint is publicly displayable
811 */
812 function is_publicly_documentable() {
813 return '__do_not_document' !== $this->group && true !== $this->in_testing;
814 }
815
816 /**
817 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
818 * Echoes HTML.
819 */
820 function document( $show_description = true ) {
821 global $wpdb;
822 $original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
823 unset( $GLOBALS['post'] );
824
825 $doc = $this->generate_documentation();
826
827 if ( $show_description ) :
828 ?>
829 <caption>
830 <h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
831 <p><?php echo wp_kses_post( $doc['description'] ); ?></p>
832 </caption>
833
834 <?php endif; ?>
835
836 <?php if ( true === $this->deprecated ) { ?>
837 <p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $this->new_version ); ?></strong></p>
838 <?php } ?>
839
840 <section class="resource-info">
841 <h2 id="apidoc-resource-info">Resource Information</h2>
842
843 <table class="api-doc api-doc-resource-parameters api-doc-resource">
844
845 <thead>
846 <tr>
847 <th class="api-index-title" scope="column">&nbsp;</th>
848 <th class="api-index-title" scope="column">&nbsp;</th>
849 </tr>
850 </thead>
851 <tbody>
852
853 <tr class="api-index-item">
854 <th scope="row" class="parameter api-index-item-title">Method</th>
855 <td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
856 </tr>
857
858 <tr class="api-index-item">
859 <th scope="row" class="parameter api-index-item-title">URL</th>
860 <?php
861 $version = WPCOM_JSON_API__CURRENT_VERSION;
862 if ( ! empty( $this->max_version ) ) {
863 $version = $this->max_version;
864 }
865 ?>
866 <td class="type api-index-item-title">https://public-api.wordpress.com/rest/v<?php echo floatval( $version ); ?><?php echo wp_kses_post( $doc['path_labeled'] ); ?></td>
867 </tr>
868
869 <tr class="api-index-item">
870 <th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
871 <?php
872 $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'] ) );
873 ?>
874 <td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
875 </tr>
876
877 </tbody>
878 </table>
879
880 </section>
881
882 <?php
883
884 foreach ( array(
885 'path' => 'Method Parameters',
886 'query' => 'Query Parameters',
887 'body' => 'Request Parameters',
888 'response' => 'Response Parameters',
889 ) as $doc_section_key => $label ) :
890 $doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][ $doc_section_key ];
891 if ( ! $doc_section ) {
892 continue;
893 }
894
895 $param_label = strtolower( str_replace( ' ', '-', $label ) );
896 ?>
897
898 <section class="<?php echo $param_label; ?>">
899
900 <h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
901
902 <table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
903
904 <thead>
905 <tr>
906 <th class="api-index-title" scope="column">Parameter</th>
907 <th class="api-index-title" scope="column">Type</th>
908 <th class="api-index-title" scope="column">Description</th>
909 </tr>
910 </thead>
911 <tbody>
912
913 <?php foreach ( $doc_section as $key => $item ) : ?>
914
915 <tr class="api-index-item">
916 <th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
917 <td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
918 <td class="description api-index-item-body">
919 <?php
920
921 $this->generate_doc_description( $item['description'] );
922
923 ?>
924 </td>
925 </tr>
926
927 <?php endforeach; ?>
928 </tbody>
929 </table>
930 </section>
931 <?php endforeach; ?>
932
933 <?php
934 if ( 'unset' !== $original_post ) {
935 $GLOBALS['post'] = $original_post;
936 }
937 }
938
939 function add_http_build_query_to_php_content_example( $matches ) {
940 $trimmed_match = ltrim( $matches[0] );
941 $pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
942 $pad = ltrim( $pad, ' ' );
943 $return = ' ' . str_replace( "\n", "\n ", $matches[0] );
944 return " http_build_query({$return}{$pad})";
945 }
946
947 /**
948 * Recursively generates the <dl>'s to document item descriptions.
949 * Echoes HTML.
950 */
951 function generate_doc_description( $item ) {
952 if ( is_array( $item ) ) :
953 ?>
954
955 <dl>
956 <?php foreach ( $item as $description_key => $description_value ) : ?>
957
958 <dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
959 <dd><?php $this->generate_doc_description( $description_value ); ?></dd>
960
961 <?php endforeach; ?>
962
963 </dl>
964
965 <?php
966 else :
967 echo wp_kses_post( $item );
968 endif;
969 }
970
971 /**
972 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
973 * Echoes HTML.
974 */
975 function generate_documentation() {
976 $format = str_replace( '%d', '%s', $this->path );
977 $path_labeled = $format;
978 if ( ! empty( $this->path_labels ) ) {
979 $path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
980 }
981 $boolean_arg = array( 'false', 'true' );
982 $naeloob_arg = array( 'true', 'false' );
983
984 $doc = array(
985 'description' => $this->description,
986 'method' => $this->method,
987 'path_format' => $this->path,
988 'path_labeled' => $path_labeled,
989 'group' => $this->group,
990 'request' => array(
991 'path' => array(),
992 'query' => array(),
993 'body' => array(),
994 ),
995 'response' => array(
996 'body' => array(),
997 ),
998 );
999
1000 foreach ( array(
1001 'path_labels' => 'path',
1002 'query' => 'query',
1003 'request_format' => 'body',
1004 'response_format' => 'body',
1005 ) as $_property => $doc_item ) {
1006 foreach ( (array) $this->$_property as $key => $description ) {
1007 if ( is_array( $description ) ) {
1008 $description_keys = array_keys( $description );
1009 if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
1010 $type = '(bool)';
1011 } else {
1012 $type = '(string)';
1013 }
1014
1015 if ( 'response_format' !== $_property ) {
1016 // hack - don't show "(default)" in response format
1017 reset( $description );
1018 $description_key = key( $description );
1019 $description[ $description_key ] = "(default) {$description[$description_key]}";
1020 }
1021 } else {
1022 $types = $this->parse_types( $description );
1023 $type = array();
1024 $default = '';
1025
1026 if ( 'none' == $types ) {
1027 $types = array();
1028 $types[]['type'] = 'none';
1029 }
1030
1031 foreach ( $types as $type_array ) {
1032 $type[] = $type_array['type'];
1033 if ( isset( $type_array['default'] ) ) {
1034 $default = $type_array['default'];
1035 if ( 'string' === $type_array['type'] ) {
1036 $default = "'$default'";
1037 }
1038 }
1039 }
1040 $type = '(' . join( '|', $type ) . ')';
1041 $noop = ''; // skip an index in list below
1042 list( $noop, $description ) = explode( ')', $description, 2 );
1043 $description = trim( $description );
1044 if ( $default ) {
1045 $description .= " Default: $default.";
1046 }
1047 }
1048
1049 $item = compact( 'type', 'description' );
1050
1051 if ( 'response_format' === $_property ) {
1052 $doc['response'][ $doc_item ][ $key ] = $item;
1053 } else {
1054 $doc['request'][ $doc_item ][ $key ] = $item;
1055 }
1056 }
1057 }
1058
1059 return $doc;
1060 }
1061
1062 function user_can_view_post( $post_id ) {
1063 $post = get_post( $post_id );
1064 if ( ! $post || is_wp_error( $post ) ) {
1065 return false;
1066 }
1067
1068 if ( 'inherit' === $post->post_status ) {
1069 $parent_post = get_post( $post->post_parent );
1070 $post_status_obj = get_post_status_object( $parent_post->post_status );
1071 } else {
1072 $post_status_obj = get_post_status_object( $post->post_status );
1073 }
1074
1075 if ( ! $post_status_obj->public ) {
1076 if ( is_user_logged_in() ) {
1077 if ( $post_status_obj->protected ) {
1078 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1079 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1080 }
1081 } elseif ( $post_status_obj->private ) {
1082 if ( ! current_user_can( 'read_post', $post->ID ) ) {
1083 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1084 }
1085 } elseif ( in_array( $post->post_status, array( 'inherit', 'trash' ) ) ) {
1086 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1087 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1088 }
1089 } elseif ( 'auto-draft' === $post->post_status ) {
1090 // allow auto-drafts
1091 } else {
1092 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1093 }
1094 } else {
1095 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1096 }
1097 }
1098
1099 if (
1100 -1 == get_option( 'blog_public' ) &&
1101 /**
1102 * Filter access to a specific post.
1103 *
1104 * @module json-api
1105 *
1106 * @since 3.4.0
1107 *
1108 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1109 * @param WP_Post $post Post data.
1110 */
1111 ! apply_filters(
1112 'wpcom_json_api_user_can_view_post',
1113 current_user_can( 'read_post', $post->ID ),
1114 $post
1115 )
1116 ) {
1117 return new WP_Error(
1118 'unauthorized',
1119 'User cannot view post',
1120 array(
1121 'status_code' => 403,
1122 'error' => 'private_blog',
1123 )
1124 );
1125 }
1126
1127 if ( strlen( $post->post_password ) && ! current_user_can( 'edit_post', $post->ID ) ) {
1128 return new WP_Error(
1129 'unauthorized',
1130 'User cannot view password protected post',
1131 array(
1132 'status_code' => 403,
1133 'error' => 'password_protected',
1134 )
1135 );
1136 }
1137
1138 return true;
1139 }
1140
1141 /**
1142 * Returns author object.
1143 *
1144 * @param object $author user ID, user row, WP_User object, comment row, post row
1145 * @param bool $show_email_and_ip output the author's email address and IP address?
1146 *
1147 * @return object
1148 */
1149 function get_author( $author, $show_email_and_ip = false ) {
1150 $ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1151
1152 if ( isset( $author->comment_author_email ) ) {
1153 $ID = 0;
1154 $login = '';
1155 $email = $author->comment_author_email;
1156 $name = $author->comment_author;
1157 $first_name = '';
1158 $last_name = '';
1159 $URL = $author->comment_author_url;
1160 $avatar_URL = $this->api->get_avatar_url( $author );
1161 $profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1162 $nice = '';
1163 $site_id = -1;
1164
1165 // Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1166 // "&" is the only email/URL character altered by wp_kses()
1167 foreach ( array( 'email', 'URL' ) as $field ) {
1168 $$field = str_replace( '&amp;', '&', $$field );
1169 }
1170 } else {
1171 if ( isset( $author->user_id ) && $author->user_id ) {
1172 $author = $author->user_id;
1173 } elseif ( isset( $author->user_email ) ) {
1174 $author = $author->ID;
1175 } elseif ( isset( $author->post_author ) ) {
1176 // then $author is a Post Object.
1177 if ( 0 == $author->post_author ) {
1178 return null;
1179 }
1180 /**
1181 * Filter whether the current site is a Jetpack site.
1182 *
1183 * @module json-api
1184 *
1185 * @since 3.3.0
1186 *
1187 * @param bool false Is the current site a Jetpack site. Default to false.
1188 * @param int get_current_blog_id() Blog ID.
1189 */
1190 $is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1191 $post_id = $author->ID;
1192 if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1193 $ID = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1194 $email = get_post_meta( $post_id, '_jetpack_author_email', true );
1195 $login = '';
1196 $name = get_post_meta( $post_id, '_jetpack_author', true );
1197 $first_name = '';
1198 $last_name = '';
1199 $URL = '';
1200 $nice = '';
1201 } else {
1202 $author = $author->post_author;
1203 }
1204 }
1205
1206 if ( ! isset( $ID ) ) {
1207 $user = get_user_by( 'id', $author );
1208 if ( ! $user || is_wp_error( $user ) ) {
1209 trigger_error( 'Unknown user', E_USER_WARNING );
1210
1211 return null;
1212 }
1213 $ID = $user->ID;
1214 $email = $user->user_email;
1215 $login = $user->user_login;
1216 $name = $user->display_name;
1217 $first_name = $user->first_name;
1218 $last_name = $user->last_name;
1219 $URL = $user->user_url;
1220 $nice = $user->user_nicename;
1221 }
1222 if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1223 $active_blog = get_active_blog_for_user( $ID );
1224 $site_id = $active_blog->blog_id;
1225 if ( $site_id > -1 ) {
1226 $site_visible = (
1227 -1 != $active_blog->public ||
1228 is_private_blog_user( $site_id, get_current_user_id() )
1229 );
1230 }
1231 $profile_URL = "https://en.gravatar.com/{$login}";
1232 } else {
1233 $profile_URL = 'https://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1234 $site_id = -1;
1235 }
1236
1237 $avatar_URL = $this->api->get_avatar_url( $email );
1238 }
1239
1240 if ( $show_email_and_ip ) {
1241 $email = (string) $email;
1242 $ip_address = (string) $ip_address;
1243 } else {
1244 $email = false;
1245 $ip_address = false;
1246 }
1247
1248 $author = array(
1249 'ID' => (int) $ID,
1250 'login' => (string) $login,
1251 'email' => $email, // (string|bool)
1252 'name' => (string) $name,
1253 'first_name' => (string) $first_name,
1254 'last_name' => (string) $last_name,
1255 'nice_name' => (string) $nice,
1256 'URL' => (string) esc_url_raw( $URL ),
1257 'avatar_URL' => (string) esc_url_raw( $avatar_URL ),
1258 'profile_URL' => (string) esc_url_raw( $profile_URL ),
1259 'ip_address' => $ip_address, // (string|bool)
1260 );
1261
1262 if ( $site_id > -1 ) {
1263 $author['site_ID'] = (int) $site_id;
1264 $author['site_visible'] = $site_visible;
1265 }
1266
1267 return (object) $author;
1268 }
1269
1270 function get_media_item( $media_id ) {
1271 $media_item = get_post( $media_id );
1272
1273 if ( ! $media_item || is_wp_error( $media_item ) ) {
1274 return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1275 }
1276
1277 $response = array(
1278 'id' => strval( $media_item->ID ),
1279 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1280 'parent' => $media_item->post_parent,
1281 'link' => wp_get_attachment_url( $media_item->ID ),
1282 'title' => $media_item->post_title,
1283 'caption' => $media_item->post_excerpt,
1284 'description' => $media_item->post_content,
1285 'metadata' => wp_get_attachment_metadata( $media_item->ID ),
1286 );
1287
1288 if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1289 remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1290 $response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1291 add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1292 }
1293
1294 $response['meta'] = (object) array(
1295 'links' => (object) array(
1296 'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1297 'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1298 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1299 ),
1300 );
1301
1302 return (object) $response;
1303 }
1304
1305 function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1306
1307 if ( ! $media_item ) {
1308 $media_item = get_post( $media_id );
1309 }
1310
1311 if ( ! $media_item || is_wp_error( $media_item ) ) {
1312 return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1313 }
1314
1315 $attachment_file = get_attached_file( $media_item->ID );
1316
1317 $file = basename( $attachment_file ? $attachment_file : $file );
1318 $file_info = pathinfo( $file );
1319 $ext = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1320
1321 $response = array(
1322 'ID' => $media_item->ID,
1323 'URL' => wp_get_attachment_url( $media_item->ID ),
1324 'guid' => $media_item->guid,
1325 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1326 'post_ID' => $media_item->post_parent,
1327 'author_ID' => (int) $media_item->post_author,
1328 'file' => $file,
1329 'mime_type' => $media_item->post_mime_type,
1330 'extension' => $ext,
1331 'title' => $media_item->post_title,
1332 'caption' => $media_item->post_excerpt,
1333 'description' => $media_item->post_content,
1334 'alt' => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1335 'icon' => wp_mime_type_icon( $media_item->ID ),
1336 'thumbnails' => array(),
1337 );
1338
1339 if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1340 $metadata = wp_get_attachment_metadata( $media_item->ID );
1341 if ( isset( $metadata['height'], $metadata['width'] ) ) {
1342 $response['height'] = $metadata['height'];
1343 $response['width'] = $metadata['width'];
1344 }
1345
1346 if ( isset( $metadata['sizes'] ) ) {
1347 /**
1348 * Filter the thumbnail sizes available for each attachment ID.
1349 *
1350 * @module json-api
1351 *
1352 * @since 3.9.0
1353 *
1354 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1355 * @param string $media_id Attachment ID.
1356 */
1357 $sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
1358 if ( is_array( $sizes ) ) {
1359 foreach ( $sizes as $size => $size_details ) {
1360 $response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1361 }
1362 /**
1363 * Filter the thumbnail URLs for attachment files.
1364 *
1365 * @module json-api
1366 *
1367 * @since 7.1.0
1368 *
1369 * @param array $metadata['sizes'] Array with thumbnail sizes as keys and URLs as values.
1370 */
1371 $response['thumbnails'] = apply_filters( 'rest_api_thumbnail_size_urls', $response['thumbnails'] );
1372 }
1373 }
1374
1375 if ( isset( $metadata['image_meta'] ) ) {
1376 $response['exif'] = $metadata['image_meta'];
1377 }
1378 }
1379
1380 if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1381 $metadata = wp_get_attachment_metadata( $media_item->ID );
1382 $response['length'] = $metadata['length'];
1383 $response['exif'] = $metadata;
1384 }
1385
1386 $is_video = false;
1387
1388 if (
1389 in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1390 ||
1391 $response['mime_type'] === 'video/videopress'
1392 ) {
1393 $is_video = true;
1394 }
1395
1396 if ( $is_video ) {
1397 $metadata = wp_get_attachment_metadata( $media_item->ID );
1398
1399 if ( isset( $metadata['height'], $metadata['width'] ) ) {
1400 $response['height'] = $metadata['height'];
1401 $response['width'] = $metadata['width'];
1402 }
1403
1404 if ( isset( $metadata['length'] ) ) {
1405 $response['length'] = $metadata['length'];
1406 }
1407
1408 // add VideoPress info
1409 if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1410 $info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1411
1412 // If we failed to get VideoPress info, but it exists in the meta data (for some reason)
1413 // then let's use that.
1414 if ( false === $info && isset( $metadata['videopress'] ) ) {
1415 $info = (object) $metadata['videopress'];
1416 }
1417
1418 // Thumbnails
1419 if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1420 $response['thumbnails'] = array(
1421 'fmt_hd' => '',
1422 'fmt_dvd' => '',
1423 'fmt_std' => '',
1424 );
1425 foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1426 if ( video_format_done( $info, $size ) ) {
1427 $response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1428 } else {
1429 unset( $response['thumbnails'][ $size ] );
1430 }
1431 }
1432 }
1433
1434 // If we didn't get VideoPress information (for some reason) then let's
1435 // not try and include it in the response.
1436 if ( isset( $info->guid ) ) {
1437 $response['videopress_guid'] = $info->guid;
1438 $response['videopress_processing_done'] = true;
1439 if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1440 $response['videopress_processing_done'] = false;
1441 }
1442 }
1443 }
1444 }
1445
1446 $response['thumbnails'] = (object) $response['thumbnails'];
1447
1448 $response['meta'] = (object) array(
1449 'links' => (object) array(
1450 'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1451 'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1452 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1453 ),
1454 );
1455
1456 // add VideoPress link to the meta
1457 if ( isset( $response['videopress_guid'] ) ) {
1458 if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1459 $response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1460 }
1461 }
1462
1463 if ( $media_item->post_parent > 0 ) {
1464 $response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1465 }
1466
1467 return (object) $response;
1468 }
1469
1470 function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1471
1472 $taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1473 // keep updating this function
1474 if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1475 return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1476 }
1477
1478 return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1479 }
1480
1481 function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1482 // Permissions
1483 switch ( $context ) {
1484 case 'edit':
1485 $tax = get_taxonomy( $taxonomy_type );
1486 if ( ! current_user_can( $tax->cap->edit_terms ) ) {
1487 return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1488 }
1489 break;
1490 case 'display':
1491 if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1492 return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1493 }
1494 break;
1495 default:
1496 return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1497 }
1498
1499 $response = array();
1500 $response['ID'] = (int) $taxonomy->term_id;
1501 $response['name'] = (string) $taxonomy->name;
1502 $response['slug'] = (string) $taxonomy->slug;
1503 $response['description'] = (string) $taxonomy->description;
1504 $response['post_count'] = (int) $taxonomy->count;
1505 $response['feed_url'] = get_term_feed_link( $taxonomy->term_id, $taxonomy_type );
1506
1507 if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1508 $response['parent'] = (int) $taxonomy->parent;
1509 }
1510
1511 $response['meta'] = (object) array(
1512 'links' => (object) array(
1513 'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1514 'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1515 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1516 ),
1517 );
1518
1519 return (object) $response;
1520 }
1521
1522 /**
1523 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1524 *
1525 * @param $date_gmt (string) GMT datetime string.
1526 * @param $date (string) Optional. Used to calculate the offset from GMT.
1527 *
1528 * @return string
1529 */
1530 function format_date( $date_gmt, $date = null ) {
1531 return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1532 }
1533
1534 /**
1535 * Parses a date string and returns the local and GMT representations
1536 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1537 * timezones or offsets. If the parsed datetime was not localized to a
1538 * particular timezone or offset we will assume it was given in GMT
1539 * relative to now and will convert it to local time using either the
1540 * timezone set in the options table for the blog or the GMT offset.
1541 *
1542 * @param datetime string $date_string Date to parse.
1543 *
1544 * @return array( $local_time_string, $gmt_time_string )
1545 */
1546 public function parse_date( $date_string ) {
1547 $date_string_info = date_parse( $date_string );
1548 if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1549 // Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1550 if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1551 $dt_utc = new DateTime( $date_string );
1552 $dt_local = clone $dt_utc;
1553 $dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1554 return array(
1555 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1556 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1557 );
1558 }
1559
1560 // It's parseable but no TZ info so assume UTC.
1561 $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1562 $dt_local = clone $dt_utc;
1563 } else {
1564 // Could not parse time, use now in UTC.
1565 $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1566 $dt_local = clone $dt_utc;
1567 }
1568
1569 $dt_local->setTimezone( wp_timezone() );
1570
1571 return array(
1572 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1573 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1574 );
1575 }
1576
1577 // Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1578 function load_theme_functions() {
1579 // bail if we've done this already (can happen when calling /batch endpoint)
1580 if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) ) {
1581 return;
1582 }
1583
1584 // VIP context loading is handled elsewhere, so bail to prevent
1585 // duplicate loading. See `switch_to_blog_and_validate_user()`
1586 if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1587 return;
1588 }
1589
1590 define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1591
1592 // the theme info we care about is found either within functions.php or one of the jetpack files.
1593 $function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1594
1595 $copy_dirs = array( get_template_directory() );
1596
1597 // Is this a child theme? Load the child theme's functions file.
1598 if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1599 foreach ( $function_files as $function_file ) {
1600 if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1601 require_once get_stylesheet_directory() . $function_file;
1602 }
1603 }
1604 $copy_dirs[] = get_stylesheet_directory();
1605 }
1606
1607 foreach ( $function_files as $function_file ) {
1608 if ( file_exists( get_template_directory() . $function_file ) ) {
1609 require_once get_template_directory() . $function_file;
1610 }
1611 }
1612
1613 // add inc/wpcom.php and/or includes/wpcom.php
1614 wpcom_load_theme_compat_file();
1615
1616 // Enable including additional directories or files in actions to be copied
1617 $copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1618
1619 // since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1620 $this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1621
1622 /**
1623 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1624 *
1625 * The REST API does not load the theme when processing requests.
1626 * To enable theme-based functionality, the API will load the '/functions.php',
1627 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1628 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1629 *
1630 * @module json-api
1631 *
1632 * @since 3.2.0
1633 */
1634 do_action( 'restapi_theme_after_setup_theme' );
1635 $this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1636
1637 /**
1638 * Fires functions hooked onto `init` 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 'init' within those files.
1644 *
1645 * @module json-api
1646 *
1647 * @since 3.2.0
1648 */
1649 do_action( 'restapi_theme_init' );
1650 }
1651
1652 function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1653 global $wp_filter;
1654 foreach ( $wp_filter as $hook => $actions ) {
1655
1656 if ( $from_hook != $hook ) {
1657 continue;
1658 }
1659 if ( ! has_action( $hook ) ) {
1660 continue;
1661 }
1662
1663 foreach ( $actions as $priority => $callbacks ) {
1664 foreach ( $callbacks as $callback_key => $callback_data ) {
1665 $callback = $callback_data['function'];
1666
1667 // use reflection api to determine filename where function is defined
1668 $reflection = $this->get_reflection( $callback );
1669
1670 if ( false !== $reflection ) {
1671 $file_name = $reflection->getFileName();
1672 foreach ( $base_paths as $base_path ) {
1673
1674 // only copy hooks with functions which are part of the specified files
1675 if ( 0 === strpos( $file_name, $base_path ) ) {
1676 add_action(
1677 $to_hook,
1678 $callback_data['function'],
1679 $priority,
1680 $callback_data['accepted_args']
1681 );
1682 }
1683 }
1684 }
1685 }
1686 }
1687 }
1688 }
1689
1690 function get_reflection( $callback ) {
1691 if ( is_array( $callback ) ) {
1692 list( $class, $method ) = $callback;
1693 return new ReflectionMethod( $class, $method );
1694 }
1695
1696 if ( is_string( $callback ) && strpos( $callback, '::' ) !== false ) {
1697 list( $class, $method ) = explode( '::', $callback );
1698 return new ReflectionMethod( $class, $method );
1699 }
1700
1701 if ( method_exists( $callback, "__invoke" ) ) {
1702 return new ReflectionMethod( $callback, "__invoke" );
1703 }
1704
1705 if ( is_string( $callback ) && strpos( $callback, '::' ) == false && function_exists( $callback ) ) {
1706 return new ReflectionFunction( $callback );
1707 }
1708
1709 return false;
1710 }
1711
1712 /**
1713 * Check whether a user can view or edit a post type
1714 *
1715 * @param string $post_type post type to check
1716 * @param string $context 'display' or 'edit'
1717 * @return bool
1718 */
1719 function current_user_can_access_post_type( $post_type, $context = 'display' ) {
1720 $post_type_object = get_post_type_object( $post_type );
1721 if ( ! $post_type_object ) {
1722 return false;
1723 }
1724
1725 switch ( $context ) {
1726 case 'edit':
1727 return current_user_can( $post_type_object->cap->edit_posts );
1728 case 'display':
1729 return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1730 default:
1731 return false;
1732 }
1733 }
1734
1735 function is_post_type_allowed( $post_type ) {
1736 // if the post type is empty, that's fine, WordPress will default to post
1737 if ( empty( $post_type ) ) {
1738 return true;
1739 }
1740
1741 // allow special 'any' type
1742 if ( 'any' == $post_type ) {
1743 return true;
1744 }
1745
1746 // check for allowed types
1747 if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1748 return true;
1749 }
1750
1751 if ( $post_type_object = get_post_type_object( $post_type ) ) {
1752 if ( ! empty( $post_type_object->show_in_rest ) ) {
1753 return $post_type_object->show_in_rest;
1754 }
1755 if ( ! empty( $post_type_object->publicly_queryable ) ) {
1756 return $post_type_object->publicly_queryable;
1757 }
1758 }
1759
1760 return ! empty( $post_type_object->public );
1761 }
1762
1763 /**
1764 * Gets the whitelisted post types that JP should allow access to.
1765 *
1766 * @return array Whitelisted post types.
1767 */
1768 protected function _get_whitelisted_post_types() {
1769 $allowed_types = array( 'post', 'page', 'revision' );
1770
1771 /**
1772 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1773 *
1774 * @module json-api
1775 *
1776 * @since 2.2.3
1777 *
1778 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1779 */
1780 $allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1781
1782 return array_unique( $allowed_types );
1783 }
1784
1785 function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1786
1787 add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1788
1789 $media_ids = $errors = array();
1790 $user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
1791 $media_attrs = array_values( $media_attrs ); // reset the keys
1792 $i = 0;
1793
1794 if ( ! empty( $media_files ) ) {
1795 $this->api->trap_wp_die( 'upload_error' );
1796 foreach ( $media_files as $media_item ) {
1797 $_FILES['.api.media.item.'] = $media_item;
1798 if ( ! $user_can_upload_files ) {
1799 $media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1800 } else {
1801 if ( $force_parent_id ) {
1802 $parent_id = absint( $force_parent_id );
1803 } elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1804 $parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1805 } else {
1806 $parent_id = 0;
1807 }
1808 $media_id = media_handle_upload( '.api.media.item.', $parent_id );
1809 }
1810 if ( is_wp_error( $media_id ) ) {
1811 $errors[ $i ]['file'] = $media_item['name'];
1812 $errors[ $i ]['error'] = $media_id->get_error_code();
1813 $errors[ $i ]['message'] = $media_id->get_error_message();
1814 } else {
1815 $media_ids[ $i ] = $media_id;
1816 }
1817
1818 $i++;
1819 }
1820 $this->api->trap_wp_die( null );
1821 unset( $_FILES['.api.media.item.'] );
1822 }
1823
1824 if ( ! empty( $media_urls ) ) {
1825 foreach ( $media_urls as $url ) {
1826 if ( ! $user_can_upload_files ) {
1827 $media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1828 } else {
1829 if ( $force_parent_id ) {
1830 $parent_id = absint( $force_parent_id );
1831 } elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1832 $parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1833 } else {
1834 $parent_id = 0;
1835 }
1836 $media_id = $this->handle_media_sideload( $url, $parent_id );
1837 }
1838 if ( is_wp_error( $media_id ) ) {
1839 $errors[ $i ] = array(
1840 'file' => $url,
1841 'error' => $media_id->get_error_code(),
1842 'message' => $media_id->get_error_message(),
1843 );
1844 } elseif ( ! empty( $media_id ) ) {
1845 $media_ids[ $i ] = $media_id;
1846 }
1847
1848 $i++;
1849 }
1850 }
1851
1852 if ( ! empty( $media_attrs ) ) {
1853 foreach ( $media_ids as $index => $media_id ) {
1854 if ( empty( $media_attrs[ $index ] ) ) {
1855 continue;
1856 }
1857
1858 $attrs = $media_attrs[ $index ];
1859 $insert = array();
1860
1861 // Attributes: Title, Caption, Description
1862
1863 if ( isset( $attrs['title'] ) ) {
1864 $insert['post_title'] = $attrs['title'];
1865 }
1866
1867 if ( isset( $attrs['caption'] ) ) {
1868 $insert['post_excerpt'] = $attrs['caption'];
1869 }
1870
1871 if ( isset( $attrs['description'] ) ) {
1872 $insert['post_content'] = $attrs['description'];
1873 }
1874
1875 if ( ! empty( $insert ) ) {
1876 $insert['ID'] = $media_id;
1877 wp_update_post( (object) $insert );
1878 }
1879
1880 // Attributes: Alt
1881
1882 if ( isset( $attrs['alt'] ) ) {
1883 $alt = wp_strip_all_tags( $attrs['alt'], true );
1884 update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1885 }
1886
1887 // Attributes: Artist, Album
1888
1889 $id3_meta = array();
1890
1891 foreach ( array( 'artist', 'album' ) as $key ) {
1892 if ( isset( $attrs[ $key ] ) ) {
1893 $id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1894 }
1895 }
1896
1897 if ( ! empty( $id3_meta ) ) {
1898 // Before updating metadata, ensure that the item is audio
1899 $item = $this->get_media_item_v1_1( $media_id );
1900 if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1901 wp_update_attachment_metadata( $media_id, $id3_meta );
1902 }
1903 }
1904 }
1905 }
1906
1907 return array(
1908 'media_ids' => $media_ids,
1909 'errors' => $errors,
1910 );
1911
1912 }
1913
1914 function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1915 if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) ) {
1916 return false;
1917 }
1918
1919 // if we didn't get a URL, let's bail
1920 $parsed = wp_parse_url( $url );
1921 if ( empty( $parsed ) ) {
1922 return false;
1923 }
1924
1925 $tmp = download_url( $url );
1926 if ( is_wp_error( $tmp ) ) {
1927 return $tmp;
1928 }
1929
1930 // First check to see if we get a mime-type match by file, otherwise, check to
1931 // see if WordPress supports this file as an image. If neither, then it is not supported.
1932 if ( ! $this->is_file_supported_for_sideloading( $tmp ) || 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1933 @unlink( $tmp );
1934 return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
1935 }
1936
1937 // emulate a $_FILES entry
1938 $file_array = array(
1939 'name' => basename( wp_parse_url( $url, PHP_URL_PATH ) ),
1940 'tmp_name' => $tmp,
1941 );
1942
1943 $id = media_handle_sideload( $file_array, $parent_post_id );
1944 if ( file_exists( $tmp ) ) {
1945 @unlink( $tmp );
1946 }
1947
1948 if ( is_wp_error( $id ) ) {
1949 return $id;
1950 }
1951
1952 if ( ! $id || ! is_int( $id ) ) {
1953 return false;
1954 }
1955
1956 return $id;
1957 }
1958
1959 /**
1960 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1961 *
1962 * @param string $file Path to file to get its mime type.
1963 *
1964 * @return bool
1965 */
1966 protected function is_file_supported_for_sideloading( $file ) {
1967 return jetpack_is_file_supported_for_sideloading( $file );
1968 }
1969
1970 function allow_video_uploads( $mimes ) {
1971 // if we are on Jetpack, bail - Videos are already allowed
1972 if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
1973 return $mimes;
1974 }
1975
1976 // extra check that this filter is only ever applied during REST API requests
1977 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1978 return $mimes;
1979 }
1980
1981 // bail early if they already have the upgrade..
1982 if ( get_option( 'video_upgrade' ) == '1' ) {
1983 return $mimes;
1984 }
1985
1986 // lets whitelist to only specific clients right now
1987 $clients_allowed_video_uploads = array();
1988 /**
1989 * Filter the list of whitelisted video clients.
1990 *
1991 * @module json-api
1992 *
1993 * @since 3.2.0
1994 *
1995 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1996 */
1997 $clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1998 if ( ! in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1999 return $mimes;
2000 }
2001
2002 $mime_list = wp_get_mime_types();
2003
2004 $video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
2005 /**
2006 * Filter the video filetypes allowed on the site.
2007 *
2008 * @module json-api
2009 *
2010 * @since 3.2.0
2011 *
2012 * @param array $video_exts Array of video filetypes allowed on the site.
2013 */
2014 $video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
2015 $video_mimes = array();
2016
2017 if ( ! empty( $video_exts ) ) {
2018 foreach ( $video_exts as $ext ) {
2019 foreach ( $mime_list as $ext_pattern => $mime ) {
2020 if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false ) {
2021 $video_mimes[ $ext_pattern ] = $mime;
2022 }
2023 }
2024 }
2025
2026 $mimes = array_merge( $mimes, $video_mimes );
2027 }
2028
2029 return $mimes;
2030 }
2031
2032 function is_current_site_multi_user() {
2033 $users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2034 if ( false === $users ) {
2035 $user_query = new WP_User_Query(
2036 array(
2037 'blog_id' => get_current_blog_id(),
2038 'fields' => 'ID',
2039 )
2040 );
2041 $users = (int) $user_query->get_total();
2042 wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2043 }
2044 return $users > 1;
2045 }
2046
2047 function allows_cross_origin_requests() {
2048 return 'GET' == $this->method || $this->allow_cross_origin_request;
2049 }
2050
2051 function allows_unauthorized_requests( $origin, $complete_access_origins ) {
2052 return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2053 }
2054
2055 function get_platform() {
2056 return wpcom_get_sal_platform( $this->api->token_details );
2057 }
2058
2059 /**
2060 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2061 * response from the WPCOM API, or potentially go to the Jetpack blog.
2062 *
2063 * Override this method if you want to do something different.
2064 *
2065 * @param int $blog_id
2066 * @return bool
2067 */
2068 function force_wpcom_request( $blog_id ) {
2069 return false;
2070 }
2071
2072 /**
2073 * Return endpoint response
2074 *
2075 * @param ... determined by ->$path
2076 *
2077 * @return
2078 * falsy: HTTP 500, no response body
2079 * WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2080 * $data: HTTP 200, json_encode( $data ) response body
2081 */
2082 abstract function callback( $path = '' );
2083
2084
2085 }
2086
2087 require_once dirname( __FILE__ ) . '/json-endpoints.php';
2088