JezK
Edit File: LegacySelect2UsageTracker.php
<?php /** * Tracks usage of WooCommerce's bundled legacy Select2 handles. */ declare( strict_types=1 ); namespace Automattic\WooCommerce\Internal\LegacyAssets; use Automattic\Woocommerce_Analytics\WC_Analytics_Tracking; use Automattic\WooCommerce\Internal\RegisterHooksInterface; use WP_Scripts; use WC_Site_Tracking; defined( 'ABSPATH' ) || exit; /** * Tracks extensions that still depend on the legacy Select2 handles bundled by WooCommerce. */ class LegacySelect2UsageTracker implements RegisterHooksInterface { public const EVENT_NAME = 'legacy_select2_usage_detected'; private const CONTEXT_ADMIN = 'admin'; private const CONTEXT_FRONTEND = 'frontend'; private const LEGACY_HANDLES = array( 'select2', 'wc-select2', ); private const TRANSIENT_KEY_PREFIX = 'wc_legacy_select2_check_'; /** * Register hook callbacks. * * @return void * * @since 11.0.0 */ public function register() { add_action( 'wp_print_footer_scripts', array( $this, 'handle_wp_print_footer_scripts' ), PHP_INT_MAX ); if ( WC_Site_Tracking::is_tracking_enabled() ) { add_action( 'admin_print_footer_scripts', array( $this, 'handle_admin_print_footer_scripts' ), PHP_INT_MAX ); } } /** * Handle the admin_print_footer_scripts hook. * * @internal * * @return void */ public function handle_admin_print_footer_scripts(): void { $this->track_usage( self::CONTEXT_ADMIN ); } /** * Handle the wp_print_footer_scripts hook. * * @internal * * @return void */ public function handle_wp_print_footer_scripts(): void { if ( ! $this->is_frontend_tracking_available() ) { return; } $this->track_usage( self::CONTEXT_FRONTEND ); } /** * Build and record legacy Select2 usage events for the current request. * * @param string $context The request context. * @return void */ private function track_usage( string $context ): void { if ( ! $this->is_legacy_select2_printed() ) { return; } $event = $this->get_usage_event( $context ); if ( empty( $event ) ) { return; } if ( $this->was_recently_checked( $event ) ) { return; } $this->mark_recently_checked( $event ); if ( self::CONTEXT_FRONTEND === $context ) { $this->record_frontend_event( self::EVENT_NAME, $event ); return; } $this->record_event( self::EVENT_NAME, $event ); } /** * Get a legacy Select2 usage event for the current script registry. * * @internal * * @param string $context The request context. * @return array<string, string> */ public function get_usage_event( string $context ): array { $wp_scripts = wp_scripts(); if ( ! $wp_scripts instanceof WP_Scripts ) { return array(); } // `done` includes dependencies that WordPress printed while resolving the queue. // Keep only queued handles so dependents identify scripts explicitly enqueued for the page. $printed_queued_scripts = array_intersect( $wp_scripts->queue, $wp_scripts->done ); $handles = array(); $dependents = array(); $dependents_sources = array(); $plugins_path = wp_parse_url( plugins_url( '/' ), PHP_URL_PATH ); foreach ( $printed_queued_scripts as $handle ) { $legacy_handles = $this->get_legacy_handles( $wp_scripts, $handle ); if ( empty( $legacy_handles ) ) { continue; } $handles += array_fill_keys( $legacy_handles, true ); $dependents[ $handle ] = true; $source = isset( $wp_scripts->registered[ $handle ] ) ? $wp_scripts->registered[ $handle ]->src : ''; $source = self::get_plugin_relative_source( $source, $plugins_path ); if ( '' !== $source ) { $dependents_sources[] = $source; } } if ( empty( $handles ) ) { return array(); } $handles = array_keys( $handles ); $dependents = array_keys( $dependents ); $dependents_sources = array_unique( $dependents_sources ); $handles_sources = array(); foreach ( $handles as $handle ) { $source = self::get_legacy_handle_source( $wp_scripts, $handle ); $source = self::get_plugin_relative_source( $source, $plugins_path ); if ( '' !== $source ) { $handles_sources[] = $source; } } $handles_sources = array_unique( $handles_sources ); sort( $handles ); sort( $dependents ); sort( $dependents_sources ); sort( $handles_sources ); return array( 'context' => $context, 'page_type' => $this->get_current_page_type( $context ), 'handles' => implode( ',', $handles ), 'dependents' => implode( ',', $dependents ), 'dependents_sources' => implode( ',', $dependents_sources ), 'handles_sources' => implode( ',', $handles_sources ), ); } /** * Record a Tracks event. * * @param string $event_name Event name. * @param array<string, string> $properties Event properties. * @return void * * @since 11.0.0 */ protected function record_event( string $event_name, array $properties ): void { if ( ! class_exists( 'WC_Tracks' ) ) { return; } \WC_Tracks::record_event( $event_name, $properties ); } /** * Add a storefront event to the WooCommerce Analytics client queue. * * @param string $event_name Event name. * @param array<string, string> $properties Event properties. * @return void * * @since 11.0.0 */ protected function record_frontend_event( string $event_name, array $properties ): void { if ( ! $this->is_frontend_tracking_available() ) { return; } WC_Analytics_Tracking::add_event_to_queue( $event_name, $properties ); } /** * Whether the WooCommerce Analytics storefront tracker is initialized. * * @return bool * * @since 11.0.0 */ protected function is_frontend_tracking_available(): bool { return did_action( 'woocommerce_analytics_init' ) && class_exists( WC_Analytics_Tracking::class ) && is_callable( array( WC_Analytics_Tracking::class, 'add_event_to_queue' ) ); } /** * Whether this usage event was already checked recently. * * @param array<string, string> $event Usage event. * @return bool */ private function was_recently_checked( array $event ): bool { return false !== get_transient( $this->get_transient_key( $event ) ); } /** * Mark this usage event as recently checked. * * @param array<string, string> $event Usage event. * @return void */ private function mark_recently_checked( array $event ): void { set_transient( $this->get_transient_key( $event ), 'yes', WEEK_IN_SECONDS ); } /** * Check whether a legacy Select2 handle has been printed. * * @return bool */ private function is_legacy_select2_printed(): bool { foreach ( self::LEGACY_HANDLES as $handle ) { if ( wp_script_is( $handle, 'done' ) ) { return true; } } return false; } /** * Get the transient key for a usage event. * * @param array<string, string> $event Usage event. * @return string */ private function get_transient_key( array $event ): string { ksort( $event ); $event_json = wp_json_encode( $event ); return self::TRANSIENT_KEY_PREFIX . md5( is_string( $event_json ) ? $event_json : '' ); } /** * Get legacy Select2 handles for a printed top-level handle. * * @param WP_Scripts $wp_scripts WordPress scripts registry. * @param string $handle Script handle. * @return array<int, string> */ private function get_legacy_handles( WP_Scripts $wp_scripts, string $handle ): array { $legacy_handles = array(); if ( in_array( $handle, self::LEGACY_HANDLES, true ) ) { $legacy_handles[ $handle ] = true; return array_keys( $legacy_handles ); } if ( isset( $wp_scripts->registered[ $handle ] ) ) { foreach ( $wp_scripts->registered[ $handle ]->deps as $dependency_handle ) { if ( in_array( $dependency_handle, self::LEGACY_HANDLES, true ) ) { $legacy_handles[ $dependency_handle ] = true; } } } return array_keys( $legacy_handles ); } /** * Get a script source relative to the plugins directory. * * @param string|false $source Script source. * @param string|false|null $plugins_path Plugins directory URL path. * @return string */ private static function get_plugin_relative_source( $source, $plugins_path ): string { if ( ! is_string( $source ) || '' === $source || ! is_string( $plugins_path ) || '' === $plugins_path ) { return ''; } $source_path = wp_parse_url( $source, PHP_URL_PATH ); if ( ! is_string( $source_path ) || ! str_starts_with( $source_path, $plugins_path ) ) { return ''; } return ltrim( substr( $source_path, strlen( $plugins_path ) ), '/' ); } /** * Get the source for a legacy Select2 handle. * * @param WP_Scripts $wp_scripts WordPress scripts registry. * @param string $handle Script handle. * @return string */ private static function get_legacy_handle_source( WP_Scripts $wp_scripts, string $handle ): string { $source_handle = 'select2' === $handle ? 'wc-select2' : $handle; $registered_script = $wp_scripts->registered[ $source_handle ] ?? null; if ( null === $registered_script ) { return ''; } return is_string( $registered_script->src ) ? $registered_script->src : ''; } /** * Get the current admin screen ID. * * @return string */ private function get_current_screen_id(): string { if ( ! function_exists( 'get_current_screen' ) ) { return ''; } $screen = get_current_screen(); return $screen ? (string) $screen->id : ''; } /** * Get the current page type. * * @param string $context The request context. * @return string */ private function get_current_page_type( string $context ): string { if ( self::CONTEXT_ADMIN === $context ) { return $this->get_current_screen_id(); } if ( self::CONTEXT_FRONTEND === $context ) { return $this->get_current_frontend_page_type(); } return ''; } /** * Get the current frontend page type. * * @return string */ private function get_current_frontend_page_type(): string { if ( is_cart() ) { return 'cart'; } if ( is_checkout() ) { return 'checkout'; } if ( is_account_page() ) { return 'my_account'; } if ( is_shop() ) { return 'shop'; } if ( is_product() ) { return 'product'; } if ( is_product_category() ) { return 'product_category'; } if ( is_product_tag() ) { return 'product_tag'; } if ( is_product_taxonomy() ) { return 'product_taxonomy'; } if ( is_front_page() ) { return 'front_page'; } if ( is_home() ) { return 'home'; } if ( is_search() ) { return 'search'; } if ( is_archive() ) { return 'archive'; } if ( is_singular() ) { return 'singular'; } return 'other'; } }