|
1 <?php |
|
2 /** |
|
3 * REST API: WP_REST_Plugins_Controller class |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage REST_API |
|
7 * @since 5.5.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Core class to access plugins via the REST API. |
|
12 * |
|
13 * @since 5.5.0 |
|
14 * |
|
15 * @see WP_REST_Controller |
|
16 */ |
|
17 class WP_REST_Plugins_Controller extends WP_REST_Controller { |
|
18 |
|
19 const PATTERN = '[^.\/]+(?:\/[^.\/]+)?'; |
|
20 |
|
21 /** |
|
22 * Plugins controller constructor. |
|
23 * |
|
24 * @since 5.5.0 |
|
25 */ |
|
26 public function __construct() { |
|
27 $this->namespace = 'wp/v2'; |
|
28 $this->rest_base = 'plugins'; |
|
29 } |
|
30 |
|
31 /** |
|
32 * Registers the routes for the plugins controller. |
|
33 * |
|
34 * @since 5.5.0 |
|
35 */ |
|
36 public function register_routes() { |
|
37 register_rest_route( |
|
38 $this->namespace, |
|
39 '/' . $this->rest_base, |
|
40 array( |
|
41 array( |
|
42 'methods' => WP_REST_Server::READABLE, |
|
43 'callback' => array( $this, 'get_items' ), |
|
44 'permission_callback' => array( $this, 'get_items_permissions_check' ), |
|
45 'args' => $this->get_collection_params(), |
|
46 ), |
|
47 array( |
|
48 'methods' => WP_REST_Server::CREATABLE, |
|
49 'callback' => array( $this, 'create_item' ), |
|
50 'permission_callback' => array( $this, 'create_item_permissions_check' ), |
|
51 'args' => array( |
|
52 'slug' => array( |
|
53 'type' => 'string', |
|
54 'required' => true, |
|
55 'description' => __( 'WordPress.org plugin directory slug.' ), |
|
56 'pattern' => '[\w\-]+', |
|
57 ), |
|
58 'status' => array( |
|
59 'description' => __( 'The plugin activation status.' ), |
|
60 'type' => 'string', |
|
61 'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ), |
|
62 'default' => 'inactive', |
|
63 ), |
|
64 ), |
|
65 ), |
|
66 'schema' => array( $this, 'get_public_item_schema' ), |
|
67 ) |
|
68 ); |
|
69 |
|
70 register_rest_route( |
|
71 $this->namespace, |
|
72 '/' . $this->rest_base . '/(?P<plugin>' . self::PATTERN . ')', |
|
73 array( |
|
74 array( |
|
75 'methods' => WP_REST_Server::READABLE, |
|
76 'callback' => array( $this, 'get_item' ), |
|
77 'permission_callback' => array( $this, 'get_item_permissions_check' ), |
|
78 ), |
|
79 array( |
|
80 'methods' => WP_REST_Server::EDITABLE, |
|
81 'callback' => array( $this, 'update_item' ), |
|
82 'permission_callback' => array( $this, 'update_item_permissions_check' ), |
|
83 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), |
|
84 ), |
|
85 array( |
|
86 'methods' => WP_REST_Server::DELETABLE, |
|
87 'callback' => array( $this, 'delete_item' ), |
|
88 'permission_callback' => array( $this, 'delete_item_permissions_check' ), |
|
89 ), |
|
90 'args' => array( |
|
91 'context' => $this->get_context_param( array( 'default' => 'view' ) ), |
|
92 'plugin' => array( |
|
93 'type' => 'string', |
|
94 'pattern' => self::PATTERN, |
|
95 'validate_callback' => array( $this, 'validate_plugin_param' ), |
|
96 'sanitize_callback' => array( $this, 'sanitize_plugin_param' ), |
|
97 ), |
|
98 ), |
|
99 'schema' => array( $this, 'get_public_item_schema' ), |
|
100 ) |
|
101 ); |
|
102 } |
|
103 |
|
104 /** |
|
105 * Checks if a given request has access to get plugins. |
|
106 * |
|
107 * @since 5.5.0 |
|
108 * |
|
109 * @param WP_REST_Request $request Full details about the request. |
|
110 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. |
|
111 */ |
|
112 public function get_items_permissions_check( $request ) { |
|
113 if ( ! current_user_can( 'activate_plugins' ) ) { |
|
114 return new WP_Error( |
|
115 'rest_cannot_view_plugins', |
|
116 __( 'Sorry, you are not allowed to manage plugins for this site.' ), |
|
117 array( 'status' => rest_authorization_required_code() ) |
|
118 ); |
|
119 } |
|
120 |
|
121 return true; |
|
122 } |
|
123 |
|
124 /** |
|
125 * Retrieves a collection of plugins. |
|
126 * |
|
127 * @since 5.5.0 |
|
128 * |
|
129 * @param WP_REST_Request $request Full details about the request. |
|
130 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
131 */ |
|
132 public function get_items( $request ) { |
|
133 require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
|
134 |
|
135 $plugins = array(); |
|
136 |
|
137 foreach ( get_plugins() as $file => $data ) { |
|
138 if ( is_wp_error( $this->check_read_permission( $file ) ) ) { |
|
139 continue; |
|
140 } |
|
141 |
|
142 $data['_file'] = $file; |
|
143 |
|
144 if ( ! $this->does_plugin_match_request( $request, $data ) ) { |
|
145 continue; |
|
146 } |
|
147 |
|
148 $plugins[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( $data, $request ) ); |
|
149 } |
|
150 |
|
151 return new WP_REST_Response( $plugins ); |
|
152 } |
|
153 |
|
154 /** |
|
155 * Checks if a given request has access to get a specific plugin. |
|
156 * |
|
157 * @since 5.5.0 |
|
158 * |
|
159 * @param WP_REST_Request $request Full details about the request. |
|
160 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. |
|
161 */ |
|
162 public function get_item_permissions_check( $request ) { |
|
163 if ( ! current_user_can( 'activate_plugins' ) ) { |
|
164 return new WP_Error( |
|
165 'rest_cannot_view_plugin', |
|
166 __( 'Sorry, you are not allowed to manage plugins for this site.' ), |
|
167 array( 'status' => rest_authorization_required_code() ) |
|
168 ); |
|
169 } |
|
170 |
|
171 $can_read = $this->check_read_permission( $request['plugin'] ); |
|
172 |
|
173 if ( is_wp_error( $can_read ) ) { |
|
174 return $can_read; |
|
175 } |
|
176 |
|
177 return true; |
|
178 } |
|
179 |
|
180 /** |
|
181 * Retrieves one plugin from the site. |
|
182 * |
|
183 * @since 5.5.0 |
|
184 * |
|
185 * @param WP_REST_Request $request Full details about the request. |
|
186 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
187 */ |
|
188 public function get_item( $request ) { |
|
189 require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
|
190 |
|
191 $data = $this->get_plugin_data( $request['plugin'] ); |
|
192 |
|
193 if ( is_wp_error( $data ) ) { |
|
194 return $data; |
|
195 } |
|
196 |
|
197 return $this->prepare_item_for_response( $data, $request ); |
|
198 } |
|
199 |
|
200 /** |
|
201 * Checks if the given plugin can be viewed by the current user. |
|
202 * |
|
203 * On multisite, this hides non-active network only plugins if the user does not have permission |
|
204 * to manage network plugins. |
|
205 * |
|
206 * @since 5.5.0 |
|
207 * |
|
208 * @param string $plugin The plugin file to check. |
|
209 * @return true|WP_Error True if can read, a WP_Error instance otherwise. |
|
210 */ |
|
211 protected function check_read_permission( $plugin ) { |
|
212 if ( ! $this->is_plugin_installed( $plugin ) ) { |
|
213 return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) ); |
|
214 } |
|
215 |
|
216 if ( ! is_multisite() ) { |
|
217 return true; |
|
218 } |
|
219 |
|
220 if ( ! is_network_only_plugin( $plugin ) || is_plugin_active( $plugin ) || current_user_can( 'manage_network_plugins' ) ) { |
|
221 return true; |
|
222 } |
|
223 |
|
224 return new WP_Error( |
|
225 'rest_cannot_view_plugin', |
|
226 __( 'Sorry, you are not allowed to manage this plugin.' ), |
|
227 array( 'status' => rest_authorization_required_code() ) |
|
228 ); |
|
229 } |
|
230 |
|
231 /** |
|
232 * Checks if a given request has access to upload plugins. |
|
233 * |
|
234 * @since 5.5.0 |
|
235 * |
|
236 * @param WP_REST_Request $request Full details about the request. |
|
237 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. |
|
238 */ |
|
239 public function create_item_permissions_check( $request ) { |
|
240 if ( ! current_user_can( 'install_plugins' ) ) { |
|
241 return new WP_Error( |
|
242 'rest_cannot_install_plugin', |
|
243 __( 'Sorry, you are not allowed to install plugins on this site.' ), |
|
244 array( 'status' => rest_authorization_required_code() ) |
|
245 ); |
|
246 } |
|
247 |
|
248 if ( 'inactive' !== $request['status'] && ! current_user_can( 'activate_plugins' ) ) { |
|
249 return new WP_Error( |
|
250 'rest_cannot_activate_plugin', |
|
251 __( 'Sorry, you are not allowed to activate plugins.' ), |
|
252 array( |
|
253 'status' => rest_authorization_required_code(), |
|
254 ) |
|
255 ); |
|
256 } |
|
257 |
|
258 return true; |
|
259 } |
|
260 |
|
261 /** |
|
262 * Uploads a plugin and optionally activates it. |
|
263 * |
|
264 * @since 5.5.0 |
|
265 * |
|
266 * @param WP_REST_Request $request Full details about the request. |
|
267 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
268 */ |
|
269 public function create_item( $request ) { |
|
270 require_once ABSPATH . 'wp-admin/includes/file.php'; |
|
271 require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
|
272 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; |
|
273 require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; |
|
274 |
|
275 $slug = $request['slug']; |
|
276 |
|
277 // Verify filesystem is accessible first. |
|
278 $filesystem_available = $this->is_filesystem_available(); |
|
279 if ( is_wp_error( $filesystem_available ) ) { |
|
280 return $filesystem_available; |
|
281 } |
|
282 |
|
283 $api = plugins_api( |
|
284 'plugin_information', |
|
285 array( |
|
286 'slug' => $slug, |
|
287 'fields' => array( |
|
288 'sections' => false, |
|
289 'language_packs' => true, |
|
290 ), |
|
291 ) |
|
292 ); |
|
293 |
|
294 if ( is_wp_error( $api ) ) { |
|
295 if ( false !== strpos( $api->get_error_message(), 'Plugin not found.' ) ) { |
|
296 $api->add_data( array( 'status' => 404 ) ); |
|
297 } else { |
|
298 $api->add_data( array( 'status' => 500 ) ); |
|
299 } |
|
300 |
|
301 return $api; |
|
302 } |
|
303 |
|
304 $skin = new WP_Ajax_Upgrader_Skin(); |
|
305 $upgrader = new Plugin_Upgrader( $skin ); |
|
306 |
|
307 $result = $upgrader->install( $api->download_link ); |
|
308 |
|
309 if ( is_wp_error( $result ) ) { |
|
310 $result->add_data( array( 'status' => 500 ) ); |
|
311 |
|
312 return $result; |
|
313 } |
|
314 |
|
315 // This should be the same as $result above. |
|
316 if ( is_wp_error( $skin->result ) ) { |
|
317 $skin->result->add_data( array( 'status' => 500 ) ); |
|
318 |
|
319 return $skin->result; |
|
320 } |
|
321 |
|
322 if ( $skin->get_errors()->has_errors() ) { |
|
323 $error = $skin->get_errors(); |
|
324 $error->add_data( array( 'status' => 500 ) ); |
|
325 |
|
326 return $error; |
|
327 } |
|
328 |
|
329 if ( is_null( $result ) ) { |
|
330 global $wp_filesystem; |
|
331 // Pass through the error from WP_Filesystem if one was raised. |
|
332 if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { |
|
333 return new WP_Error( 'unable_to_connect_to_filesystem', $wp_filesystem->errors->get_error_message(), array( 'status' => 500 ) ); |
|
334 } |
|
335 |
|
336 return new WP_Error( 'unable_to_connect_to_filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.' ), array( 'status' => 500 ) ); |
|
337 } |
|
338 |
|
339 $file = $upgrader->plugin_info(); |
|
340 |
|
341 if ( ! $file ) { |
|
342 return new WP_Error( 'unable_to_determine_installed_plugin', __( 'Unable to determine what plugin was installed.' ), array( 'status' => 500 ) ); |
|
343 } |
|
344 |
|
345 if ( 'inactive' !== $request['status'] ) { |
|
346 $can_change_status = $this->plugin_status_permission_check( $file, $request['status'], 'inactive' ); |
|
347 |
|
348 if ( is_wp_error( $can_change_status ) ) { |
|
349 return $can_change_status; |
|
350 } |
|
351 |
|
352 $changed_status = $this->handle_plugin_status( $file, $request['status'], 'inactive' ); |
|
353 |
|
354 if ( is_wp_error( $changed_status ) ) { |
|
355 return $changed_status; |
|
356 } |
|
357 } |
|
358 |
|
359 // Install translations. |
|
360 $installed_locales = array_values( get_available_languages() ); |
|
361 /** This filter is documented in wp-includes/update.php */ |
|
362 $installed_locales = apply_filters( 'plugins_update_check_locales', $installed_locales ); |
|
363 |
|
364 $language_packs = array_map( |
|
365 function( $item ) { |
|
366 return (object) $item; |
|
367 }, |
|
368 $api->language_packs |
|
369 ); |
|
370 |
|
371 $language_packs = array_filter( |
|
372 $language_packs, |
|
373 function( $pack ) use ( $installed_locales ) { |
|
374 return in_array( $pack->language, $installed_locales, true ); |
|
375 } |
|
376 ); |
|
377 |
|
378 if ( $language_packs ) { |
|
379 $lp_upgrader = new Language_Pack_Upgrader( $skin ); |
|
380 |
|
381 // Install all applicable language packs for the plugin. |
|
382 $lp_upgrader->bulk_upgrade( $language_packs ); |
|
383 } |
|
384 |
|
385 $path = WP_PLUGIN_DIR . '/' . $file; |
|
386 $data = get_plugin_data( $path, false, false ); |
|
387 $data['_file'] = $file; |
|
388 |
|
389 $response = $this->prepare_item_for_response( $data, $request ); |
|
390 $response->set_status( 201 ); |
|
391 $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $file, 0, - 4 ) ) ) ); |
|
392 |
|
393 return $response; |
|
394 } |
|
395 |
|
396 /** |
|
397 * Checks if a given request has access to update a specific plugin. |
|
398 * |
|
399 * @since 5.5.0 |
|
400 * |
|
401 * @param WP_REST_Request $request Full details about the request. |
|
402 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. |
|
403 */ |
|
404 public function update_item_permissions_check( $request ) { |
|
405 require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
|
406 |
|
407 if ( ! current_user_can( 'activate_plugins' ) ) { |
|
408 return new WP_Error( |
|
409 'rest_cannot_manage_plugins', |
|
410 __( 'Sorry, you are not allowed to manage plugins for this site.' ), |
|
411 array( 'status' => rest_authorization_required_code() ) |
|
412 ); |
|
413 } |
|
414 |
|
415 $can_read = $this->check_read_permission( $request['plugin'] ); |
|
416 |
|
417 if ( is_wp_error( $can_read ) ) { |
|
418 return $can_read; |
|
419 } |
|
420 |
|
421 $status = $this->get_plugin_status( $request['plugin'] ); |
|
422 |
|
423 if ( $request['status'] && $status !== $request['status'] ) { |
|
424 $can_change_status = $this->plugin_status_permission_check( $request['plugin'], $request['status'], $status ); |
|
425 |
|
426 if ( is_wp_error( $can_change_status ) ) { |
|
427 return $can_change_status; |
|
428 } |
|
429 } |
|
430 |
|
431 return true; |
|
432 } |
|
433 |
|
434 /** |
|
435 * Updates one plugin. |
|
436 * |
|
437 * @since 5.5.0 |
|
438 * |
|
439 * @param WP_REST_Request $request Full details about the request. |
|
440 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
441 */ |
|
442 public function update_item( $request ) { |
|
443 require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
|
444 |
|
445 $data = $this->get_plugin_data( $request['plugin'] ); |
|
446 |
|
447 if ( is_wp_error( $data ) ) { |
|
448 return $data; |
|
449 } |
|
450 |
|
451 $status = $this->get_plugin_status( $request['plugin'] ); |
|
452 |
|
453 if ( $request['status'] && $status !== $request['status'] ) { |
|
454 $handled = $this->handle_plugin_status( $request['plugin'], $request['status'], $status ); |
|
455 |
|
456 if ( is_wp_error( $handled ) ) { |
|
457 return $handled; |
|
458 } |
|
459 } |
|
460 |
|
461 $this->update_additional_fields_for_object( $data, $request ); |
|
462 |
|
463 $request['context'] = 'edit'; |
|
464 |
|
465 return $this->prepare_item_for_response( $data, $request ); |
|
466 } |
|
467 |
|
468 /** |
|
469 * Checks if a given request has access to delete a specific plugin. |
|
470 * |
|
471 * @since 5.5.0 |
|
472 * |
|
473 * @param WP_REST_Request $request Full details about the request. |
|
474 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. |
|
475 */ |
|
476 public function delete_item_permissions_check( $request ) { |
|
477 if ( ! current_user_can( 'activate_plugins' ) ) { |
|
478 return new WP_Error( |
|
479 'rest_cannot_manage_plugins', |
|
480 __( 'Sorry, you are not allowed to manage plugins for this site.' ), |
|
481 array( 'status' => rest_authorization_required_code() ) |
|
482 ); |
|
483 } |
|
484 |
|
485 if ( ! current_user_can( 'delete_plugins' ) ) { |
|
486 return new WP_Error( |
|
487 'rest_cannot_manage_plugins', |
|
488 __( 'Sorry, you are not allowed to delete plugins for this site.' ), |
|
489 array( 'status' => rest_authorization_required_code() ) |
|
490 ); |
|
491 } |
|
492 |
|
493 $can_read = $this->check_read_permission( $request['plugin'] ); |
|
494 |
|
495 if ( is_wp_error( $can_read ) ) { |
|
496 return $can_read; |
|
497 } |
|
498 |
|
499 return true; |
|
500 } |
|
501 |
|
502 /** |
|
503 * Deletes one plugin from the site. |
|
504 * |
|
505 * @since 5.5.0 |
|
506 * |
|
507 * @param WP_REST_Request $request Full details about the request. |
|
508 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
509 */ |
|
510 public function delete_item( $request ) { |
|
511 require_once ABSPATH . 'wp-admin/includes/file.php'; |
|
512 require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
|
513 |
|
514 $data = $this->get_plugin_data( $request['plugin'] ); |
|
515 |
|
516 if ( is_wp_error( $data ) ) { |
|
517 return $data; |
|
518 } |
|
519 |
|
520 if ( is_plugin_active( $request['plugin'] ) ) { |
|
521 return new WP_Error( |
|
522 'rest_cannot_delete_active_plugin', |
|
523 __( 'Cannot delete an active plugin. Please deactivate it first.' ), |
|
524 array( 'status' => 400 ) |
|
525 ); |
|
526 } |
|
527 |
|
528 $filesystem_available = $this->is_filesystem_available(); |
|
529 if ( is_wp_error( $filesystem_available ) ) { |
|
530 return $filesystem_available; |
|
531 } |
|
532 |
|
533 $prepared = $this->prepare_item_for_response( $data, $request ); |
|
534 $deleted = delete_plugins( array( $request['plugin'] ) ); |
|
535 |
|
536 if ( is_wp_error( $deleted ) ) { |
|
537 $deleted->add_data( array( 'status' => 500 ) ); |
|
538 |
|
539 return $deleted; |
|
540 } |
|
541 |
|
542 return new WP_REST_Response( |
|
543 array( |
|
544 'deleted' => true, |
|
545 'previous' => $prepared->get_data(), |
|
546 ) |
|
547 ); |
|
548 } |
|
549 |
|
550 /** |
|
551 * Prepares the plugin for the REST response. |
|
552 * |
|
553 * @since 5.5.0 |
|
554 * |
|
555 * @param mixed $item Unmarked up and untranslated plugin data from {@see get_plugin_data()}. |
|
556 * @param WP_REST_Request $request Request object. |
|
557 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
558 */ |
|
559 public function prepare_item_for_response( $item, $request ) { |
|
560 $item = _get_plugin_data_markup_translate( $item['_file'], $item, false ); |
|
561 $marked = _get_plugin_data_markup_translate( $item['_file'], $item, true ); |
|
562 |
|
563 $data = array( |
|
564 'plugin' => substr( $item['_file'], 0, - 4 ), |
|
565 'status' => $this->get_plugin_status( $item['_file'] ), |
|
566 'name' => $item['Name'], |
|
567 'plugin_uri' => $item['PluginURI'], |
|
568 'author' => $item['Author'], |
|
569 'author_uri' => $item['AuthorURI'], |
|
570 'description' => array( |
|
571 'raw' => $item['Description'], |
|
572 'rendered' => $marked['Description'], |
|
573 ), |
|
574 'version' => $item['Version'], |
|
575 'network_only' => $item['Network'], |
|
576 'requires_wp' => $item['RequiresWP'], |
|
577 'requires_php' => $item['RequiresPHP'], |
|
578 'textdomain' => $item['TextDomain'], |
|
579 ); |
|
580 |
|
581 $data = $this->add_additional_fields_to_object( $data, $request ); |
|
582 |
|
583 $response = new WP_REST_Response( $data ); |
|
584 $response->add_links( $this->prepare_links( $item ) ); |
|
585 |
|
586 /** |
|
587 * Filters the plugin data for a response. |
|
588 * |
|
589 * @since 5.5.0 |
|
590 * |
|
591 * @param WP_REST_Response $response The response object. |
|
592 * @param array $item The plugin item from {@see get_plugin_data()}. |
|
593 * @param WP_REST_Request $request The request object. |
|
594 */ |
|
595 return apply_filters( 'rest_prepare_plugin', $response, $item, $request ); |
|
596 } |
|
597 |
|
598 /** |
|
599 * Prepares links for the request. |
|
600 * |
|
601 * @since 5.5.0 |
|
602 * |
|
603 * @param array $item The plugin item. |
|
604 * @return array[] |
|
605 */ |
|
606 protected function prepare_links( $item ) { |
|
607 return array( |
|
608 'self' => array( |
|
609 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $item['_file'], 0, - 4 ) ) ), |
|
610 ), |
|
611 ); |
|
612 } |
|
613 |
|
614 /** |
|
615 * Gets the plugin header data for a plugin. |
|
616 * |
|
617 * @since 5.5.0 |
|
618 * |
|
619 * @param string $plugin The plugin file to get data for. |
|
620 * @return array|WP_Error The plugin data, or a WP_Error if the plugin is not installed. |
|
621 */ |
|
622 protected function get_plugin_data( $plugin ) { |
|
623 $plugins = get_plugins(); |
|
624 |
|
625 if ( ! isset( $plugins[ $plugin ] ) ) { |
|
626 return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) ); |
|
627 } |
|
628 |
|
629 $data = $plugins[ $plugin ]; |
|
630 $data['_file'] = $plugin; |
|
631 |
|
632 return $data; |
|
633 } |
|
634 |
|
635 /** |
|
636 * Get's the activation status for a plugin. |
|
637 * |
|
638 * @since 5.5.0 |
|
639 * |
|
640 * @param string $plugin The plugin file to check. |
|
641 * @return string Either 'network-active', 'active' or 'inactive'. |
|
642 */ |
|
643 protected function get_plugin_status( $plugin ) { |
|
644 if ( is_plugin_active_for_network( $plugin ) ) { |
|
645 return 'network-active'; |
|
646 } |
|
647 |
|
648 if ( is_plugin_active( $plugin ) ) { |
|
649 return 'active'; |
|
650 } |
|
651 |
|
652 return 'inactive'; |
|
653 } |
|
654 |
|
655 /** |
|
656 * Handle updating a plugin's status. |
|
657 * |
|
658 * @since 5.5.0 |
|
659 * |
|
660 * @param string $plugin The plugin file to update. |
|
661 * @param string $new_status The plugin's new status. |
|
662 * @param string $current_status The plugin's current status. |
|
663 * |
|
664 * @return true|WP_Error |
|
665 */ |
|
666 protected function plugin_status_permission_check( $plugin, $new_status, $current_status ) { |
|
667 if ( is_multisite() && ( 'network-active' === $current_status || 'network-active' === $new_status ) && ! current_user_can( 'manage_network_plugins' ) ) { |
|
668 return new WP_Error( |
|
669 'rest_cannot_manage_network_plugins', |
|
670 __( 'Sorry, you are not allowed to manage network plugins.' ), |
|
671 array( 'status' => rest_authorization_required_code() ) |
|
672 ); |
|
673 } |
|
674 |
|
675 if ( ( 'active' === $new_status || 'network-active' === $new_status ) && ! current_user_can( 'activate_plugin', $plugin ) ) { |
|
676 return new WP_Error( |
|
677 'rest_cannot_activate_plugin', |
|
678 __( 'Sorry, you are not allowed to activate this plugin.' ), |
|
679 array( 'status' => rest_authorization_required_code() ) |
|
680 ); |
|
681 } |
|
682 |
|
683 if ( 'inactive' === $new_status && ! current_user_can( 'deactivate_plugin', $plugin ) ) { |
|
684 return new WP_Error( |
|
685 'rest_cannot_deactivate_plugin', |
|
686 __( 'Sorry, you are not allowed to deactivate this plugin.' ), |
|
687 array( 'status' => rest_authorization_required_code() ) |
|
688 ); |
|
689 } |
|
690 |
|
691 return true; |
|
692 } |
|
693 |
|
694 /** |
|
695 * Handle updating a plugin's status. |
|
696 * |
|
697 * @since 5.5.0 |
|
698 * |
|
699 * @param string $plugin The plugin file to update. |
|
700 * @param string $new_status The plugin's new status. |
|
701 * @param string $current_status The plugin's current status. |
|
702 * @return true|WP_Error |
|
703 */ |
|
704 protected function handle_plugin_status( $plugin, $new_status, $current_status ) { |
|
705 if ( 'inactive' === $new_status ) { |
|
706 deactivate_plugins( $plugin, false, 'network-active' === $current_status ); |
|
707 |
|
708 return true; |
|
709 } |
|
710 |
|
711 if ( 'active' === $new_status && 'network-active' === $current_status ) { |
|
712 return true; |
|
713 } |
|
714 |
|
715 $network_activate = 'network-active' === $new_status; |
|
716 |
|
717 if ( is_multisite() && ! $network_activate && is_network_only_plugin( $plugin ) ) { |
|
718 return new WP_Error( |
|
719 'rest_network_only_plugin', |
|
720 __( 'Network only plugin must be network activated.' ), |
|
721 array( 'status' => 400 ) |
|
722 ); |
|
723 } |
|
724 |
|
725 $activated = activate_plugin( $plugin, '', $network_activate ); |
|
726 |
|
727 if ( is_wp_error( $activated ) ) { |
|
728 $activated->add_data( array( 'status' => 500 ) ); |
|
729 |
|
730 return $activated; |
|
731 } |
|
732 |
|
733 return true; |
|
734 } |
|
735 |
|
736 /** |
|
737 * Checks that the "plugin" parameter is a valid path. |
|
738 * |
|
739 * @since 5.5.0 |
|
740 * |
|
741 * @param string $file The plugin file parameter. |
|
742 * @return bool |
|
743 */ |
|
744 public function validate_plugin_param( $file ) { |
|
745 if ( ! is_string( $file ) || ! preg_match( '/' . self::PATTERN . '/u', $file ) ) { |
|
746 return false; |
|
747 } |
|
748 |
|
749 $validated = validate_file( plugin_basename( $file ) ); |
|
750 |
|
751 return 0 === $validated; |
|
752 } |
|
753 |
|
754 /** |
|
755 * Sanitizes the "plugin" parameter to be a proper plugin file with ".php" appended. |
|
756 * |
|
757 * @since 5.5.0 |
|
758 * |
|
759 * @param string $file The plugin file parameter. |
|
760 * @return string |
|
761 */ |
|
762 public function sanitize_plugin_param( $file ) { |
|
763 return plugin_basename( sanitize_text_field( $file . '.php' ) ); |
|
764 } |
|
765 |
|
766 /** |
|
767 * Checks if the plugin matches the requested parameters. |
|
768 * |
|
769 * @since 5.5.0 |
|
770 * |
|
771 * @param WP_REST_Request $request The request to require the plugin matches against. |
|
772 * @param array $item The plugin item. |
|
773 * |
|
774 * @return bool |
|
775 */ |
|
776 protected function does_plugin_match_request( $request, $item ) { |
|
777 $search = $request['search']; |
|
778 |
|
779 if ( $search ) { |
|
780 $matched_search = false; |
|
781 |
|
782 foreach ( $item as $field ) { |
|
783 if ( is_string( $field ) && false !== strpos( strip_tags( $field ), $search ) ) { |
|
784 $matched_search = true; |
|
785 break; |
|
786 } |
|
787 } |
|
788 |
|
789 if ( ! $matched_search ) { |
|
790 return false; |
|
791 } |
|
792 } |
|
793 |
|
794 $status = $request['status']; |
|
795 |
|
796 if ( $status && ! in_array( $this->get_plugin_status( $item['_file'] ), $status, true ) ) { |
|
797 return false; |
|
798 } |
|
799 |
|
800 return true; |
|
801 } |
|
802 |
|
803 /** |
|
804 * Checks if the plugin is installed. |
|
805 * |
|
806 * @since 5.5.0 |
|
807 * |
|
808 * @param string $plugin The plugin file. |
|
809 * @return bool |
|
810 */ |
|
811 protected function is_plugin_installed( $plugin ) { |
|
812 return file_exists( WP_PLUGIN_DIR . '/' . $plugin ); |
|
813 } |
|
814 |
|
815 /** |
|
816 * Determine if the endpoints are available. |
|
817 * |
|
818 * Only the 'Direct' filesystem transport, and SSH/FTP when credentials are stored are supported at present. |
|
819 * |
|
820 * @since 5.5.0 |
|
821 * |
|
822 * @return true|WP_Error True if filesystem is available, WP_Error otherwise. |
|
823 */ |
|
824 protected function is_filesystem_available() { |
|
825 $filesystem_method = get_filesystem_method(); |
|
826 |
|
827 if ( 'direct' === $filesystem_method ) { |
|
828 return true; |
|
829 } |
|
830 |
|
831 ob_start(); |
|
832 $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() ); |
|
833 ob_end_clean(); |
|
834 |
|
835 if ( $filesystem_credentials_are_stored ) { |
|
836 return true; |
|
837 } |
|
838 |
|
839 return new WP_Error( 'fs_unavailable', __( 'The filesystem is currently unavailable for managing plugins.' ), array( 'status' => 500 ) ); |
|
840 } |
|
841 |
|
842 /** |
|
843 * Retrieves the plugin's schema, conforming to JSON Schema. |
|
844 * |
|
845 * @since 5.5.0 |
|
846 * |
|
847 * @return array Item schema data. |
|
848 */ |
|
849 public function get_item_schema() { |
|
850 if ( $this->schema ) { |
|
851 return $this->add_additional_fields_schema( $this->schema ); |
|
852 } |
|
853 |
|
854 $this->schema = array( |
|
855 '$schema' => 'http://json-schema.org/draft-04/schema#', |
|
856 'title' => 'plugin', |
|
857 'type' => 'object', |
|
858 'properties' => array( |
|
859 'plugin' => array( |
|
860 'description' => __( 'The plugin file.' ), |
|
861 'type' => 'string', |
|
862 'pattern' => self::PATTERN, |
|
863 'readonly' => true, |
|
864 'context' => array( 'view', 'edit', 'embed' ), |
|
865 ), |
|
866 'status' => array( |
|
867 'description' => __( 'The plugin activation status.' ), |
|
868 'type' => 'string', |
|
869 'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ), |
|
870 'context' => array( 'view', 'edit', 'embed' ), |
|
871 ), |
|
872 'name' => array( |
|
873 'description' => __( 'The plugin name.' ), |
|
874 'type' => 'string', |
|
875 'readonly' => true, |
|
876 'context' => array( 'view', 'edit', 'embed' ), |
|
877 ), |
|
878 'plugin_uri' => array( |
|
879 'description' => __( 'The plugin\'s website address.' ), |
|
880 'type' => 'string', |
|
881 'format' => 'uri', |
|
882 'readonly' => true, |
|
883 'context' => array( 'view', 'edit' ), |
|
884 ), |
|
885 'author' => array( |
|
886 'description' => __( 'The plugin author.' ), |
|
887 'type' => 'object', |
|
888 'readonly' => true, |
|
889 'context' => array( 'view', 'edit' ), |
|
890 ), |
|
891 'author_uri' => array( |
|
892 'description' => __( 'Plugin author\'s website address.' ), |
|
893 'type' => 'string', |
|
894 'format' => 'uri', |
|
895 'readonly' => true, |
|
896 'context' => array( 'view', 'edit' ), |
|
897 ), |
|
898 'description' => array( |
|
899 'description' => __( 'The plugin description.' ), |
|
900 'type' => 'object', |
|
901 'readonly' => true, |
|
902 'context' => array( 'view', 'edit' ), |
|
903 'properties' => array( |
|
904 'raw' => array( |
|
905 'description' => __( 'The raw plugin description.' ), |
|
906 'type' => 'string', |
|
907 ), |
|
908 'rendered' => array( |
|
909 'description' => __( 'The plugin description formatted for display.' ), |
|
910 'type' => 'string', |
|
911 ), |
|
912 ), |
|
913 ), |
|
914 'version' => array( |
|
915 'description' => __( 'The plugin version number.' ), |
|
916 'type' => 'string', |
|
917 'readonly' => true, |
|
918 'context' => array( 'view', 'edit' ), |
|
919 ), |
|
920 'network_only' => array( |
|
921 'description' => __( 'Whether the plugin can only be activated network-wide.' ), |
|
922 'type' => 'boolean', |
|
923 'readonly' => true, |
|
924 'context' => array( 'view', 'edit', 'embed' ), |
|
925 ), |
|
926 'requires_wp' => array( |
|
927 'description' => __( 'Minimum required version of WordPress.' ), |
|
928 'type' => 'string', |
|
929 'readonly' => true, |
|
930 'context' => array( 'view', 'edit', 'embed' ), |
|
931 ), |
|
932 'requires_php' => array( |
|
933 'description' => __( 'Minimum required version of PHP.' ), |
|
934 'type' => 'string', |
|
935 'readonly' => true, |
|
936 'context' => array( 'view', 'edit', 'embed' ), |
|
937 ), |
|
938 'textdomain' => array( |
|
939 'description' => __( 'The plugin\'s text domain.' ), |
|
940 'type' => 'string', |
|
941 'readonly' => true, |
|
942 'context' => array( 'view', 'edit' ), |
|
943 ), |
|
944 ), |
|
945 ); |
|
946 |
|
947 return $this->add_additional_fields_schema( $this->schema ); |
|
948 } |
|
949 |
|
950 /** |
|
951 * Retrieves the query params for the collections. |
|
952 * |
|
953 * @since 5.5.0 |
|
954 * |
|
955 * @return array Query parameters for the collection. |
|
956 */ |
|
957 public function get_collection_params() { |
|
958 $query_params = parent::get_collection_params(); |
|
959 |
|
960 $query_params['context']['default'] = 'view'; |
|
961 |
|
962 $query_params['status'] = array( |
|
963 'description' => __( 'Limits results to plugins with the given status.' ), |
|
964 'type' => 'array', |
|
965 'items' => array( |
|
966 'type' => 'string', |
|
967 'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ), |
|
968 ), |
|
969 ); |
|
970 |
|
971 unset( $query_params['page'], $query_params['per_page'] ); |
|
972 |
|
973 return $query_params; |
|
974 } |
|
975 } |