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 */ 17class 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 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 213 214 if ( ! $this->is_plugin_installed( $plugin ) ) { 215 return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) ); 216 } 217 218 if ( ! is_multisite() ) { 219 return true; 220 } 221 222 if ( ! is_network_only_plugin( $plugin ) || is_plugin_active( $plugin ) || current_user_can( 'manage_network_plugins' ) ) { 223 return true; 224 } 225 226 return new WP_Error( 227 'rest_cannot_view_plugin', 228 __( 'Sorry, you are not allowed to manage this plugin.' ), 229 array( 'status' => rest_authorization_required_code() ) 230 ); 231 } 232 233 /** 234 * Checks if a given request has access to upload plugins. 235 * 236 * @since 5.5.0 237 * 238 * @param WP_REST_Request $request Full details about the request. 239 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. 240 */ 241 public function create_item_permissions_check( $request ) { 242 if ( ! current_user_can( 'install_plugins' ) ) { 243 return new WP_Error( 244 'rest_cannot_install_plugin', 245 __( 'Sorry, you are not allowed to install plugins on this site.' ), 246 array( 'status' => rest_authorization_required_code() ) 247 ); 248 } 249 250 if ( 'inactive' !== $request['status'] && ! current_user_can( 'activate_plugins' ) ) { 251 return new WP_Error( 252 'rest_cannot_activate_plugin', 253 __( 'Sorry, you are not allowed to activate plugins.' ), 254 array( 255 'status' => rest_authorization_required_code(), 256 ) 257 ); 258 } 259 260 return true; 261 } 262 263 /** 264 * Uploads a plugin and optionally activates it. 265 * 266 * @since 5.5.0 267 * 268 * @param WP_REST_Request $request Full details about the request. 269 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 270 */ 271 public function create_item( $request ) { 272 require_once ABSPATH . 'wp-admin/includes/file.php'; 273 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 274 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; 275 require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; 276 277 $slug = $request['slug']; 278 279 // Verify filesystem is accessible first. 280 $filesystem_available = $this->is_filesystem_available(); 281 if ( is_wp_error( $filesystem_available ) ) { 282 return $filesystem_available; 283 } 284 285 $api = plugins_api( 286 'plugin_information', 287 array( 288 'slug' => $slug, 289 'fields' => array( 290 'sections' => false, 291 'language_packs' => true, 292 ), 293 ) 294 ); 295 296 if ( is_wp_error( $api ) ) { 297 if ( false !== strpos( $api->get_error_message(), 'Plugin not found.' ) ) { 298 $api->add_data( array( 'status' => 404 ) ); 299 } else { 300 $api->add_data( array( 'status' => 500 ) ); 301 } 302 303 return $api; 304 } 305 306 $skin = new WP_Ajax_Upgrader_Skin(); 307 $upgrader = new Plugin_Upgrader( $skin ); 308 309 $result = $upgrader->install( $api->download_link ); 310 311 if ( is_wp_error( $result ) ) { 312 $result->add_data( array( 'status' => 500 ) ); 313 314 return $result; 315 } 316 317 // This should be the same as $result above. 318 if ( is_wp_error( $skin->result ) ) { 319 $skin->result->add_data( array( 'status' => 500 ) ); 320 321 return $skin->result; 322 } 323 324 if ( $skin->get_errors()->has_errors() ) { 325 $error = $skin->get_errors(); 326 $error->add_data( array( 'status' => 500 ) ); 327 328 return $error; 329 } 330 331 if ( is_null( $result ) ) { 332 global $wp_filesystem; 333 // Pass through the error from WP_Filesystem if one was raised. 334 if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { 335 return new WP_Error( 'unable_to_connect_to_filesystem', $wp_filesystem->errors->get_error_message(), array( 'status' => 500 ) ); 336 } 337 338 return new WP_Error( 'unable_to_connect_to_filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.' ), array( 'status' => 500 ) ); 339 } 340 341 $file = $upgrader->plugin_info(); 342 343 if ( ! $file ) { 344 return new WP_Error( 'unable_to_determine_installed_plugin', __( 'Unable to determine what plugin was installed.' ), array( 'status' => 500 ) ); 345 } 346 347 if ( 'inactive' !== $request['status'] ) { 348 $can_change_status = $this->plugin_status_permission_check( $file, $request['status'], 'inactive' ); 349 350 if ( is_wp_error( $can_change_status ) ) { 351 return $can_change_status; 352 } 353 354 $changed_status = $this->handle_plugin_status( $file, $request['status'], 'inactive' ); 355 356 if ( is_wp_error( $changed_status ) ) { 357 return $changed_status; 358 } 359 } 360 361 // Install translations. 362 $installed_locales = array_values( get_available_languages() ); 363 /** This filter is documented in wp-includes/update.php */ 364 $installed_locales = apply_filters( 'plugins_update_check_locales', $installed_locales ); 365 366 $language_packs = array_map( 367 function( $item ) { 368 return (object) $item; 369 }, 370 $api->language_packs 371 ); 372 373 $language_packs = array_filter( 374 $language_packs, 375 function( $pack ) use ( $installed_locales ) { 376 return in_array( $pack->language, $installed_locales, true ); 377 } 378 ); 379 380 if ( $language_packs ) { 381 $lp_upgrader = new Language_Pack_Upgrader( $skin ); 382 383 // Install all applicable language packs for the plugin. 384 $lp_upgrader->bulk_upgrade( $language_packs ); 385 } 386 387 $path = WP_PLUGIN_DIR . '/' . $file; 388 $data = get_plugin_data( $path, false, false ); 389 $data['_file'] = $file; 390 391 $response = $this->prepare_item_for_response( $data, $request ); 392 $response->set_status( 201 ); 393 $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $file, 0, - 4 ) ) ) ); 394 395 return $response; 396 } 397 398 /** 399 * Checks if a given request has access to update a specific plugin. 400 * 401 * @since 5.5.0 402 * 403 * @param WP_REST_Request $request Full details about the request. 404 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. 405 */ 406 public function update_item_permissions_check( $request ) { 407 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 408 409 if ( ! current_user_can( 'activate_plugins' ) ) { 410 return new WP_Error( 411 'rest_cannot_manage_plugins', 412 __( 'Sorry, you are not allowed to manage plugins for this site.' ), 413 array( 'status' => rest_authorization_required_code() ) 414 ); 415 } 416 417 $can_read = $this->check_read_permission( $request['plugin'] ); 418 419 if ( is_wp_error( $can_read ) ) { 420 return $can_read; 421 } 422 423 $status = $this->get_plugin_status( $request['plugin'] ); 424 425 if ( $request['status'] && $status !== $request['status'] ) { 426 $can_change_status = $this->plugin_status_permission_check( $request['plugin'], $request['status'], $status ); 427 428 if ( is_wp_error( $can_change_status ) ) { 429 return $can_change_status; 430 } 431 } 432 433 return true; 434 } 435 436 /** 437 * Updates one plugin. 438 * 439 * @since 5.5.0 440 * 441 * @param WP_REST_Request $request Full details about the request. 442 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 443 */ 444 public function update_item( $request ) { 445 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 446 447 $data = $this->get_plugin_data( $request['plugin'] ); 448 449 if ( is_wp_error( $data ) ) { 450 return $data; 451 } 452 453 $status = $this->get_plugin_status( $request['plugin'] ); 454 455 if ( $request['status'] && $status !== $request['status'] ) { 456 $handled = $this->handle_plugin_status( $request['plugin'], $request['status'], $status ); 457 458 if ( is_wp_error( $handled ) ) { 459 return $handled; 460 } 461 } 462 463 $this->update_additional_fields_for_object( $data, $request ); 464 465 $request['context'] = 'edit'; 466 467 return $this->prepare_item_for_response( $data, $request ); 468 } 469 470 /** 471 * Checks if a given request has access to delete a specific plugin. 472 * 473 * @since 5.5.0 474 * 475 * @param WP_REST_Request $request Full details about the request. 476 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. 477 */ 478 public function delete_item_permissions_check( $request ) { 479 if ( ! current_user_can( 'activate_plugins' ) ) { 480 return new WP_Error( 481 'rest_cannot_manage_plugins', 482 __( 'Sorry, you are not allowed to manage plugins for this site.' ), 483 array( 'status' => rest_authorization_required_code() ) 484 ); 485 } 486 487 if ( ! current_user_can( 'delete_plugins' ) ) { 488 return new WP_Error( 489 'rest_cannot_manage_plugins', 490 __( 'Sorry, you are not allowed to delete plugins for this site.' ), 491 array( 'status' => rest_authorization_required_code() ) 492 ); 493 } 494 495 $can_read = $this->check_read_permission( $request['plugin'] ); 496 497 if ( is_wp_error( $can_read ) ) { 498 return $can_read; 499 } 500 501 return true; 502 } 503 504 /** 505 * Deletes one plugin from the site. 506 * 507 * @since 5.5.0 508 * 509 * @param WP_REST_Request $request Full details about the request. 510 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 511 */ 512 public function delete_item( $request ) { 513 require_once ABSPATH . 'wp-admin/includes/file.php'; 514 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 515 516 $data = $this->get_plugin_data( $request['plugin'] ); 517 518 if ( is_wp_error( $data ) ) { 519 return $data; 520 } 521 522 if ( is_plugin_active( $request['plugin'] ) ) { 523 return new WP_Error( 524 'rest_cannot_delete_active_plugin', 525 __( 'Cannot delete an active plugin. Please deactivate it first.' ), 526 array( 'status' => 400 ) 527 ); 528 } 529 530 $filesystem_available = $this->is_filesystem_available(); 531 if ( is_wp_error( $filesystem_available ) ) { 532 return $filesystem_available; 533 } 534 535 $prepared = $this->prepare_item_for_response( $data, $request ); 536 $deleted = delete_plugins( array( $request['plugin'] ) ); 537 538 if ( is_wp_error( $deleted ) ) { 539 $deleted->add_data( array( 'status' => 500 ) ); 540 541 return $deleted; 542 } 543 544 return new WP_REST_Response( 545 array( 546 'deleted' => true, 547 'previous' => $prepared->get_data(), 548 ) 549 ); 550 } 551 552 /** 553 * Prepares the plugin for the REST response. 554 * 555 * @since 5.5.0 556 * 557 * @param mixed $item Unmarked up and untranslated plugin data from {@see get_plugin_data()}. 558 * @param WP_REST_Request $request Request object. 559 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 560 */ 561 public function prepare_item_for_response( $item, $request ) { 562 $item = _get_plugin_data_markup_translate( $item['_file'], $item, false ); 563 $marked = _get_plugin_data_markup_translate( $item['_file'], $item, true ); 564 565 $data = array( 566 'plugin' => substr( $item['_file'], 0, - 4 ), 567 'status' => $this->get_plugin_status( $item['_file'] ), 568 'name' => $item['Name'], 569 'plugin_uri' => $item['PluginURI'], 570 'author' => $item['Author'], 571 'author_uri' => $item['AuthorURI'], 572 'description' => array( 573 'raw' => $item['Description'], 574 'rendered' => $marked['Description'], 575 ), 576 'version' => $item['Version'], 577 'network_only' => $item['Network'], 578 'requires_wp' => $item['RequiresWP'], 579 'requires_php' => $item['RequiresPHP'], 580 'textdomain' => $item['TextDomain'], 581 ); 582 583 $data = $this->add_additional_fields_to_object( $data, $request ); 584 585 $response = new WP_REST_Response( $data ); 586 $response->add_links( $this->prepare_links( $item ) ); 587 588 /** 589 * Filters plugin data for a REST API response. 590 * 591 * @since 5.5.0 592 * 593 * @param WP_REST_Response $response The response object. 594 * @param array $item The plugin item from {@see get_plugin_data()}. 595 * @param WP_REST_Request $request The request object. 596 */ 597 return apply_filters( 'rest_prepare_plugin', $response, $item, $request ); 598 } 599 600 /** 601 * Prepares links for the request. 602 * 603 * @since 5.5.0 604 * 605 * @param array $item The plugin item. 606 * @return array[] 607 */ 608 protected function prepare_links( $item ) { 609 return array( 610 'self' => array( 611 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $item['_file'], 0, - 4 ) ) ), 612 ), 613 ); 614 } 615 616 /** 617 * Gets the plugin header data for a plugin. 618 * 619 * @since 5.5.0 620 * 621 * @param string $plugin The plugin file to get data for. 622 * @return array|WP_Error The plugin data, or a WP_Error if the plugin is not installed. 623 */ 624 protected function get_plugin_data( $plugin ) { 625 $plugins = get_plugins(); 626 627 if ( ! isset( $plugins[ $plugin ] ) ) { 628 return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) ); 629 } 630 631 $data = $plugins[ $plugin ]; 632 $data['_file'] = $plugin; 633 634 return $data; 635 } 636 637 /** 638 * Get's the activation status for a plugin. 639 * 640 * @since 5.5.0 641 * 642 * @param string $plugin The plugin file to check. 643 * @return string Either 'network-active', 'active' or 'inactive'. 644 */ 645 protected function get_plugin_status( $plugin ) { 646 if ( is_plugin_active_for_network( $plugin ) ) { 647 return 'network-active'; 648 } 649 650 if ( is_plugin_active( $plugin ) ) { 651 return 'active'; 652 } 653 654 return 'inactive'; 655 } 656 657 /** 658 * Handle updating a plugin's status. 659 * 660 * @since 5.5.0 661 * 662 * @param string $plugin The plugin file to update. 663 * @param string $new_status The plugin's new status. 664 * @param string $current_status The plugin's current status. 665 * @return true|WP_Error 666 */ 667 protected function plugin_status_permission_check( $plugin, $new_status, $current_status ) { 668 if ( is_multisite() && ( 'network-active' === $current_status || 'network-active' === $new_status ) && ! current_user_can( 'manage_network_plugins' ) ) { 669 return new WP_Error( 670 'rest_cannot_manage_network_plugins', 671 __( 'Sorry, you are not allowed to manage network plugins.' ), 672 array( 'status' => rest_authorization_required_code() ) 673 ); 674 } 675 676 if ( ( 'active' === $new_status || 'network-active' === $new_status ) && ! current_user_can( 'activate_plugin', $plugin ) ) { 677 return new WP_Error( 678 'rest_cannot_activate_plugin', 679 __( 'Sorry, you are not allowed to activate this plugin.' ), 680 array( 'status' => rest_authorization_required_code() ) 681 ); 682 } 683 684 if ( 'inactive' === $new_status && ! current_user_can( 'deactivate_plugin', $plugin ) ) { 685 return new WP_Error( 686 'rest_cannot_deactivate_plugin', 687 __( 'Sorry, you are not allowed to deactivate this plugin.' ), 688 array( 'status' => rest_authorization_required_code() ) 689 ); 690 } 691 692 return true; 693 } 694 695 /** 696 * Handle updating a plugin's status. 697 * 698 * @since 5.5.0 699 * 700 * @param string $plugin The plugin file to update. 701 * @param string $new_status The plugin's new status. 702 * @param string $current_status The plugin's current status. 703 * @return true|WP_Error 704 */ 705 protected function handle_plugin_status( $plugin, $new_status, $current_status ) { 706 if ( 'inactive' === $new_status ) { 707 deactivate_plugins( $plugin, false, 'network-active' === $current_status ); 708 709 return true; 710 } 711 712 if ( 'active' === $new_status && 'network-active' === $current_status ) { 713 return true; 714 } 715 716 $network_activate = 'network-active' === $new_status; 717 718 if ( is_multisite() && ! $network_activate && is_network_only_plugin( $plugin ) ) { 719 return new WP_Error( 720 'rest_network_only_plugin', 721 __( 'Network only plugin must be network activated.' ), 722 array( 'status' => 400 ) 723 ); 724 } 725 726 $activated = activate_plugin( $plugin, '', $network_activate ); 727 728 if ( is_wp_error( $activated ) ) { 729 $activated->add_data( array( 'status' => 500 ) ); 730 731 return $activated; 732 } 733 734 return true; 735 } 736 737 /** 738 * Checks that the "plugin" parameter is a valid path. 739 * 740 * @since 5.5.0 741 * 742 * @param string $file The plugin file parameter. 743 * @return bool 744 */ 745 public function validate_plugin_param( $file ) { 746 if ( ! is_string( $file ) || ! preg_match( '/' . self::PATTERN . '/u', $file ) ) { 747 return false; 748 } 749 750 $validated = validate_file( plugin_basename( $file ) ); 751 752 return 0 === $validated; 753 } 754 755 /** 756 * Sanitizes the "plugin" parameter to be a proper plugin file with ".php" appended. 757 * 758 * @since 5.5.0 759 * 760 * @param string $file The plugin file parameter. 761 * @return string 762 */ 763 public function sanitize_plugin_param( $file ) { 764 return plugin_basename( sanitize_text_field( $file . '.php' ) ); 765 } 766 767 /** 768 * Checks if the plugin matches the requested parameters. 769 * 770 * @since 5.5.0 771 * 772 * @param WP_REST_Request $request The request to require the plugin matches against. 773 * @param array $item The plugin item. 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} 976