diff --git a/_inc/client/rest-api/index.js b/_inc/client/rest-api/index.js index 1ef3a08345e9..21d255516116 100644 --- a/_inc/client/rest-api/index.js +++ b/_inc/client/rest-api/index.js @@ -170,7 +170,9 @@ function JetpackRestApiClient( root, nonce ) { .then( parseJsonResponse ), activateAkismet: () => - postRequest( `${ apiRoot }jetpack/v4/plugins/akismet/activate`, postParams ) + postRequest( `${ apiRoot }jetpack/v4/plugins`, postParams, { + body: JSON.stringify( { slug: 'akismet', status: 'active' } ), + } ) .then( checkStatus ) .then( parseJsonResponse ), diff --git a/_inc/lib/class.core-rest-api-endpoints.php b/_inc/lib/class.core-rest-api-endpoints.php index 02e20fc84c0a..219751ba0822 100644 --- a/_inc/lib/class.core-rest-api-endpoints.php +++ b/_inc/lib/class.core-rest-api-endpoints.php @@ -413,18 +413,94 @@ public static function register_endpoints() { 'permission_callback' => __CLASS__ . '::view_admin_page_permission_check', ) ); - // Plugins: get list of all plugins. - register_rest_route( 'jetpack/v4', '/plugins', array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => __CLASS__ . '::get_plugins', - 'permission_callback' => __CLASS__ . '::activate_plugins_permission_check', - ) ); + /* + * Plugins: manage plugins on your site. + * + * @since 8.9.0 + * + * @to-do: deprecate and switch to /wp/v2/plugins when WordPress 5.5 is the minimum required version. + * Noting that the `source` parameter is Jetpack-specific (not implemented in Core). + */ + register_rest_route( + 'jetpack/v4', + '/plugins', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => __CLASS__ . '::get_plugins', + 'permission_callback' => __CLASS__ . '::activate_plugins_permission_check', + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => __CLASS__ . '::install_plugin', + 'permission_callback' => __CLASS__ . '::activate_plugins_permission_check', + 'args' => array( + 'slug' => array( + 'type' => 'string', + 'required' => true, + 'description' => __( 'WordPress.org plugin directory slug.', 'jetpack' ), + 'pattern' => '[\w\-]+', + ), + 'status' => array( + 'description' => __( 'The plugin activation status.', 'jetpack' ), + 'type' => 'string', + 'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ), + 'default' => 'inactive', + ), + 'source' => array( + 'required' => false, + 'type' => 'string', + 'validate_callback' => __CLASS__ . '::validate_string', + ), + ), + ), + ) + ); - register_rest_route( 'jetpack/v4', '/plugins/akismet/activate', array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => __CLASS__ . '::activate_akismet', - 'permission_callback' => __CLASS__ . '::activate_plugins_permission_check', - ) ); + /* + * Plugins: activate a specific plugin. + * + * @since 8.9.0 + * + * @to-do: deprecate and switch to /wp/v2/plugins when WordPress 5.5 is the minimum required version. + * Noting that the `source` parameter is Jetpack-specific (not implemented in Core). + */ + register_rest_route( + 'jetpack/v4', + '/plugins/(?P[^.\/]+(?:\/[^.\/]+)?)', + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => __CLASS__ . '::activate_plugin', + 'permission_callback' => __CLASS__ . '::activate_plugins_permission_check', + 'args' => array( + 'status' => array( + 'required' => true, + 'type' => 'string', + 'validate_callback' => __CLASS__ . '::validate_activate_plugin', + ), + 'source' => array( + 'required' => false, + 'type' => 'string', + 'validate_callback' => __CLASS__ . '::validate_string', + ), + ), + ) + ); + + /** + * Install and Activate the Akismet plugin. + * + * @deprecated 8.9.0 Use the /plugins route instead. + */ + register_rest_route( + 'jetpack/v4', + '/plugins/akismet/activate', + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => __CLASS__ . '::activate_akismet', + 'permission_callback' => __CLASS__ . '::activate_plugins_permission_check', + ) + ); // Plugins: check if the plugin is active. register_rest_route( 'jetpack/v4', '/plugin/(?P[a-z\/\.\-_]+)', array( @@ -3275,31 +3351,6 @@ public static function get_plugin_update_count() { } - /** - * Returns a list of all plugins in the site. - * - * @since 4.2.0 - * @uses get_plugins() - * - * @return array - */ - private static function core_get_plugins() { - if ( ! function_exists( 'get_plugins' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ - $plugins = apply_filters( 'all_plugins', get_plugins() ); - - if ( is_array( $plugins ) && ! empty( $plugins ) ) { - foreach ( $plugins as $plugin_slug => $plugin_data ) { - $plugins[ $plugin_slug ]['active'] = self::core_is_plugin_active( $plugin_slug ); - } - return $plugins; - } - - return array(); - } - /** * Deprecated - Get third party plugin API keys. * @deprecated @@ -3394,22 +3445,6 @@ public static function validate_service_api_key_mapbox( $key ) { } - /** - * Checks if the queried plugin is active. - * - * @since 4.2.0 - * @uses is_plugin_active() - * - * @return bool - */ - private static function core_is_plugin_active( $plugin ) { - if ( ! function_exists( 'is_plugin_active' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - - return is_plugin_active( $plugin ); - } - /** * Get plugins data in site. * @@ -3418,7 +3453,8 @@ private static function core_is_plugin_active( $plugin ) { * @return WP_REST_Response|WP_Error List of plugins in the site. Otherwise, a WP_Error instance with the corresponding error. */ public static function get_plugins() { - $plugins = self::core_get_plugins(); + jetpack_require_lib( 'plugins' ); + $plugins = Jetpack_Plugins::get_plugins(); if ( ! empty( $plugins ) ) { return rest_ensure_response( $plugins ); @@ -3432,25 +3468,219 @@ public static function get_plugins() { * * @since 7.7 * + * @deprecated 8.9.0 Use install_plugin instead. + * * @return WP_REST_Response A response indicating whether or not the installation was successful. */ public static function activate_akismet() { + _deprecated_function( __METHOD__, 'jetpack-8.9.0', 'install_plugin' ); + + $args = array( + 'slug' => 'akismet', + 'status' => 'active', + ); + return self::install_plugin( $args ); + } + + /** + * Install a specific plugin and optionally activates it. + * + * @since 8.9.0 + * + * @param WP_REST_Request $request { + * Array of parameters received by request. + * + * @type string $slug Plugin slug. + * @type string $status Plugin status. + * @type string $source Where did the plugin installation request originate. + * } + * + * @return WP_REST_Response|WP_Error A response object if the installation and / or activation was successful, or a WP_Error object if it failed. + */ + public static function install_plugin( $request ) { + $plugin = stripslashes( $request['slug'] ); + jetpack_require_lib( 'plugins' ); - $result = Jetpack_Plugins::install_and_activate_plugin('akismet'); - if ( is_wp_error( $result ) ) { - return rest_ensure_response( array( - 'code' => 'failure', - 'message' => esc_html__( 'Unable to activate Akismet', 'jetpack' ) - ) ); + // Let's make sure the plugin isn't already installed. + $plugin_id = Jetpack_Plugins::get_plugin_id_by_slug( $plugin ); + + // If not installed, let's install now. + if ( ! $plugin_id ) { + $result = Jetpack_Plugins::install_plugin( $plugin ); + + if ( is_wp_error( $result ) ) { + return new WP_Error( + 'install_plugin_failed', + sprintf( + /* translators: %1$s: plugin name. -- %2$s: error message. */ + __( 'Unable to install %1$s: %2$s ', 'jetpack' ), + $plugin, + $result->get_error_message() + ), + array( 'status' => 500 ) + ); + } + } + + /* + * We may want to activate the plugin as well. + * Let's check for the status parameter in the request to find out. + * If none was passed (or something other than active), let's return now. + */ + if ( empty( $request['status'] ) || 'active' !== $request['status'] ) { + return rest_ensure_response( + array( + 'code' => 'success', + 'message' => esc_html( + sprintf( + /* translators: placeholder is a plugin name. */ + __( 'Installed %s', 'jetpack' ), + $plugin + ) + ), + ) + ); + } + + /* + * Proceed with plugin activation. + * Let's check again for the plugin's ID if we don't already have it. + */ + if ( ! $plugin_id ) { + $plugin_id = Jetpack_Plugins::get_plugin_id_by_slug( $plugin ); + if ( ! $plugin_id ) { + return new WP_Error( + 'unable_to_determine_installed_plugin', + __( 'Unable to determine what plugin was installed.', 'jetpack' ), + array( 'status' => 500 ) + ); + } + } + + $source = ! empty( $request['source'] ) ? stripslashes( $request['source'] ) : 'rest_api'; + $plugin_args = array( + 'plugin' => substr( $plugin_id, 0, - 4 ), + 'status' => 'active', + 'source' => $source, + ); + return self::activate_plugin( $plugin_args ); + } + + /** + * Activate a specific plugin. + * + * @since 8.9.0 + * + * @param WP_REST_Request $request { + * Array of parameters received by request. + * + * @type string $plugin Plugin long slug (slug/index-file) + * @type string $status Plugin status. We only support active in Jetpack. + * @type string $source Where did the plugin installation request originate. + * } + * + * @return WP_REST_Response|WP_Error A response object if the activation was successful, or a WP_Error object if the activation failed. + */ + public static function activate_plugin( $request ) { + /* + * We need an "active" status parameter to be passed to the request + * just like the core plugins endpoind we'll eventually switch to. + */ + if ( empty( $request['status'] ) || 'active' !== $request['status'] ) { + return new WP_Error( + 'missing_status_parameter', + esc_html__( 'Status parameter missing.', 'jetpack' ), + array( 'status' => 403 ) + ); + } + + jetpack_require_lib( 'plugins' ); + $plugins = Jetpack_Plugins::get_plugins(); + + if ( empty( $plugins ) ) { + return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) ); + } + + if ( empty( $request['plugin'] ) ) { + return new WP_Error( 'no_plugin_specified', esc_html__( 'You did not specify a plugin.', 'jetpack' ), array( 'status' => 404 ) ); + } + + $plugin = $request['plugin'] . '.php'; + + // Is the plugin installed? + if ( ! in_array( $plugin, array_keys( $plugins ), true ) ) { + return new WP_Error( + 'plugin_not_found', + esc_html( + sprintf( + /* translators: placeholder is a plugin slug. */ + __( 'Plugin %s is not installed.', 'jetpack' ), + $plugin + ) + ), + array( 'status' => 404 ) + ); + } + + // Is the plugin active already? + $status = Jetpack_Plugins::get_plugin_status( $plugin ); + if ( in_array( $status, array( 'active', 'network-active' ), true ) ) { + return new WP_Error( + 'plugin_already_active', + esc_html( + sprintf( + /* translators: placeholder is a plugin slug. */ + __( 'Plugin %s is already active.', 'jetpack' ), + $plugin + ) + ), + array( 'status' => 404 ) + ); + } + + // Now try to activate the plugin. + $activated = activate_plugin( $plugin ); + + if ( is_wp_error( $activated ) ) { + return $activated; } else { - return rest_ensure_response( array( - 'code' => 'success', - 'message' => esc_html__( 'Activated Akismet', 'jetpack' ) - ) ); + $source = ! empty( $request['source'] ) ? stripslashes( $request['source'] ) : 'rest_api'; + /** + * Fires when Jetpack installs a plugin for you. + * + * @since 8.9.0 + * + * @param string $plugin_file Plugin file. + * @param string $source Where did the plugin installation originate. + */ + do_action( 'jetpack_activated_plugin', $plugin, $source ); + return rest_ensure_response( + array( + 'code' => 'success', + 'message' => sprintf( + /* translators: placeholder is a plugin name. */ + esc_html__( 'Activated %s', 'jetpack' ), + $plugin + ), + ) + ); } } + /** + * Check if a plugin can be activated. + * + * @since 8.9.0 + * + * @param string|bool $value Value to check. + * @param WP_REST_Request $request The request sent to the WP REST API. + * @param string $param Name of the parameter passed to endpoint holding $value. + */ + public static function validate_activate_plugin( $value, $request, $param ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return 'active' === $value; + } + /** * Get data about the queried plugin. Currently it only returns whether the plugin is active or not. * @@ -3465,8 +3695,8 @@ public static function activate_akismet() { * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error. */ public static function get_plugin( $request ) { - - $plugins = self::core_get_plugins(); + jetpack_require_lib( 'plugins' ); + $plugins = Jetpack_Plugins::get_plugins(); if ( empty( $plugins ) ) { return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) ); @@ -3480,7 +3710,7 @@ public static function get_plugin( $request ) { $plugin_data = $plugins[ $plugin ]; - $plugin_data['active'] = self::core_is_plugin_active( $plugin ); + $plugin_data['active'] = in_array( Jetpack_Plugins::get_plugin_status( $plugin ), array( 'active', 'network-active' ), true ); return rest_ensure_response( array( 'code' => 'success', diff --git a/_inc/lib/plugins.php b/_inc/lib/plugins.php index 9c8e3bc4da48..78b386159366 100644 --- a/_inc/lib/plugins.php +++ b/_inc/lib/plugins.php @@ -1,4 +1,4 @@ -install( $zip_url ); if ( is_wp_error( $result ) ) { - return $result; + return $result; } - $plugin = Jetpack_Plugins::get_plugin_id_by_slug( $slug ); + $plugin = self::get_plugin_id_by_slug( $slug ); $error_code = 'install_error'; if ( ! $plugin ) { - $error = __( 'There was an error installing your plugin', 'jetpack' ); + $error = __( 'There was an error installing your plugin', 'jetpack' ); } if ( ! $result ) { - $error_code = $upgrader->skin->get_main_error_code(); - $message = $upgrader->skin->get_main_error_message(); - $error = $message ? $message : __( 'An unknown error occurred during installation', 'jetpack' ); + $error_code = $upgrader->skin->get_main_error_code(); + $message = $upgrader->skin->get_main_error_message(); + $error = $message ? $message : __( 'An unknown error occurred during installation', 'jetpack' ); } if ( ! empty( $error ) ) { @@ -95,11 +98,21 @@ public static function install_plugin( $slug ) { return (array) $upgrader->skin->get_upgrade_messages(); } - protected static function generate_wordpress_org_plugin_download_link( $plugin_slug ) { + /** + * Get WordPress.org zip download link from a plugin slug + * + * @param string $plugin_slug Plugin slug. + */ + protected static function generate_wordpress_org_plugin_download_link( $plugin_slug ) { return "https://downloads.wordpress.org/plugin/$plugin_slug.latest-stable.zip"; - } + } - public static function get_plugin_id_by_slug( $slug ) { + /** + * Get the plugin ID (composed of the plugin slug and the name of the main plugin file) from a plugin slug. + * + * @param string $slug Plugin slug. + */ + public static function get_plugin_id_by_slug( $slug ) { // Check if get_plugins() function exists. This is required on the front end of the // site, since it is in a file that is normally only loaded in the admin. if ( ! function_exists( 'get_plugins' ) ) { @@ -111,6 +124,7 @@ public static function get_plugin_id_by_slug( $slug ) { if ( ! is_array( $plugins ) ) { return false; } + foreach ( $plugins as $plugin_file => $plugin_data ) { if ( self::get_slug_from_file_path( $plugin_file ) === $slug ) { return $plugin_file; @@ -120,13 +134,67 @@ public static function get_plugin_id_by_slug( $slug ) { return false; } + /** + * Get the plugin slug from the plugin ID (composed of the plugin slug and the name of the main plugin file) + * + * @param string $plugin_file Plugin file (ID -- e.g. hello-dolly/hello.php). + */ protected static function get_slug_from_file_path( $plugin_file ) { // Similar to get_plugin_slug() method. $slug = dirname( $plugin_file ); if ( '.' === $slug ) { - $slug = preg_replace( "/(.+)\.php$/", "$1", $plugin_file ); + $slug = preg_replace( '/(.+)\.php$/', '$1', $plugin_file ); } return $slug; } + + /** + * Get the activation status for a plugin. + * + * @since 8.9.0 + * + * @param string $plugin_file The plugin file to check. + * @return string Either 'network-active', 'active' or 'inactive'. + */ + public static function get_plugin_status( $plugin_file ) { + if ( is_plugin_active_for_network( $plugin_file ) ) { + return 'network-active'; + } + + if ( is_plugin_active( $plugin_file ) ) { + return 'active'; + } + + return 'inactive'; + } + + /** + * Returns a list of all plugins in the site. + * + * @since 8.9.0 + * @uses get_plugins() + * + * @return array + */ + public static function get_plugins() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ + $plugins = apply_filters( 'all_plugins', get_plugins() ); + + if ( is_array( $plugins ) && ! empty( $plugins ) ) { + foreach ( $plugins as $plugin_slug => $plugin_data ) { + $plugins[ $plugin_slug ]['active'] = in_array( + self::get_plugin_status( $plugin_slug ), + array( 'active', 'network-active' ), + true + ); + } + return $plugins; + } + + return array(); + } } diff --git a/bin/phpcs-requirelist.js b/bin/phpcs-requirelist.js index 7806f50811d3..9a1e30fe8196 100644 --- a/bin/phpcs-requirelist.js +++ b/bin/phpcs-requirelist.js @@ -37,6 +37,7 @@ module.exports = [ '_inc/lib/core-api/wpcom-endpoints/memberships.php', '_inc/lib/debugger/', '_inc/lib/plans.php', + '_inc/lib/plugins.php', '_inc/social-logos.php', 'jetpack.php', 'json-endpoints/jetpack/class-jetpack-json-api-delete-backup-helper-script-endpoint.php', diff --git a/extensions/shared/plugin-management.js b/extensions/shared/plugin-management.js new file mode 100644 index 000000000000..eeb46bce82ff --- /dev/null +++ b/extensions/shared/plugin-management.js @@ -0,0 +1,87 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { isSimpleSite } from './site-type-utils'; + +/** + * Returns a list of all active plugins on the site. + * + * @returns {Promise} Resolves to a list of plugins, or reject if not retrievable (like on a wpcom simple site or a site running an older version of WP) + */ +export async function getPlugins() { + // Bail early on WordPress.com Simple sites. + if ( isSimpleSite() ) { + return Promise.reject(); + } + + try { + const plugins = await apiFetch( { + path: '/jetpack/v4/plugins', + } ); + return plugins; + } catch ( error ) { + return Promise.reject( error.message ); + } +} + +/** + * Install and activate a plugin from the WordPress.org plugin directory. + * + * @param {string} slug - The slug of the plugin we want to activate. + * + * @returns {Promise} Resolves to true if the plugin has been successfully activated, or reject. + */ +export async function installAndActivatePlugin( slug ) { + // Bail early on WordPress.com Simple sites. + if ( isSimpleSite() ) { + return Promise.reject(); + } + + try { + const attemptInstall = await apiFetch( { + path: '/jetpack/v4/plugins', + method: 'POST', + data: { + slug, + status: 'active', + source: 'block-editor', + }, + } ); + return attemptInstall; + } catch ( error ) { + return Promise.reject( error.message ); + } +} + +/** + * Activate a plugin from the WordPress.org plugin directory. + * + * @param {string} pluginFile - The plugin long slug (slug/index-file, without the .php suffix) we want to activate. + * + * @returns {Promise} Resolves to true if the plugin has been successfully activated, or reject. + */ +export async function activatePlugin( pluginFile ) { + // Bail early on WordPress.com Simple sites. + if ( isSimpleSite() ) { + return Promise.reject(); + } + + try { + const attemptActivate = await apiFetch( { + path: `/jetpack/v4/plugins/${ pluginFile }`, + method: 'POST', + data: { + status: 'active', + source: 'block-editor', + }, + } ); + return attemptActivate; + } catch ( error ) { + return Promise.reject( error.message ); + } +} diff --git a/packages/sync/src/modules/class-plugins.php b/packages/sync/src/modules/class-plugins.php index 9f2575577f41..2a0b538f9d83 100644 --- a/packages/sync/src/modules/class-plugins.php +++ b/packages/sync/src/modules/class-plugins.php @@ -94,6 +94,9 @@ public function init_before_send() { * @param bool|WP_Error $response Install response, true if successful, WP_Error if not. */ public function populate_plugins( $response ) { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } $this->plugins = get_plugins(); return $response; }