plugin_path . $filepath . '.asset.php'; if ( file_exists( $asset_path ) ) { return include $asset_path; } return array( 'dependencies' => array(), 'version' => UCA_VERSION, ); } /** * Enqueue frontend assets with cache-proof chunk loading */ public function enqueue_frontend_assets() { // Prevent multiple executions static $executed = false; if ( $executed ) { return; } $executed = true; $settings = get_option( 'ultimate_cursor_settings', array() ); $asset_data = $this->get_asset_file( 'build/frontend' ); // SERVER-SIDE PREMIUM GATE: Sanitize settings before sending to frontend. // This strips premium-only fields if no valid license exists, // preventing bypasses even if premium values were injected into the DB. $settings = Ultimate_Cursor_License_Gate::sanitize( $settings, 'cursor' ); // Normalize enableMultipleCursors to boolean $enable_multiple = isset( $settings['enableMultipleCursors'] ) && ( $settings['enableMultipleCursors'] === true || $settings['enableMultipleCursors'] === '1' || $settings['enableMultipleCursors'] === 1 ); // FORCE CHECK: If premium is not valid, disable multiple cursors // This ensures the feature doesn't work even if enabled in DB if ( ! Ultimate_Cursor_License_Gate::is_premium_active() ) { $enable_multiple = false; $settings['enableMultipleCursors'] = false; unset( $settings['cursorConfigurations'] ); } // Check if we should load the script $should_load = false; if ( $enable_multiple ) { $should_load = true; } elseif ( ( isset( $settings['effect'] ) && $settings['effect'] !== 'none' ) || ( isset( $settings['cursorType'] ) && $settings['cursorType'] !== null ) ) { $should_load = true; } if ( $should_load ) { // Get frontend.js file path for cache busting $frontend_js_path = ultimate_cursor()->plugin_path . 'build/frontend.js'; $frontend_js_url = ultimate_cursor()->plugin_url . 'build/frontend.js'; // Add filemtime-based cache busting to version $version = $asset_data['version']; if ( file_exists( $frontend_js_path ) ) { $version .= '.' . filemtime( $frontend_js_path ); } // Enqueue the script with cache-busting version wp_enqueue_script( 'ultimate-cursor-frontend', $frontend_js_url, $asset_data['dependencies'], $version, array( 'in_footer' => true, 'strategy' => 'defer', // Defer for optimal loading ) ); // Load JS translations for any __() strings rendered on the frontend. wp_set_script_translations( 'ultimate-cursor-frontend', 'ultimate-cursor', ultimate_cursor()->plugin_path . 'languages' ); // CRITICAL: Inject public path BEFORE the main script loads // This ensures webpack knows where to load dynamic chunks from // even when the main script is cached/minified by WP Rocket, LiteSpeed, etc. $public_path_script = sprintf( 'window.__ultimateCursorPublicPath = %s;', wp_json_encode( ultimate_cursor()->plugin_url . 'build/' ) ); wp_add_inline_script( 'ultimate-cursor-frontend', $public_path_script, 'before' // Execute BEFORE the main script ); // Use wp_add_inline_script + wp_json_encode instead of // wp_localize_script to preserve data types (numbers, booleans). // wp_localize_script casts every scalar to a string, which breaks // components that do arithmetic on their settings (e.g. the // snowflake cursor's fall speed turned "1" + Math.random() into // string concatenation, rendering particles at NaN coordinates). $cursor_data_script = sprintf( 'var ultimateCursorData = %s;', wp_json_encode( $settings ) ); wp_add_inline_script( 'ultimate-cursor-frontend', $cursor_data_script, 'before' ); } } /** * Enqueue frontend background animation assets with performance-first approach. * Three.js is lazy-loaded only when background animation is enabled. */ public function enqueue_frontend_background_assets() { // Prevent multiple executions static $executed = false; if ( $executed ) { return; } $executed = true; $bg_settings = get_option( 'ultimate_cursor_background_settings', array() ); // SERVER-SIDE PREMIUM GATE: Strip premium-only background settings if no // valid license exists, even if premium values were injected into the DB. $bg_settings = Ultimate_Cursor_License_Gate::sanitize( $bg_settings, 'background' ); // Only load if background animation is enabled if ( empty( $bg_settings['enabled'] ) ) { return; } $enable_multiple = ! empty( $bg_settings['enableMultipleBackgrounds'] ); if ( $enable_multiple ) { // Multiple backgrounds mode: check each config's scope individually. // Filter out configs that don't match the current page. $configs = isset( $bg_settings['backgroundConfigurations'] ) && is_array( $bg_settings['backgroundConfigurations'] ) ? $bg_settings['backgroundConfigurations'] : array(); $filtered_configs = array(); foreach ( $configs as $config ) { $config_scope = isset( $config['scope'] ) ? $config['scope'] : 'entire-website'; if ( $config_scope === 'specific-pages' ) { $specific_pages = isset( $config['specificPages'] ) ? $config['specificPages'] : ''; if ( $this->is_matching_page( $specific_pages ) ) { $filtered_configs[] = $config; } } else { // 'entire-website' or 'css-selector' — always include $filtered_configs[] = $config; } } // Don't load the script if no configs match the current page if ( empty( $filtered_configs ) ) { return; } // Pass only the matching configs to the frontend $bg_settings['backgroundConfigurations'] = array_values( $filtered_configs ); } else { // Single background mode: check top-level scope $scope = isset( $bg_settings['scope'] ) ? $bg_settings['scope'] : 'entire-website'; if ( $scope === 'specific-pages' ) { $specific_pages = isset( $bg_settings['specificPages'] ) ? $bg_settings['specificPages'] : ''; if ( ! $this->is_matching_page( $specific_pages ) ) { return; } } } $asset_data = $this->get_asset_file( 'build/frontend-background' ); $bg_js_path = ultimate_cursor()->plugin_path . 'build/frontend-background.js'; $bg_js_url = ultimate_cursor()->plugin_url . 'build/frontend-background.js'; // Add filemtime-based cache busting $version = $asset_data['version']; if ( file_exists( $bg_js_path ) ) { $version .= '.' . filemtime( $bg_js_path ); } wp_enqueue_script( 'ultimate-cursor-frontend-background', $bg_js_url, $asset_data['dependencies'], $version, array( 'in_footer' => true, 'strategy' => 'defer', ) ); // Load JS translations for any __() strings rendered by the background runtime. wp_set_script_translations( 'ultimate-cursor-frontend-background', 'ultimate-cursor', ultimate_cursor()->plugin_path . 'languages' ); // Inject public path for chunk loading $public_path_script = sprintf( 'window.__ultimateCursorBgPublicPath = %s;', wp_json_encode( ultimate_cursor()->plugin_url . 'build/' ) ); wp_add_inline_script( 'ultimate-cursor-frontend-background', $public_path_script, 'before' ); // Use wp_add_inline_script + wp_json_encode instead of wp_localize_script // to preserve data types (numbers, booleans) in backgroundConfigurations. // wp_localize_script converts all scalar values to strings which breaks rendering. $bg_data_script = sprintf( 'var ultimateCursorBgData = %s;', wp_json_encode( $bg_settings ) ); wp_add_inline_script( 'ultimate-cursor-frontend-background', $bg_data_script, 'before' ); } /** * Check if the current page matches the specific pages list. * * Matching rules (kept in sync with JS src/frontend/cursor/scope.js * matchesCurrentPage): * - `home` or `/` → the site front page * - a number → post/page ID * - `foo` → exact slug (last path segment) or exact top-level path * - `foo/bar` → exact full path * - `foo/*` → prefix wildcard: `foo` and anything under `foo/…` * * No bare substring matching — `press` no longer matches `/pressroom`. * * @param string $pages_string Comma-separated list of page slugs, paths, or IDs. * @return bool */ private function is_matching_page( $pages_string ) { if ( empty( $pages_string ) ) { return false; } $current_url = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $current_path = trim( (string) wp_parse_url( $current_url, PHP_URL_PATH ), '/' ); // Strip a leading index.php for consistency with the JS matcher. if ( 'index.php' === $current_path ) { $current_path = ''; } $segments = array_filter( explode( '/', $current_path ) ); $current_slug = ! empty( $segments ) ? end( $segments ) : ''; $is_home = ( is_front_page() || is_home() ); foreach ( array_map( 'trim', explode( ',', $pages_string ) ) as $page ) { $page = trim( $page, '/' ); // Homepage. if ( ( $page === '' || strtolower( $page ) === 'home' ) && $is_home ) { return true; } if ( $page === '' ) { continue; } // Prefix wildcard: `foo/*`. if ( substr( $page, -2 ) === '/*' ) { $prefix = substr( $page, 0, -2 ); if ( $current_path === $prefix || strpos( $current_path, $prefix . '/' ) === 0 ) { return true; } continue; } // Post/page ID. if ( is_numeric( $page ) ) { if ( is_singular() && (int) $page === get_queried_object_id() ) { return true; } continue; } // Exact full path or exact slug. if ( $current_path === $page || $current_slug === $page ) { return true; } } return false; } /** * Enqueue admin pages assets. */ public function admin_enqueue_scripts() { $screen = get_current_screen(); if ( ! $screen || 'toplevel_page_ultimate-cursor' !== $screen->id ) { return; } wp_add_inline_style( 'wp-admin', '.php-error #adminmenuback, .php-error #adminmenuwrap { margin-top: 0px !important; }' ); $asset_data = $this->get_asset_file( 'build/admin' ); wp_enqueue_script( 'ultimate-cursor-admin', ultimate_cursor()->plugin_url . 'build/admin.js', $asset_data['dependencies'], $asset_data['version'], true ); // Load JS translations so the React dashboard's __() strings are translatable. wp_set_script_translations( 'ultimate-cursor-admin', 'ultimate-cursor', ultimate_cursor()->plugin_path . 'languages' ); // Pass the cursor images $cursor_images = array(); $cursor_shapes = array(); $cursor_dir = ultimate_cursor()->plugin_path . 'assets/cursors/'; $cursor_url = ultimate_cursor()->plugin_url . 'assets/cursors/'; $cursor_shapes_dir = ultimate_cursor()->plugin_path . 'assets/shapes/'; $cursor_shapes_url = ultimate_cursor()->plugin_url . 'assets/shapes/'; $extensions = array( 'png', 'jpg', 'jpeg', 'gif', 'svg', 'cur' ); foreach ( $extensions as $ext ) { $files = glob( $cursor_dir . '*.' . $ext ); if ( $files ) { foreach ( $files as $file ) { $cursor_images[] = $cursor_url . basename( $file ); } } } foreach ( $extensions as $ext ) { $files = glob( $cursor_shapes_dir . '*.' . $ext ); if ( $files ) { foreach ( $files as $file ) { $cursor_shapes[] = $cursor_shapes_url . basename( $file ); } } } // Use wp_add_inline_script + wp_json_encode instead of wp_localize_script: // localize casts top-level scalars to strings ('isPro' => "1"/""), and the // dashboard treats these as real booleans. Same rule as the frontend paths. $admin_data = array( 'settings' => ( function () { $settings = get_option( 'ultimate_cursor_settings', array() ); // SERVER-SIDE PREMIUM GATE: Sanitize admin settings output. // This ensures premium fields are stripped if license is invalid. $settings = Ultimate_Cursor_License_Gate::sanitize( $settings, 'cursor' ); return $settings; } )(), 'backgroundSettings' => Ultimate_Cursor_License_Gate::sanitize( get_option( 'ultimate_cursor_background_settings', array() ), 'background' ), 'cursors' => $cursor_images, 'plugin_url' => ultimate_cursor()->plugin_url, 'version' => UCA_VERSION, 'shapes' => $cursor_shapes, // isPro requires BOTH pro plugin active AND valid Freemius license 'isPro' => UltimateCursor::is_premium_active(), 'isLicenseValid' => UltimateCursor::is_premium_active(), 'proUrl' => 'https://wpxero.com/plugins/ultimate-cursor/pricing', 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'ultimate_cursor_admin_nonce' ), 'activePlugins' => ( function () { require_once ABSPATH . 'wp-admin/includes/plugin.php'; $active = get_option( 'active_plugins', array() ); if ( is_multisite() ) { $active = array_merge( $active, array_keys( get_site_option( 'active_sitewide_plugins', array() ) ) ); } $slugs = array(); foreach ( $active as $plugin ) { $dirname = dirname( $plugin ); if ( $dirname !== '.' ) { $slugs[] = $dirname; } } return $slugs; } )(), ); $encoded = wp_json_encode( $admin_data ); wp_add_inline_script( 'ultimate-cursor-admin', sprintf( 'var ultimateCursorAdminData = %s;', $encoded !== false ? $encoded : '{}' ), 'before' ); wp_enqueue_style( 'ultimate-cursor-admin', ultimate_cursor()->plugin_url . 'build/style-admin.css', array(), $asset_data['version'] ); // RTL locales: swap in the rtlcss-generated stylesheet the build emits. wp_style_add_data( 'ultimate-cursor-admin', 'rtl', 'replace' ); // @wordpress/scripts splits stylesheets: `style.scss` imports land in // build/style-admin.css (above), while any other-named CSS import // (e.g. the control kit's kit.scss) is emitted to build/admin.css. // Enqueue it too so those component styles actually load. $admin_css = ultimate_cursor()->plugin_path . 'build/admin.css'; if ( file_exists( $admin_css ) ) { wp_enqueue_style( 'ultimate-cursor-admin-components', ultimate_cursor()->plugin_url . 'build/admin.css', array( 'ultimate-cursor-admin' ), $asset_data['version'] ); wp_style_add_data( 'ultimate-cursor-admin-components', 'rtl', 'replace' ); } wp_enqueue_style( 'wp-components' ); } } Ultimate_Cursor_Assets::instance();