r( 'rest_cannot_manage_plugins', __( 'Sorry, you are not allowed to manage plugins for this site.' ), array( 'status' => rest_authorization_required_code() ) ); } $can_read = $this->check_read_permission( $request['plugin'] ); if ( is_wp_error( $can_read ) ) { return $can_read; } $status = $this->get_plugin_status( $request['plugin'] ); if ( $request['status'] && $status !== $request['status'] ) { $can_change_status = $this->plugin_status_permission_check( $request['plugin'], $request['status'], $status ); if ( is_wp_error( $can_change_status ) ) { return $can_change_status; } } return true; } /** * Updates one plugin. * * @since 5.5.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function update_item( $request ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; $data = $this->get_plugin_data( $request['plugin'] ); if ( is_wp_error( $data ) ) { return $data; } $status = $this->get_plugin_status( $request['plugin'] ); if ( $request['status'] && $status !== $request['status'] ) { $handled = $this->handle_plugin_status( $request['plugin'], $request['status'], $status ); if ( is_wp_error( $handled ) ) { return $handled; } } $this->update_additional_fields_for_object( $data, $request ); $request['context'] = 'edit'; return $this->prepare_item_for_response( $data, $request ); } /** * Checks if a given request has access to delete a specific plugin. * * @since 5.5.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. */ public function delete_item_permissions_check( $request ) { if ( ! current_user_can( 'activate_plugins' ) ) { return new WP_Error( 'rest_cannot_manage_plugins', __( 'Sorry, you are not allowed to manage plugins for this site.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( ! current_user_can( 'delete_plugins' ) ) { return new WP_Error( 'rest_cannot_manage_plugins', __( 'Sorry, you are not allowed to delete plugins for this site.' ), array( 'status' => rest_authorization_required_code() ) ); } $can_read = $this->check_read_permission( $request['plugin'] ); if ( is_wp_error( $can_read ) ) { return $can_read; } return true; } /** * Deletes one plugin from the site. * * @since 5.5.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function delete_item( $request ) { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/plugin.php'; $data = $this->get_plugin_data( $request['plugin'] ); if ( is_wp_error( $data ) ) { return $data; } if ( is_plugin_active( $request['plugin'] ) ) { return new WP_Error( 'rest_cannot_delete_active_plugin', __( 'Cannot delete an active plugin. Please deactivate it first.' ), array( 'status' => 400 ) ); } $filesystem_available = $this->is_filesystem_available(); if ( is_wp_error( $filesystem_available ) ) { return $filesystem_available; } $prepared = $this->prepare_item_for_response( $data, $request ); $deleted = delete_plugins( array( $request['plugin'] ) ); if ( is_wp_error( $deleted ) ) { $deleted->add_data( array( 'status' => 500 ) ); return $deleted; } return new WP_REST_Response( array( 'deleted' => true, 'previous' => $prepared->get_data(), ) ); } /** * Prepares the plugin for the REST response. * * @since 5.5.0 * * @param array $item Unmarked up and untranslated plugin data from {@see get_plugin_data()}. * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function prepare_item_for_response( $item, $request ) { $fields = $this->get_fields_for_response( $request ); $item = _get_plugin_data_markup_translate( $item['_file'], $item, false ); $marked = _get_plugin_data_markup_translate( $item['_file'], $item, true ); $data = array( 'plugin' => substr( $item['_file'], 0, - 4 ), 'status' => $this->get_plugin_status( $item['_file'] ), 'name' => $item['Name'], 'plugin_uri' => $item['PluginURI'], 'author' => $item['Author'], 'author_uri' => $item['AuthorURI'], 'description' => array( 'raw' => $item['Description'], 'rendered' => $marked['Description'], ), 'version' => $item['Version'], 'network_only' => $item['Network'], 'requires_wp' => $item['RequiresWP'], 'requires_php' => $item['RequiresPHP'], 'textdomain' => $item['TextDomain'], ); $data = $this->add_additional_fields_to_object( $data, $request ); $response = new WP_REST_Response( $data ); if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { $response->add_links( $this->prepare_links( $item ) ); } /** * Filters plugin data for a REST API response. * * @since 5.5.0 * * @param WP_REST_Response $response The response object. * @param array $item The plugin item from {@see get_plugin_data()}. * @param WP_REST_Request $request The request object. */ return apply_filters( 'rest_prepare_plugin', $response, $item, $request ); } /** * Prepares links for the request. * * @since 5.5.0 * * @param array $item The plugin item. * @return array[] */ protected function prepare_links( $item ) { return array( 'self' => array( 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $item['_file'], 0, - 4 ) ) ), ), ); } /** * Gets the plugin header data for a plugin. * * @since 5.5.0 * * @param string $plugin The plugin file to get data for. * @return array|WP_Error The plugin data, or a WP_Error if the plugin is not installed. */ protected function get_plugin_data( $plugin ) { $plugins = get_plugins(); if ( ! isset( $plugins[ $plugin ] ) ) { return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) ); } $data = $plugins[ $plugin ]; $data['_file'] = $plugin; return $data; } /** * Get's the activation status for a plugin. * * @since 5.5.0 * * @param string $plugin The plugin file to check. * @return string Either 'network-active', 'active' or 'inactive'. */ protected function get_plugin_status( $plugin ) { if ( is_plugin_active_for_network( $plugin ) ) { return 'network-active'; } if ( is_plugin_active( $plugin ) ) { return 'active'; } return 'inactive'; } /** * Handle updating a plugin's status. * * @since 5.5.0 * * @param string $plugin The plugin file to update. * @param string $new_status The plugin's new status. * @param string $current_status The plugin's current status. * @return true|WP_Error */ protected function plugin_status_permission_check( $plugin, $new_status, $current_status ) { if ( is_multisite() && ( 'network-active' === $current_status || 'network-active' === $new_status ) && ! current_user_can( 'manage_network_plugins' ) ) { return new WP_Error( 'rest_cannot_manage_network_plugins', __( 'Sorry, you are not allowed to manage network plugins.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( ( 'active' === $new_status || 'network-active' === $new_status ) && ! current_user_can( 'activate_plugin', $plugin ) ) { return new WP_Error( 'rest_cannot_activate_plugin', __( 'Sorry, you are not allowed to activate this plugin.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( 'inactive' === $new_status && ! current_user_can( 'deactivate_plugin', $plugin ) ) { return new WP_Error( 'rest_cannot_deactivate_plugin', __( 'Sorry, you are not allowed to deactivate this plugin.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Handle updating a plugin's status. * * @since 5.5.0 * * @param string $plugin The plugin file to update. * @param string $new_status The plugin's new status. * @param string $current_status The plugin's current status. * @return true|WP_Error */ protected function handle_plugin_status( $plugin, $new_status, $current_status ) { if ( 'inactive' === $new_status ) { deactivate_plugins( $plugin, false, 'network-active' === $current_status ); return true; } if ( 'active' === $new_status && 'network-active' === $current_status ) { return true; } $network_activate = 'network-active' === $new_status; if ( is_multisite() && ! $network_activate && is_network_only_plugin( $plugin ) ) { return new WP_Error( 'rest_network_only_plugin', __( 'Network only plugin must be network activated.' ), array( 'status' => 400 ) ); } $activated = activate_plugin( $plugin, '', $network_activate ); if ( is_wp_error( $activated ) ) { $activated->add_data( array( 'status' => 500 ) ); return $activated; } return true; } /** * Checks that the "plugin" parameter is a valid path. * * @since 5.5.0 * * @param string $file The plugin file parameter. * @return bool */ public function validate_plugin_param( $file ) { if ( ! is_string( $file ) || ! preg_match( '/' . self::PATTERN . '/u', $file ) ) { return false; } $validated = validate_file( plugin_basename( $file ) ); return 0 === $validated; } /** * Sanitizes the "plugin" parameter to be a proper plugin file with ".php" appended. * * @since 5.5.0 * * @param string $file The plugin file parameter. * @return string */ public function sanitize_plugin_param( $file ) { return plugin_basename( sanitize_text_field( $file . '.php' ) ); } /** * Checks if the plugin matches the requested parameters. * * @since 5.5.0 * * @param WP_REST_Request $request The request to require the plugin matches against. * @param array $item The plugin item. * @return bool */ protected function does_plugin_match_request( $request, $item ) { $search = $request['search']; if ( $search ) { $matched_search = false; foreach ( $item as $field ) { if ( is_string( $field ) && str_contains( strip_tags( $field ), $search ) ) { $matched_search = true; break; } } if ( ! $matched_search ) { return false; } } $status = $request['status']; if ( $status && ! in_array( $this->get_plugin_status( $item['_file'] ), $status, true ) ) { return false; } return true; } /** * Checks if the plugin is installed. * * @since 5.5.0 * * @param string $plugin The plugin file. * @return bool */ protected function is_plugin_installed( $plugin ) { return file_exists( WP_PLUGIN_DIR . '/' . $plugin ); } /** * Determine if the endpoints are available. * * Only the 'Direct' filesystem transport, and SSH/FTP when credentials are stored are supported at present. * * @since 5.5.0 * * @return true|WP_Error True if filesystem is available, WP_Error otherwise. */ protected function is_filesystem_available() { $filesystem_method = get_filesystem_method(); if ( 'direct' === $filesystem_method ) { return true; } ob_start(); $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() ); ob_end_clean(); if ( $filesystem_credentials_are_stored ) { return true; } return new WP_Error( 'fs_unavailable', __( 'The filesystem is currently unavailable for managing plugins.' ), array( 'status' => 500 ) ); } /** * Retrieves the plugin's schema, conforming to JSON Schema. * * @since 5.5.0 * * @return array Item schema data. */ public function get_item_schema() { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $this->schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'plugin', 'type' => 'object', 'properties' => array( 'plugin' => array( 'description' => __( 'The plugin file.' ), 'type' => 'string', 'pattern' => self::PATTERN, 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), 'status' => array( 'description' => __( 'The plugin activation status.' ), 'type' => 'string', 'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ), 'context' => array( 'view', 'edit', 'embed' ), ), 'name' => array( 'description' => __( 'The plugin name.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), 'plugin_uri' => array( 'description' => __( 'The plugin\'s website address.' ), 'type' => 'string', 'format' => 'uri', 'readonly' => true, 'context' => array( 'view', 'edit' ), ), 'author' => array( 'description' => __( 'The plugin author.' ), 'type' => 'object', 'readonly' => true, 'context' => array( 'view', 'edit' ), ), 'author_uri' => array( 'description' => __( 'Plugin author\'s website address.' ), 'type' => 'string', 'format' => 'uri', 'readonly' => true, 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'The plugin description.' ), 'type' => 'object', 'readonly' => true, 'context' => array( 'view', 'edit' ), 'properties' => array( 'raw' => array( 'description' => __( 'The raw plugin description.' ), 'type' => 'string', ), 'rendered' => array( 'description' => __( 'The plugin description formatted for display.' ), 'type' => 'string', ), ), ), 'version' => array( 'description' => __( 'The plugin version number.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit' ), ), 'network_only' => array( 'description' => __( 'Whether the plugin can only be activated network-wide.' ), 'type' => 'boolean', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), 'requires_wp' => array( 'description' => __( 'Minimum required version of WordPress.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), 'requires_php' => array( 'description' => __( 'Minimum required version of PHP.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), 'textdomain' => array( 'description' => __( 'The plugin\'s text domain.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit' ), ), ), ); return $this->add_additional_fields_schema( $this->schema ); } /** * Retrieves the query params for the collections. * * @since 5.5.0 * * @return array Query parameters for the collection. */ public function get_collection_params() { $query_params = parent::get_collection_params(); $query_params['context']['default'] = 'view'; $query_params['status'] = array( 'description' => __( 'Limits results to plugins with the given status.' ), 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ), ), ); unset( $query_params['page'], $query_params['per_page'] ); return $query_params; } }