diff --git a/plugins/woocommerce-admin/client/marketplace/components/constants.ts b/plugins/woocommerce-admin/client/marketplace/components/constants.ts index 97dfffbfcf3..a2fb27a665a 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/constants.ts +++ b/plugins/woocommerce-admin/client/marketplace/components/constants.ts @@ -16,4 +16,6 @@ export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/'; export const MARKETPLACE_COLLABORATION_PATH = MARKETPLACE_HOST + '/document/managing-woocommerce-com-subscriptions/#transfer-a-woocommerce-com-subscription'; -export const WP_ADMIN_PLUGIN_LIST_URL = ADMIN_URL + '/plugins.php'; +export const WP_ADMIN_PLUGIN_LIST_URL = ADMIN_URL + 'plugins.php'; +export const WOO_CONNECT_PLUGIN_DOWNLOAD_URL = + MARKETPLACE_HOST + '/product-download/woo-update-manager'; diff --git a/plugins/woocommerce-admin/client/marketplace/components/discover/discover.tsx b/plugins/woocommerce-admin/client/marketplace/components/discover/discover.tsx index 06862502169..d8aa612ee1c 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/discover/discover.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/discover/discover.tsx @@ -13,6 +13,7 @@ import ProductLoader from '../product-loader/product-loader'; import { MarketplaceContext } from '../../contexts/marketplace-context'; import { ProductType } from '../product-list/types'; import './discover.scss'; +import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice'; export default function Discover(): JSX.Element | null { const [ productGroups, setProductGroups ] = useState< @@ -70,6 +71,7 @@ export default function Discover(): JSX.Element | null { const groupsList = productGroups.flatMap( ( group ) => group ); return (
+ { groupsList.map( ( groups ) => ( = subscriptions.filter( - ( subscription: Subscription ) => ! subscription.subscription_installed + ( subscription: Subscription ) => + ! subscription.subscription_installed && + wccomSettings?.wooUpdateManagerPluginSlug !== + subscription.product_slug ); if ( ! wccomSettings?.isConnected ) { @@ -67,12 +71,12 @@ export default function MySubscriptions(): JSX.Element {

{ __( - 'Connect your account to get updates, manage your subscriptions, and get seamless support. Once connected, your Woo.com subscriptions will appear here.', + "Connect your store to Woo.com using the Woo.com Update Manager. Once connected, you'll be able to manage your subscriptions, receive product updates, and access streamlined support from this screen.", 'woocommerce' ) }

); @@ -84,6 +88,7 @@ export default function MySubscriptions(): JSX.Element {
+
diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/update.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/update.tsx index 41241dee1d5..46c90bebb23 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/update.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/update.tsx @@ -20,9 +20,11 @@ import { updateProduct, } from '../../../../utils/functions'; import { NoticeStatus } from '../../../../contexts/types'; +import InstallWooConnectModal from '../../../woo-update-manager-plugin/install-woo-connect-modal'; interface UpdateProps { subscription: Subscription; + wooUpdateManagerActive: boolean; } export default function Update( props: UpdateProps ) { @@ -34,7 +36,8 @@ export default function Update( props: UpdateProps ) { props.subscription.active && props.subscription.local && props.subscription.local.slug && - props.subscription.local.path; + props.subscription.local.path && + props.wooUpdateManagerActive; function update() { recordEvent( 'marketplace_product_update_button_clicked', { @@ -156,6 +159,13 @@ export default function Update( props: UpdateProps ) { onClose={ () => setShowModal( false ) } /> ); + } else if ( ! props.wooUpdateManagerActive ) { + return ( + setShowModal( false ) } + /> + ); } return null; diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx index c234f49ebed..76567fd7b21 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx @@ -25,6 +25,7 @@ import { subscribeUrl, } from '../../../../utils/functions'; import { MARKETPLACE_COLLABORATION_PATH } from '../../../constants'; +import { getAdminSetting } from '../../../../../utils/admin-settings'; type StatusBadge = { text: string; @@ -126,12 +127,19 @@ function getStatusBadge( subscription: Subscription ): StatusBadge | false { } function getVersion( subscription: Subscription ): string | JSX.Element { + const wccomSettings = getAdminSetting( 'wccomHelper', {} ); + if ( subscription.local.version === subscription.version ) { return ; } if ( subscription.local.version && subscription.version ) { - return ; + return ( + + ); } if ( subscription.version ) { diff --git a/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx b/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx index dc704b3d1de..028d02c75e2 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx @@ -27,6 +27,7 @@ import { Product, ProductType, SearchResultType } from '../product-list/types'; import { MARKETPLACE_ITEMS_PER_PAGE } from '../constants'; import { ADMIN_URL } from '~/utils/admin-settings'; import { ThemeSwitchWarningModal } from '~/customize-store/intro/warning-modals'; +import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice'; interface ProductsProps { categorySelector?: boolean; @@ -149,6 +150,7 @@ export default function Products( props: ProductsProps ) { return (
+

{ isLoading ? ' ' : title }

diff --git a/plugins/woocommerce-admin/client/marketplace/components/woo-update-manager-plugin/install-woo-connect-modal.tsx b/plugins/woocommerce-admin/client/marketplace/components/woo-update-manager-plugin/install-woo-connect-modal.tsx new file mode 100644 index 00000000000..5f45bcf354e --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/woo-update-manager-plugin/install-woo-connect-modal.tsx @@ -0,0 +1,104 @@ +/** + * External dependencies + */ +import { Button, ButtonGroup, Modal } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import sanitizeHTML from '../../../lib/sanitize-html'; +import { Subscription } from '../my-subscriptions/types'; +import { + WOO_CONNECT_PLUGIN_DOWNLOAD_URL, + WP_ADMIN_PLUGIN_LIST_URL, +} from '../constants'; +import { getAdminSetting } from '../../../utils/admin-settings'; + +interface ConnectProps { + subscription: Subscription; + onClose: () => void; +} + +export default function InstallWooConnectModal( props: ConnectProps ) { + const wccomSettings = getAdminSetting( 'wccomHelper', {} ); + if ( ! wccomSettings?.wooUpdateManagerInstalled ) { + return ( + +

+ install the Woo.com Update Manager extension. Alternatively, you can download and install it manually.', + 'woocommerce' + ), + props.subscription.version + ) + ) } + /> +

+ + + + +
+ ); + } + + if ( ! wccomSettings?.wooUpdateManagerActive ) { + return ( + +

+ activate the Woo.com Update Manager extension.', + 'woocommerce' + ), + props.subscription.version + ) + ) } + /> +

+ + + + +
+ ); + } + + return null; +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/woo-update-manager-plugin/plugin-install-notice.tsx b/plugins/woocommerce-admin/client/marketplace/components/woo-update-manager-plugin/plugin-install-notice.tsx new file mode 100644 index 00000000000..4533dbdbd56 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/woo-update-manager-plugin/plugin-install-notice.tsx @@ -0,0 +1,84 @@ +/** + * External dependencies + */ +import { Button, Notice } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import sanitizeHTML from '../../../lib/sanitize-html'; +import { getAdminSetting } from '../../../utils/admin-settings'; +import { + WP_ADMIN_PLUGIN_LIST_URL, + WOO_CONNECT_PLUGIN_DOWNLOAD_URL, +} from '../constants'; +import './woo-update-manager-plugin.scss'; + +export default function PluginInstallNotice() { + const wccomSettings = getAdminSetting( 'wccomHelper', {} ); + if ( ! wccomSettings?.isConnected ) { + return null; + } + + if ( + ! wccomSettings?.wooUpdateManagerActive && + ! wccomSettings?.wooUpdateManagerInstalled + ) { + return ( +
+ + Woo.com Update Manager to continue receiving the updates and streamlined support included in your Woo.com subscriptions.
Alternatively, you can download and install it manually.', + 'woocommerce' + ) + ) } + >
+
+ + +
+
+
+ ); + } else if ( + wccomSettings?.wooUpdateManagerInstalled && + ! wccomSettings?.wooUpdateManagerActive + ) { + return ( +
+ + Woo.com Update Manager to continue receiving the updates and streamlined support included in your Woo.com subscriptions.', + 'woocommerce' + ) + ) } + > +
+ +
+
+
+ ); + } + + return null; +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/woo-update-manager-plugin/woo-update-manager-plugin.scss b/plugins/woocommerce-admin/client/marketplace/components/woo-update-manager-plugin/woo-update-manager-plugin.scss new file mode 100644 index 00000000000..d2aa47cfc69 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/woo-update-manager-plugin/woo-update-manager-plugin.scss @@ -0,0 +1,67 @@ +@import '@wordpress/base-styles/_colors.native.scss'; +@import '../../stylesheets/_variables.scss'; + +.woocommerce-marketplace__woo-update-manager-plugin__notices { + .components-notice { + margin-left: 0; + margin-right: 0; + background-color: #fff; + box-shadow: 0 2px 6px 0 rgba($gray-100, 0.05); + border: 1px solid var(--gutenberg-gray-100, #f0f0f0); + padding-right: $grid-unit-15; + position: relative; + + &::before { + content: ''; + display: block; + width: 4px; + height: 100%; + background-color: var(--wp-admin-theme-color, #007cba); + position: absolute; + left: 0; + top: 0; + bottom: 0; + } + + &.is-error::before { + background-color: $alert-red; + } + + .components-notice__content { + align-items: center; + gap: $grid-unit-15; + padding: 0; + margin-left: 12px; + } + + .components-notice__buttons { + margin-top: 12px; + } + } + + .components-button { + &.is-link { + text-decoration: none; + padding: 6px 12px; + } + } +} + +.woocommerce-marketplace__woo-update-manager-plugin__notices { + margin-bottom: $grid-unit-50; +} + +.woocommerce-marketplace__discover { + .woocommerce-marketplace__woo-update-manager-plugin__notices { + margin-bottom: 0; + } +} + +.woocommerce-marketplace__header-account-modal { + .components-button { + &.is-link { + text-decoration: none; + padding: 6px 12px; + } + } +} diff --git a/plugins/woocommerce/changelog/remove-updates-for-woo.com-extensions b/plugins/woocommerce/changelog/remove-updates-for-woo.com-extensions new file mode 100644 index 00000000000..0fdd0a0de82 --- /dev/null +++ b/plugins/woocommerce/changelog/remove-updates-for-woo.com-extensions @@ -0,0 +1,4 @@ +Significance: major +Type: update + +Remove the ability to update Woo.com plugins that are not available under WordPress.org plugin directory diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php index 6d58b6eea51..fe3dab53335 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php @@ -49,13 +49,17 @@ class WC_Helper_Admin { ); $settings['wccomHelper'] = array( - 'isConnected' => WC_Helper::is_site_connected(), - 'connectURL' => self::get_connection_url(), - 'userEmail' => $auth_user_email, - 'userAvatar' => get_avatar_url( $auth_user_email, array( 'size' => '48' ) ), - 'storeCountry' => wc_get_base_location()['country'], - 'inAppPurchaseURLParams' => WC_Admin_Addons::get_in_app_purchase_url_params(), - 'installedProducts' => $installed_products, + 'isConnected' => WC_Helper::is_site_connected(), + 'connectURL' => self::get_connection_url(), + 'userEmail' => $auth_user_email, + 'userAvatar' => get_avatar_url( $auth_user_email, array( 'size' => '48' ) ), + 'storeCountry' => wc_get_base_location()['country'], + 'inAppPurchaseURLParams' => WC_Admin_Addons::get_in_app_purchase_url_params(), + 'installedProducts' => $installed_products, + 'wooUpdateManagerInstalled' => WC_Woo_Update_Manager_Plugin::is_plugin_installed(), + 'wooUpdateManagerActive' => WC_Woo_Update_Manager_Plugin::is_plugin_active(), + 'wooUpdateManagerInstallUrl' => WC_Woo_Update_Manager_Plugin::generate_install_url(), + 'wooUpdateManagerPluginSlug' => WC_Woo_Update_Manager_Plugin::WOO_UPDATE_MANAGER_SLUG, ); return $settings; diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-api.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-api.php index 8798aaf8f6f..911d6d206f9 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-api.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-api.php @@ -67,6 +67,61 @@ class WC_Helper_API { return wp_safe_remote_request( $url, $args ); } + /** + * Create signature for a request. + * + * @param string $access_token_secret The access token secret. + * @param string $url The URL to add the access token and signature to. + * @param string $method The request method. + * @param array $body The body of the request. + * @return string The signature. + */ + private static function create_request_signature( string $access_token_secret, string $url, string $method, $body = null ): string { + + $request_uri = wp_parse_url( $url, PHP_URL_PATH ); + $query_string = wp_parse_url( $url, PHP_URL_QUERY ); + + if ( is_string( $query_string ) ) { + $request_uri .= '?' . $query_string; + } + + $data = array( + 'host' => wp_parse_url( $url, PHP_URL_HOST ), + 'request_uri' => $request_uri, + 'method' => $method, + ); + + if ( ! empty( $body ) ) { + $data['body'] = $body; + } + + return hash_hmac( 'sha256', wp_json_encode( $data ), $access_token_secret ); + } + + /** + * Add the access token and signature to the provided URL. + * + * @param string $url The URL to add the access token and signature to. + * @return string + */ + public static function add_auth_parameters( string $url ): string { + $auth = WC_Helper_Options::get( 'auth' ); + + if ( empty( $auth['access_token'] ) || empty( $auth['access_token_secret'] ) ) { + return false; + } + + $signature = self::create_request_signature( (string) $auth['access_token_secret'], $url, 'GET' ); + + return add_query_arg( + array( + 'token' => $auth['access_token'], + 'signature' => $signature, + ), + $url + ); + } + /** * Adds authentication headers to an HTTP request. * @@ -81,24 +136,13 @@ class WC_Helper_API { return false; } - $request_uri = parse_url( $url, PHP_URL_PATH ); - $query_string = parse_url( $url, PHP_URL_QUERY ); - - if ( is_string( $query_string ) ) { - $request_uri .= '?' . $query_string; - } - - $data = array( - 'host' => parse_url( $url, PHP_URL_HOST ), - 'request_uri' => $request_uri, - 'method' => ! empty( $args['method'] ) ? $args['method'] : 'GET', + $signature = self::create_request_signature( + (string) $auth['access_token_secret'], + $url, + ! empty( $args['method'] ) ? $args['method'] : 'GET', + $args['body'] ?? null ); - if ( ! empty( $args['body'] ) ) { - $data['body'] = $args['body']; - } - - $signature = hash_hmac( 'sha256', json_encode( $data ), $auth['access_token_secret'] ); if ( empty( $args['headers'] ) ) { $args['headers'] = array(); } diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-subscriptions-api.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-subscriptions-api.php index c19f57fc0a8..09b1b4d1ae2 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-subscriptions-api.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-subscriptions-api.php @@ -300,7 +300,10 @@ class WC_Helper_Subscriptions_API { ); } - $install_url = WC_Helper::get_subscription_install_url( $subscription['product_key'] ); + $install_url = WC_Helper::get_subscription_install_url( + $subscription['product_key'], + $subscription['product_slug'] + ); if ( ! $install_url ) { wp_send_json_error( diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php index 4d583d5dde9..4cd3b6886c9 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php @@ -26,6 +26,16 @@ class WC_Helper_Updater { add_action( 'pre_set_site_transient_update_themes', array( __CLASS__, 'transient_update_themes' ), 21, 1 ); add_action( 'upgrader_process_complete', array( __CLASS__, 'upgrader_process_complete' ) ); add_action( 'upgrader_pre_download', array( __CLASS__, 'block_expired_updates' ), 10, 2 ); + add_action( 'plugins_loaded', array( __CLASS__, 'add_hook_for_modifying_update_notices' ) ); + } + + /** + * Add the hook for modifying default WPCore update notices on the plugins management page. + */ + public static function add_hook_for_modifying_update_notices() { + if ( ! WC_Woo_Update_Manager_Plugin::is_plugin_active() ) { + add_action( 'load-plugins.php', array( __CLASS__, 'setup_update_plugins_messages' ), 11 ); + } } /** @@ -53,21 +63,25 @@ class WC_Helper_Updater { 'plugin' => $filename, 'new_version' => $data['version'], 'url' => $data['url'], - 'package' => $data['package'], + 'package' => '', 'upgrade_notice' => $data['upgrade_notice'], ); + /** + * Filters the Woo plugin data before saving it in transient used for updates. + * + * @since 8.7.0 + * + * @param array $item Plugin item to modify. + * @param array $data Subscription data fetched from Helper API for the plugin. + * @param int $product_id Woo product id assigned to the plugin. + */ + $item = apply_filters( 'update_woo_com_subscription_details', $item, $data, $plugin['_product_id'] ); + if ( isset( $data['requires_php'] ) ) { $item['requires_php'] = $data['requires_php']; } - // We don't want to deliver a valid upgrade package when their subscription has expired. - // To avoid the generic "no_package" error that empty strings give, we will store an - // indication of expiration for the `upgrader_pre_download` filter to error on. - if ( ! self::_has_active_subscription( $plugin['_product_id'] ) ) { - $item['package'] = 'woocommerce-com-expired-' . $plugin['_product_id']; - } - if ( $transient instanceof stdClass ) { if ( version_compare( $plugin['Version'], $data['version'], '<' ) ) { $transient->response[ $filename ] = (object) $item; @@ -113,9 +127,16 @@ class WC_Helper_Updater { 'package' => '', ); - if ( self::_has_active_subscription( $theme['_product_id'] ) ) { - $item['package'] = $data['package']; - } + /** + * Filters the Woo plugin data before saving it in transient used for updates. + * + * @since 8.7.0 + * + * @param array $item Plugin item to modify. + * @param array $data Subscription data fetched from Helper API for the plugin. + * @param int $product_id Woo product id assigned to the plugin. + */ + $item = apply_filters( 'update_woo_com_subscription_details', $item, $data, $theme['_product_id'] ); if ( version_compare( $theme['Version'], $data['version'], '<' ) ) { $transient->response[ $slug ] = $item; @@ -128,6 +149,53 @@ class WC_Helper_Updater { return $transient; } + /** + * Runs on load-plugins.php, adds a hook to show a custom plugin update message for Woo.com hosted plugins. + * + * @return void. + */ + public static function setup_update_plugins_messages() { + foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) { + $filename = $plugin['_filename']; + add_action( 'in_plugin_update_message-' . $filename, array( __CLASS__, 'add_install_marketplace_plugin_message' ), 10, 2 ); + } + } + + /** + * Runs on in_plugin_update_message-{file-name}, show a message to install the Woo Marketplace plugin, on plugin update notification, + * if the Woo Marketplace plugin isn't already installed. + * + * @param object $plugin_data TAn array of plugin metadata. + * @param object $response An object of metadata about the available plugin update. + * + * @return void. + */ + public static function add_install_marketplace_plugin_message( $plugin_data, $response ) { + if ( ! empty( $response->package ) || WC_Woo_Update_Manager_Plugin::is_plugin_active() ) { + return; + } + + if ( ! WC_Woo_Update_Manager_Plugin::is_plugin_installed() ) { + printf( + wp_kses( + /* translators: 1: Woo Update Manager plugin install URL */ + __( ' Install Woo.com Update Manager to update.', 'woocommerce' ), + array( + 'a' => array( + 'href' => array(), + ), + ) + ), + esc_url( WC_Woo_Update_Manager_Plugin::generate_install_url() ), + ); + return; + } + + if ( ! WC_Woo_Update_Manager_Plugin::is_plugin_active() ) { + echo esc_html_e( ' Activate Woo.com Update Manager to update.', 'woocommerce' ); + } + } + /** * Get update data for all plugins. * @@ -236,8 +304,8 @@ class WC_Helper_Updater { } // Scan local plugins which may or may not have a subscription. - $plugins = WC_Helper::get_local_woo_plugins(); - $active_woo_plugins = array_intersect( array_keys( $plugins ), get_option( 'active_plugins', array() ) ); + $plugins = WC_Helper::get_local_woo_plugins(); + $active_woo_plugins = array_intersect( array_keys( $plugins ), get_option( 'active_plugins', array() ) ); /* * Use only plugins that are subscribed to the automatic translations updates. @@ -245,6 +313,11 @@ class WC_Helper_Updater { $active_for_translations = array_filter( $active_woo_plugins, function( $plugin ) use ( $plugins ) { + /** + * Filters the plugins that are subscribed to the automatic translations updates. + * + * @since 3.7.0 + */ return apply_filters( 'woocommerce_translations_updates_for_' . $plugins[ $plugin ]['slug'], false ); } ); @@ -267,16 +340,16 @@ class WC_Helper_Updater { ); foreach ( $active_for_translations as $active_plugin ) { - $plugin = $plugins[ $active_plugin ]; + $plugin = $plugins[ $active_plugin ]; $request_body['plugins'][ $plugin['slug'] ] = array( 'version' => $plugin['Version'] ); } $raw_response = wp_remote_post( 'https://translate.wordpress.com/api/translations-updates/woocommerce', array( - 'body' => json_encode( $request_body ), - 'headers' => array( 'Content-Type: application/json' ), - 'timeout' => $timeout, + 'body' => wp_json_encode( $request_body ), + 'headers' => array( 'Content-Type: application/json' ), + 'timeout' => $timeout, ) ); @@ -367,44 +440,6 @@ class WC_Helper_Updater { return $data['products']; } - /** - * Check for an active subscription. - * - * Checks a given product id against all subscriptions on - * the current site. Returns true if at least one active - * subscription is found. - * - * @param int $product_id The product id to look for. - * - * @return bool True if active subscription found. - */ - private static function _has_active_subscription( $product_id ) { - if ( ! isset( $auth ) ) { - $auth = WC_Helper_Options::get( 'auth' ); - } - - if ( ! isset( $subscriptions ) ) { - $subscriptions = WC_Helper::get_subscriptions(); - } - - if ( empty( $auth['site_id'] ) || empty( $subscriptions ) ) { - return false; - } - - // Check for an active subscription. - foreach ( $subscriptions as $subscription ) { - if ( $subscription['product_id'] != $product_id ) { - continue; - } - - if ( in_array( absint( $auth['site_id'] ), $subscription['connections'] ) ) { - return true; - } - } - - return false; - } - /** * Get the number of products that have updates. * diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php index fd264411104..10e0ac60e7c 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php @@ -54,14 +54,15 @@ class WC_Helper { * Include supporting helper classes. */ protected static function includes() { - include_once dirname( __FILE__ ) . '/class-wc-helper-options.php'; - include_once dirname( __FILE__ ) . '/class-wc-helper-api.php'; - include_once dirname( __FILE__ ) . '/class-wc-helper-updater.php'; - include_once dirname( __FILE__ ) . '/class-wc-helper-plugin-info.php'; - include_once dirname( __FILE__ ) . '/class-wc-helper-compat.php'; - include_once dirname( __FILE__ ) . '/class-wc-helper-admin.php'; - include_once dirname( __FILE__ ) . '/class-wc-helper-subscriptions-api.php'; - include_once dirname( __FILE__ ) . '/class-wc-helper-orders-api.php'; + include_once __DIR__ . '/class-wc-helper-options.php'; + include_once __DIR__ . '/class-wc-helper-api.php'; + include_once __DIR__ . '/class-wc-woo-update-manager-plugin.php'; + include_once __DIR__ . '/class-wc-helper-updater.php'; + include_once __DIR__ . '/class-wc-plugin-api-updater.php'; + include_once __DIR__ . '/class-wc-helper-compat.php'; + include_once __DIR__ . '/class-wc-helper-admin.php'; + include_once __DIR__ . '/class-wc-helper-subscriptions-api.php'; + include_once __DIR__ . '/class-wc-helper-orders-api.php'; } /** @@ -832,6 +833,7 @@ class WC_Helper { ) ) : '', + 'wum-installed' => WC_Woo_Update_Manager_Plugin::is_plugin_installed() ? '1' : '0', ), WC_Helper_API::url( 'oauth/authorize' ) ); @@ -1193,34 +1195,18 @@ class WC_Helper { * Get a subscriptions install URL. * * @param string $product_key Subscription product key. + * @param string $product_slug Subscription product slug. * @return string */ - public static function get_subscription_install_url( $product_key ) { - $install_url_response = WC_Helper_API::post( - 'install-url', + public static function get_subscription_install_url( $product_key, $product_slug ) { + $install_url = add_query_arg( array( - 'authenticated' => true, - 'body' => wp_json_encode( - array( - 'product_key' => $product_key, - 'wc_version' => WC()->version, - ) - ), - ) + 'product-key' => $product_key, + ), + self::get_install_base_url() . "{$product_slug}/" ); - $code = wp_remote_retrieve_response_code( $install_url_response ); - if ( 200 !== $code ) { - self::log( sprintf( 'Install URL API call returned a non-200 response code (%d)', $code ) ); - return ''; - } - - $body = json_decode( wp_remote_retrieve_body( $install_url_response ), true ); - if ( empty( $body['data']['url'] ) ) { - self::log( sprintf( 'Install URL API call returned an invalid body: %s', wp_remote_retrieve_body( $install_url_response ) ) ); - return ''; - } - return $body['data']['url']; + return WC_Helper_API::add_auth_parameters( $install_url ); } /** @@ -1517,6 +1503,8 @@ class WC_Helper { $source = 'inbox-notes'; elseif ( stripos( $request_uri, 'admin-ajax.php' ) ) : $source = 'heartbeat-api'; + elseif ( stripos( $request_uri, 'installer' ) ) : + $source = 'wccom-site-installer'; elseif ( defined( 'WP_CLI' ) && WP_CLI ) : $source = 'wc-cli'; endif; @@ -2257,6 +2245,22 @@ class WC_Helper { self::_flush_subscriptions_cache(); self::_flush_updates_cache(); } + + /** + * Get base URL for plugin auto installer. + * + * @return string + */ + public static function get_install_base_url() { + /** + * Filter the base URL used to install the Woo hosted plugins. + * + * @since 8.7.0 + */ + $woo_com_base_url = apply_filters( 'woo_com_base_url', 'https://woo.com/' ); + + return $woo_com_base_url . 'auto-install-init/'; + } } WC_Helper::load(); diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-plugin-info.php b/plugins/woocommerce/includes/admin/helper/class-wc-plugin-api-updater.php similarity index 81% rename from plugins/woocommerce/includes/admin/helper/class-wc-helper-plugin-info.php rename to plugins/woocommerce/includes/admin/helper/class-wc-plugin-api-updater.php index a33de0a5f13..bbd2d21168f 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-plugin-info.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-plugin-api-updater.php @@ -1,21 +1,17 @@ slug ) ) { return $response; } - // Only for slugs that start with woo- + // Only for slugs that start with woocommerce-com-. if ( 0 !== strpos( $args->slug, 'woocommerce-com-' ) ) { return $response; } @@ -105,4 +103,4 @@ class WC_Helper_Plugin_Info { } } -WC_Helper_Plugin_Info::load(); +WC_Plugin_Api_Updater::load(); diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-woo-update-manager-plugin.php b/plugins/woocommerce/includes/admin/helper/class-wc-woo-update-manager-plugin.php new file mode 100644 index 00000000000..7054a4dbdae --- /dev/null +++ b/plugins/woocommerce/includes/admin/helper/class-wc-woo-update-manager-plugin.php @@ -0,0 +1,130 @@ + +
+ +

+ activate the Woo.com Update Manager to continue receiving the updates and streamlined support included in your Woo.com subscriptions.', + 'woocommerce' + ), + esc_url( admin_url( 'plugins.php' ) ), + ) + ); + ?> +

+
diff --git a/plugins/woocommerce/includes/admin/helper/views/html-notice-woo-updater-not-installed.php b/plugins/woocommerce/includes/admin/helper/views/html-notice-woo-updater-not-installed.php new file mode 100644 index 00000000000..a897e71f47a --- /dev/null +++ b/plugins/woocommerce/includes/admin/helper/views/html-notice-woo-updater-not-installed.php @@ -0,0 +1,27 @@ + +
+ +

+ Install the Woo.com Update Manager to continue receiving the updates and streamlined support included in your Woo.com subscriptions. Alternatively, you can download and install it manually.', + 'woocommerce' + ), + esc_url( WC_Woo_Update_Manager_Plugin::generate_install_url() ), + esc_url( WC_Woo_Update_Manager_Plugin::WOO_UPDATE_MANAGER_DOWNLOAD_URL ) + ) + ); + ?> +

+
diff --git a/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php b/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php index f97cfcc75c3..68a9e25ee8d 100644 --- a/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php +++ b/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php @@ -223,6 +223,7 @@ class WC_WCCOM_Site { require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/abstract-wc-rest-wccom-site-controller.php'; require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php'; require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-ssr-controller.php'; + require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-status-controller.php'; require_once WC_ABSPATH . 'includes/wccom-site/installation/class-wc-wccom-site-installation-state.php'; require_once WC_ABSPATH . 'includes/wccom-site/installation/class-wc-wccom-site-installation-state-storage.php'; @@ -238,6 +239,7 @@ class WC_WCCOM_Site { $namespaces['wccom-site/v2'] = array( 'installer' => 'WC_REST_WCCOM_Site_Installer_Controller', 'ssr' => 'WC_REST_WCCOM_Site_SSR_Controller', + 'status' => 'WC_REST_WCCOM_Site_Status_Controller', ); return $namespaces; diff --git a/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-status-controller.php b/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-status-controller.php new file mode 100644 index 00000000000..eb8fd26d2e4 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-status-controller.php @@ -0,0 +1,78 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'handle_status_request' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ), + ); + } + + /** + * Check whether user has permission to access controller's endpoints. + * + * @since 8.7.0 + * @param WP_USER $user User object. + * @return bool + */ + public function user_has_permission( $user ): bool { + return user_can( $user, 'install_plugins' ) && user_can( $user, 'activate_plugins' ); + } + + /** + * Get the status details of the site. + * + * @since 8.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response + */ + public function handle_status_request( $request ) { + + return rest_ensure_response( + array( + 'success' => true, + 'data' => array( + 'wc_version' => WC()->version, + 'woo_update_manager_installed' => WC_Woo_Update_Manager_Plugin::is_plugin_installed(), + 'woo_update_manager_active' => WC_Woo_Update_Manager_Plugin::is_plugin_active(), + ), + ) + ); + } +} diff --git a/plugins/woocommerce/src/Admin/PageController.php b/plugins/woocommerce/src/Admin/PageController.php index 3a38577cd4a..c44525cef5c 100644 --- a/plugins/woocommerce/src/Admin/PageController.php +++ b/plugins/woocommerce/src/Admin/PageController.php @@ -167,16 +167,17 @@ class PageController { return apply_filters( 'woocommerce_navigation_get_breadcrumbs', array( '' ), $current_page ); } - $current_page['title'] = (array) $current_page['title']; - if ( 1 === count( $current_page['title'] ) ) { - $breadcrumbs = $current_page['title']; + $page_title = ! empty( $current_page['page_title'] ) ? $current_page['page_title'] : $current_page['title']; + $page_title = (array) $page_title; + if ( 1 === count( $page_title ) ) { + $breadcrumbs = $page_title; } else { // If this page has multiple title pieces, only link the first one. $breadcrumbs = array_merge( array( - array( $current_page['path'], reset( $current_page['title'] ) ), + array( $current_page['path'], reset( $page_title ) ), ), - array_slice( $current_page['title'], 1 ) + array_slice( $page_title, 1 ) ); } @@ -438,6 +439,7 @@ class PageController { 'id' => null, 'parent' => null, 'title' => '', + 'page_title' => '', 'capability' => 'view_woocommerce_reports', 'path' => '', 'icon' => '', @@ -455,9 +457,13 @@ class PageController { $options['position'] = intval( round( $options['position'] ) ); } + if ( empty( $options['page_title'] ) ) { + $options['page_title'] = $options['title']; + } + if ( is_null( $options['parent'] ) ) { add_menu_page( - $options['title'], + $options['page_title'], $options['title'], $options['capability'], $options['path'], @@ -470,7 +476,7 @@ class PageController { // @todo check for null path. add_submenu_page( $parent_path, - $options['title'], + $options['page_title'], $options['title'], $options['capability'], $options['path'], diff --git a/plugins/woocommerce/src/Internal/Admin/Marketplace.php b/plugins/woocommerce/src/Internal/Admin/Marketplace.php index 198a4de0de7..888a3e7600d 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketplace.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketplace.php @@ -7,6 +7,8 @@ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Utilities\FeaturesUtil; use Automattic\WooCommerce\Internal\Features\FeaturesController; +use WC_Helper_Updater; +use WC_Woo_Update_Manager_Plugin; /** * Contains backend logic for the Marketplace feature. @@ -17,6 +19,8 @@ class Marketplace { /** * Class initialization, to be executed when the class is resolved by the container. + * + * @internal */ final public function init() { if ( false === FeaturesUtil::feature_is_enabled( 'marketplace' ) ) { @@ -55,10 +59,11 @@ class Marketplace { public static function get_marketplace_pages() { $marketplace_pages = array( array( - 'id' => 'woocommerce-marketplace', - 'parent' => 'woocommerce', - 'title' => __( 'Extensions', 'woocommerce' ), - 'path' => '/extensions', + 'id' => 'woocommerce-marketplace', + 'parent' => 'woocommerce', + 'title' => __( 'Extensions', 'woocommerce' ) . self::get_marketplace_update_count_html(), + 'page_title' => __( 'Extensions', 'woocommerce' ), + 'path' => '/extensions', ), ); @@ -70,6 +75,29 @@ class Marketplace { return apply_filters( 'woocommerce_marketplace_menu_items', $marketplace_pages ); } + /** + * Create the menu bubble for extensions menu based on number of updates available. + * + * @return string + */ + private static function get_marketplace_update_count_html() { + $count = WC_Helper_Updater::get_updates_count(); + if ( empty( $count ) ) { + $count = 0; + } + + $count = intval( $count ); + if ( ! WC_Woo_Update_Manager_Plugin::is_plugin_installed() || ! WC_Woo_Update_Manager_Plugin::is_plugin_active() ) { + ++$count; + } + + if ( 0 === $count ) { + return ''; + } + + return sprintf( ' %d', $count, number_format_i18n( $count ) ); + } + /** * Enqueue update script. *