first commit

This commit is contained in:
User A0264400
2026-04-01 23:20:16 +03:00
commit a766acdc90
23071 changed files with 4933189 additions and 0 deletions

View File

@@ -0,0 +1,300 @@
<?php
/**
* WooCommerce Admin Helper - React admin interface
*
* @package WooCommerce\Admin\Helper
*/
use Automattic\WooCommerce\Internal\Admin\Marketplace;
use Automattic\WooCommerce\Admin\PluginsHelper;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper Class
*
* The main entry-point for all things related to the Helper.
* The Helper manages the connection between the store and
* an account on WooCommerce.com.
*/
class WC_Helper_Admin {
/**
* Clear cache tool identifier.
*/
const CACHE_TOOL_ID = 'clear_woocommerce_helper_cache';
/**
* Loads the class, runs on init
*
* @return void
*/
public static function load() {
if ( is_admin() ) {
$is_wc_home_or_in_app_marketplace = (
isset( $_GET['page'] ) && 'wc-admin' === $_GET['page'] //phpcs:ignore WordPress.Security.NonceVerification.Recommended
);
if ( $is_wc_home_or_in_app_marketplace ) {
add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'add_marketplace_settings' ) );
}
add_filter( 'woocommerce_debug_tools', array( __CLASS__, 'register_cache_clear_tool' ) );
}
add_filter( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
}
/**
* Pushes settings onto the WooCommerce Admin global settings object (wcSettings).
*
* @param mixed $settings The settings object we're amending.
*
* @return mixed $settings
*/
public static function add_marketplace_settings( $settings ) {
if ( ! WC_Helper::is_site_connected() && isset( $_GET['connect'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
wp_safe_redirect( self::get_connection_url() );
exit;
}
$auth_user_data = WC_Helper_Options::get( 'auth_user_data', array() );
$auth_user_email = isset( $auth_user_data['email'] ) ? $auth_user_data['email'] : '';
// Get the all installed themes and plugins. Knowing this will help us decide to show Add to Store button on product cards.
$installed_products = array_merge( WC_Helper::get_local_plugins(), WC_Helper::get_local_themes() );
$installed_products = array_map(
function ( $product ) {
return $product['slug'];
},
$installed_products
);
$blog_name = get_bloginfo( 'name' );
$settings['wccomHelper'] = array(
'isConnected' => WC_Helper::is_site_connected(),
'connectURL' => self::get_connection_url(),
'reConnectURL' => self::get_connection_url( true ),
'userEmail' => $auth_user_email,
'userAvatar' => get_avatar_url( $auth_user_email, array( 'size' => '48' ) ),
'storeCountry' => wc_get_base_location()['country'],
'storeName' => $blog_name ? $blog_name : '',
'inAppPurchaseURLParams' => WC_Admin_Addons::get_in_app_purchase_url_params(),
'installedProducts' => $installed_products,
'mySubscriptionsTabLoaded' => WC_Helper_Options::get( 'my_subscriptions_tab_loaded' ),
'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,
'dismissNoticeNonce' => wp_create_nonce( 'dismiss_notice' ),
'trackingAllowed' => 'yes' === get_option( 'woocommerce_allow_tracking' ),
);
// This data is only used in the `Extensions` screen, so only populate it there.
// More specifically, it's used in `My Subscriptions`, however, switching tabs doesn't require
// a page reload, so we just check for `path` (/extensions), rather than `tab` (my-subscriptions).
if ( ! empty( $_GET['path'] ) && '/extensions' === $_GET['path'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$settings['wccomHelper']['wooUpdateCount'] = WC_Helper_Updater::get_updates_count_based_on_site_status();
$settings['wccomHelper']['connected_notice'] = PluginsHelper::get_wccom_connected_notice( $auth_user_email );
$settings['wccomHelper']['woocomConnectNoticeType'] = WC_Helper_Updater::get_woo_connect_notice_type();
if ( WC_Helper::is_site_connected() ) {
$settings['wccomHelper']['subscription_expired_notice'] = PluginsHelper::get_expired_subscription_notice( false );
$settings['wccomHelper']['subscription_expiring_notice'] = PluginsHelper::get_expiring_subscription_notice( false );
$settings['wccomHelper']['subscription_missing_notice'] = PluginsHelper::get_missing_subscription_notice();
$settings['wccomHelper']['connection_url_notice'] = WC_Woo_Helper_Connection::get_connection_url_notice();
$settings['wccomHelper']['has_host_plan_orders'] = WC_Woo_Helper_Connection::has_host_plan_orders();
} else {
$settings['wccomHelper']['disconnected_notice'] = PluginsHelper::get_wccom_disconnected_notice();
}
}
return $settings;
}
/**
* Generates the URL for connecting or disconnecting the store to/from WooCommerce.com.
* Approach taken from existing helper code that isn't exposed.
*
* @param bool $reconnect indicate if the site is being reconnected.
*
* @return string
*/
public static function get_connection_url( $reconnect = false ) {
// Default to wc-addons, although this can be changed from the frontend
// in the function `connectUrl()` within marketplace functions.tsx.
$connect_url_args = array(
'page' => 'wc-addons',
'section' => 'helper',
);
// No active connection.
if ( WC_Helper::is_site_connected() && ! $reconnect ) {
$connect_url_args['wc-helper-disconnect'] = 1;
$connect_url_args['wc-helper-nonce'] = wp_create_nonce( 'disconnect' );
} else {
$connect_url_args['wc-helper-connect'] = 1;
$connect_url_args['wc-helper-nonce'] = wp_create_nonce( 'connect' );
}
if ( ! empty( $_GET['utm_source'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$connect_url_args['utm_source'] = wc_clean( wp_unslash( $_GET['utm_source'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
if ( ! empty( $_GET['utm_campaign'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$connect_url_args['utm_campaign'] = wc_clean( wp_unslash( $_GET['utm_campaign'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
return add_query_arg(
$connect_url_args,
admin_url( 'admin.php' )
);
}
/**
* Registers the REST routes for the featured products and product
* previews endpoints.
*/
public static function register_rest_routes() {
/* Used by the WooCommerce > Extensions > Discover page. */
register_rest_route(
'wc/v3',
'/marketplace/featured',
array(
'methods' => 'GET',
'callback' => array( __CLASS__, 'get_featured' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
)
);
/* Used to show previews of products in a modal in in-app marketplace. */
register_rest_route(
'wc/v1',
'/marketplace/product-preview',
array(
'methods' => 'GET',
'callback' => array( __CLASS__, 'get_product_preview' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
)
);
}
/**
* The Extensions page can only be accessed by users with the manage_woocommerce
* capability. So the API mimics that behavior.
*/
public static function get_permission() {
return current_user_can( 'manage_woocommerce' );
}
/**
* Fetch featured products from WooCommerce.com and serve them
* as JSON.
*/
public static function get_featured() {
$featured = WC_Admin_Addons::fetch_featured();
if ( is_wp_error( $featured ) ) {
wp_send_json_error( array( 'message' => $featured->get_error_message() ) );
}
wp_send_json( $featured );
}
/**
* Fetch data for product previews from WooCommerce.com.
*
* @param WP_REST_Request $request Request object.
*/
public static function get_product_preview( $request ) {
$product_id = (int) $request->get_param( 'product_id' );
if ( ! $product_id ) {
wp_send_json_error(
array(
'message' => __( 'Missing product ID', 'woocommerce' ),
),
400
);
}
$product_preview = WC_Admin_Addons::fetch_product_preview( $product_id );
if ( ! $product_preview ) {
wp_send_json_error(
array(
'message' => __( 'We couldn\'t find a preview for this product.', 'woocommerce' ),
),
404
);
}
if ( is_wp_error( $product_preview ) ) {
wp_send_json_error(
array(
'message' => $product_preview->get_error_message(),
)
);
}
if (
! isset( $product_preview['css'] )
|| ! is_string( $product_preview['css'] )
|| ! isset( $product_preview['html'] )
|| ! is_string( $product_preview['html'] )
) {
wp_send_json_error(
array(
'message' => __(
'API response is missing required elements, or they are in the wrong form.',
'woocommerce'
),
),
500
);
}
$sanitized_product_preview = array(
'css' => WC_Helper_Sanitization::sanitize_css( $product_preview['css'] ),
'html' => WC_Helper_Sanitization::sanitize_html( $product_preview['html'] ),
);
wp_send_json( $sanitized_product_preview );
}
/**
* Register the cache clearing tool on the WooCommerce > Status > Tools page.
*
* @param array $debug_tools Available debug tool registrations.
* @return array Filtered debug tool registrations.
*/
public static function register_cache_clear_tool( $debug_tools ) {
$debug_tools[ self::CACHE_TOOL_ID ] = array(
'name' => __( 'Clear WooCommerce.com cache', 'woocommerce' ),
'button' => __( 'Clear', 'woocommerce' ),
'desc' => sprintf(
__( 'This tool will empty the WooCommerce.com data cache, used in WooCommerce Extensions.', 'woocommerce' ),
),
'callback' => array( __CLASS__, 'run_clear_cache_tool' ),
);
return $debug_tools;
}
/**
* "Clear" helper cache by invalidating it.
*/
public static function run_clear_cache_tool() {
WC_Helper::_flush_subscriptions_cache();
WC_Helper::flush_product_usage_notice_rules_cache();
WC_Helper::flush_connection_data_cache();
WC_Helper_Updater::flush_updates_cache();
return __( 'Helper cache cleared.', 'woocommerce' );
}
}
WC_Helper_Admin::load();

View File

@@ -0,0 +1,223 @@
<?php
/**
* WooCommerce Admin Helper API
*
* @package WooCommerce\Admin\Helper
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_API Class
*
* Provides a communication interface with the WooCommerce.com Helper API.
*/
class WC_Helper_API {
/**
* Base path for API routes.
*
* @var $api_base
*/
public static $api_base;
/**
* Load
*
* Allow devs to point the API base to a local API development or staging server.
* Note that sslverify will be turned off for the woocommerce.dev + WP_DEBUG combination.
* The URL can be changed on plugins_loaded before priority 10.
*/
public static function load() {
self::$api_base = apply_filters( 'woocommerce_helper_api_base', 'https://woocommerce.com/wp-json/helper/1.0' );
}
/**
* Perform an HTTP request to the Helper API.
*
* @param string $endpoint The endpoint to request.
* @param array $args Additional data for the request. Set authenticated to a truthy value to enable auth.
*
* @return array|WP_Error The response from wp_safe_remote_request()
*/
public static function request( $endpoint, $args = array() ) {
if ( ! isset( $args['query_string'] ) ) {
$args['query_string'] = '';
}
$url = self::url( $endpoint, $args['query_string'] );
if ( ! empty( $args['authenticated'] ) ) {
if ( ! self::_authenticate( $url, $args ) ) {
return new WP_Error( 'authentication', __( 'Authentication failed. Please try again after a few minutes. If the issue persists, disconnect your store from WooCommerce.com and reconnect.', 'woocommerce' ), 401 );
}
}
if ( ! isset( $args['user-agent'] ) ) {
$args['user-agent'] = 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' );
}
/**
* Allow developers to filter the request args passed to wp_safe_remote_request().
* Useful to remove sslverify when working on a local api dev environment.
*/
$args = apply_filters( 'woocommerce_helper_api_request_args', $args, $endpoint );
// TODO: Check response signatures on certain endpoints.
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.
*
* @param string $url The request URI.
* @param array $args By-ref, the args that will be passed to wp_remote_request().
* @return bool Were the headers added?
*/
private static function _authenticate( &$url, &$args ) {
$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,
! empty( $args['method'] ) ? $args['method'] : 'GET',
$args['body'] ?? null
);
if ( empty( $args['headers'] ) ) {
$args['headers'] = array();
}
$headers = array(
'Authorization' => 'Bearer ' . $auth['access_token'],
'X-Woo-Signature' => $signature,
);
$args['headers'] = wp_parse_args( $headers, $args['headers'] );
$url = add_query_arg(
array(
'token' => $auth['access_token'],
'signature' => $signature,
),
$url
);
return true;
}
/**
* Wrapper for self::request().
*
* @param string $endpoint The helper API endpoint to request.
* @param array $args Arguments passed to wp_remote_request().
*
* @return array The response object from wp_safe_remote_request().
*/
public static function get( $endpoint, $args = array() ) {
$args['method'] = 'GET';
return self::request( $endpoint, $args );
}
/**
* Wrapper for self::request().
*
* @param string $endpoint The helper API endpoint to request.
* @param array $args Arguments passed to wp_remote_request().
*
* @return array The response object from wp_safe_remote_request().
*/
public static function post( $endpoint, $args = array() ) {
$args['method'] = 'POST';
return self::request( $endpoint, $args );
}
/**
* Wrapper for self::request().
*
* @param string $endpoint The helper API endpoint to request.
* @param array $args Arguments passed to wp_remote_request().
*
* @return array The response object from wp_safe_remote_request().
*/
public static function put( $endpoint, $args = array() ) {
$args['method'] = 'PUT';
return self::request( $endpoint, $args );
}
/**
* Using the API base, form a request URL from a given endpoint.
*
* @param string $endpoint The endpoint to request.
* @param string $query_string Optional query string to append to the URL.
*
* @return string The absolute endpoint URL.
*/
public static function url( $endpoint, $query_string = '' ) {
$endpoint = ltrim( $endpoint, '/' );
$endpoint = sprintf( '%s/%s/%s', self::$api_base, $endpoint, $query_string );
$endpoint = esc_url_raw( $endpoint );
$endpoint = rtrim( $endpoint, '/' );
return $endpoint;
}
}
WC_Helper_API::load();

View File

@@ -0,0 +1,204 @@
<?php
/**
* WooCommerce Admin Helper Compat
*
* @package WooCommerce\Admin\Helper
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_Compat Class
*
* Some level of compatibility with the legacy WooCommerce Helper plugin.
*/
class WC_Helper_Compat {
/**
* Loads the class, runs on init.
*/
public static function load() {
add_action( 'woocommerce_helper_loaded', array( __CLASS__, 'helper_loaded' ) );
}
/**
* Runs during woocommerce_helper_loaded
*/
public static function helper_loaded() {
// Stop the nagging about WooThemes Updater
remove_action( 'admin_notices', 'woothemes_updater_notice' );
// A placeholder dashboard menu for legacy helper users.
add_action( 'admin_menu', array( __CLASS__, 'admin_menu' ) );
if ( empty( $GLOBALS['woothemes_updater'] ) ) {
return;
}
self::remove_actions();
self::migrate_connection();
self::deactivate_plugin();
}
/**
* Remove legacy helper actions (notices, menus, etc.)
*/
public static function remove_actions() {
// Remove WooThemes Updater notices
remove_action( 'network_admin_notices', array( $GLOBALS['woothemes_updater']->admin, 'maybe_display_activation_notice' ) );
remove_action( 'admin_notices', array( $GLOBALS['woothemes_updater']->admin, 'maybe_display_activation_notice' ) );
remove_action( 'network_admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) );
remove_action( 'admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) );
}
/**
* Attempt to migrate a legacy connection to a new one.
*/
public static function migrate_connection() {
// Don't attempt to migrate if attempted before.
if ( WC_Helper_Options::get( 'did-migrate' ) ) {
return;
}
$auth = WC_Helper_Options::get( 'auth' );
if ( ! empty( $auth ) ) {
return;
}
WC_Helper::log( 'Attempting oauth/migrate' );
WC_Helper_Options::update( 'did-migrate', true );
$master_key = get_option( 'woothemes_helper_master_key' );
if ( empty( $master_key ) ) {
WC_Helper::log( 'Master key not found, aborting' );
return;
}
$request = WC_Helper_API::post(
'oauth/migrate',
array(
'body' => array(
'home_url' => home_url(),
'master_key' => $master_key,
),
)
);
if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) {
WC_Helper::log( 'Call to oauth/migrate returned a non-200 response code' );
return;
}
$request_token = json_decode( wp_remote_retrieve_body( $request ) );
if ( empty( $request_token ) ) {
WC_Helper::log( 'Call to oauth/migrate returned an empty token' );
return;
}
// Obtain an access token.
$request = WC_Helper_API::post(
'oauth/access_token',
array(
'body' => array(
'request_token' => $request_token,
'home_url' => home_url(),
'migrate' => true,
),
)
);
if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) {
WC_Helper::log( 'Call to oauth/access_token returned a non-200 response code' );
return;
}
$access_token = json_decode( wp_remote_retrieve_body( $request ), true );
if ( empty( $access_token ) ) {
WC_Helper::log( 'Call to oauth/access_token returned an invalid token' );
return;
}
WC_Helper_Options::update(
'auth',
array(
'access_token' => $access_token['access_token'],
'access_token_secret' => $access_token['access_token_secret'],
'site_id' => $access_token['site_id'],
'user_id' => null, // Set this later
'updated' => time(),
)
);
// Obtain the connected user info.
if ( ! WC_Helper::_flush_authentication_cache() ) {
WC_Helper::log( 'Could not obtain connected user info in migrate_connection' );
WC_Helper_Options::update( 'auth', array() );
return;
}
}
/**
* Attempt to deactivate the legacy helper plugin.
*/
public static function deactivate_plugin() {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
if ( ! function_exists( 'deactivate_plugins' ) ) {
return;
}
if ( is_plugin_active( 'woothemes-updater/woothemes-updater.php' ) ) {
deactivate_plugins( 'woothemes-updater/woothemes-updater.php' );
// Notify the user when the plugin is deactivated.
add_action( 'pre_current_active_plugins', array( __CLASS__, 'plugin_deactivation_notice' ) );
}
}
/**
* Display admin notice directing the user where to go.
*/
public static function plugin_deactivation_notice() {
?>
<div id="message" class="error is-dismissible">
<p><?php printf( __( 'The WooCommerce Helper plugin is no longer needed. <a href="%s">Manage subscriptions</a> from the extensions tab instead.', 'woocommerce' ), esc_url( admin_url( 'admin.php?page=wc-addons&section=helper' ) ) ); ?></p>
</div>
<?php
}
/**
* Register menu item.
*/
public static function admin_menu() {
// No additional menu items for users who did not have a connected helper before.
$master_key = get_option( 'woothemes_helper_master_key' );
if ( empty( $master_key ) ) {
return;
}
// Do not show the menu item if user has already seen the new screen.
$auth = WC_Helper_Options::get( 'auth' );
if ( ! empty( $auth['user_id'] ) ) {
return;
}
add_dashboard_page( __( 'WooCommerce Helper', 'woocommerce' ), __( 'WooCommerce Helper', 'woocommerce' ), 'manage_options', 'woothemes-helper', array( __CLASS__, 'render_compat_menu' ) );
}
/**
* Render the legacy helper compat view.
*/
public static function render_compat_menu() {
$helper_url = add_query_arg(
array(
'page' => 'wc-addons',
'section' => 'helper',
),
admin_url( 'admin.php' )
);
include WC_Helper::get_view_filename( 'html-helper-compat.php' );
}
}
WC_Helper_Compat::load();

View File

@@ -0,0 +1,60 @@
<?php
/**
* WooCommerce Admin Helper Options
*
* @package WooCommerce\Admin\Helper
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_Options Class
*
* An interface to the woocommerce_helper_data entry in the wp_options table.
*/
class WC_Helper_Options {
/**
* The option name used to store the helper data.
*
* @var string
*/
private static $option_name = 'woocommerce_helper_data';
/**
* Update an option by key
*
* All helper options are grouped in a single options entry. This method
* is not thread-safe, use with caution.
*
* @param string $key The key to update.
* @param mixed $value The new option value.
*
* @return bool True if the option has been updated.
*/
public static function update( $key, $value ) {
$options = get_option( self::$option_name, array() );
$options[ $key ] = $value;
return update_option( self::$option_name, $options, true );
}
/**
* Get an option by key
*
* @see self::update
*
* @param string $key The key to fetch.
* @param mixed $default The default option to return if the key does not exist.
*
* @return mixed An option or the default.
*/
public static function get( $key, $default = false ) {
$options = get_option( self::$option_name, array() );
if ( is_array( $options ) && array_key_exists( $key, $options ) ) {
return $options[ $key ];
}
return $default;
}
}

View File

@@ -0,0 +1,107 @@
<?php
/**
* WooCommerce Admin Helper - React admin interface
*
* @package WooCommerce\Admin\Helper
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_Orders_API
*
* Pings WooCommerce.com to create an order and pull in the necessary data to start the installation process.
*/
class WC_Helper_Orders_API {
/**
* Loads the class, runs on init
*
* @return void
*/
public static function load() {
add_filter( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
}
/**
* Registers the REST routes for the Marketplace Orders API.
* These endpoints are used by the Marketplace Subscriptions React UI.
*/
public static function register_rest_routes() {
register_rest_route(
'wc/v3',
'/marketplace/create-order',
array(
'methods' => 'POST',
'callback' => array( __CLASS__, 'create_order' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
'args' => array(
'product_id' => array(
'required' => true,
'validate_callback' => function( $argument ) {
return is_int( $argument );
},
),
),
)
);
}
/**
* The Extensions page can only be accessed by users with the manage_woocommerce
* capability. So the API mimics that behavior.
*
* @return bool
*/
public static function get_permission() {
return WC_Helper_Subscriptions_API::get_permission();
}
/**
* Core function to create an order on WooCommerce.com. Pings the API and catches the exceptions if any.
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response
*/
public static function create_order( $request ) {
if ( ! current_user_can( 'install_plugins' ) ) {
return new \WP_REST_Response(
array(
'message' => __( 'You do not have permission to install plugins.', 'woocommerce' ),
),
403
);
}
try {
$response = WC_Helper_API::post(
'create-order',
array(
'authenticated' => true,
'body' => http_build_query(
array(
'product_id' => $request['product_id'],
),
),
)
);
return new \WP_REST_Response(
json_decode( wp_remote_retrieve_body( $response ), true ),
wp_remote_retrieve_response_code( $response )
);
} catch ( Exception $e ) {
return new \WP_REST_Response(
array(
'message' => __( 'Could not start the installation process. Reason: ', 'woocommerce' ) . $e->getMessage(),
'code' => 'could-not-install',
),
500
);
}
}
}
WC_Helper_Orders_API::load();

View File

@@ -0,0 +1,234 @@
<?php
/**
* WooCommerce Admin Sanitization Helper
*
* @package WooCommerce\Admin\Helper
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_Sanitization Class
*
* Provides sanitization functions for admin content.
*/
class WC_Helper_Sanitization {
/**
* Sanitize CSS markup from API responses for safe rendering in admin pages.
*
* @param string $css The raw CSS to sanitize.
*
* @return string Sanitized CSS safe for inclusion in style blocks.
*/
public static function sanitize_css( $css ) {
// Handle non-string inputs (return empty string).
if ( ! is_string( $css ) ) {
return '';
}
// Remove potentially harmful constructs.
$css = preg_replace( '/@import\s+[^;]+;?/', '', $css );
// Block all data URIs.
$css = preg_replace( '/url\s*\(\s*([\'"]?)data:/i', 'url($1invalid:', $css );
// Only allow URLs from specific trusted domains and their subdomains.
$css = preg_replace_callback(
'/url\s*\(\s*([\'"]?)(https?:\/\/[^)]+)\1\s*\)/i',
function ( $matches ) {
$url = $matches[2];
$quote = $matches[1];
// Check if URL belongs to allowed domains.
if ( preg_match(
'/^https?:\/\/(([\w-]+\.)*woocommerce\.com|' .
'([\w-]+\.)*woocommerce\.test|' .
'([\w-]+\.)*WordPress\.com|' .
'([\w-]+\.)*wp\.com)/ix',
$url
) ) {
// URL is from a trusted domain, keep it.
return "url({$quote}{$url}{$quote})";
} else {
// URL is not from a trusted domain, make it ineffective.
return "url({$quote}#blocked-url{$quote})";
}
},
$css
);
// Preserve all asterisks by temporarily replacing them.
$css = str_replace( '*', '__PRESERVED_ASTERISK__', $css );
// Remove HTML tags and PHP.
$css = wp_strip_all_tags( $css );
// Remove any JavaScript events.
$css = preg_replace( '/\s*expression\s*\(.*?\)/', '', $css );
$css = preg_replace( '/\s*javascript\s*:/', '', $css );
// Block other potentially dangerous protocols.
$css = preg_replace( '/(behavior|eval|calc|mocha)(\s*:|\s*\()/i', 'blocked', $css );
// Restore all asterisks.
$css = str_replace( '__PRESERVED_ASTERISK__', '*', $css );
// We assume relative and root-relative URLs are safe because they point to resources on the same domain.
// Limit size of CSS to prevent DoS.
$css = substr( $css, 0, 100000 );
return $css;
}
/**
* Sanitize HTML content allowing a subset of SVG elements.
*
* @param string $html The HTML to sanitize.
*
* @return string Sanitized HTML with SVG support.
*/
public static function sanitize_html( $html ) {
$allowed_html = wp_kses_allowed_html( 'post' );
// Selected SVG tags and attributes.
$svg_tags = self::wc_kses_safe_svg_tags();
$allowed_html = array_merge( $allowed_html, $svg_tags );
return wp_kses( self::wc_pre_sanitize_svg( $html ), $allowed_html );
}
/**
* Sanitize SVG content before processing with wp_kses.
*
* @param string $content The SVG content to sanitize.
* @return string Sanitized SVG content.
*/
public static function wc_pre_sanitize_svg( $content ) {
// Remove any xlink:href attributes containing javascript.
$content = preg_replace( '/xlink:href\s*=\s*(["\'])\s*javascript:.*?\1/i', '', $content );
// Remove foreignObject elements (can contain arbitrary HTML).
$content = preg_replace( '/<foreignObject\b[^>]*>.*?<\/foreignObject>/is', '', $content );
return $content;
}
/**
* Add limited SVG support to wp_kses_post with XSS protection.
*
* @return array Array of allowed SVG tags and their attributes.
*/
public static function wc_kses_safe_svg_tags() {
// SVG elements and attributes - security focused.
return array(
'svg' => array(
'class' => true,
'aria-hidden' => true,
'aria-labelledby' => true,
'role' => true,
'xmlns' => true,
'width' => true,
'height' => true,
'viewbox' => true,
'viewBox' => true,
'preserveAspectRatio' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
// Explicitly exclude dangerous attributes.
'onload' => false,
'onclick' => false,
),
'g' => array(
'fill' => true,
'transform' => true,
'stroke' => true,
),
'title' => array(
'title' => true,
),
'path' => array(
'd' => true,
'fill' => true,
'transform' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
),
'polyline' => array(
'points' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'polygon' => array(
'points' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'r' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'rect' => array(
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'rx' => true,
'ry' => true,
),
'line' => array(
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
'stroke' => true,
'stroke-width' => true,
),
'defs' => array(),
'linearGradient' => array(
'id' => true,
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
'gradientUnits' => true,
),
'radialGradient' => array(
'id' => true,
'cx' => true,
'cy' => true,
'r' => true,
'gradientUnits' => true,
),
'stop' => array(
'offset' => true,
'stop-color' => true,
'stop-opacity' => true,
// Remove style which can contain JavaScript.
'style' => false,
),
// Removed potentially risky elements.
// 'use' - can reference external content.
// 'mask' - not commonly needed and adds complexity.
);
}
}

View File

@@ -0,0 +1,402 @@
<?php
/**
* WooCommerce Admin Helper - React admin interface
*
* @package WooCommerce\Admin\Helper
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_Subscriptions_API
*
* The main entry-point for all things related to the Marketplace Subscriptions API.
* The Subscriptions API manages WooCommerce.com Subscriptions.
*/
class WC_Helper_Subscriptions_API {
/**
* Loads the class, runs on init
*
* @return void
*/
public static function load() {
add_filter( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
}
/**
* Registers the REST routes for the Marketplace Subscriptions API.
* These endpoints are used by the Marketplace Subscriptions React UI.
*/
public static function register_rest_routes() {
register_rest_route(
'wc/v3',
'/marketplace/refresh',
array(
'methods' => 'POST',
'callback' => array( __CLASS__, 'refresh' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
)
);
register_rest_route(
'wc/v3',
'/marketplace/subscriptions',
array(
'methods' => 'GET',
'callback' => array( __CLASS__, 'get_subscriptions' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
)
);
register_rest_route(
'wc/v3',
'/marketplace/subscriptions/connect',
array(
'methods' => 'POST',
'callback' => array( __CLASS__, 'connect' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
'args' => array(
'product_key' => array(
'required' => true,
'type' => 'string',
),
),
)
);
register_rest_route(
'wc/v3',
'/marketplace/subscriptions/activate-plugin',
array(
'methods' => 'POST',
'callback' => array( __CLASS__, 'activate_plugin' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
'args' => array(
'product_key' => array(
'required' => true,
'type' => 'string',
),
),
)
);
register_rest_route(
'wc/v3',
'/marketplace/subscriptions/disconnect',
array(
'methods' => 'POST',
'callback' => array( __CLASS__, 'disconnect' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
'args' => array(
'product_key' => array(
'required' => true,
'type' => 'string',
),
),
)
);
register_rest_route(
'wc/v3',
'/marketplace/subscriptions/activate',
array(
'methods' => 'POST',
'callback' => array( __CLASS__, 'activate' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
'args' => array(
'product_key' => array(
'required' => true,
'type' => 'string',
),
),
)
);
register_rest_route(
'wc/v3',
'/marketplace/subscriptions/install-url',
array(
'methods' => 'GET',
'callback' => array( __CLASS__, 'install_url' ),
'permission_callback' => array( __CLASS__, 'get_permission' ),
'args' => array(
'product_key' => array(
'required' => true,
'type' => 'string',
),
),
)
);
}
/**
* The Extensions page can only be accessed by users with the manage_woocommerce
* capability. So the API mimics that behavior.
*/
public static function get_permission() {
return current_user_can( 'manage_woocommerce' );
}
/**
* Fetch subscriptions from WooCommerce.com and serve them
* as JSON.
*/
public static function get_subscriptions() {
// If the site is connected, mark the time when the my subscriptions tab is first loaded.
if ( WC_Helper::is_site_connected() === true && empty( WC_Helper_Options::get( 'my_subscriptions_tab_loaded' ) ) ) {
WC_Helper_Options::update( 'my_subscriptions_tab_loaded', date( 'Y-m-d H:i:s' ) );
}
$subscriptions = WC_Helper::get_subscription_list_data();
wp_send_json(
array_values(
$subscriptions
)
);
}
/**
* Refresh account and subscriptions from WooCommerce.com and serve subscriptions
* as JSON.
*/
public static function refresh() {
try {
WC_Helper::refresh_helper_subscriptions();
WC_Helper::get_subscriptions();
WC_Helper::get_product_usage_notice_rules();
self::get_subscriptions();
} catch ( Exception $e ) {
wp_send_json_error(
array(
'message' => $e->getMessage(),
),
400
);
}
WC_Helper::fetch_helper_connection_info();
}
/**
* Connect a WooCommerce.com subscription.
*
* @param WP_REST_Request $request Request object.
*/
public static function connect( $request ) {
$product_key = $request->get_param( 'product_key' );
try {
$success = WC_Helper::activate_helper_subscription( $product_key );
} catch ( Exception $e ) {
$error_data = array(
'message' => $e->getMessage(),
);
if ( $e instanceof WC_Data_Exception ) {
$error_data['code'] = $e->getErrorCode();
// Include extra data from the exception so the client can render contextual UI (e.g. maxed out sites list).
$error_data['data'] = $e->getErrorData();
$status_code = (int) $e->getCode();
if ( 100 > $status_code || 599 < $status_code ) {
$status_code = 400;
}
} else {
$status_code = 400;
}
wp_send_json_error( $error_data, $status_code );
}
if ( $success ) {
wp_send_json_success(
array(
'message' => __( 'Your subscription has been connected.', 'woocommerce' ),
)
);
} else {
wp_send_json_error(
array(
'message' => __( 'There was an error connecting your subscription. Please try again.', 'woocommerce' ),
),
400
);
}
}
/**
* Activate a plugin for a WooCommerce.com subscription.
*
* @param WP_REST_Request $request Request object.
*/
public static function activate_plugin( $request ) {
$product_key = $request->get_param( 'product_key' );
try {
$success = WC_Helper::activate_plugin( $product_key );
} catch ( Exception $e ) {
wp_send_json_error(
array(
'message' => $e->getMessage(),
),
400
);
}
if ( $success ) {
wp_send_json_success(
array(
'message' => __( 'The plugin for your subscription has been activated.', 'woocommerce' ),
)
);
} else {
wp_send_json_error(
array(
'message' => __( 'The plugin for your subscription couldn\'t be activated.', 'woocommerce' ),
),
400
);
}
}
/**
* Disconnect a WooCommerce.com subscription.
*
* @param WP_REST_Request $request Request object.
*/
public static function disconnect( $request ) {
$product_key = $request->get_param( 'product_key' );
try {
$success = WC_Helper::deactivate_helper_subscription( $product_key );
} catch ( Exception $e ) {
wp_send_json_error(
array(
'message' => $e->getMessage(),
),
400
);
}
if ( $success ) {
wp_send_json_success(
array(
'message' => __( 'Your subscription has been disconnected.', 'woocommerce' ),
)
);
} else {
wp_send_json_error(
array(
'message' => __( 'There was an error disconnecting your subscription. Please try again.', 'woocommerce' ),
),
400
);
}
}
/**
* Activate a WooCommerce.com product.
* This activates the plugin/theme on the site.
*
* @param WP_REST_Request $request Request object.
*/
public static function activate( $request ) {
$product_key = $request->get_param( 'product_key' );
$subscription = WC_Helper::get_subscription( $product_key );
if ( ! $subscription ) {
wp_send_json_error(
array(
'message' => __( 'We couldn\'t find a subscription for this product.', 'woocommerce' ),
),
400
);
}
if ( true !== $subscription['local']['installed'] || ! isset( $subscription['local']['active'] ) ) {
wp_send_json_error(
array(
'message' => __( 'This product is not installed.', 'woocommerce' ),
),
400
);
}
if ( true === $subscription['local']['active'] ) {
wp_send_json_success(
array(
'message' => __( 'This product is already active.', 'woocommerce' ),
),
);
}
if ( 'plugin' === $subscription['product_type'] ) {
$success = activate_plugin( $subscription['local']['path'] );
if ( is_wp_error( $success ) ) {
wp_send_json_error(
array(
'message' => __( 'There was an error activating this plugin.', 'woocommerce' ),
),
400
);
}
} elseif ( 'theme' === $subscription['product_type'] ) {
switch_theme( $subscription['local']['slug'] );
$theme = wp_get_theme();
if ( $subscription['local']['slug'] !== $theme->get_stylesheet() ) {
wp_send_json_error(
array(
'message' => __( 'There was an error activating this theme.', 'woocommerce' ),
),
400
);
}
}
wp_send_json_success(
array(
'message' => __( 'This product has been activated.', 'woocommerce' ),
),
);
}
/**
* Get the install URL for a WooCommerce.com product.
*
* @param WP_REST_Request $request Request object.
*/
public static function install_url( $request ) {
$product_key = $request->get_param( 'product_key' );
$subscription = WC_Helper::get_subscription( $product_key );
if ( ! $subscription ) {
wp_send_json_error(
array(
'message' => __( 'We couldn\'t find a subscription for this product.', 'woocommerce' ),
),
400
);
}
if ( true === $subscription['local']['installed'] ) {
wp_send_json_success(
array(
'message' => __( 'This product is already installed.', 'woocommerce' ),
),
);
}
$install_url = WC_Helper::get_subscription_install_url(
$subscription['product_key'],
$subscription['product_slug']
);
if ( ! $install_url ) {
wp_send_json_error(
array(
'message' => __( 'There was an error getting the install URL for this product.', 'woocommerce' ),
),
400
);
}
wp_send_json_success(
array(
'url' => $install_url,
),
);
}
}
WC_Helper_Subscriptions_API::load();

View File

@@ -0,0 +1,883 @@
<?php
/**
* The update helper for WooCommerce.com plugins.
*
* @class WC_Helper_Updater
* @package WooCommerce\Admin\Helper
*/
use Automattic\WooCommerce\Admin\PluginsHelper;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_Updater Class
*
* Contains the logic to fetch available updates and hook into Core's update
* routines to serve WooCommerce.com-provided packages.
*/
class WC_Helper_Updater {
/**
* Loads the class, runs on init.
*/
public static function load() {
add_action( 'pre_set_site_transient_update_plugins', array( __CLASS__, 'transient_update_plugins' ), 21, 1 );
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( 'admin_init', 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() || ! WC_Helper::is_site_connected() ) {
add_action( 'load-plugins.php', array( __CLASS__, 'setup_update_plugins_messages' ), 11 );
}
if ( WC_Helper::is_site_connected() ) {
add_action( 'load-plugins.php', array( __CLASS__, 'setup_message_for_expired_and_expiring_subscriptions' ), 11 );
add_action( 'load-plugins.php', array( __CLASS__, 'setup_message_for_plugins_without_subscription' ), 11 );
}
}
/**
* Add the hook for modifying default WPCore update notices on the plugins management page.
* This is for plugins with expired or expiring subscriptions.
*/
public static function setup_message_for_expired_and_expiring_subscriptions() {
foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) {
add_action( 'in_plugin_update_message-' . $plugin['_filename'], array( __CLASS__, 'display_notice_for_expired_and_expiring_subscriptions' ), 10, 2 );
}
}
/**
* Add the hook for modifying default WPCore update notices on the plugins management page.
* This is for plugins without a subscription.
*/
public static function setup_message_for_plugins_without_subscription() {
foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) {
add_action( 'in_plugin_update_message-' . $plugin['_filename'], array( __CLASS__, 'display_notice_for_plugins_without_subscription' ), 10, 2 );
}
}
/**
* Runs in a cron thread, or in a visitor thread if triggered
* by _maybe_update_plugins(), or in an auto-update thread.
*
* @param object $transient The update_plugins transient object.
*
* @return object The same or a modified version of the transient.
*/
public static function transient_update_plugins( $transient ) {
$update_data = self::get_update_data();
foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) {
if ( empty( $update_data[ $plugin['_product_id'] ] ) ) {
continue;
}
$data = $update_data[ $plugin['_product_id'] ];
$filename = $plugin['_filename'];
$item = array(
'id' => 'woocommerce-com-' . $plugin['_product_id'],
'slug' => 'woocommerce-com-' . $data['slug'],
'plugin' => $filename,
'new_version' => $data['version'],
'url' => $data['url'],
'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'];
}
if ( $transient instanceof stdClass ) {
if ( version_compare( $plugin['Version'], $data['version'], '<' ) ) {
$transient->response[ $filename ] = (object) $item;
unset( $transient->no_update[ $filename ] );
} else {
$transient->no_update[ $filename ] = (object) $item;
unset( $transient->response[ $filename ] );
}
}
}
if ( $transient instanceof stdClass ) {
$translations = self::get_translations_update_data();
$transient->translations = array_merge( isset( $transient->translations ) ? $transient->translations : array(), $translations );
}
return $transient;
}
/**
* Runs on pre_set_site_transient_update_themes, provides custom
* packages for WooCommerce.com-hosted extensions.
*
* @param object $transient The update_themes transient object.
*
* @return object The same or a modified version of the transient.
*/
public static function transient_update_themes( $transient ) {
$update_data = self::get_update_data();
foreach ( WC_Helper::get_local_woo_themes() as $theme ) {
if ( empty( $update_data[ $theme['_product_id'] ] ) ) {
continue;
}
$data = $update_data[ $theme['_product_id'] ];
$slug = $theme['_stylesheet'];
$item = array(
'theme' => $slug,
'new_version' => $data['version'],
'url' => $data['url'],
'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;
} else {
unset( $transient->response[ $slug ] );
$transient->checked[ $slug ] = $data['version'];
}
}
return $transient;
}
/**
* Runs on load-plugins.php, adds a hook to show a custom plugin update message for WooCommerce.com hosted plugins.
*
* @return void.
*/
public static function setup_update_plugins_messages() {
$is_site_connected = WC_Helper::is_site_connected();
foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) {
$filename = $plugin['_filename'];
if ( $is_site_connected ) {
add_action( 'in_plugin_update_message-' . $filename, array( __CLASS__, 'add_install_marketplace_plugin_message' ), 10, 2 );
} else {
add_action( 'in_plugin_update_message-' . $filename, array( __CLASS__, 'add_connect_woocom_plugin_message' ) );
}
}
}
/**
* Runs on in_plugin_update_message-{file-name}, show a message to connect to woocommerce.com for unconnected stores
*
* @return void.
*/
public static function add_connect_woocom_plugin_message() {
$connect_page_url = add_query_arg(
array(
'page' => 'wc-admin',
'tab' => 'my-subscriptions',
'path' => rawurlencode( '/extensions' ),
'utm_source' => 'pu',
'utm_campaign' => 'pu_plugin_screen_connect',
),
admin_url( 'admin.php' )
);
printf(
wp_kses(
/* translators: 1: Woo Update Manager plugin install URL */
__( ' <a href="%1$s" class="woocommerce-connect-your-store">Connect your store</a> to woocommerce.com to update.', 'woocommerce' ),
array(
'a' => array(
'href' => array(),
'class' => array(),
),
)
),
esc_url( $connect_page_url ),
);
}
/**
* 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 */
__( ' <a href="%1$s">Install WooCommerce.com Update Manager</a> 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() ) {
esc_html_e( ' Activate WooCommerce.com Update Manager to update.', 'woocommerce' );
}
}
/**
* Runs on in_plugin_update_message-{file-name}, show a message if plugins subscription expired or expiring soon.
*
* @param object $plugin_data An array of plugin metadata.
* @param object $response An object of metadata about the available plugin update.
*
* @return void.
*/
public static function display_notice_for_expired_and_expiring_subscriptions( $plugin_data, $response ) {
// Extract product ID from the response.
$product_id = preg_replace( '/[^0-9]/', '', $response->id );
$installed_or_unconnected = array_merge(
WC_Helper::get_installed_subscriptions(),
WC_Helper::get_unconnected_subscriptions()
);
// Product subscriptions.
$subscriptions = wp_list_filter( $installed_or_unconnected, array( 'product_id' => $product_id ) );
if ( empty( $subscriptions ) ) {
return;
}
$expired_subscription = current(
array_filter(
$subscriptions,
function ( $subscription ) {
return ! empty( $subscription['expired'] ) && ! $subscription['lifetime'];
}
)
);
$expiring_subscription = current(
array_filter(
$subscriptions,
function ( $subscription ) {
return ! empty( $subscription['expiring'] ) && ! $subscription['autorenew'];
}
)
);
// Prepare the expiry notice based on subscription status.
$expiry_notice = '';
if ( ! empty( $expired_subscription ) ) {
$renew_link = add_query_arg(
array(
'add-to-cart' => $product_id,
'utm_source' => 'pu',
'utm_campaign' => 'pu_plugin_screen_renew',
),
PluginsHelper::WOO_CART_PAGE_URL
);
/* translators: 1: Product regular price */
$product_price = ! empty( $expired_subscription['product_regular_price'] ) ? sprintf( __( 'for %s ', 'woocommerce' ), esc_html( $expired_subscription['product_regular_price'] ) ) : '';
$expiry_notice = sprintf(
/* translators: 1: URL to My Subscriptions page 2: Product price */
__( ' Your subscription expired, <a href="%1$s" class="woocommerce-renew-subscription">renew %2$s</a>to update.', 'woocommerce' ),
esc_url( $renew_link ),
$product_price
);
} elseif ( ! empty( $expiring_subscription ) ) {
$renew_link = add_query_arg(
array(
'utm_source' => 'pu',
'utm_campaign' => 'pu_plugin_screen_enable_autorenew',
),
PluginsHelper::WOO_SUBSCRIPTION_PAGE_URL
);
$expiry_notice = sprintf(
/* translators: 1: Expiry date 1: URL to My Subscriptions page */
__( ' Your subscription expires on %1$s, <a href="%2$s" class="woocommerce-enable-autorenew">enable auto-renew</a> to continue receiving updates.', 'woocommerce' ),
date_i18n( 'F jS', $expiring_subscription['expires'] ),
esc_url( $renew_link )
);
}
// Display the expiry notice.
if ( ! empty( $expiry_notice ) ) {
echo wp_kses(
$expiry_notice,
array(
'a' => array(
'href' => array(),
'class' => array(),
),
)
);
}
}
/**
* Runs on in_plugin_update_message-{file-name}, show a message if plugin is without a subscription.
* Only Woo local plugins are passed to this function.
*
* @see setup_message_for_plugins_without_subscription
* @param object $plugin_data An array of plugin metadata.
* @param object $response An object of metadata about the available plugin update.
*
* @return void.
*/
public static function display_notice_for_plugins_without_subscription( $plugin_data, $response ) {
// Extract product ID from the response.
$product_id = preg_replace( '/[^0-9]/', '', $response->id );
if ( WC_Helper::has_product_subscription( $product_id ) ) {
return;
}
// Prepare the expiry notice based on subscription status.
$purchase_link = add_query_arg(
array(
'add-to-cart' => $product_id,
'utm_source' => 'pu',
'utm_campaign' => 'pu_plugin_screen_purchase',
),
PluginsHelper::WOO_CART_PAGE_URL,
);
$notice = sprintf(
/* translators: 1: URL to My Subscriptions page */
__( ' You don\'t have a subscription, <a href="%1$s" class="woocommerce-purchase-subscription">subscribe</a> to update.', 'woocommerce' ),
esc_url( $purchase_link ),
);
// Display the expiry notice.
echo wp_kses(
$notice,
array(
'a' => array(
'href' => array(),
'class' => array(),
),
)
);
}
/**
* Get update data for all plugins.
*
* @return array Update data {product_id => data}
* @see get_update_data
*/
public static function get_available_extensions_downloads_data() {
$payload = array();
// Scan subscriptions.
$subscriptions = WC_Helper::get_subscriptions();
foreach ( $subscriptions as $subscription ) {
$payload[ $subscription['product_id'] ] = array(
'product_id' => $subscription['product_id'],
'file_id' => '',
);
}
// Scan local plugins which may or may not have a subscription.
foreach ( WC_Helper::get_local_woo_plugins() as $data ) {
if ( ! isset( $payload[ $data['_product_id'] ] ) ) {
$payload[ $data['_product_id'] ] = array(
'product_id' => $data['_product_id'],
);
}
$payload[ $data['_product_id'] ]['file_id'] = $data['_file_id'];
}
return self::_update_check( $payload );
}
/**
* Get update data for all extensions.
*
* Scans through all subscriptions for the connected user, as well
* as all Woo extensions without a subscription, and obtains update
* data for each product.
*
* @return array Update data {product_id => data}
*/
public static function get_update_data() {
$payload = array();
// Scan subscriptions.
$subscriptions = WC_Helper::get_subscriptions();
foreach ( $subscriptions as $subscription ) {
$payload[ $subscription['product_id'] ] = array(
'product_id' => $subscription['product_id'],
'file_id' => '',
);
}
// Scan local plugins which may or may not have a subscription.
foreach ( WC_Helper::get_local_woo_plugins() as $data ) {
if ( ! isset( $payload[ $data['_product_id'] ] ) ) {
$payload[ $data['_product_id'] ] = array(
'product_id' => $data['_product_id'],
);
}
$payload[ $data['_product_id'] ]['file_id'] = $data['_file_id'];
}
// Scan local themes.
foreach ( WC_Helper::get_local_woo_themes() as $data ) {
if ( ! isset( $payload[ $data['_product_id'] ] ) ) {
$payload[ $data['_product_id'] ] = array(
'product_id' => $data['_product_id'],
);
}
$payload[ $data['_product_id'] ]['file_id'] = $data['_file_id'];
}
return self::_update_check( $payload );
}
/**
* Get translations updates information.
*
* Scans through all subscriptions for the connected user, as well
* as all Woo extensions without a subscription, and obtains update
* data for each product.
*
* @return array Update data {product_id => data}
*/
public static function get_translations_update_data() {
$payload = array();
$installed_translations = wp_get_installed_translations( 'plugins' );
$locales = array_values( get_available_languages() );
/**
* Filters the locales requested for plugin translations.
*
* @since 3.7.0
* @since 4.5.0 The default value of the `$locales` parameter changed to include all locales.
*
* @param array $locales Plugin locales. Default is all available locales of the site.
*/
$locales = apply_filters( 'plugins_update_check_locales', $locales );
$locales = array_unique( $locales );
// No locales, the response will be empty, we can return now.
if ( empty( $locales ) ) {
return array();
}
// 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() ) );
/*
* Use only plugins that are subscribed to the automatic translations updates.
*/
$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 );
}
);
// Nothing to check for, exit.
if ( empty( $active_for_translations ) ) {
return array();
}
if ( wp_doing_cron() ) {
$timeout = 30;
} else {
// Three seconds, plus one extra second for every 10 plugins.
$timeout = 3 + (int) ( count( $active_for_translations ) / 10 );
}
$request_body = array(
'locales' => $locales,
'plugins' => array(),
);
foreach ( $active_for_translations as $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' => wp_json_encode( $request_body ),
'headers' => array( 'Content-Type: application/json' ),
'timeout' => $timeout,
)
);
// Something wrong happened on the translate server side.
$response_code = wp_remote_retrieve_response_code( $raw_response );
if ( 200 !== $response_code ) {
return array();
}
$response = json_decode( wp_remote_retrieve_body( $raw_response ), true );
// API error, api returned but something was wrong.
if ( array_key_exists( 'success', $response ) && false === $response['success'] ) {
return array();
}
$translations = array();
foreach ( $response['data'] as $plugin_name => $language_packs ) {
foreach ( $language_packs as $language_pack ) {
// Maybe we have this language pack already installed so lets check revision date.
if ( array_key_exists( $plugin_name, $installed_translations ) && array_key_exists( $language_pack['wp_locale'], $installed_translations[ $plugin_name ] ) ) {
$installed_translation_revision_time = new DateTime( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] );
$new_translation_revision_time = new DateTime( $language_pack['last_modified'] );
// Skip if translation language pack is not newer than what is installed already.
if ( $new_translation_revision_time <= $installed_translation_revision_time ) {
continue;
}
}
$translations[] = array(
'type' => 'plugin',
'slug' => $plugin_name,
'language' => $language_pack['wp_locale'],
'version' => $language_pack['version'],
'updated' => $language_pack['last_modified'],
'package' => $language_pack['package'],
'autoupdate' => true,
);
}
}
return $translations;
}
/**
* Validates cached update data and checks if it matches the expected hash.
*
* Ensures the cached data is properly structured and corresponds to the current
* payload to prevent fatal errors and avoid stale cache returns.
*
* @since 10.3.6
*
* @param mixed $data The data retrieved from the transient.
* @param string $hash The expected hash to compare against.
* @return bool True if the data is valid and hash matches, false otherwise.
*/
private static function should_use_cached_update_data( $data, $hash ) {
if ( ! is_array( $data ) ) {
return false;
}
if ( ! isset( $data['hash'], $data['products'] ) ) {
return false;
}
if ( ! is_string( $data['hash'] ) || ! is_array( $data['products'] ) ) {
return false;
}
return hash_equals( $hash, $data['hash'] );
}
/**
* Run an update check API call.
*
* The call is cached based on the payload (product ids, file ids). If
* the payload changes, the cache is going to miss.
*
* @param array $payload Information about the plugin to update.
* @return array Update data for each requested product.
*/
private static function _update_check( $payload ) {
if ( empty( $payload ) ) {
return array();
}
ksort( $payload );
$hash = md5( wp_json_encode( $payload ) );
$cache_key = '_woocommerce_helper_updates';
$data = get_transient( $cache_key );
if ( self::should_use_cached_update_data( $data, $hash ) ) {
return $data['products'];
}
$data = array(
'hash' => $hash,
'updated' => time(),
'products' => array(),
'errors' => array(),
);
// Detect if this is a manual refresh button click.
$request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$source = '';
if ( stripos( $request_uri, 'wc/v3/marketplace/refresh' ) !== false ) {
$source = 'refresh-button';
}
$request_body = array( 'products' => $payload );
if ( ! empty( $source ) ) {
$request_body['source'] = $source;
}
if ( WC_Helper::is_site_connected() ) {
$request = WC_Helper_API::post(
'update-check',
array(
'body' => wp_json_encode( $request_body ),
'authenticated' => true,
)
);
} else {
$request = WC_Helper_API::post(
'update-check-public',
array(
'body' => wp_json_encode( $request_body ),
)
);
}
if ( wp_remote_retrieve_response_code( $request ) !== 200 ) {
$data['errors'][] = 'http-error';
} else {
$data['products'] = json_decode( wp_remote_retrieve_body( $request ), true );
}
set_transient( $cache_key, $data, 12 * HOUR_IN_SECONDS );
return $data['products'];
}
/**
* Get the number of products that have updates.
*
* @return int The number of products with updates.
*/
public static function get_updates_count() {
$cache_key = '_woocommerce_helper_updates_count';
$count = get_transient( $cache_key );
if ( false !== $count ) {
return $count;
}
// Don't fetch any new data since this function in high-frequency.
if ( ! get_transient( '_woocommerce_helper_subscriptions' ) ) {
return 0;
}
if ( ! get_transient( '_woocommerce_helper_updates' ) ) {
return 0;
}
$count = 0;
$update_data = self::get_update_data();
if ( empty( $update_data ) ) {
set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS );
return $count;
}
// Scan local plugins.
foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) {
if ( empty( $update_data[ $plugin['_product_id'] ] ) ) {
continue;
}
if ( ! is_plugin_active( $plugin['_filename'] ) ) {
continue;
}
if ( version_compare( $plugin['Version'], $update_data[ $plugin['_product_id'] ]['version'], '<' ) ) {
++$count;
}
}
// Scan local themes.
foreach ( WC_Helper::get_local_woo_themes() as $theme ) {
if ( empty( $update_data[ $theme['_product_id'] ] ) ) {
continue;
}
if ( get_stylesheet() !== $theme['_stylesheet'] ) {
continue;
}
if ( version_compare( $theme['Version'], $update_data[ $theme['_product_id'] ]['version'], '<' ) ) {
++$count;
}
}
set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS );
return $count;
}
/**
* Get the update count to based on the status of the site.
*
* @return int
*/
public static function get_updates_count_based_on_site_status() {
if ( ! WC_Helper::is_site_connected() ) {
return 0;
}
$count = self::get_updates_count() ?? 0;
if ( ! WC_Woo_Update_Manager_Plugin::is_plugin_installed() || ! WC_Woo_Update_Manager_Plugin::is_plugin_active() ) {
++$count;
}
return $count;
}
/**
* Get the type of woo connect notice to be shown in the WC Settings and Marketplace pages.
* - If a store is connected to woocommerce.com or has no installed woo plugins, return 'none'.
* - If a store has installed woo plugins but no updates, return 'short'.
* - If a store has an installed woo plugin with update, return 'long'.
*
* @return string The notice type, 'none', 'short', or 'long'.
*/
public static function get_woo_connect_notice_type() {
if ( WC_Helper::is_site_connected() ) {
return 'none';
}
$woo_plugins = WC_Helper::get_local_woo_plugins();
if ( empty( $woo_plugins ) ) {
return 'none';
}
$update_data = self::get_update_data();
if ( empty( $update_data ) ) {
return 'short';
}
// Scan local plugins.
foreach ( $woo_plugins as $plugin ) {
if ( empty( $update_data[ $plugin['_product_id'] ] ) ) {
continue;
}
if ( version_compare( $plugin['Version'], $update_data[ $plugin['_product_id'] ]['version'], '<' ) ) {
return 'long';
}
}
return 'short';
}
/**
* Return the updates count markup.
*
* @return string Updates count markup, empty string if no updates avairable.
*/
public static function get_updates_count_html() {
$count = self::get_updates_count_based_on_site_status();
$count_html = sprintf( ' <span class="update-plugins count-%d"><span class="update-count">%d</span></span>', $count, number_format_i18n( $count ) );
return $count_html;
}
/**
* Flushes cached update data.
*/
public static function flush_updates_cache() {
delete_transient( '_woocommerce_helper_updates' );
delete_transient( '_woocommerce_helper_updates_count' );
delete_site_transient( 'update_plugins' );
delete_site_transient( 'update_themes' );
}
/**
* Fires when a user successfully updated a theme or a plugin.
*/
public static function upgrader_process_complete() {
delete_transient( '_woocommerce_helper_updates_count' );
}
/**
* Hooked into the upgrader_pre_download filter in order to better handle error messaging around expired
* plugin updates. Initially we were using an empty string, but the error message that no_package
* results in does not fit the cause.
*
* @since 4.1.0
* @param bool $reply Holds the current filtered response.
* @param string $package The path to the package file for the update.
* @return false|WP_Error False to proceed with the update as normal, anything else to be returned instead of updating.
*/
public static function block_expired_updates( $reply, $package ) {
// Don't override a reply that was set already.
if ( false !== $reply ) {
return $reply;
}
// Only for packages with expired subscriptions.
if ( 0 !== strpos( $package, 'woocommerce-com-expired-' ) ) {
return false;
}
return new WP_Error(
'woocommerce_subscription_expired',
sprintf(
// translators: %s: URL of WooCommerce.com subscriptions tab.
__( 'Please visit the <a href="%s" target="_blank">subscriptions page</a> and renew to continue receiving updates.', 'woocommerce' ),
esc_url( admin_url( 'admin.php?page=wc-admin&tab=my-subscriptions&path=%2Fextensions' ) )
)
);
}
}
WC_Helper_Updater::load();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
<?php
/**
* Updates the Product API response from WP.org.
*
* @class WC_Plugin_Api_Updater
* @package WooCommerce\Admin\Helper
*/
defined( 'ABSPATH' ) || exit;
/**
* Class WC_Plugin_Api_Updater
*/
class WC_Plugin_Api_Updater {
/**
* Loads the class, runs on init.
*/
public static function load() {
add_filter( 'plugins_api', array( __CLASS__, 'plugins_api' ), 20, 3 );
add_filter( 'themes_api', array( __CLASS__, 'themes_api' ), 20, 3 );
}
/**
* Plugin information callback for Woo extensions.
*
* @param object $response The response core needs to display the modal.
* @param string $action The requested plugins_api() action.
* @param object $args Arguments passed to plugins_api().
*
* @return object An updated $response.
*/
public static function plugins_api( $response, $action, $args ) {
if ( 'plugin_information' !== $action ) {
return $response;
}
return self::override_products_api_response( $response, $action, $args );
}
/**
* Theme information callback for Woo themes.
*
* @param object $response The response core needs to display the modal.
* @param string $action The requested themes_api() action.
* @param object $args Arguments passed to themes_api().
*/
public static function themes_api( $response, $action, $args ) {
if ( 'theme_information' !== $action ) {
return $response;
}
return self::override_products_api_response( $response, $action, $args );
}
/**
* Override the products API to fetch data from the Helper API if it's a Woo product.
*
* @param object $response The response core needs to display the modal.
* @param string $action The requested action.
* @param object $args Arguments passed to the API.
*/
public static function override_products_api_response( $response, $action, $args ) {
if ( empty( $args->slug ) ) {
return $response;
}
// Only for slugs that start with woocommerce-com-.
if ( 0 !== strpos( $args->slug, 'woocommerce-com-' ) ) {
return $response;
}
$clean_slug = str_replace( 'woocommerce-com-', '', $args->slug );
// Look through update data by slug.
$update_data = WC_Helper_Updater::get_update_data();
$products = wp_list_filter( $update_data, array( 'slug' => $clean_slug ) );
if ( empty( $products ) ) {
return $response;
}
$product_id = array_keys( $products );
$product_id = array_shift( $product_id );
$is_site_connected = WC_Helper::is_site_connected();
$endpoint = add_query_arg(
array( 'product_id' => absint( $product_id ) ),
'info'
);
// Fetch the product information from the Helper API.
$request = WC_Helper_API::get(
$endpoint,
array( 'authenticated' => $is_site_connected )
);
// If we tried to authenticate and failed, try again without authentication.
if ( is_wp_error( $request ) && $is_site_connected ) {
$request = WC_Helper_API::get( $endpoint );
}
$results = json_decode( wp_remote_retrieve_body( $request ), true );
if ( ! empty( $results ) ) {
$response = (object) $results;
}
return $response;
}
}
WC_Plugin_Api_Updater::load();

View File

@@ -0,0 +1,389 @@
<?php
/**
* WooCommerce Product Usage Notice.
*
* @package WooCommerce\Admin\Helper
*/
declare( strict_types = 1 );
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Product usage notice class.
*/
class WC_Product_Usage_Notice {
/**
* User meta key prefix to store dismiss counts per product. Product ID is
* the suffix part.
*
* @var string
*/
const DISMISSED_COUNT_META_PREFIX = '_woocommerce_product_usage_notice_dismissed_count_';
/**
* User meta key prefix to store timestamp of last dismissed product usage notice.
* Product ID is the suffix part.
*
* @var string
*/
const DISMISSED_TIMESTAMP_META_PREFIX = '_woocommerce_product_usage_notice_dismissed_timestamp_';
/**
* User meta key prefix to store timestamp of last clicked remind later from
* product usage notice. Product ID is the suffix part.
*
* @var string
*/
const REMIND_LATER_TIMESTAMP_META_PREFIX = '_woocommerce_product_usage_notice_remind_later_timestamp_';
/**
* User meta key to store timestamp of last dismissed of any product usage
* notices. There's no product ID in the meta key.
*
* @var string
*/
const LAST_DISMISSED_TIMESTAMP_META = '_woocommerce_product_usage_notice_last_dismissed_timestamp';
/**
* Array of product usage notice rules from helper API.
*
* @var array
*/
private static $product_usage_notice_rules = array();
/**
* Current product usage notice rule applied to the current admin screen.
*
* @var array
*/
private static $current_notice_rule = array();
/**
* Loads the class, runs on init.
*
* @return void
*/
public static function load() {
add_action( 'current_screen', array( __CLASS__, 'maybe_show_product_usage_notice' ) );
add_action( 'wp_ajax_woocommerce_dismiss_product_usage_notice', array( __CLASS__, 'ajax_dismiss' ) );
add_action( 'wp_ajax_woocommerce_remind_later_product_usage_notice', array( __CLASS__, 'ajax_remind_later' ) );
}
/**
* Maybe show product usage notice in a given screen object.
*
* @param \WP_Screen $screen Current \WP_Screen object.
*/
public static function maybe_show_product_usage_notice( $screen ) {
$user_id = get_current_user_id();
if ( ! $user_id ) {
return;
}
if ( ! WC_Helper::is_site_connected() ) {
return;
}
try {
self::$product_usage_notice_rules = WC_Helper::get_product_usage_notice_rules();
} catch ( Exception $e ) {
return;
}
if ( empty( self::$product_usage_notice_rules ) ) {
return;
}
self::$current_notice_rule = self::get_current_notice_rule( $screen );
if ( empty( self::$current_notice_rule ) ) {
return;
}
$product_id = self::$current_notice_rule['id'];
if ( self::is_notice_throttled( $user_id, $product_id ) ) {
return;
}
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_product_usage_notice_scripts' ) );
}
/**
* Check whether the user clicked "remind later" recently.
*
* @param int $user_id User ID.
* @param int $product_id Product ID.
*
* @return bool
*/
private static function is_remind_later_clicked_recently( int $user_id, int $product_id ): bool {
$last_remind_later_ts = absint(
get_user_meta(
$user_id,
self::REMIND_LATER_TIMESTAMP_META_PREFIX . $product_id,
true
)
);
if ( 0 === $last_remind_later_ts ) {
return false;
}
$seconds_since_clicked_remind_later = time() - $last_remind_later_ts;
$wait_after_remind_later = self::$current_notice_rule['wait_in_seconds_after_remind_later'];
return $seconds_since_clicked_remind_later < $wait_after_remind_later;
}
/**
* Check whether the user has reached max dismissals of product usage notice.
*
* @param int $user_id User ID.
* @param int $product_id Product ID.
*
* @return bool
*/
private static function has_reached_max_dismissals( int $user_id, int $product_id ): bool {
$dismiss_count = absint(
get_user_meta(
$user_id,
self::DISMISSED_COUNT_META_PREFIX . $product_id,
true
)
);
$max_dismissals = self::$current_notice_rule['max_dismissals'];
return $dismiss_count >= $max_dismissals;
}
/**
* Check whether the user dismissed any product usage notices recently.
*
* @param int $user_id User ID.
*
* @return bool
*/
private static function is_any_notices_dismissed_recently( int $user_id ): bool {
$global_last_dismissed_ts = absint(
get_user_meta(
$user_id,
self::LAST_DISMISSED_TIMESTAMP_META,
true
)
);
if ( 0 === $global_last_dismissed_ts ) {
return false;
}
$seconds_since_dismissed = time() - $global_last_dismissed_ts;
$wait_after_any_dismisses = self::$product_usage_notice_rules['wait_in_seconds_after_any_dismisses'];
return $seconds_since_dismissed < $wait_after_any_dismisses;
}
/**
* Check whether the user dismissed given product usage notice recently.
*
* @param int $user_id User ID.
* @param int $product_id Product ID.
*
* @return bool
*/
private static function is_product_notice_dismissed_recently( int $user_id, int $product_id ): bool {
$last_dismissed_ts = absint(
get_user_meta(
$user_id,
self::DISMISSED_TIMESTAMP_META_PREFIX . $product_id,
true
)
);
if ( 0 === $last_dismissed_ts ) {
return false;
}
$seconds_since_dismissed = time() - $last_dismissed_ts;
$wait_after_dismiss = self::$current_notice_rule['wait_in_seconds_after_dismiss'];
return $seconds_since_dismissed < $wait_after_dismiss;
}
/**
* Check whether current notice is throttled for the user and product.
*
* @param int $user_id User ID.
* @param int $product_id Product ID.
*
* @return bool
*/
private static function is_notice_throttled( int $user_id, int $product_id ): bool {
return self::is_remind_later_clicked_recently( $user_id, $product_id ) ||
self::has_reached_max_dismissals( $user_id, $product_id ) ||
self::is_any_notices_dismissed_recently( $user_id ) ||
self::is_product_notice_dismissed_recently( $user_id, $product_id );
}
/**
* Enqueue scripts needed to display product usage notice (or modal).
*/
public static function enqueue_product_usage_notice_scripts() {
WCAdminAssets::register_style( 'woo-product-usage-notice', 'style', array( 'wp-components' ) );
WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-product-usage-notice', true );
$subscribe_url = add_query_arg(
array(
'add-to-cart' => self::$current_notice_rule['id'],
'utm_source' => 'pu',
'utm_medium' => 'product',
'utm_campaign' => 'pu_modal_subscribe',
),
'https://woocommerce.com/cart/'
);
$renew_url = add_query_arg(
array(
'renew_product' => self::$current_notice_rule['id'],
'product_key' => self::$current_notice_rule['state']['key'],
'order_id' => self::$current_notice_rule['state']['order_id'],
'utm_source' => 'pu',
'utm_medium' => 'product',
'utm_campaign' => 'pu_modal_renew',
),
'https://woocommerce.com/cart/'
);
wp_localize_script(
'wc-admin-woo-product-usage-notice',
'wooProductUsageNotice',
array(
'subscribeUrl' => $subscribe_url,
'renewUrl' => $renew_url,
'dismissAction' => 'woocommerce_dismiss_product_usage_notice',
'remindLaterAction' => 'woocommerce_remind_later_product_usage_notice',
'productId' => self::$current_notice_rule['id'],
'productName' => self::$current_notice_rule['name'],
'productRegularPrice' => self::$current_notice_rule['regular_price'],
'dismissNonce' => wp_create_nonce( 'dismiss_product_usage_notice' ),
'remindLaterNonce' => wp_create_nonce( 'remind_later_product_usage_notice' ),
'showAs' => self::$current_notice_rule['show_as'],
'colorScheme' => self::$current_notice_rule['color_scheme'],
'subscriptionState' => self::$current_notice_rule['state'],
'screenId' => get_current_screen()->id,
)
);
}
/**
* Get product usage notice rule from a given WP_Screen object.
*
* @param \WP_Screen $screen Current \WP_Screen object.
*
* @return array
*/
private static function get_current_notice_rule( $screen ) {
foreach ( self::$product_usage_notice_rules['products'] as $product_id => $rule ) {
if ( ! isset( $rule['screens'][ $screen->id ] ) ) {
continue;
}
// Check query strings.
if ( ! self::query_string_matches( $screen, $rule ) ) {
continue;
}
$product_id = absint( $product_id );
$state = WC_Helper::get_product_subscription_state( $product_id );
if ( $state['expired'] || $state['unregistered'] ) {
$rule['id'] = $product_id;
$rule['state'] = $state;
return $rule;
}
}
return array();
}
/**
* Check whether the screen and GET parameter matches a given rule.
*
* @param \WP_Screen $screen Current \WP_Screen object.
* @param array $rule Product usage notice rule.
*
* @return bool
*/
private static function query_string_matches( $screen, $rule ) {
if ( empty( $rule['screens'][ $screen->id ]['qs'] ) ) {
return true;
}
$qs = $rule['screens'][ $screen->id ]['qs'];
foreach ( $qs as $key => $val ) {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( empty( $_GET[ $key ] ) || $_GET[ $key ] !== $val ) {
return false;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
return true;
}
/**
* AJAX handler for dismiss action of product usage notice.
*/
public static function ajax_dismiss() {
if ( ! check_ajax_referer( 'dismiss_product_usage_notice' ) ) {
wp_die( -1 );
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_die( -1 );
}
$product_id = absint( $_GET['product_id'] ?? 0 );
if ( ! $product_id ) {
wp_die( -1 );
}
$dismiss_count = absint( get_user_meta( $user_id, self::DISMISSED_COUNT_META_PREFIX . $product_id, true ) );
update_user_meta( $user_id, self::DISMISSED_COUNT_META_PREFIX . $product_id, $dismiss_count + 1 );
update_user_meta( $user_id, self::DISMISSED_TIMESTAMP_META_PREFIX . $product_id, time() );
update_user_meta( $user_id, self::LAST_DISMISSED_TIMESTAMP_META, time() );
wp_die( 1 );
}
/**
* AJAX handler for "remind later" action of product usage notice.
*/
public static function ajax_remind_later() {
if ( ! check_ajax_referer( 'remind_later_product_usage_notice' ) ) {
wp_die( -1 );
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_die( -1 );
}
$product_id = absint( $_GET['product_id'] ?? 0 );
if ( ! $product_id ) {
wp_die( -1 );
}
update_user_meta( $user_id, self::REMIND_LATER_TIMESTAMP_META_PREFIX . $product_id, time() );
wp_die( 1 );
}
}
WC_Product_Usage_Notice::load();

View File

@@ -0,0 +1,62 @@
<?php
/**
* A utility class to handle WooCommerce.com connection.
*
* @class WC_Woo_Update_Manager_Plugin
* @package WooCommerce\Admin\Helper
*/
declare( strict_types = 1 );
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_Plugin Class
*
* Contains the logic to manage WooCommerce.com Helper Connection.
*/
class WC_Woo_Helper_Connection {
/**
* Check if the Woo Update Manager plugin is active.
*
* @return bool
*/
public static function get_connection_url_notice(): string {
$connection_data = WC_Helper::get_cached_connection_data();
if ( false === $connection_data || false === $connection_data['alert_url_mismatch'] ) {
return '';
}
$auth = WC_Helper_Options::get( 'auth' );
$url = rtrim( $auth['url'], '/' );
$home_url = rtrim( home_url(), '/' );
if ( empty( $url ) || $home_url === $url ) {
return '';
}
return sprintf(
/* translators: 1: WooCommerce.com connection URL, 2: home URL */
__( 'Your site is currently connected to WooCommerce.com using <b>%1$s</b>, but your actual site URL is <b>%2$s</b>. To fix this, please reconnect your site to <b>WooCommerce.com</b> to ensure everything works correctly.', 'woocommerce' ),
$url,
$home_url
);
}
/**
* Check if the site has and linked host-plan orders.
*
* @return bool
*/
public static function has_host_plan_orders(): bool {
$subscriptions = WC_Helper::get_subscriptions();
foreach ( $subscriptions as $subscription ) {
if ( isset( $subscription['included_in_host_plan'] ) && true === (bool) $subscription['included_in_host_plan'] ) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* A utility class for Woo Update Manager plugin.
*
* @class WC_Woo_Update_Manager_Plugin
* @package WooCommerce\Admin\Helper
*/
use Automattic\WooCommerce\Admin\PageController;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_Plugin Class
*
* Contains the logic to manage the Woo Update Manager plugin.
*/
class WC_Woo_Update_Manager_Plugin {
const WOO_UPDATE_MANAGER_PLUGIN_MAIN_FILE = 'woo-update-manager/woo-update-manager.php';
const WOO_UPDATE_MANAGER_DOWNLOAD_URL = 'https://woocommerce.com/product-download/woo-update-manager';
const WOO_UPDATE_MANAGER_SLUG = 'woo-update-manager';
/**
* Loads the class, runs on init.
*
* @return void
*/
public static function load(): void {
add_action( 'admin_notices', array( __CLASS__, 'show_woo_update_manager_install_notice' ) );
}
/**
* Check if the Woo Update Manager plugin is active.
*
* @return bool
*/
public static function is_plugin_active(): bool {
return is_plugin_active_for_network( self::WOO_UPDATE_MANAGER_PLUGIN_MAIN_FILE ) || is_plugin_active( self::WOO_UPDATE_MANAGER_PLUGIN_MAIN_FILE );
}
/**
* Check if the Woo Update Manager plugin is installed.
*
* @return bool
*/
public static function is_plugin_installed(): bool {
return file_exists( WP_PLUGIN_DIR . '/' . self::WOO_UPDATE_MANAGER_PLUGIN_MAIN_FILE );
}
/**
* Generate the URL to install the Woo Update Manager plugin.
*
* @return string
*/
public static function generate_install_url(): string {
$install_url = WC_Helper::get_install_base_url() . self::WOO_UPDATE_MANAGER_SLUG . '/';
return WC_Helper_API::add_auth_parameters( $install_url );
}
/**
* Get the id of the Woo Update Manager plugin.
*
* @return int
*/
public static function get_plugin_slug(): string {
return self::WOO_UPDATE_MANAGER_SLUG;
}
/**
* Show a notice on the WC admin pages to install or activate the Woo Update Manager plugin.
*
* @return void
*/
public static function show_woo_update_manager_install_notice(): void {
if ( ! current_user_can( 'install_plugins' ) ) {
return;
}
if ( ! WC_Helper::is_site_connected() ) {
return;
}
if ( ! PageController::is_admin_or_embed_page() ) {
return;
}
if ( self::is_plugin_installed() && self::is_plugin_active() ) {
return;
}
if ( ! self::is_plugin_installed() ) {
if ( self::install_admin_notice_dismissed() ) {
return;
}
include __DIR__ . '/views/html-notice-woo-updater-not-installed.php';
return;
}
if ( self::activate_admin_notice_dismissed() ) {
return;
}
include __DIR__ . '/views/html-notice-woo-updater-not-activated.php';
}
/**
* Check if the installation notice has been dismissed.
*
* @return bool
*/
protected static function install_admin_notice_dismissed(): bool {
return get_user_meta( get_current_user_id(), 'dismissed_woo_updater_not_installed_notice', true );
}
/**
* Check if the activation notice has been dismissed.
*
* @return bool
*/
protected static function activate_admin_notice_dismissed(): bool {
return get_user_meta( get_current_user_id(), 'dismissed_woo_updater_not_activated_notice', true );
}
}
WC_Woo_Update_Manager_Plugin::load();

View File

@@ -0,0 +1,6 @@
<?php defined( 'ABSPATH' ) or exit(); ?>
<div class="wrap">
<h1><?php _e( 'Looking for the WooCommerce Helper?', 'woocommerce' ); ?></h1>
<p><?php printf( __( 'We\'ve made things simpler and easier to manage moving forward. From now on you can manage all your WooCommerce purchases directly from the Extensions menu within the WooCommerce plugin itself. <a href="%s">View and manage</a> your extensions now.', 'woocommerce' ), esc_url( $helper_url ) ); ?></p>
</div>

View File

@@ -0,0 +1,260 @@
<?php
/**
* Helper main view
*
* @package WooCommerce\Helper
*/
?>
<?php defined( 'ABSPATH' ) || exit(); ?>
<div class="wrap woocommerce wc-subscriptions-wrap wc-helper">
<?php require WC_Helper::get_view_filename( 'html-section-nav.php' ); ?>
<h1 class="screen-reader-text"><?php esc_html_e( 'WooCommerce Extensions', 'woocommerce' ); ?></h1>
<?php require WC_Helper::get_view_filename( 'html-section-notices.php' ); ?>
<div class="subscriptions-header">
<h2><?php esc_html_e( 'Subscriptions', 'woocommerce' ); ?></h2>
<?php require WC_Helper::get_view_filename( 'html-section-account.php' ); ?>
<p>
<?php
printf(
wp_kses(
/* translators: Introduction to list of WooCommerce.com extensions the merchant has subscriptions for. */
__(
'Below is a list of extensions available on your WooCommerce.com account. To receive extension updates please make sure the extension is installed, and its subscription activated and connected to your WooCommerce.com account. Extensions can be activated from the <a href="%s">Plugins</a> screen.',
'woocommerce'
),
array(
'a' => array(
'href' => array(),
),
)
),
esc_url(
admin_url( 'plugins.php' )
)
);
?>
</p>
</div>
<ul class="subscription-filter">
<label><?php esc_html_e( 'Sort by:', 'woocommerce' ); ?> <span class="chevron dashicons dashicons-arrow-up-alt2"></span></label>
<?php
$filters = array_keys( WC_Helper::get_filters() );
$last_filter = array_pop( $filters );
$current_filter = WC_Helper::get_current_filter();
$counts = WC_Helper::get_filters_counts();
?>
<?php
foreach ( WC_Helper::get_filters() as $key => $label ) :
// Don't show empty filters.
if ( empty( $counts[ $key ] ) ) {
continue;
}
$url = admin_url( 'admin.php?page=wc-addons&section=helper&filter=' . $key );
$class_html = $current_filter === $key ? 'class="current"' : '';
?>
<li>
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<a <?php echo $class_html; ?> href="<?php echo esc_url( $url ); ?>">
<?php echo esc_html( $label ); ?>
<span class="count">(<?php echo absint( $counts[ $key ] ); ?>)</span>
</a>
</li>
<?php endforeach; ?>
</ul>
<table class="wp-list-table widefat fixed striped">
<?php if ( ! empty( $subscriptions ) ) : ?>
<?php foreach ( $subscriptions as $subscription ) : ?>
<tbody>
<tr class="wp-list-table__row is-ext-header">
<td class="wp-list-table__ext-details">
<div class="wp-list-table__ext-title">
<a href="<?php echo esc_url( WC_Helper::add_utm_params_to_url_for_subscription_link( $subscription['product_url'], 'product-name' ) ); ?>" target="_blank">
<?php echo esc_html( $subscription['product_name'] ); ?>
</a>
</div>
<div class="wp-list-table__ext-description">
<?php if ( $subscription['lifetime'] ) : ?>
<span class="renews">
<?php esc_html_e( 'Lifetime Subscription', 'woocommerce' ); ?>
</span>
<?php elseif ( $subscription['expired'] ) : ?>
<span class="renews">
<strong><?php esc_html_e( 'Expired :(', 'woocommerce' ); ?></strong>
<?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?>
</span>
<?php elseif ( $subscription['autorenew'] ) : ?>
<span class="renews">
<?php esc_html_e( 'Auto renews on:', 'woocommerce' ); ?>
<?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?>
</span>
<?php elseif ( $subscription['expiring'] ) : ?>
<span class="renews">
<strong><?php esc_html_e( 'Expiring soon!', 'woocommerce' ); ?></strong>
<?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?>
</span>
<?php else : ?>
<span class="renews">
<?php esc_html_e( 'Expires on:', 'woocommerce' ); ?>
<?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?>
</span>
<?php endif; ?>
<br/>
<span class="subscription">
<?php
if ( ! $subscription['active'] && $subscription['maxed'] ) {
/* translators: %1$d: sites active, %2$d max sites active */
printf( esc_html__( 'Subscription: Not available - %1$d of %2$d already in use', 'woocommerce' ), absint( $subscription['sites_active'] ), absint( $subscription['sites_max'] ) );
} elseif ( $subscription['sites_max'] > 0 ) {
/* translators: %1$d: sites active, %2$d max sites active */
printf( esc_html__( 'Subscription: Using %1$d of %2$d sites available', 'woocommerce' ), absint( $subscription['sites_active'] ), absint( $subscription['sites_max'] ) );
} else {
esc_html_e( 'Subscription: Unlimited', 'woocommerce' );
}
// Check shared.
if ( ! empty( $subscription['is_shared'] ) && ! empty( $subscription['owner_email'] ) ) {
/* translators: Email address of person who shared the subscription. */
printf( '</br>' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['owner_email'] ) );
} elseif ( isset( $subscription['master_user_email'] ) ) {
/* translators: Email address of person who shared the subscription. */
printf( '</br>' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['master_user_email'] ) );
}
?>
</span>
</div>
</td>
<td class="wp-list-table__ext-actions">
<?php if ( ! $subscription['active'] && $subscription['maxed'] ) : ?>
<a class="button" href="https://woocommerce.com/my-account/my-subscriptions/" target="_blank"><?php esc_html_e( 'Upgrade', 'woocommerce' ); ?></a>
<?php elseif ( ! $subscription['local']['installed'] && ! $subscription['expired'] ) : ?>
<a class="button <?php echo empty( $subscription['download_primary'] ) ? 'button-secondary' : ''; ?>" href="<?php echo esc_url( $subscription['download_url'] ); ?>" target="_blank"><?php esc_html_e( 'Download', 'woocommerce' ); ?></a>
<?php elseif ( $subscription['active'] ) : ?>
<span class="form-toggle__wrapper">
<a href="<?php echo esc_url( $subscription['deactivate_url'] ); ?>" class="form-toggle active is-compact" role="link" aria-checked="true"><?php esc_html_e( 'Active', 'woocommerce' ); ?></a>
<label class="form-toggle__label" for="activate-extension">
<span class="form-toggle__label-content">
<label for="activate-extension"><?php esc_html_e( 'Active', 'woocommerce' ); ?></label>
</span>
<span class="form-toggle__switch"></span>
</label>
</span>
<?php elseif ( ! $subscription['expired'] ) : ?>
<span class="form-toggle__wrapper">
<a href="<?php echo esc_url( $subscription['activate_url'] ); ?>" class="form-toggle is-compact" role="link" aria-checked="false"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></a>
<label class="form-toggle__label" for="activate-extension">
<span class="form-toggle__label-content">
<label for="activate-extension"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></label>
</span>
<span class="form-toggle__switch"></span>
</label>
</span>
<?php else : ?>
<span class="form-toggle__wrapper">
<span class="form-toggle disabled is-compact"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></span>
<label class="form-toggle__label" for="activate-extension">
<span class="form-toggle__label-content">
<label for="activate-extension"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></label>
</span>
</label>
</span>
<?php endif; ?>
</td>
</tr>
<?php foreach ( $subscription['actions'] as $subscription_action ) : ?>
<tr class="wp-list-table__row wp-list-table__ext-updates">
<td class="wp-list-table__ext-status <?php echo sanitize_html_class( $subscription_action['status'] ); ?>">
<p><span class="dashicons <?php echo sanitize_html_class( $subscription_action['icon'] ); ?>"></span>
<?php echo wp_kses_post( $subscription_action['message'] ); ?>
</p>
</td>
<td class="wp-list-table__ext-actions">
<?php if ( ! empty( $subscription_action['button_label'] ) && ! empty( $subscription_action['button_url'] ) ) : ?>
<a class="button <?php echo empty( $subscription_action['primary'] ) ? 'button-secondary' : ''; ?>" href="<?php echo esc_url( $subscription_action['button_url'] ); ?>"><?php echo esc_html( $subscription_action['button_label'] ); ?></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="3"><em><?php esc_html_e( 'Could not find any subscriptions on your WooCommerce.com account', 'woocommerce' ); ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php if ( ! empty( $no_subscriptions ) ) : ?>
<h2><?php esc_html_e( 'Installed Extensions without a Subscription', 'woocommerce' ); ?></h2>
<p>Below is a list of WooCommerce.com products available on your site - but are either out-dated or do not have a valid subscription.</p>
<table class="wp-list-table widefat fixed striped">
<?php /* Extensions without a subscription. */ ?>
<?php foreach ( $no_subscriptions as $filename => $data ) : ?>
<tbody>
<tr class="wp-list-table__row is-ext-header">
<td class="wp-list-table__ext-details color-bar autorenews">
<div class="wp-list-table__ext-title">
<a href="<?php echo esc_url( WC_Helper::add_utm_params_to_url_for_subscription_link( $data['_product_url'], 'product-name' ) ); ?>" target="_blank">
<?php echo esc_html( $data['Name'] ); ?>
</a>
</div>
<div class="wp-list-table__ext-description">
</div>
</td>
<td class="wp-list-table__ext-actions">
<span class="form-toggle__wrapper">
<span class="form-toggle disabled is-compact" ><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></span>
<label class="form-toggle__label" for="activate-extension">
<span class="form-toggle__label-content">
<label for="activate-extension"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></label>
</span>
</label>
</span>
</td>
</tr>
<?php foreach ( $data['_actions'] as $subscription_action ) : ?>
<tr class="wp-list-table__row wp-list-table__ext-updates">
<td class="wp-list-table__ext-status <?php echo sanitize_html_class( $subscription_action['status'] ); ?>">
<p><span class="dashicons <?php echo sanitize_html_class( $subscription_action['icon'] ); ?>"></span>
<?php
echo wp_kses(
$subscription_action['message'],
array(
'a' => array(
'href' => array(),
'title' => array(),
),
'br' => array(),
'em' => array(),
'strong' => array(),
)
);
?>
</p>
</td>
<td class="wp-list-table__ext-actions">
<a class="button" href="<?php echo esc_url( $subscription_action['button_url'] ); ?>" target="_blank"><?php echo esc_html( $subscription_action['button_label'] ); ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<?php endforeach; ?>
</table>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,26 @@
<?php
/**
* Helper Admin Notice - Woo Updater Plugin is not activated.
*
* @package WooCommerce\Views
*/
defined( 'ABSPATH' ) || exit;
?>
<div id="message" class="error woocommerce-message">
<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'woo_updater_not_activated' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a>
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: 1: WP plugin management URL */
__(
'Please <a href="%1$s">activate the WooCommerce.com Update Manager</a> to continue receiving the updates and streamlined support included in your WooCommerce.com subscriptions.',
'woocommerce'
),
esc_url( admin_url( 'plugins.php' ) ),
)
);
?>
</p>
</div>

View File

@@ -0,0 +1,27 @@
<?php
/**
* Helper Admin Notice - Woo Updater Plugin is not Installed.
*
* @package WooCommerce\Views
*/
defined( 'ABSPATH' ) || exit;
?>
<div id="message" class="error woocommerce-message">
<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'woo_updater_not_installed' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a>
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: 1: Woo Update Manager plugin install URL 2: Woo Update Manager plugin download URL */
__(
'Please <a href="%1$s">Install the WooCommerce.com Update Manager</a> to continue receiving the updates and streamlined support included in your WooCommerce.com subscriptions. Alternatively, you can <a href="%2$s">download</a> 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 )
)
);
?>
</p>
</div>

View File

@@ -0,0 +1,41 @@
<?php
/**
* Admin -> WooCommerce -> Extensions -> WooCommerce.com Subscriptions main page.
*
* @package WooCommerce\Views
*/
defined( 'ABSPATH' ) || exit();
?>
<div class="wrap woocommerce wc-addons-wrap wc-helper">
<?php require WC_Helper::get_view_filename( 'html-section-nav.php' ); ?>
<h1 class="screen-reader-text"><?php esc_html_e( 'WooCommerce Extensions', 'woocommerce' ); ?></h1>
<?php require WC_Helper::get_view_filename( 'html-section-notices.php' ); ?>
<div class="start-container">
<div class="text">
<img src="<?php echo esc_url( WC()->plugin_url() . '/assets/images/woo-logo.svg' ); ?>" alt="
<?php
esc_attr_e(
'WooCommerce',
'woocommerce'
);
?>
" style="width:180px;">
<?php
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['wc-helper-status'] ) && 'helper-disconnected' === $_GET['wc-helper-status'] ) :
// phpcs:enable WordPress.Security.NonceVerification.Recommended
?>
<p><strong><?php esc_html_e( 'Sorry to see you go.', 'woocommerce' ); ?></strong> <?php esc_html_e( 'Feel free to reconnect again using the button below.', 'woocommerce' ); ?></p>
<?php endif; ?>
<h2><?php esc_html_e( 'Manage your subscriptions, get important product notifications, and updates, all from the convenience of your WooCommerce dashboard', 'woocommerce' ); ?></h2>
<p><?php esc_html_e( 'Once connected, your WooCommerce.com purchases will be listed here.', 'woocommerce' ); ?></p>
<p><a class="button button-primary button-helper-connect" href="<?php echo esc_url( $connect_url ); ?>"><?php esc_html_e( 'Connect', 'woocommerce' ); ?></a></p>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<?php defined( 'ABSPATH' ) or exit(); ?>
<a class="button button-update" href="<?php echo esc_url( $refresh_url ); ?>"><span class="dashicons dashicons-image-rotate"></span> <?php esc_html_e( 'Update', 'woocommerce' ); ?></a>
<div class="user-info">
<header>
<p><?php esc_html_e( 'Connected to WooCommerce.com', 'woocommerce' ); ?> <span class="chevron dashicons dashicons-arrow-down-alt2"></span></p>
</header>
<section>
<p><?php echo get_avatar( $auth_user_data['email'], 48 ); ?> <?php echo esc_html( $auth_user_data['email'] ); ?></p>
<div class="actions">
<a class="" href="https://woocommerce.com/my-account/my-subscriptions/" target="_blank"><span class="dashicons dashicons-admin-generic"></span> <?php esc_html_e( 'My Subscriptions', 'woocommerce' ); ?></a>
<a class="" href="<?php echo esc_url( $disconnect_url ); ?>"><span class="dashicons dashicons-no"></span> <?php esc_html_e( 'Disconnect', 'woocommerce' ); ?></a>
</div>
</section>
</div>

View File

@@ -0,0 +1,23 @@
<?php
/**
* Helper admin navigation.
*
* @package WooCommerce\Helper
*
* @deprecated 5.7.0
*/
$addons_url = admin_url( 'admin.php?page=wc-admin&path=/extensions&tab=extensions' );
defined( 'ABSPATH' ) || exit(); ?>
<nav class="nav-tab-wrapper woo-nav-tab-wrapper">
<a href="<?php echo esc_url( $addons_url ); ?>" class="nav-tab"><?php esc_html_e( 'Browse Extensions', 'woocommerce' ); ?></a>
<?php
$count_html = WC_Helper_Updater::get_updates_count_html();
/* translators: %s: WooCommerce.com Subscriptions tab count HTML. */
$menu_title = sprintf( __( 'My Subscriptions %s', 'woocommerce' ), $count_html );
?>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-addons&section=helper' ) ); ?>" class="nav-tab nav-tab-active"><?php echo wp_kses_post( $menu_title ); ?></a>
</nav>

View File

@@ -0,0 +1,7 @@
<?php defined( 'ABSPATH' ) or exit(); ?>
<?php foreach ( $notices as $notice ) : ?>
<div class="notice <?php echo sanitize_html_class( $notice['type'] ); ?>">
<?php echo wpautop( $notice['message'] ); ?>
</div>
<?php endforeach; ?>