From 7c7ed0d7c4a30716d572e24ce78430ecbdca583b Mon Sep 17 00:00:00 2001 From: Remi Corson Date: Wed, 20 Mar 2024 15:00:41 +0100 Subject: [PATCH] Use WC built-in Action Scheduler to fetch in-app promotions (#45628) * Use WC built-in ActionScheduler * Calling `WC_Admin_Marketplace_Promotions::init` immediately from `WC_Admin` constructor. `WC_Admin` is only instantiated in an admin request. If we init marketplace promotions from `woocommerce_init`, we'll be too late to add the callback for the scheduled action that fetches promotion data. * Changed transient life to 12 hours to match frequency of ActionScheduler action. * Changelog. * Updated `fetch_marketplace_promotions` to replace the transient every time it runs. This allows more frequent changes. * Added filter to allow promotions to be suppressed. * Fix indentation * More indentation fix * Clearing scheduled action if `woocommerce_marketplace_suppress_promotions` filter returns "true". * Running `clear_scheduled_event` on `init`. * WP_CLI check * We were including and instantiating `WC_Admin_Marketplace_Promotions` from the `WC_Admin` constructor. But `WC_Admin` is only instantiated during `is_admin` requests. We also need to respond to cron requests. So we're now including the class from `class-woocommerce.php` if the context is admin or cron, and instantiating it on `init`. This fixes the error in Scheduled Action `action failed via WP Cron: Scheduled action for woocommerce_marketplace_fetch_promotions will not be executed as no callbacks are registered.` * Linter errors. * Enhance append_bubble() method * introduce SCHEDULED_ACTION_INTERVAL * Make linter happier * Linter errors. --------- Co-authored-by: Remi Corson <1649788+corsonr@users.noreply.github.com> Co-authored-by: And Finally --- .../fix-19763-marketplace-promotions | 4 + .../class-wc-admin-marketplace-promotions.php | 124 +++++++++++------- .../includes/admin/class-wc-admin.php | 6 - .../includes/class-woocommerce.php | 4 + 4 files changed, 83 insertions(+), 55 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-19763-marketplace-promotions diff --git a/plugins/woocommerce/changelog/fix-19763-marketplace-promotions b/plugins/woocommerce/changelog/fix-19763-marketplace-promotions new file mode 100644 index 000000000000..181b796ced3a --- /dev/null +++ b/plugins/woocommerce/changelog/fix-19763-marketplace-promotions @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Using ActionScheduler to schedule fetching of in-app marketplace promotions. diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-marketplace-promotions.php b/plugins/woocommerce/includes/admin/class-wc-admin-marketplace-promotions.php index 5a4b9b38a2d1..1b406c720894 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-marketplace-promotions.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-marketplace-promotions.php @@ -15,8 +15,11 @@ */ class WC_Admin_Marketplace_Promotions { - const TRANSIENT_NAME = 'woocommerce_marketplace_promotions'; - const SCHEDULED_ACTION_HOOK = 'woocommerce_marketplace_fetch_promotions'; + const TRANSIENT_NAME = 'woocommerce_marketplace_promotions'; + const SCHEDULED_ACTION_HOOK = 'woocommerce_marketplace_fetch_promotions'; + const PROMOTIONS_API_URL = 'https://woo.com/wp-json/wccom-extensions/3.0/promotions'; + const SCHEDULED_ACTION_INTERVAL = 12 * HOUR_IN_SECONDS; + /** * The user's locale, for example en_US. * @@ -26,69 +29,72 @@ class WC_Admin_Marketplace_Promotions { /** * On all admin pages, schedule an action to fetch promotions data. - * Add menu badge to WooCommerce Extensions item if the promotions - * API requests one. + * Shows notice and adds menu badge to WooCommerce Extensions item + * if the promotions API requests them. + * + * WC_Admin calls this method when it is instantiated during + * is_admin requests. * * @return void */ - public static function init_marketplace_promotions() { + public static function init() { + register_deactivation_hook( WC_PLUGIN_FILE, array( __CLASS__, 'clear_scheduled_event' ) ); + + /** + * Filter to suppress the requests for and showing of marketplace promotions. + * + * @since 8.8 + */ + if ( apply_filters( 'woocommerce_marketplace_suppress_promotions', false ) ) { + add_action( 'init', array( __CLASS__, 'clear_scheduled_event' ), 13 ); + + return; + } + // Add the callback for our scheduled action. if ( ! has_action( self::SCHEDULED_ACTION_HOOK, array( __CLASS__, 'fetch_marketplace_promotions' ) ) ) { add_action( self::SCHEDULED_ACTION_HOOK, array( __CLASS__, 'fetch_marketplace_promotions' ) ); } - if ( self::is_admin_page() ) { - // Schedule the action twice a day, starting now. - if ( false === wp_next_scheduled( self::SCHEDULED_ACTION_HOOK ) ) { - wp_schedule_event( time(), 'twicedaily', self::SCHEDULED_ACTION_HOOK ); - } + if ( is_admin() ) { + add_action( 'init', array( __CLASS__, 'schedule_promotion_fetch' ), 12 ); + } - self::$locale = ( self::$locale ?? get_user_locale() ) ?? 'en_US'; - self::maybe_show_bubble_promotions(); + if ( + defined( 'DOING_AJAX' ) && DOING_AJAX + || defined( 'DOING_CRON' ) && DOING_CRON + || defined( 'WP_CLI' ) && WP_CLI + ) { + return; } - register_deactivation_hook( WC_PLUGIN_FILE, array( __CLASS__, 'clear_scheduled_event' ) ); + self::$locale = ( self::$locale ?? get_user_locale() ) ?? 'en_US'; + self::maybe_show_bubble_promotions(); } /** - * Check if the request is for an admin page, and not ajax. - * We may want to add a menu bubble to WooCommerce Extensions - * on any admin page, as the user may view the WooCommerce flyout - * menu. - * - * @return bool + * Schedule the action to fetch promotions data. */ - private static function is_admin_page(): bool { - if ( - ( defined( 'DOING_AJAX' ) && DOING_AJAX ) - || ! is_admin() - ) { - return false; + public static function schedule_promotion_fetch() { + // Schedule the action twice a day using Action Scheduler. + if ( false === as_has_scheduled_action( self::SCHEDULED_ACTION_HOOK ) ) { + as_schedule_recurring_action( time(), self::SCHEDULED_ACTION_INTERVAL, self::SCHEDULED_ACTION_HOOK ); } - - return true; } /** - * Get promotions to show in the Woo in-app marketplace. - * Only run on selected pages in the main WooCommerce menu in wp-admin. - * Loads promotions in transient with one day life. + * Get promotions to show in the Woo in-app marketplace and load them into a transient + * with a 12-hour life. Run as a recurring scheduled action. * * @return void */ public static function fetch_marketplace_promotions() { - $url = 'https://woo.com/wp-json/wccom-extensions/3.0/promotions'; - $promotions = get_transient( self::TRANSIENT_NAME ); - - if ( false !== $promotions ) { - return; - } - + // Fetch promotions from the API. $fetch_options = array( 'auth' => true, 'country' => true, ); - $raw_promotions = WC_Admin_Addons::fetch( $url, $fetch_options ); + $raw_promotions = WC_Admin_Addons::fetch( self::PROMOTIONS_API_URL, $fetch_options ); // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores if ( is_wp_error( $raw_promotions ) ) { @@ -98,6 +104,8 @@ public static function fetch_marketplace_promotions() { * @since 8.7 */ do_action( 'woocommerce_page_wc-addons_connection_error', $raw_promotions->get_error_message() ); + + return; } $response_code = (int) wp_remote_retrieve_response_code( $raw_promotions ); @@ -108,6 +116,8 @@ public static function fetch_marketplace_promotions() { * @since 8.7 */ do_action( 'woocommerce_page_wc-addons_connection_error', $response_code ); + + return; } $promotions = json_decode( wp_remote_retrieve_body( $raw_promotions ), true ); @@ -118,14 +128,14 @@ public static function fetch_marketplace_promotions() { * @since 8.7 */ do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' ); + + return; } // phpcs:enable WordPress.NamingConventions.ValidHookName.UseUnderscores - if ( $promotions ) { - // Filter out any expired promotions. - $promotions = self::get_active_promotions( $promotions ); - set_transient( self::TRANSIENT_NAME, $promotions, DAY_IN_SECONDS ); - } + // Filter out any expired promotions. + $active_promotions = self::get_active_promotions( $promotions ); + set_transient( self::TRANSIENT_NAME, $active_promotions, 12 * HOUR_IN_SECONDS ); } /** @@ -246,7 +256,7 @@ function ( $a, $b ) { * * @return array */ - public static function filter_marketplace_menu_items( $menu_items, $promotion = array() ) { + public static function filter_marketplace_menu_items( $menu_items, $promotion = array() ): array { if ( ! isset( $promotion['menu_item_id'] ) || ! isset( $promotion['content'] ) ) { return $menu_items; } @@ -266,14 +276,26 @@ public static function filter_marketplace_menu_items( $menu_items, $promotion = } /** - * Return the markup for a menu item bubble with a given text. + * Return the markup for a menu item bubble with a given text and optional additional attributes. * * @param string $bubble_text Text of bubble. + * @param array $attributes Optional. Additional attributes for the bubble, such as class or style. * * @return string */ - private static function append_bubble( $bubble_text ) { - return ' ' . esc_html( $bubble_text ) . ''; + private static function append_bubble( $bubble_text, $attributes = array() ) { + $default_attributes = array( + 'class' => 'awaiting-mod update-plugins remaining-tasks-badge woocommerce-task-list-remaining-tasks-badge', + 'style' => '', + ); + + $attributes = wp_parse_args( $attributes, $default_attributes ); + $class_attr = ! empty( $attributes['class'] ) ? sprintf( 'class="%s"', esc_attr( $attributes['class'] ) ) : ''; + $style_attr = ! empty( $attributes['style'] ) ? sprintf( 'style="%s"', esc_attr( $attributes['style'] ) ) : ''; + + $bubble_html = sprintf( ' %s', $class_attr, $style_attr, esc_html( $bubble_text ) ); + + return $bubble_html; } /** @@ -282,7 +304,11 @@ private static function append_bubble( $bubble_text ) { * @return void */ public static function clear_scheduled_event() { - $timestamp = wp_next_scheduled( self::SCHEDULED_ACTION_HOOK ); - wp_unschedule_event( $timestamp, self::SCHEDULED_ACTION_HOOK ); + as_unschedule_all_actions( self::SCHEDULED_ACTION_HOOK ); } } + +// Fetch list of promotions from Woo.com for WooCommerce admin UI. +if ( ! has_action( 'init', array( 'WC_Admin_Marketplace_Promotions', 'init' ) ) ) { + add_action( 'init', array( 'WC_Admin_Marketplace_Promotions', 'init' ), 11 ); +} diff --git a/plugins/woocommerce/includes/admin/class-wc-admin.php b/plugins/woocommerce/includes/admin/class-wc-admin.php index a8f3c777dbd0..d70da06898d6 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin.php @@ -39,9 +39,6 @@ public function __construct() { if ( isset( $_GET['page'] ) && 'wc-addons' === $_GET['page'] ) { add_filter( 'admin_body_class', array( 'WC_Admin_Addons', 'filter_admin_body_classes' ) ); } - - // Fetch list of promotions from Woo.com for WooCommerce admin UI. We need to fire earlier than admin_init so we can filter menu items. - add_action( 'woocommerce_init', array( 'WC_Admin_Marketplace_Promotions', 'init_marketplace_promotions' ) ); } /** @@ -80,9 +77,6 @@ public function includes() { // Marketplace suggestions & related REST API. include_once __DIR__ . '/marketplace-suggestions/class-wc-marketplace-suggestions.php'; include_once __DIR__ . '/marketplace-suggestions/class-wc-marketplace-updater.php'; - - // Marketplace promotions. - include_once __DIR__ . '/class-wc-admin-marketplace-promotions.php'; } /** diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php index 4c52111de91d..b45b90b6e253 100644 --- a/plugins/woocommerce/includes/class-woocommerce.php +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -681,6 +681,10 @@ public function includes() { include_once WC_ABSPATH . 'includes/admin/class-wc-admin.php'; } + if ( $this->is_request( 'admin' ) || $this->is_request( 'cron' ) ) { + include_once WC_ABSPATH . 'includes/admin/class-wc-admin-marketplace-promotions.php'; + } + // We load frontend includes in the post editor, because they may be invoked via pre-loading of blocks. $in_post_editor = doing_action( 'load-post.php' ) || doing_action( 'load-post-new.php' );