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