PluginProbe
Ultimate Cursor – Interactive and Animated Custom Cursor and Background Effects Toolkit / trunk
Ultimate Cursor – Interactive and Animated Custom Cursor and Background Effects Toolkit vtrunk
2.4.1 2.4.0 2.3.3 2.3.2 2.3.1 2.3.0 2.2.3 2.2.2 2.2.1 trunk 1.0.0 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 All 56 releases
ultimate-cursor / classes / class-assets.php

class-assets.php in Ultimate Cursor – Interactive and Animated Custom Cursor and Background Effects Toolkit trunk, at classes/class-assets.php

501 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Plugin assets functions.
5 *
6 * @package ultimate-cursor
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Ultimate Cursor Assets class.
15 */
16 class Ultimate_Cursor_Assets {
17 /**
18 * The single class instance.
19 *
20 * @var $instance
21 */
22 private static $instance = null;
23
24 /**
25 * Get instance
26 */
27 public static function instance() {
28 if ( is_null( self::$instance ) ) {
29 self::$instance = new self();
30 }
31 return self::$instance;
32 }
33
34 /**
35 * Ultimate_Cursor_Assets constructor.
36 */
37 private function __construct() {
38 if ( ! is_admin() ) {
39 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
40 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_background_assets' ) );
41 }
42 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
43 }
44
45 /**
46 * Loads the asset file for the given script or style.
47 * Returns a default if the asset file is not found.
48 *
49 * @param string $filepath The name of the file without the extension.
50 *
51 * @return array The asset file contents.
52 */
53 public function get_asset_file( $filepath ) {
54 $asset_path = ultimate_cursor()->plugin_path . $filepath . '.asset.php';
55
56 if ( file_exists( $asset_path ) ) {
57 return include $asset_path;
58 }
59
60 return array(
61 'dependencies' => array(),
62 'version' => UCA_VERSION,
63 );
64 }
65
66 /**
67 * Enqueue frontend assets with cache-proof chunk loading
68 */
69 public function enqueue_frontend_assets() {
70 // Prevent multiple executions
71 static $executed = false;
72 if ( $executed ) {
73 return;
74 }
75 $executed = true;
76
77 $settings = get_option( 'ultimate_cursor_settings', array() );
78 $asset_data = $this->get_asset_file( 'build/frontend' );
79
80 // SERVER-SIDE PREMIUM GATE: Sanitize settings before sending to frontend.
81 // This strips premium-only fields if no valid license exists,
82 // preventing bypasses even if premium values were injected into the DB.
83 $settings = Ultimate_Cursor_License_Gate::sanitize( $settings, 'cursor' );
84
85 // Normalize enableMultipleCursors to boolean
86 $enable_multiple = isset( $settings['enableMultipleCursors'] ) &&
87 ( $settings['enableMultipleCursors'] === true || $settings['enableMultipleCursors'] === '1' || $settings['enableMultipleCursors'] === 1 );
88
89 // FORCE CHECK: If premium is not valid, disable multiple cursors
90 // This ensures the feature doesn't work even if enabled in DB
91 if ( ! Ultimate_Cursor_License_Gate::is_premium_active() ) {
92 $enable_multiple = false;
93 $settings['enableMultipleCursors'] = false;
94 unset( $settings['cursorConfigurations'] );
95 }
96
97 // Check if we should load the script
98 $should_load = false;
99
100 if ( $enable_multiple ) {
101 $should_load = true;
102 } elseif ( ( isset( $settings['effect'] ) && $settings['effect'] !== 'none' ) || ( isset( $settings['cursorType'] ) && $settings['cursorType'] !== null ) ) {
103 $should_load = true;
104 }
105
106 if ( $should_load ) {
107 // Get frontend.js file path for cache busting
108 $frontend_js_path = ultimate_cursor()->plugin_path . 'build/frontend.js';
109 $frontend_js_url = ultimate_cursor()->plugin_url . 'build/frontend.js';
110
111 // Add filemtime-based cache busting to version
112 $version = $asset_data['version'];
113 if ( file_exists( $frontend_js_path ) ) {
114 $version .= '.' . filemtime( $frontend_js_path );
115 }
116
117 // Enqueue the script with cache-busting version
118 wp_enqueue_script(
119 'ultimate-cursor-frontend',
120 $frontend_js_url,
121 $asset_data['dependencies'],
122 $version,
123 array(
124 'in_footer' => true,
125 'strategy' => 'defer', // Defer for optimal loading
126 )
127 );
128
129 // Load JS translations for any __() strings rendered on the frontend.
130 wp_set_script_translations(
131 'ultimate-cursor-frontend',
132 'ultimate-cursor',
133 ultimate_cursor()->plugin_path . 'languages'
134 );
135
136 // CRITICAL: Inject public path BEFORE the main script loads
137 // This ensures webpack knows where to load dynamic chunks from
138 // even when the main script is cached/minified by WP Rocket, LiteSpeed, etc.
139 $public_path_script = sprintf(
140 'window.__ultimateCursorPublicPath = %s;',
141 wp_json_encode( ultimate_cursor()->plugin_url . 'build/' )
142 );
143
144 wp_add_inline_script(
145 'ultimate-cursor-frontend',
146 $public_path_script,
147 'before' // Execute BEFORE the main script
148 );
149
150 // Use wp_add_inline_script + wp_json_encode instead of
151 // wp_localize_script to preserve data types (numbers, booleans).
152 // wp_localize_script casts every scalar to a string, which breaks
153 // components that do arithmetic on their settings (e.g. the
154 // snowflake cursor's fall speed turned "1" + Math.random() into
155 // string concatenation, rendering particles at NaN coordinates).
156 $cursor_data_script = sprintf(
157 'var ultimateCursorData = %s;',
158 wp_json_encode( $settings )
159 );
160
161 wp_add_inline_script(
162 'ultimate-cursor-frontend',
163 $cursor_data_script,
164 'before'
165 );
166 }
167 }
168
169
170 /**
171 * Enqueue frontend background animation assets with performance-first approach.
172 * Three.js is lazy-loaded only when background animation is enabled.
173 */
174 public function enqueue_frontend_background_assets() {
175 // Prevent multiple executions
176 static $executed = false;
177 if ( $executed ) {
178 return;
179 }
180 $executed = true;
181
182 $bg_settings = get_option( 'ultimate_cursor_background_settings', array() );
183
184 // SERVER-SIDE PREMIUM GATE: Strip premium-only background settings if no
185 // valid license exists, even if premium values were injected into the DB.
186 $bg_settings = Ultimate_Cursor_License_Gate::sanitize( $bg_settings, 'background' );
187
188 // Only load if background animation is enabled
189 if ( empty( $bg_settings['enabled'] ) ) {
190 return;
191 }
192
193 $enable_multiple = ! empty( $bg_settings['enableMultipleBackgrounds'] );
194
195 if ( $enable_multiple ) {
196 // Multiple backgrounds mode: check each config's scope individually.
197 // Filter out configs that don't match the current page.
198 $configs = isset( $bg_settings['backgroundConfigurations'] ) && is_array( $bg_settings['backgroundConfigurations'] )
199 ? $bg_settings['backgroundConfigurations']
200 : array();
201
202 $filtered_configs = array();
203 foreach ( $configs as $config ) {
204 $config_scope = isset( $config['scope'] ) ? $config['scope'] : 'entire-website';
205
206 if ( $config_scope === 'specific-pages' ) {
207 $specific_pages = isset( $config['specificPages'] ) ? $config['specificPages'] : '';
208 if ( $this->is_matching_page( $specific_pages ) ) {
209 $filtered_configs[] = $config;
210 }
211 } else {
212 // 'entire-website' or 'css-selector' — always include
213 $filtered_configs[] = $config;
214 }
215 }
216
217 // Don't load the script if no configs match the current page
218 if ( empty( $filtered_configs ) ) {
219 return;
220 }
221
222 // Pass only the matching configs to the frontend
223 $bg_settings['backgroundConfigurations'] = array_values( $filtered_configs );
224 } else {
225 // Single background mode: check top-level scope
226 $scope = isset( $bg_settings['scope'] ) ? $bg_settings['scope'] : 'entire-website';
227
228 if ( $scope === 'specific-pages' ) {
229 $specific_pages = isset( $bg_settings['specificPages'] ) ? $bg_settings['specificPages'] : '';
230 if ( ! $this->is_matching_page( $specific_pages ) ) {
231 return;
232 }
233 }
234 }
235
236 $asset_data = $this->get_asset_file( 'build/frontend-background' );
237
238 $bg_js_path = ultimate_cursor()->plugin_path . 'build/frontend-background.js';
239 $bg_js_url = ultimate_cursor()->plugin_url . 'build/frontend-background.js';
240
241 // Add filemtime-based cache busting
242 $version = $asset_data['version'];
243 if ( file_exists( $bg_js_path ) ) {
244 $version .= '.' . filemtime( $bg_js_path );
245 }
246
247 wp_enqueue_script(
248 'ultimate-cursor-frontend-background',
249 $bg_js_url,
250 $asset_data['dependencies'],
251 $version,
252 array(
253 'in_footer' => true,
254 'strategy' => 'defer',
255 )
256 );
257
258 // Load JS translations for any __() strings rendered by the background runtime.
259 wp_set_script_translations(
260 'ultimate-cursor-frontend-background',
261 'ultimate-cursor',
262 ultimate_cursor()->plugin_path . 'languages'
263 );
264
265 // Inject public path for chunk loading
266 $public_path_script = sprintf(
267 'window.__ultimateCursorBgPublicPath = %s;',
268 wp_json_encode( ultimate_cursor()->plugin_url . 'build/' )
269 );
270
271 wp_add_inline_script(
272 'ultimate-cursor-frontend-background',
273 $public_path_script,
274 'before'
275 );
276
277 // Use wp_add_inline_script + wp_json_encode instead of wp_localize_script
278 // to preserve data types (numbers, booleans) in backgroundConfigurations.
279 // wp_localize_script converts all scalar values to strings which breaks rendering.
280 $bg_data_script = sprintf(
281 'var ultimateCursorBgData = %s;',
282 wp_json_encode( $bg_settings )
283 );
284
285 wp_add_inline_script(
286 'ultimate-cursor-frontend-background',
287 $bg_data_script,
288 'before'
289 );
290 }
291
292 /**
293 * Check if the current page matches the specific pages list.
294 *
295 * Matching rules (kept in sync with JS src/frontend/cursor/scope.js
296 * matchesCurrentPage):
297 * - `home` or `/` → the site front page
298 * - a number → post/page ID
299 * - `foo` → exact slug (last path segment) or exact top-level path
300 * - `foo/bar` → exact full path
301 * - `foo/*` → prefix wildcard: `foo` and anything under `foo/…`
302 *
303 * No bare substring matching — `press` no longer matches `/pressroom`.
304 *
305 * @param string $pages_string Comma-separated list of page slugs, paths, or IDs.
306 * @return bool
307 */
308 private function is_matching_page( $pages_string ) {
309 if ( empty( $pages_string ) ) {
310 return false;
311 }
312
313 $current_url = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
314 $current_path = trim( (string) wp_parse_url( $current_url, PHP_URL_PATH ), '/' );
315
316 // Strip a leading index.php for consistency with the JS matcher.
317 if ( 'index.php' === $current_path ) {
318 $current_path = '';
319 }
320
321 $segments = array_filter( explode( '/', $current_path ) );
322 $current_slug = ! empty( $segments ) ? end( $segments ) : '';
323
324 $is_home = ( is_front_page() || is_home() );
325
326 foreach ( array_map( 'trim', explode( ',', $pages_string ) ) as $page ) {
327 $page = trim( $page, '/' );
328
329 // Homepage.
330 if ( ( $page === '' || strtolower( $page ) === 'home' ) && $is_home ) {
331 return true;
332 }
333
334 if ( $page === '' ) {
335 continue;
336 }
337
338 // Prefix wildcard: `foo/*`.
339 if ( substr( $page, -2 ) === '/*' ) {
340 $prefix = substr( $page, 0, -2 );
341 if ( $current_path === $prefix || strpos( $current_path, $prefix . '/' ) === 0 ) {
342 return true;
343 }
344 continue;
345 }
346
347 // Post/page ID.
348 if ( is_numeric( $page ) ) {
349 if ( is_singular() && (int) $page === get_queried_object_id() ) {
350 return true;
351 }
352 continue;
353 }
354
355 // Exact full path or exact slug.
356 if ( $current_path === $page || $current_slug === $page ) {
357 return true;
358 }
359 }
360
361 return false;
362 }
363
364 /**
365 * Enqueue admin pages assets.
366 */
367 public function admin_enqueue_scripts() {
368 $screen = get_current_screen();
369
370 if ( ! $screen || 'toplevel_page_ultimate-cursor' !== $screen->id ) {
371 return;
372 }
373
374 wp_add_inline_style( 'wp-admin', '.php-error #adminmenuback, .php-error #adminmenuwrap { margin-top: 0px !important; }' );
375
376 $asset_data = $this->get_asset_file( 'build/admin' );
377
378 wp_enqueue_script(
379 'ultimate-cursor-admin',
380 ultimate_cursor()->plugin_url . 'build/admin.js',
381 $asset_data['dependencies'],
382 $asset_data['version'],
383 true
384 );
385
386 // Load JS translations so the React dashboard's __() strings are translatable.
387 wp_set_script_translations(
388 'ultimate-cursor-admin',
389 'ultimate-cursor',
390 ultimate_cursor()->plugin_path . 'languages'
391 );
392
393 // Pass the cursor images
394 $cursor_images = array();
395 $cursor_shapes = array();
396
397 $cursor_dir = ultimate_cursor()->plugin_path . 'assets/cursors/';
398 $cursor_url = ultimate_cursor()->plugin_url . 'assets/cursors/';
399 $cursor_shapes_dir = ultimate_cursor()->plugin_path . 'assets/shapes/';
400 $cursor_shapes_url = ultimate_cursor()->plugin_url . 'assets/shapes/';
401
402 $extensions = array( 'png', 'jpg', 'jpeg', 'gif', 'svg', 'cur' );
403
404 foreach ( $extensions as $ext ) {
405 $files = glob( $cursor_dir . '*.' . $ext );
406 if ( $files ) {
407 foreach ( $files as $file ) {
408 $cursor_images[] = $cursor_url . basename( $file );
409 }
410 }
411 }
412
413 foreach ( $extensions as $ext ) {
414 $files = glob( $cursor_shapes_dir . '*.' . $ext );
415 if ( $files ) {
416 foreach ( $files as $file ) {
417 $cursor_shapes[] = $cursor_shapes_url . basename( $file );
418 }
419 }
420 }
421
422 // Use wp_add_inline_script + wp_json_encode instead of wp_localize_script:
423 // localize casts top-level scalars to strings ('isPro' => "1"/""), and the
424 // dashboard treats these as real booleans. Same rule as the frontend paths.
425 $admin_data = array(
426 'settings' => ( function () {
427 $settings = get_option( 'ultimate_cursor_settings', array() );
428 // SERVER-SIDE PREMIUM GATE: Sanitize admin settings output.
429 // This ensures premium fields are stripped if license is invalid.
430 $settings = Ultimate_Cursor_License_Gate::sanitize( $settings, 'cursor' );
431 return $settings;
432 } )(),
433 'backgroundSettings' => Ultimate_Cursor_License_Gate::sanitize(
434 get_option( 'ultimate_cursor_background_settings', array() ),
435 'background'
436 ),
437 'cursors' => $cursor_images,
438 'plugin_url' => ultimate_cursor()->plugin_url,
439 'version' => UCA_VERSION,
440 'shapes' => $cursor_shapes,
441 // isPro requires BOTH pro plugin active AND valid Freemius license
442 'isPro' => UltimateCursor::is_premium_active(),
443 'isLicenseValid' => UltimateCursor::is_premium_active(),
444 'proUrl' => 'https://wpxero.com/plugins/ultimate-cursor/pricing',
445 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
446 'nonce' => wp_create_nonce( 'ultimate_cursor_admin_nonce' ),
447 'activePlugins' => ( function () {
448 require_once ABSPATH . 'wp-admin/includes/plugin.php';
449 $active = get_option( 'active_plugins', array() );
450 if ( is_multisite() ) {
451 $active = array_merge( $active, array_keys( get_site_option( 'active_sitewide_plugins', array() ) ) );
452 }
453 $slugs = array();
454 foreach ( $active as $plugin ) {
455 $dirname = dirname( $plugin );
456 if ( $dirname !== '.' ) {
457 $slugs[] = $dirname;
458 }
459 }
460 return $slugs;
461 } )(),
462 );
463
464 $encoded = wp_json_encode( $admin_data );
465 wp_add_inline_script(
466 'ultimate-cursor-admin',
467 sprintf( 'var ultimateCursorAdminData = %s;', $encoded !== false ? $encoded : '{}' ),
468 'before'
469 );
470
471 wp_enqueue_style(
472 'ultimate-cursor-admin',
473 ultimate_cursor()->plugin_url . 'build/style-admin.css',
474 array(),
475 $asset_data['version']
476 );
477
478 // RTL locales: swap in the rtlcss-generated stylesheet the build emits.
479 wp_style_add_data( 'ultimate-cursor-admin', 'rtl', 'replace' );
480
481 // @wordpress/scripts splits stylesheets: `style.scss` imports land in
482 // build/style-admin.css (above), while any other-named CSS import
483 // (e.g. the control kit's kit.scss) is emitted to build/admin.css.
484 // Enqueue it too so those component styles actually load.
485 $admin_css = ultimate_cursor()->plugin_path . 'build/admin.css';
486 if ( file_exists( $admin_css ) ) {
487 wp_enqueue_style(
488 'ultimate-cursor-admin-components',
489 ultimate_cursor()->plugin_url . 'build/admin.css',
490 array( 'ultimate-cursor-admin' ),
491 $asset_data['version']
492 );
493 wp_style_add_data( 'ultimate-cursor-admin-components', 'rtl', 'replace' );
494 }
495
496 wp_enqueue_style( 'wp-components' );
497 }
498 }
499
500 Ultimate_Cursor_Assets::instance();
501