Add plugin installer to allow installation of plugins via URL (https://github.com/woocommerce/woocommerce-admin/pull/6805)

* Allow any plugin to be installed or activated

* Add PluginInstaller class

* Redirect to referring page if one exists

* Store message and show after redirect

* Add changelog and testing instructions
This commit is contained in:
Joshua T Flowers 2021-04-16 15:45:43 -04:00 committed by GitHub
parent 9e05116326
commit 8f018fc518
12 changed files with 156 additions and 157 deletions

View File

@ -2,6 +2,12 @@
## Unreleased
### Add plugin installer to allow installation of plugins via URL #6805
1. Visit any admin page with the params `plugin_action` (`install`, `activate`, or `install-activate`) and `plugins` (list of comma separated plugins). `wp-admin/admin.php?page=wc-admin&plugin_action=install&plugins=jetpack`
2. If visiting this URL from a link, make sure you are sent back to the referer.
3. Check that the plugins provided are installed, activated, or both depending on your query.
### Retain persisted queries when navigating to Homescreen #6614
1. Go to Analytics Report.

View File

@ -14,9 +14,9 @@ To enable the new onboarding experience manually, log-in to `wp-admin`, and go t
To power the new onboarding flow client side, new REST API endpoints have been introduced. These are purpose built endpoints that exist under the `/wc-admin/onboarding/` namespace, and are not meant to be shipped in the core rest API package. The source is stored in `src/API/Plugins.php`, `src/API/OnboardingProfile.php`, and `src/API/OnboardingTasks.php` respectively.
* POST `/wc-admin/plugins/install` - Installs a requested plugin, if present in the `woocommerce_admin_plugins_whitelist` array.
* POST `/wc-admin/plugins/install` - Install requested plugins.
* GET `/wc-admin/plugins/active` - Returns a list of the currently active plugins.
* POST `/wc-admin/plugins/activate` - Activates the requested plugins, if present in the `woocommerce_admin_plugins_whitelist` array. Multiple plugins can be passed to activate at once.
* POST `/wc-admin/plugins/activate` - Activates the requested plugins. Multiple plugins can be passed to activate at once.
* GET `/wc-admin/plugins/connect-jetpack` - Generates a URL for connecting to Jetpack. A `redirect_url` is accepted, which is used upon a successful connection.
* POST `/wc-admin/plugins/request-wccom-connect` - Generates a URL for the WooCommerce.com connection process.
* POST `/wc-admin/plugins/finish-wccom-connect` - Finishes the WooCommerce.com connection process by storing the received access token.

View File

@ -75,6 +75,7 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt
== Unreleased ==
- Add: Add plugin installer to allow installation of plugins via URL #6805
- Update: Adding setup required icon for non-configured payment methods #6811
- Update: UI updates to Payment Task screen #6766
- Dev: Add data source filter to remote inbox notification system #6794

View File

@ -80,13 +80,9 @@ class MarketingOverview extends \WC_REST_Data_Controller {
* @return \WP_Error|\WP_REST_Response
*/
public function activate_plugin( $request ) {
$allowed_plugins = InstalledExtensions::get_allowed_plugins();
$plugin_slug = $request->get_param( 'plugin' );
$plugin_slug = $request->get_param( 'plugin' );
if (
! PluginsHelper::is_plugin_installed( $plugin_slug ) ||
! in_array( $plugin_slug, $allowed_plugins, true )
) {
if ( ! PluginsHelper::is_plugin_installed( $plugin_slug ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce-admin' ), 404 );
}

View File

@ -204,8 +204,7 @@ class Plugins extends \WC_REST_Data_Controller {
* @return WP_Error|array Plugin Status
*/
public function install_plugins( $request ) {
$allowed_plugins = self::get_allowed_plugins();
$plugins = explode( ',', $request['plugins'] );
$plugins = explode( ',', $request['plugins'] );
if ( empty( $request['plugins'] ) || ! is_array( $plugins ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce-admin' ), 404 );
@ -218,25 +217,15 @@ class Plugins extends \WC_REST_Data_Controller {
include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php';
$existing_plugins = get_plugins();
$existing_plugins = PluginsHelper::get_installed_plugins_paths();
$installed_plugins = array();
$results = array();
$errors = new \WP_Error();
foreach ( $plugins as $plugin ) {
$slug = sanitize_key( $plugin );
$path = isset( $allowed_plugins[ $slug ] ) ? $allowed_plugins[ $slug ] : false;
if ( ! $path ) {
$errors->add(
$plugin,
/* translators: %s: plugin slug (example: woocommerce-services) */
sprintf( __( 'The requested plugin `%s` is not in the list of allowed plugins.', 'woocommerce-admin' ), $slug )
);
continue;
}
if ( in_array( $path, array_keys( $existing_plugins ), true ) ) {
if ( isset( $existing_plugins[ $slug ] ) ) {
$installed_plugins[] = $plugin;
continue;
}
@ -318,25 +307,14 @@ class Plugins extends \WC_REST_Data_Controller {
);
}
/**
* Gets an array of plugins that can be installed & activated.
*
* @return array
*/
public static function get_allowed_plugins() {
return apply_filters( 'woocommerce_admin_plugins_whitelist', array() );
}
/**
* Returns a list of active plugins in API format.
*
* @return array Active plugins
*/
public static function active_plugins() {
$allowed = self::get_allowed_plugins();
$plugins = array_values( array_intersect( PluginsHelper::get_active_plugin_slugs(), array_keys( $allowed ) ) );
return( array(
'plugins' => array_values( $plugins ),
'plugins' => PluginsHelper::get_active_plugin_slugs(),
) );
}
/**
@ -367,7 +345,7 @@ class Plugins extends \WC_REST_Data_Controller {
* @return WP_Error|array Plugin Status
*/
public function activate_plugins( $request ) {
$allowed_plugins = self::get_allowed_plugins();
$plugin_paths = PluginsHelper::get_installed_plugins_paths();
$plugins = explode( ',', $request['plugins'] );
$errors = new \WP_Error();
$activated_plugins = array();
@ -383,18 +361,9 @@ class Plugins extends \WC_REST_Data_Controller {
foreach ( $plugins as $plugin ) {
$slug = $plugin;
$path = isset( $allowed_plugins[ $slug ] ) ? $allowed_plugins[ $slug ] : false;
$path = isset( $plugin_paths[ $slug ] ) ? $plugin_paths[ $slug ] : false;
if ( ! $path ) {
$errors->add(
$plugin,
/* translators: %s: plugin slug (example: woocommerce-services) */
sprintf( __( 'The requested plugin `%s`. is not in the list of allowed plugins.', 'woocommerce-admin' ), $slug )
);
continue;
}
if ( ! PluginsHelper::is_plugin_installed( $path ) ) {
$errors->add(
$plugin,
/* translators: %s: plugin slug (example: woocommerce-services) */

View File

@ -176,6 +176,7 @@ class FeaturePlugin {
Events::instance()->init();
API\Init::instance();
ReportExporter::init();
PluginsInstaller::init();
// CRUD classes.
Notes::init();

View File

@ -48,7 +48,6 @@ class Homescreen {
// priority is 20 to run after https://github.com/woocommerce/woocommerce/blob/a55ae325306fc2179149ba9b97e66f32f84fdd9c/includes/admin/class-wc-admin-menus.php#L165.
add_action( 'admin_head', array( $this, 'update_link_structure' ), 20 );
}
add_filter( 'woocommerce_admin_plugins_whitelist', array( $this, 'get_homescreen_allowed_plugins' ) );
add_filter( 'woocommerce_admin_preload_options', array( $this, 'preload_options' ) );
add_filter( 'woocommerce_shared_settings', array( $this, 'component_settings' ), 20 );
}
@ -117,21 +116,6 @@ class Homescreen {
array_unshift( $submenu['woocommerce'], $menu );
}
/**
* Gets an array of plugins that can be installed & activated via the home screen.
*
* @param array $plugins Array of plugin slugs to be allowed.
*
* @return array
*/
public static function get_homescreen_allowed_plugins( $plugins ) {
$homescreen_plugins = array(
'jetpack' => 'jetpack/jetpack.php',
);
return array_merge( $plugins, $homescreen_plugins );
}
/**
* Preload options to prime state of the application.
*

View File

@ -204,7 +204,6 @@ class Onboarding {
private function add_filters() {
// Rest API hooks need to run before is_admin() checks.
add_filter( 'woocommerce_rest_prepare_themes', array( $this, 'add_uploaded_theme_data' ) );
add_filter( 'woocommerce_admin_plugins_whitelist', array( $this, 'get_onboarding_allowed_plugins' ), 10, 2 );
if ( ! is_admin() ) {
return;
@ -636,7 +635,7 @@ class Onboarding {
return false;
}
return in_array( $current_page['path'], $allowed_paths );
return in_array( $current_page['path'], $allowed_paths, true );
}
/**
@ -737,44 +736,6 @@ class Onboarding {
return $options;
}
/**
* Gets an array of plugins that can be installed & activated via the onboarding wizard.
*
* @param array $plugins Array of plugin slugs to be allowed.
*
* @return array
* @todo Handle edgecase of where installed plugins may have versioned folder names (i.e. `jetpack-main/jetpack.php`).
*/
public static function get_onboarding_allowed_plugins( $plugins ) {
$onboarding_plugins = apply_filters(
'woocommerce_admin_onboarding_plugins_whitelist',
array(
'facebook-for-woocommerce' => 'facebook-for-woocommerce/facebook-for-woocommerce.php',
'mailchimp-for-woocommerce' => 'mailchimp-for-woocommerce/mailchimp-woocommerce.php',
'creative-mail-by-constant-contact' => 'creative-mail-by-constant-contact/creative-mail-plugin.php',
'kliken-marketing-for-google' => 'kliken-marketing-for-google/kliken-marketing-for-google.php',
'jetpack' => 'jetpack/jetpack.php',
'woocommerce-services' => 'woocommerce-services/woocommerce-services.php',
'woocommerce-gateway-stripe' => 'woocommerce-gateway-stripe/woocommerce-gateway-stripe.php',
'woocommerce-paypal-payments' => 'woocommerce-paypal-payments/woocommerce-paypal-payments.php',
'klarna-checkout-for-woocommerce' => 'klarna-checkout-for-woocommerce/klarna-checkout-for-woocommerce.php',
'klarna-payments-for-woocommerce' => 'klarna-payments-for-woocommerce/klarna-payments-for-woocommerce.php',
'woocommerce-square' => 'woocommerce-square/woocommerce-square.php',
'woocommerce-shipstation-integration' => 'woocommerce-shipstation-integration/woocommerce-shipstation.php',
'woocommerce-payfast-gateway' => 'woocommerce-payfast-gateway/gateway-payfast.php',
'woo-paystack' => 'woo-paystack/woo-paystack.php',
'woocommerce-payments' => 'woocommerce-payments/woocommerce-payments.php',
'woocommerce-gateway-eway' => 'woocommerce-gateway-eway/woocommerce-gateway-eway.php',
'woo-razorpay' => 'woo-razorpay/woo-razorpay.php',
'mollie-payments-for-woocommerce' => 'mollie-payments-for-woocommerce/mollie-payments-for-woocommerce.php',
'payu-india' => 'payu-india/index.php',
'mailpoet' => 'mailpoet/mailpoet.php',
'woocommerce-mercadopago' => 'woocommerce-mercadopago/woocommerce-mercadopago.php',
)
);
return array_merge( $plugins, $onboarding_plugins );
}
/**
* Gets an array of themes that can be installed & activated via the onboarding wizard.
*

View File

@ -25,28 +25,12 @@ class ShippingLabelBanner {
* Constructor
*/
public function __construct() {
add_filter( 'woocommerce_admin_plugins_whitelist', array( $this, 'get_shipping_banner_allowed_plugins' ), 10, 2 );
if ( ! is_admin() ) {
return;
}
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 6, 2 );
}
/**
* Gets an array of plugins that can be installed & activated via shipping label prompt.
*
* @param array $plugins Array of plugin slugs to be allowed.
*
* @return array
*/
public static function get_shipping_banner_allowed_plugins( $plugins ) {
$shipping_banner_plugins = array(
'woocommerce-services' => 'woocommerce-services/woocommerce-services.php',
);
return array_merge( $plugins, $shipping_banner_plugins );
}
/**
* Check if WooCommerce Shipping makes sense for this merchant.
*

View File

@ -60,6 +60,24 @@ class PluginsHelper {
);
}
/**
* Get an array of installed plugins with their file paths as a key value pair.
*
* @return array
*/
public static function get_installed_plugins_paths() {
$plugins = get_plugins();
$installed_plugins = array();
foreach ( $plugins as $path => $plugin ) {
$path_parts = explode( '/', $path );
$slug = $path_parts[0];
$installed_plugins[ $slug ] = $path;
}
return $installed_plugins;
}
/**
* Get an array of active plugin slugs.
*

View File

@ -0,0 +1,119 @@
<?php
/**
* PluginsInstaller
*
* Installer to allow plugin installation via URL query.
*/
namespace Automattic\WooCommerce\Admin;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Plugins;
/**
* Class PluginsInstaller
*/
class PluginsInstaller {
/**
* Message option name.
*/
const MESSAGE_OPTION = 'woocommerce_admin_plugin_installer_message';
/**
* Constructor
*/
public static function init() {
add_action( 'admin_init', array( __CLASS__, 'possibly_install_activate_plugins' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'display_message' ) );
}
/**
* Check if an install or activation is being requested via URL query.
*/
public static function possibly_install_activate_plugins() {
/* phpcs:disable WordPress.Security.NonceVerification.Recommended */
if ( ! isset( $_GET['plugin_action'] ) || ! isset( $_GET['plugins'] ) || ! current_user_can( 'install_plugins' ) ) {
return;
}
$plugins = sanitize_text_field( wp_unslash( $_GET['plugins'] ) );
$plugin_action = sanitize_text_field( wp_unslash( $_GET['plugin_action'] ) );
/* phpcs:enable WordPress.Security.NonceVerification.Recommended */
$plugins_api = new Plugins();
$install_result = null;
$activate_result = null;
switch ( $plugin_action ) {
case 'install':
$install_result = $plugins_api->install_plugins( array( 'plugins' => $plugins ) );
break;
case 'activate':
$activate_result = $plugins_api->activate_plugins( array( 'plugins' => $plugins ) );
break;
case 'install-activate':
$install_result = $plugins_api->install_plugins( array( 'plugins' => $plugins ) );
$activate_result = $plugins_api->activate_plugins( array( 'plugins' => implode( ',', $install_result['data']['installed'] ) ) );
break;
}
self::cache_results( $install_result, $activate_result );
self::redirect_to_referer();
}
/**
* Display the results of installation and activation on the page.
*
* @param array $install_result Result of installation.
* @param array $activate_result Result of activation.
*/
public static function cache_results( $install_result, $activate_result ) {
if ( ! $install_result && ! $activate_result ) {
return;
}
$message = $activate_result ? $activate_result['message'] : $install_result['message'];
// Show install error message if one exists.
if ( $install_result && ! $install_result['success'] ) {
$message = $install_result['message'];
}
update_option( self::MESSAGE_OPTION, $message );
}
/**
* Display the results of installation and activation on the page.
*/
public static function display_message() {
$message = get_option( self::MESSAGE_OPTION );
if ( ! $message ) {
return;
}
delete_option( self::MESSAGE_OPTION );
}
/**
* Redirect back to the referring page if one exists.
*/
public static function redirect_to_referer() {
$referer = wp_get_referer();
if ( $referer && 0 !== strpos( $referer, wp_login_url() ) ) {
wp_safe_redirect( $referer );
exit();
}
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return;
}
$url = remove_query_arg( 'plugin_action', wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore sanitization ok.
$url = remove_query_arg( 'plugins', $url );
wp_safe_redirect( $url );
exit();
}
}

View File

@ -109,44 +109,4 @@ class WC_Tests_API_Plugins extends WC_REST_Unit_Test_Case {
$this->assertEquals( 'woocommerce_rest_invalid_plugins', $data['code'] );
}
/**
* Test that installing a non-whitelisted plugin fails, but installs the whitelisted.
*/
public function test_install_non_allowed_plugins() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/install' );
$request->set_query_params(
array(
'plugins' => 'facebook-for-woocommerce,hello-dolly',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( false, $data['success'] );
$this->assertArrayHasKey( 'hello-dolly', $data['errors']->errors );
$this->assertEquals( array( 'facebook-for-woocommerce' ), $data['data']['installed'] );
}
/**
* Test that activating a non-whitelisted plugin fails, but activates the whitelisted.
*/
public function test_activate_non_allowed_plugins() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/activate' );
$request->set_query_params(
array(
'plugins' => 'facebook-for-woocommerce,hello-dolly',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( false, $data['success'] );
$this->assertArrayHasKey( 'hello-dolly', $data['errors']->errors );
$this->assertEquals( array( 'facebook-for-woocommerce' ), $data['data']['activated'] );
}
}