JezK
Edit File: Features.php
<?php /** * Features loader for features developed in WooCommerce Admin. */ namespace Automattic\WooCommerce\Admin\Features; use Automattic\WooCommerce\Admin\PageController; use Automattic\WooCommerce\Internal\Admin\Analytics; use Automattic\WooCommerce\Internal\Admin\Loader; use Automattic\WooCommerce\Internal\Admin\RemoteInboxNotifications; use Automattic\WooCommerce\Internal\Admin\WCAdminAssets; use Automattic\WooCommerce\Utilities\FeaturesUtil; /** * Features Class. */ class Features { /** * Class instance. * * @var Loader instance */ protected static $instance = null; /** * Version metadata for WC Admin feature flags kept for backward compatibility. * * Keep this dictionary in sync with RETIRED_FEATURE_FLAGS in * plugins/woocommerce/client/admin/client/utils/features/retired-feature-flags.ts. * * @var array<string, array{deprecated_since: string, removed_in: ?string}> */ private static $retired_feature_compatibility_versions = array( 'activity-panels' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'analytics' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'analytics-scheduled-import' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'experimental-iapi-mini-cart' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'coupons' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'core-profiler' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'customize-store' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'customer-effort-score-tracks' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'import-products-task' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'experimental-fashion-sample-products' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'shipping-smart-defaults' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'shipping-setting-tour' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'homescreen' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'marketing' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'mobile-app-banner' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'onboarding' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'onboarding-tasks' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'pattern-toolkit-full-composability' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'payment-gateway-suggestions' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'product-custom-fields' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'printful' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'remote-inbox-notifications' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'remote-free-extensions' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'shipping-label-banner' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'subscriptions' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'transient-notices' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'wc-pay-promotion' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'wc-pay-welcome-page' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'woo-mobile-welcome' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), 'launch-your-store' => array( 'deprecated_since' => '11.1.0', 'removed_in' => null, ), ); /** * Get class instance. */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Constructor. */ public function __construct() { $this->register_internal_class_aliases(); if ( ! self::should_load_features() ) { return; } // Load feature before WooCommerce update hooks. add_action( 'init', array( __CLASS__, 'load_features' ), 4 ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'load_scripts' ), 15 ); add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) ); } /** * Gets a build configured array of enabled WooCommerce Admin features/sections, but does not respect optionally disabled features. * * @return array Enabled Woocommerce Admin features/sections. */ public static function get_features() { return apply_filters( 'woocommerce_admin_features', array() ); } /** * Gets the optional feature options as an associative array that can be toggled on or off. * * @deprecated 11.1.0 Use FeaturesUtil::feature_is_enabled() to check if a feature is enabled. * * @return array */ public static function get_optional_feature_options() { wc_deprecated_function( __METHOD__, '11.1.0', 'FeaturesUtil::feature_is_enabled()' ); return array( 'analytics' => Analytics::TOGGLE_OPTION_NAME, 'remote-inbox-notifications' => RemoteInboxNotifications::TOGGLE_OPTION_NAME, ); } /** * Returns if a specific wc-admin feature exists in the current environment. * * @param string $feature Feature slug. * @return bool Returns true if the feature exists. */ public static function exists( $feature ) { $is_legacy_compatibility_feature = self::is_legacy_compatibility_feature( $feature ); if ( $is_legacy_compatibility_feature ) { self::warn_legacy_feature_compatibility_usage( __METHOD__, $feature ); } $features = $is_legacy_compatibility_feature ? self::get_features_with_legacy_compatibility_defaults() : self::get_features(); return in_array( $feature, $features, true ); } /** * Get the feature class as a string. * * @param string $feature Feature name. * @return string|null */ public static function get_feature_class( $feature ) { $feature = str_replace( '-', '', ucwords( strtolower( $feature ), '-' ) ); $feature_class = 'Automattic\\WooCommerce\\Admin\\Features\\' . $feature; $should_autoload_class = self::should_load_features(); if ( class_exists( $feature_class, $should_autoload_class ) ) { return $feature_class; } // Handle features contained in subdirectory. if ( class_exists( $feature_class . '\\Init', $should_autoload_class ) ) { return $feature_class . '\\Init'; } return null; } /** * Class loader for enabled WooCommerce Admin features/sections. */ public static function load_features() { if ( ! self::should_load_features() ) { return; } $always_loaded_feature_classes = array( \Automattic\WooCommerce\Internal\Admin\ActivityPanels::class, \Automattic\WooCommerce\Internal\Admin\Analytics::class, \Automattic\WooCommerce\Internal\Admin\Coupons::class, \Automattic\WooCommerce\Internal\Admin\CustomerEffortScoreTracks::class, \Automattic\WooCommerce\Internal\Admin\Homescreen::class, \Automattic\WooCommerce\Internal\Admin\Marketing::class, \Automattic\WooCommerce\Internal\Admin\MobileAppBanner::class, \Automattic\WooCommerce\Admin\Features\OnboardingTasks\Init::class, \Automattic\WooCommerce\Internal\Admin\RemoteInboxNotifications::class, \Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\Init::class, \Automattic\WooCommerce\Internal\Admin\ShippingLabelBanner::class, \Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\Init::class, \Automattic\WooCommerce\Admin\Features\TransientNotices::class, \Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init::class, \Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage::class, \Automattic\WooCommerce\Admin\Features\LaunchYourStore::class, ); foreach ( $always_loaded_feature_classes as $feature_class ) { new $feature_class(); } $features = self::get_features(); foreach ( $features as $feature ) { $feature_class = self::get_feature_class( $feature ); if ( ! $feature_class ) { continue; } foreach ( $always_loaded_feature_classes as $loaded_feature_class ) { if ( is_a( $feature_class, $loaded_feature_class, true ) ) { // Skip the outer features loop because this feature was already loaded. continue 2; } } new $feature_class(); } if ( FeaturesUtil::feature_is_enabled( 'blueprint' ) ) { new \Automattic\WooCommerce\Admin\Features\Blueprint\Init(); } if ( FeaturesUtil::feature_is_enabled( 'order-detail-redesign' ) ) { new \Automattic\WooCommerce\Internal\Features\OrderDetailRedesign\Init(); } } /** * Gets a build configured array of enabled WooCommerce Admin respecting optionally disabled features. * * @return array Enabled Woocommerce Admin features/sections. */ public static function get_available_features() { $features = self::get_features_with_legacy_compatibility_defaults(); $optional_feature_keys = array( 'analytics', 'remote-inbox-notifications' ); $legacy_compatibility_values = self::get_legacy_feature_compatibility_values(); $unavailable_features = array(); $available_compatibility_keys = array_keys( array_filter( $legacy_compatibility_values ) ); $features = array_values( array_unique( array_merge( $features, $available_compatibility_keys ) ) ); /** * Filter allowing WooCommerce Admin optional features to be disabled. * * @param bool $disabled False. */ if ( apply_filters( 'woocommerce_admin_disabled', false ) ) { return array_values( array_diff( $features, $optional_feature_keys ) ); } if ( in_array( 'analytics', $features, true ) && ! self::is_analytics_enabled() ) { $unavailable_features[] = 'analytics'; } if ( in_array( 'remote-inbox-notifications', $features, true ) && 'yes' !== get_option( RemoteInboxNotifications::TOGGLE_OPTION_NAME, 'yes' ) ) { $unavailable_features[] = 'remote-inbox-notifications'; } return array_values( array_diff( $features, $unavailable_features ) ); } /** * Check if a feature is enabled. * * @param string $feature Feature slug. * @return bool */ public static function is_enabled( $feature ) { if ( self::is_legacy_compatibility_feature( $feature ) ) { self::warn_legacy_feature_compatibility_usage( __METHOD__, $feature ); } $available_features = self::get_available_features(); return in_array( $feature, $available_features, true ); } /** * Enable a toggleable optional feature. * * @deprecated 11.1.0 Use FeaturesUtil::feature_is_enabled() to check if a feature is enabled. * * @param string $feature Feature name. * @return bool */ public static function enable( $feature ) { wc_deprecated_function( __METHOD__, '11.1.0', 'FeaturesUtil::feature_is_enabled()' ); if ( 'analytics' === $feature ) { update_option( Analytics::TOGGLE_OPTION_NAME, 'yes' ); return true; } if ( 'remote-inbox-notifications' === $feature ) { update_option( RemoteInboxNotifications::TOGGLE_OPTION_NAME, 'yes' ); return true; } return false; } /** * Disable a toggleable optional feature. * * @deprecated 11.1.0 Use FeaturesUtil::feature_is_enabled() to check if a feature is enabled. * * @param string $feature Feature name. * @return bool */ public static function disable( $feature ) { wc_deprecated_function( __METHOD__, '11.1.0', 'FeaturesUtil::feature_is_enabled()' ); if ( 'analytics' === $feature ) { update_option( Analytics::TOGGLE_OPTION_NAME, 'no' ); return true; } if ( 'remote-inbox-notifications' === $feature ) { update_option( RemoteInboxNotifications::TOGGLE_OPTION_NAME, 'no' ); return true; } return false; } /** * Adds the Features section to the advanced tab of WooCommerce Settings * * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class). * * @param array $sections Sections. * @return array */ public static function add_features_section( $sections ) { return $sections; } /** * Adds the Features settings. * * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class). * * @param array $settings Settings. * @param string $current_section Current section slug. * @return array */ public static function add_features_settings( $settings, $current_section ) { return $settings; } /** * Loads the required scripts on the correct pages. */ public static function load_scripts() { if ( ! PageController::is_admin_or_embed_page() ) { return; } $available_features = self::get_available_features(); $enabled_features = array(); foreach ( self::get_features() as $key ) { $enabled_features[ $key ] = in_array( $key, $available_features, true ); } $enabled_features = array_merge( $enabled_features, self::get_legacy_feature_compatibility_values() ); wp_add_inline_script( WC_ADMIN_APP, 'window.wcAdminFeatures = ' . wp_json_encode( $enabled_features, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), 'before' ); } /** * Adds body classes to the main wp-admin wrapper, allowing us to better target elements in specific scenarios. * * @param string $admin_body_class Body class to add. */ public static function add_admin_body_classes( $admin_body_class = '' ) { if ( ! PageController::is_admin_or_embed_page() ) { return $admin_body_class; } $classes = explode( ' ', trim( $admin_body_class ) ); foreach ( self::get_available_features() as $feature_key ) { $classes[] = sanitize_html_class( 'woocommerce-feature-enabled-' . $feature_key ); } $admin_body_class = implode( ' ', array_unique( $classes ) ); return " $admin_body_class "; } /** * Gets legacy feature flag compatibility values. * * This method is intended for passive compatibility paths, such as script globals * and filtering shared settings, where emitting deprecation notices would warn on * every admin page load. * * @since 11.1.0 * @return array<string, bool> */ public static function get_legacy_feature_compatibility_values() { $compatibility_values = array_merge( array_fill_keys( array_keys( self::$retired_feature_compatibility_versions ), true ), array( 'analytics' => self::is_analytics_enabled(), 'remote-inbox-notifications' => 'yes' === get_option( RemoteInboxNotifications::TOGGLE_OPTION_NAME, 'yes' ), ) ); return array_intersect_key( $compatibility_values, array_flip( self::get_features_with_legacy_compatibility_defaults() ) ); } /** * Gets default legacy feature flag compatibility values before public filtering. * * @return array<string, bool> */ private static function get_legacy_feature_compatibility_defaults() { return array_merge( array_fill_keys( array_keys( self::$retired_feature_compatibility_versions ), true ), array( 'analytics' => true, 'remote-inbox-notifications' => true, ) ); } /** * Gets WooCommerce Admin features with legacy compatibility defaults before public filtering. * * @return array Enabled Woocommerce Admin features/sections. */ private static function get_features_with_legacy_compatibility_defaults() { /** * Filter allowing WooCommerce Admin features to be changed after legacy compatibility defaults are seeded. * * @since 11.1.0 * * @param array $features Array of feature slugs. */ return apply_filters( 'woocommerce_admin_features', array_keys( self::get_legacy_feature_compatibility_defaults() ) ); } /** * Checks if Analytics was disabled by legacy WooCommerce Admin filters. * * @return bool True if Analytics was disabled by legacy filters. */ public static function is_analytics_disabled_by_legacy_filters(): bool { /** * Filter allowing WooCommerce Admin optional features to be disabled. * * @param bool $disabled False. */ if ( apply_filters( 'woocommerce_admin_disabled', false ) ) { // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment return true; } return ! in_array( 'analytics', self::get_features_with_legacy_compatibility_defaults(), true ); } /** * Checks if analytics is enabled, without going through FeaturesController. * * @return bool */ private static function is_analytics_enabled() { return ! self::is_analytics_disabled_by_legacy_filters() && 'yes' === get_option( Analytics::TOGGLE_OPTION_NAME, 'yes' ); } /** * Checks if a feature slug is supported only by the legacy compatibility shim. * * @param string $feature Feature slug. * @return bool */ private static function is_legacy_compatibility_feature( $feature ) { return array_key_exists( $feature, self::get_legacy_feature_compatibility_defaults() ); } /** * Gets version metadata for a legacy feature flag shim. * * @param string $feature Feature slug. * @return array{deprecated_since: string, removed_in: ?string}|null */ private static function get_legacy_feature_compatibility_versions( $feature ) { return self::$retired_feature_compatibility_versions[ $feature ] ?? null; } /** * Gets the WooCommerce version where a legacy feature flag shim was deprecated. * * @param string $feature Feature slug. * @return string|null */ private static function get_legacy_feature_compatibility_deprecation_version( $feature ) { $versions = self::get_legacy_feature_compatibility_versions( $feature ); return $versions['deprecated_since'] ?? null; } /** * Gets the WooCommerce version where a legacy feature flag shim will be removed. * * @param string $feature Feature slug. * @return string|null */ private static function get_legacy_feature_compatibility_removal_version( $feature ) { $versions = self::get_legacy_feature_compatibility_versions( $feature ); return $versions['removed_in'] ?? null; } /** * Emits a deprecation notice for a direct legacy feature flag shim lookup. * * @param string $method Method name. * @param string $feature Feature slug. */ private static function warn_legacy_feature_compatibility_usage( $method, $feature ): void { $deprecation_version = self::get_legacy_feature_compatibility_deprecation_version( $feature ); $removal_version = self::get_legacy_feature_compatibility_removal_version( $feature ); if ( ! $deprecation_version ) { return; } wc_deprecated_function( sprintf( "%s( '%s' )", $method, $feature ), $deprecation_version, sprintf( 'direct feature behavior checks. The %1$s WC Admin feature flag shim will be removed in %2$s.', $feature, $removal_version ? 'WooCommerce ' . $removal_version : 'a future version of WooCommerce' ) ); } /** * Alias internal features classes to make them backward compatible. * We've moved our feature classes to src-internal as part of merging this * repository with WooCommerce Core to form a monorepo. * See https://wp.me/p90Yrv-2HY for details. */ private function register_internal_class_aliases() { $aliases = array( // new class => original class (this will be aliased). 'Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init' => 'Automattic\WooCommerce\Admin\Features\WcPayPromotion\Init', 'Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\Init' => 'Automattic\WooCommerce\Admin\Features\RemoteFreeExtensions\Init', 'Automattic\WooCommerce\Internal\Admin\ActivityPanels' => 'Automattic\WooCommerce\Admin\Features\ActivityPanels', 'Automattic\WooCommerce\Internal\Admin\Analytics' => 'Automattic\WooCommerce\Admin\Features\Analytics', 'Automattic\WooCommerce\Internal\Admin\Coupons' => 'Automattic\WooCommerce\Admin\Features\Coupons', 'Automattic\WooCommerce\Internal\Admin\CouponsMovedTrait' => 'Automattic\WooCommerce\Admin\Features\CouponsMovedTrait', 'Automattic\WooCommerce\Internal\Admin\CustomerEffortScoreTracks' => 'Automattic\WooCommerce\Admin\Features\CustomerEffortScoreTracks', 'Automattic\WooCommerce\Internal\Admin\Homescreen' => 'Automattic\WooCommerce\Admin\Features\Homescreen', 'Automattic\WooCommerce\Internal\Admin\Marketing' => 'Automattic\WooCommerce\Admin\Features\Marketing', 'Automattic\WooCommerce\Internal\Admin\MobileAppBanner' => 'Automattic\WooCommerce\Admin\Features\MobileAppBanner', 'Automattic\WooCommerce\Internal\Admin\RemoteInboxNotifications' => 'Automattic\WooCommerce\Admin\Features\RemoteInboxNotifications', 'Automattic\WooCommerce\Internal\Admin\ShippingLabelBanner' => 'Automattic\WooCommerce\Admin\Features\ShippingLabelBanner', 'Automattic\WooCommerce\Internal\Admin\ShippingLabelBannerDisplayRules' => 'Automattic\WooCommerce\Admin\Features\ShippingLabelBannerDisplayRules', 'Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage' => 'Automattic\WooCommerce\Admin\Features\WcPayWelcomePage', ); foreach ( $aliases as $new_class => $orig_class ) { class_alias( $new_class, $orig_class ); } } /** * Check if we're in an admin context where features should be loaded. * * @return boolean */ private static function should_load_features() { $should_load = ( is_admin() || wp_doing_ajax() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) || ( WC()->is_rest_api_request() && ! WC()->is_store_api_request() ) || // Allow features to be loaded in frontend for admin users. This is needed for the use case such as the coming soon footer banner. current_user_can( 'manage_woocommerce' ) ); /** * Filter to determine if admin features should be loaded. * * @since 9.6.0 * @param boolean $should_load Whether admin features should be loaded. It defaults to true when the current request is in an admin context. */ return apply_filters( 'woocommerce_admin_should_load_features', $should_load ); } }