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