Install the Legacy REST API plugin on WooCommerce upgrade if needed (#45570)

The plugin is installed and activated if it's not installed already
and either the Legacy REST API is installed in the site
or there's at least one webhook that uses the Legacy REST API
code for the payload (disabled webhooks also count).

Also the WC_Admin_Notices::remove_notices method is added.

---------

Co-authored-by: Corey McKrill <916023+coreymckrill@users.noreply.github.com>
This commit is contained in:
Néstor Soriano 2024-03-20 10:24:46 +01:00 committed by GitHub
parent 3c15ced8fd
commit 1adeaf225c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 232 additions and 57 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Install the Legacy REST API plugin on WooCommerce upgrade if needed

View File

@ -264,6 +264,28 @@ class WC_Admin_Notices {
}
}
/**
* Remove a given set of notices.
*
* An array of notice names or a regular expression string can be passed, in the later case
* all the notices whose name matches the regular expression will be removed.
*
* @param array|string $names_array_or_regex An array of notice names, or a string representing a regular expression.
* @param bool $force_save Force saving inside this method instead of at the 'shutdown'.
* @return void
*/
public static function remove_notices( $names_array_or_regex, $force_save = false ) {
if ( ! is_array( $names_array_or_regex ) ) {
$names_array_or_regex = array_filter( self::get_notices(), fn( $notice_name ) => 1 === preg_match( $names_array_or_regex, $notice_name ) );
}
self::set_notices( array_diff( self::get_notices(), $names_array_or_regex ) );
if ( $force_save ) {
// Adding early save to prevent more race conditions with notices.
self::store_notices();
}
}
/**
* See if a notice is being shown.
*
@ -391,7 +413,7 @@ class WC_Admin_Notices {
$notice_html = get_option( 'woocommerce_admin_notice_' . $notice );
if ( $notice_html ) {
include dirname( __FILE__ ) . '/views/html-notice-custom.php';
include __DIR__ . '/views/html-notice-custom.php';
}
}
}
@ -413,12 +435,12 @@ class WC_Admin_Notices {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) {
include dirname( __FILE__ ) . '/views/html-notice-updating.php';
include __DIR__ . '/views/html-notice-updating.php';
} else {
include dirname( __FILE__ ) . '/views/html-notice-update.php';
include __DIR__ . '/views/html-notice-update.php';
}
} else {
include dirname( __FILE__ ) . '/views/html-notice-updated.php';
include __DIR__ . '/views/html-notice-updated.php';
}
}
@ -463,7 +485,7 @@ class WC_Admin_Notices {
}
if ( $outdated ) {
include dirname( __FILE__ ) . '/views/html-notice-template-check.php';
include __DIR__ . '/views/html-notice-template-check.php';
} else {
self::remove_notice( 'template_files' );
}
@ -486,7 +508,7 @@ class WC_Admin_Notices {
}
if ( $enabled ) {
include dirname( __FILE__ ) . '/views/html-notice-legacy-shipping.php';
include __DIR__ . '/views/html-notice-legacy-shipping.php';
} else {
self::remove_notice( 'template_files' );
}
@ -502,7 +524,7 @@ class WC_Admin_Notices {
$method_count = wc_get_shipping_method_count();
if ( $product_count->publish > 0 && 0 === $method_count ) {
include dirname( __FILE__ ) . '/views/html-notice-no-shipping-methods.php';
include __DIR__ . '/views/html-notice-no-shipping-methods.php';
}
if ( $method_count > 0 ) {
@ -515,7 +537,7 @@ class WC_Admin_Notices {
* Notice shown when regenerating thumbnails background process is running.
*/
public static function regenerating_thumbnails_notice() {
include dirname( __FILE__ ) . '/views/html-notice-regenerating-thumbnails.php';
include __DIR__ . '/views/html-notice-regenerating-thumbnails.php';
}
/**
@ -526,7 +548,7 @@ class WC_Admin_Notices {
return;
}
include dirname( __FILE__ ) . '/views/html-notice-secure-connection.php';
include __DIR__ . '/views/html-notice-secure-connection.php';
}
/**
@ -541,7 +563,7 @@ class WC_Admin_Notices {
return;
}
include dirname( __FILE__ ) . '/views/html-notice-regenerating-lookup-table.php';
include __DIR__ . '/views/html-notice-regenerating-lookup-table.php';
}
/**
@ -628,7 +650,7 @@ class WC_Admin_Notices {
return;
}
include dirname( __FILE__ ) . '/views/html-notice-maxmind-license-key.php';
include __DIR__ . '/views/html-notice-maxmind-license-key.php';
}
/**
@ -642,7 +664,7 @@ class WC_Admin_Notices {
return;
}
include dirname( __FILE__ ) . '/views/html-notice-redirect-only-download.php';
include __DIR__ . '/views/html-notice-redirect-only-download.php';
}
/**
@ -656,7 +678,7 @@ class WC_Admin_Notices {
return;
}
include dirname( __FILE__ ) . '/views/html-notice-uploads-directory-is-unprotected.php';
include __DIR__ . '/views/html-notice-uploads-directory-is-unprotected.php';
}
/**
@ -671,7 +693,7 @@ class WC_Admin_Notices {
self::remove_notice( 'base_tables_missing' );
}
include dirname( __FILE__ ) . '/views/html-notice-base-table-missing.php';
include __DIR__ . '/views/html-notice-base-table-missing.php';
}
/**

View File

@ -18,6 +18,7 @@ use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Internal\Utilities\PluginInstaller;
defined( 'ABSPATH' ) || exit;
@ -281,6 +282,7 @@ class WC_Install {
add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) );
add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) );
self::add_action( 'admin_init', array( __CLASS__, 'newly_installed' ) );
self::add_action( 'woocommerce_activate_legacy_rest_api_plugin', array( __CLASS__, 'maybe_install_legacy_api_plugin' ) );
}
/**
@ -363,7 +365,7 @@ class WC_Install {
* @since 3.6.0
*/
public static function run_update_callback( $update_callback ) {
include_once dirname( __FILE__ ) . '/wc-update-functions.php';
include_once __DIR__ . '/wc-update-functions.php';
if ( is_callable( $update_callback ) ) {
self::run_update_callback_start( $update_callback );
@ -453,6 +455,7 @@ class WC_Install {
self::update_wc_version();
self::maybe_update_db_version();
self::maybe_set_store_id();
self::maybe_install_legacy_api_plugin();
delete_transient( 'wc_installing' );
@ -550,7 +553,8 @@ class WC_Install {
* @since 3.2.0
*/
private static function remove_admin_notices() {
include_once dirname( __FILE__ ) . '/admin/class-wc-admin-notices.php';
include_once __DIR__ . '/admin/class-wc-admin-notices.php';
WC_Admin_Notices::remove_all_notices();
}
@ -679,7 +683,7 @@ class WC_Install {
),
'woocommerce-db-updates'
);
$loop++;
++$loop;
}
}
}
@ -799,7 +803,7 @@ class WC_Install {
// Set the locale to the store locale to ensure pages are created in the correct language.
wc_switch_to_site_locale();
include_once dirname( __FILE__ ) . '/admin/wc-admin-functions.php';
include_once __DIR__ . '/admin/wc-admin-functions.php';
/**
* Determines the cart shortcode tag used for the cart page.
@ -887,7 +891,7 @@ class WC_Install {
*/
private static function create_options() {
// Include settings so that we can run through defaults.
include_once dirname( __FILE__ ) . '/admin/class-wc-admin-settings.php';
include_once __DIR__ . '/admin/class-wc-admin-settings.php';
$settings = WC_Admin_Settings::get_settings_pages();
@ -1163,6 +1167,136 @@ class WC_Install {
}
}
/**
* Install and activate the WooCommerce Legacy REST API plugin from the WordPress.org directory if all the following is true:
*
* 1. We are in a WooCommerce upgrade process (not a new install).
* 2. The 'woocommerce_skip_legacy_rest_api_plugin_auto_install' filter returns false (which is the default).
* 3. The plugin is not installed and active already (but see note about multisite below).
* 4. The Legacy REST API is enabled in the site OR the site has at least one webhook defined that uses the Legacy REST API payload format (disabled webhooks also count).
*
* In multisite setups it could happen that the plugin was installed by an installation process performed in another site.
* In this case we check if the plugin was autoinstalled in such a way, and if so we activate it if the conditions are fulfilled.
*/
private static function maybe_install_legacy_api_plugin() {
if ( self::is_new_install() ) {
return;
}
/**
* Filter to skip the automatic installation of the WooCommerce Legacy REST API plugin
* from the WordPress.org plugins directory.
*
* @since 8.8.0
*
* @param bool $skip_auto_install False, defaulting to "don't skip the plugin automatic installation".
* @returns bool True to skip the plugin automatic installation, false to install the plugin if necessary.
*/
if ( apply_filters( 'woocommerce_skip_legacy_rest_api_plugin_auto_install', false ) ) {
return;
}
if ( ( 'yes' !== get_option( 'woocommerce_api_enabled' ) &&
0 === wc_get_container()->get( Automattic\WooCommerce\Internal\Utilities\WebhookUtil::class )->get_legacy_webhooks_count( true ) ) ) {
return;
}
$plugin_name = 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php';
wp_clean_plugins_cache();
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( isset( get_plugins()[ $plugin_name ] ) ) {
if ( ! ( get_site_option( 'woocommerce_autoinstalled_plugins', array() )[ $plugin_name ] ?? null ) ) {
// The plugin was installed manually so let's not interfere.
return;
}
if ( in_array( $plugin_name, wp_get_active_and_valid_plugins(), true ) ) {
return;
}
// The plugin was automatically installed in a different installation process - can happen in multisite.
$install_ok = true;
} else {
$install_result = wc_get_container()->get( PluginInstaller::class )->install_plugin(
'https://downloads.wordpress.org/plugin/woocommerce-legacy-rest-api.latest-stable.zip',
array(
'info_link' => 'https://developer.woo.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/',
)
);
if ( $install_result['already_installing'] ?? null ) {
// The plugin is in the process of being installed already (can happen in multisite),
// but we still need to activate it for ourselves once it's installed.
as_schedule_single_action( time() + 10, 'woocommerce_activate_legacy_rest_api_plugin' );
return;
}
$install_ok = $install_result['install_ok'];
}
$plugin_page_url = 'https://wordpress.org/plugins/woocommerce-legacy-rest-api/';
$blog_post_url = 'https://developer.woo.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/';
$site_legacy_api_settings_url = get_admin_url( null, '/admin.php?page=wc-settings&tab=advanced&section=legacy_api' );
$site_webhooks_settings_url = get_admin_url( null, '/admin.php?page=wc-settings&tab=advanced&section=webhooks' );
$site_logs_url = get_admin_url( null, '/admin.php?page=wc-status&tab=logs' );
if ( $install_ok ) {
$activation_result = activate_plugin( $plugin_name );
if ( $activation_result instanceof \WP_Error ) {
$message = sprintf(
/* translators: 1 = URL of Legacy REST API plugin page, 2 = URL of Legacy API settings in current site, 3 = URL of webhooks settings in current site, 4 = URL of logs page in current site, 5 = URL of plugins page in current site, 6 = URL of blog post about the Legacy REST API removal */
__( '⚠️ WooCommerce installed <a href="%1$s">the Legacy REST API plugin</a> because this site has <a href="%2$s">the Legacy REST API enabled</a> or has <a href="%3$s">legacy webhooks defined</a>, but it failed to activate it (see error details in <a href="%4$s">the WooCommerce logs</a>). Please go to <a href="%5$s">the plugins page</a> and activate it manually. <a href="%6$s">More information</a>', 'woocommerce' ),
$plugin_page_url,
$site_legacy_api_settings_url,
$site_webhooks_settings_url,
$site_logs_url,
get_admin_url( null, '/plugins.php' ),
$blog_post_url
);
$notice_name = 'woocommerce_legacy_rest_api_plugin_activation_failed';
wc_get_logger()->error(
__( 'WooCommerce installed the Legacy REST API plugin but failed to activate it, see context for more details.', 'woocommerce' ),
array(
'source' => 'plugin_auto_installs',
'error' => $activation_result,
)
);
} else {
$message = sprintf(
/* translators: 1 = URL of Legacy REST API plugin page, 2 = URL of Legacy API settings in current site, 3 = URL of webhooks settings in current site, 4 = URL of blog post about the Legacy REST API removal */
__( ' WooCommerce installed and activated <a href="%1$s">the Legacy REST API plugin</a> because this site has <a href="%2$s">the Legacy REST API enabled</a> or has <a href="%3$s">legacy webhooks defined</a>. <a href="%4$s">More information</a>', 'woocommerce' ),
$plugin_page_url,
$site_legacy_api_settings_url,
$site_webhooks_settings_url,
$blog_post_url
);
$notice_name = 'woocommerce_legacy_rest_api_plugin_activated';
wc_get_logger()->info( 'WooCommerce activated the Legacy REST API plugin in this site.', array( 'source' => 'plugin_auto_installs' ) );
}
\WC_Admin_Notices::add_custom_notice( $notice_name, $message );
} else {
$message = sprintf(
/* translators: 1 = URL of Legacy REST API plugin page, 2 = URL of Legacy API settings in current site, 3 = URL of webhooks settings in current site, 4 = URL of logs page in current site, 5 = URL of blog post about the Legacy REST API removal */
__( '⚠️ WooCommerce attempted to install <a href="%1$s">the Legacy REST API plugin</a> because this site has <a href="%2$s">the Legacy REST API enabled</a> or has <a href="%3$s">legacy webhooks defined</a>, but the installation failed (see error details in <a href="%4$s">the WooCommerce logs</a>). Please install and activate the plugin manually. <a href="%5$s">More information</a>', 'woocommerce' ),
$plugin_page_url,
$site_legacy_api_settings_url,
$site_webhooks_settings_url,
$site_logs_url,
$blog_post_url
);
\WC_Admin_Notices::add_custom_notice( 'woocommerce_legacy_rest_api_plugin_install_failed', $message );
// Note that we aren't adding an entry to the error log because PluginInstaller->install_plugin will have done that already.
}
\WC_Admin_Notices::store_notices();
}
/**
* Set up the database tables which the plugin needs to function.
* WARNING: If you are modifying this method, make sure that its safe to call regardless of the state of database.

View File

@ -66,6 +66,9 @@ class PluginInstaller implements RegisterHooksInterface {
* - 'date', ISO-formatted installation date.
* - 'metadata', as supplied (except the 'plugin_name' key) and only if not empty.
*
* If the plugin is already in the process of being installed (can happen in multisite), the returned array
* will contain only one key: 'already_installing', with a value of true.
*
* @param string $plugin_url URL or file path of the plugin to install.
* @param array $metadata Metadata to store if the installation succeeds.
* @return array Information about the installation result.
@ -73,9 +76,24 @@ class PluginInstaller implements RegisterHooksInterface {
*/
public function install_plugin( string $plugin_url, array $metadata = array() ): array {
$this->installing_plugin = true;
$plugins_being_installed = get_site_option( 'woocommerce_autoinstalling_plugins', array() );
if ( in_array( $plugin_url, $plugins_being_installed, true ) ) {
return array( 'already_installing' => true );
}
$plugins_being_installed[] = $plugin_url;
update_site_option( 'woocommerce_autoinstalling_plugins', $plugins_being_installed );
try {
return $this->install_plugin_core( $plugin_url, $metadata );
} finally {
$plugins_being_installed = array_diff( $plugins_being_installed, array( $plugin_url ) );
if ( empty( $plugins_being_installed ) ) {
delete_site_option( 'woocommerce_autoinstalling_plugins' );
} else {
update_site_option( 'woocommerce_autoinstalling_plugins', $plugins_being_installed );
}
$this->installing_plugin = false;
}
}
@ -118,6 +136,9 @@ class PluginInstaller implements RegisterHooksInterface {
$result = array( 'messages' => $skin->get_upgrade_messages() );
if ( $install_ok ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugin_name = $upgrader->plugin_info();
$plugin_version = get_plugins()[ $plugin_name ]['Version'];
@ -130,14 +151,16 @@ class PluginInstaller implements RegisterHooksInterface {
$plugin_data['metadata'] = $metadata;
}
$auto_installed_plugins = get_site_option( 'woocommerce_autoinstalled_plugins', array() );
$auto_installed_plugins[ $plugin_name ] = $plugin_data;
update_site_option( 'woocommerce_autoinstalled_plugins', $auto_installed_plugins );
$post_install = function () use ( $plugin_name, $plugin_version, $installed_by, $plugin_url, $plugin_data ) {
$auto_installed_plugins = get_option( 'woocommerce_autoinstalled_plugins', array() );
$auto_installed_plugins[ $plugin_name ] = $plugin_data;
$log_context = array(
$log_context = array(
'source' => 'plugin_auto_installs',
'recorded_data' => $plugin_data,
);
update_option( 'woocommerce_autoinstalled_plugins', $auto_installed_plugins );
wc_get_logger()->info( "Plugin $plugin_name v{$plugin_version} installed by $installed_by, source: $plugin_url", $log_context );
};
} else {
@ -151,39 +174,25 @@ class PluginInstaller implements RegisterHooksInterface {
};
}
$this->run_callback_in_all_sites( $post_install );
if ( is_multisite() ) {
// We log the install in the main site, unless the main site doesn't have WooCommerce installed;
// in that case we fallback to logging in the current site.
switch_to_blog( get_main_site_id() );
if ( self::woocommerce_is_active_in_current_site() ) {
$post_install();
restore_current_blog();
} else {
restore_current_blog();
$post_install();
}
} else {
$post_install();
}
$result['install_ok'] = $install_ok ?? false;
return $result;
}
/**
* Run a callback in each existing site (if multisite) or just once (if single site).
*
* @param callable $callback The callback to run.
*/
private static function run_callback_in_all_sites( callable $callback ) {
if ( ! is_multisite() ) {
$callback();
return;
}
foreach ( get_sites() as $site ) {
switch_to_blog( $site->blog_id );
if ( ! self::woocommerce_is_active_in_current_site() ) {
restore_current_blog();
continue;
}
try {
$callback();
} finally {
restore_current_blog();
}
}
}
/**
* Check if WooCommerce is installed and active in the current blog.
* This is useful for multisite installs when a blog other than the one running this code is selected with 'switch_to_blog'.
@ -221,7 +230,7 @@ class PluginInstaller implements RegisterHooksInterface {
return;
}
$auto_installed_plugins_info = get_option( 'woocommerce_autoinstalled_plugins', array() );
$auto_installed_plugins_info = get_site_option( 'woocommerce_autoinstalled_plugins', array() );
$current_plugin_info = $auto_installed_plugins_info[ $plugin_file ] ?? null;
if ( is_null( $current_plugin_info ) || $current_plugin_info['version'] !== $plugin_data['Version'] ) {
return;
@ -270,7 +279,7 @@ class PluginInstaller implements RegisterHooksInterface {
return;
}
$auto_installed_plugins = get_option( 'woocommerce_autoinstalled_plugins' );
$auto_installed_plugins = get_site_option( 'woocommerce_autoinstalled_plugins' );
if ( ! $auto_installed_plugins ) {
return;
}
@ -291,9 +300,9 @@ class PluginInstaller implements RegisterHooksInterface {
$new_auto_installed_plugins = array_diff_key( $auto_installed_plugins, array_flip( $updated_auto_installed_plugin_names ) );
if ( empty( $new_auto_installed_plugins ) ) {
delete_option( 'woocommerce_autoinstalled_plugins' );
delete_site_option( 'woocommerce_autoinstalled_plugins' );
} else {
update_option( 'woocommerce_autoinstalled_plugins', $new_auto_installed_plugins );
update_site_option( 'woocommerce_autoinstalled_plugins', $new_auto_installed_plugins );
}
}
}

View File

@ -138,13 +138,19 @@ class WebhookUtil {
/**
* Gets the count of webhooks that are configured to use the Legacy REST API to compose their payloads.
*
* @param bool $clear_cache If true, the previously cached value of the count will be discarded if it exists.
*
* @return int
*/
public function get_legacy_webhooks_count(): int {
public function get_legacy_webhooks_count( bool $clear_cache = false ): int {
global $wpdb;
$cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'legacy_count';
$count = wp_cache_get( $cache_key, 'webhooks' );
if ( $clear_cache ) {
wp_cache_delete( $cache_key, 'webhooks' );
}
$count = wp_cache_get( $cache_key, 'webhooks' );
if ( false === $count ) {
$count = absint( $wpdb->get_var( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `api_version` < 1;" ) );