PluginProbe
GutSlider – All in One Slider and Carousel Blocks for Gutenberg / trunk
GutSlider – All in One Slider and Carousel Blocks for Gutenberg vtrunk
3.1.0 3.0.0 2.13.2 2.13.1 2.13.0 trunk 1.0.0 2.1.0 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.1 2.5.3 2.5.4 2.5.5 2.6.1 All 56 releases
slider-blocks / admin / admin.php

admin.php in GutSlider – All in One Slider and Carousel Blocks for Gutenberg trunk, at admin/admin.php

1,122 lines 35.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * GutSlider admin dashboard bootstrap.
4 *
5 * Registers the admin menu, loads dashboard assets, and exposes the REST
6 * endpoints used by the dashboard UI to persist changes.
7 *
8 * @package GutSliderBlocks
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 if ( ! class_exists( 'GutSlider_Admin' ) ) {
16
17 /**
18 * Admin dashboard controller.
19 */
20 class GutSlider_Admin {
21
22 /**
23 * Option key holding the general settings array.
24 *
25 * @var string
26 */
27 const SETTINGS_OPTION = 'gutslider_settings';
28
29 /**
30 * Top level menu slug.
31 *
32 * @var string
33 */
34 const MENU_SLUG = 'gutslider-blocks';
35
36 /**
37 * Page slug for the blocks manager.
38 *
39 * @var string
40 */
41 const BLOCKS_SLUG = 'gutslider-blocks-settings';
42
43 /**
44 * Page slug for the settings screen.
45 *
46 * @var string
47 */
48 const SETTINGS_SLUG = 'gutslider-settings';
49
50 /**
51 * Transient holding the cached content-usage scan.
52 *
53 * @var string
54 */
55 const USAGE_TRANSIENT = 'gutslider_usage_stats';
56
57 /**
58 * Option holding the onboarding checklist state.
59 *
60 * @var string
61 */
62 const ONBOARDING_OPTION = 'gutslider_onboarding';
63
64 /**
65 * Maximum number of posts inspected by the usage scan.
66 *
67 * @var int
68 */
69 const USAGE_SCAN_LIMIT = 500;
70
71 /**
72 * Default values for the general settings.
73 *
74 * @var array<string, mixed>
75 */
76 private static $settings_defaults = array(
77 'css_delivery' => 'file',
78 'google_fonts' => true,
79 'pattern_library' => true,
80 'remove_data' => false,
81 );
82
83 /**
84 * Constructor.
85 */
86 public function __construct() {
87 add_action( 'admin_menu', array( $this, 'admin_menu' ), 20 );
88 add_action( 'admin_enqueue_scripts', array( $this, 'admin_assets' ) );
89 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
90 add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
91 add_action( 'save_post', array( self::class, 'flush_usage_cache' ) );
92 add_action( 'deleted_post', array( self::class, 'flush_usage_cache' ) );
93 }
94
95 /* ---------------------------------------------------------------
96 * Settings helpers
97 * ------------------------------------------------------------- */
98
99 /**
100 * Get the general settings, merged over the defaults.
101 *
102 * @return array<string, mixed> Settings array.
103 */
104 public static function get_settings() {
105 $stored = get_option( self::SETTINGS_OPTION, array() );
106
107 if ( ! is_array( $stored ) ) {
108 $stored = array();
109 }
110
111 return wp_parse_args( $stored, self::$settings_defaults );
112 }
113
114 /**
115 * Get a single setting value.
116 *
117 * @param string $key Setting key.
118 * @param mixed $default Fallback when the key is unknown.
119 * @return mixed Setting value.
120 */
121 public static function get_setting( $key, $default = null ) {
122 $settings = self::get_settings();
123
124 return array_key_exists( $key, $settings ) ? $settings[ $key ] : $default;
125 }
126
127 /**
128 * Build the option key that stores a block's enabled state.
129 *
130 * @param string $block_name Block directory name.
131 * @return string Option key.
132 */
133 public static function block_option_key( $block_name ) {
134 return 'gut_' . str_replace( '-', '_', $block_name );
135 }
136
137 /**
138 * Read the block definitions from the shared data file.
139 *
140 * @return array<int, array<string, mixed>> Block definitions.
141 */
142 public function get_blocks() {
143 $blocks_file = GUTSLIDER_DIR . '/includes/Api/blocks.php';
144
145 if ( ! file_exists( $blocks_file ) ) {
146 return array();
147 }
148
149 $blocks = include $blocks_file;
150
151 return is_array( $blocks ) ? $blocks : array();
152 }
153
154 /**
155 * Decorate the block definitions with their current state.
156 *
157 * @return array<int, array<string, mixed>> Block definitions.
158 */
159 public function get_blocks_with_state() {
160 $has_pro = defined( 'GUTSLIDER_PRO_VERSION' );
161 $blocks = array();
162
163 foreach ( $this->get_blocks() as $block ) {
164 $is_pro = ! empty( $block['is_pro'] );
165
166 $block['is_pro'] = $is_pro;
167 $block['locked'] = $is_pro && ! $has_pro;
168 $block['option_key'] = self::block_option_key( $block['name'] );
169 $block['enabled'] = $block['locked']
170 ? false
171 : (bool) get_option( $block['option_key'], true );
172
173 $blocks[] = $block;
174 }
175
176 return $blocks;
177 }
178
179 /**
180 * Count how many blocks are currently enabled.
181 *
182 * @param array<int, array<string, mixed>> $blocks Decorated blocks.
183 * @return int Enabled block count.
184 */
185 public static function count_enabled( array $blocks ) {
186 $count = 0;
187
188 foreach ( $blocks as $block ) {
189 if ( ! empty( $block['enabled'] ) ) {
190 ++$count;
191 }
192 }
193
194 return $count;
195 }
196
197 /* ---------------------------------------------------------------
198 * Menu + assets
199 * ------------------------------------------------------------- */
200
201 /**
202 * Register the admin menu and its sub pages.
203 *
204 * @return void
205 */
206 public function admin_menu() {
207 $icon = 'data:image/svg+xml;base64,' . base64_encode( '<svg width="20" height="20" viewBox="0 0 45 45" xmlns="http://www.w3.org/2000/svg"><path fill="black" d="M5 45C3.625 45 2.44833 44.5108 1.47 43.5325C0.491667 42.5542 0.00166667 41.3767 0 40V5C0 3.625 0.49 2.44833 1.47 1.47C2.45 0.491667 3.62667 0.00166667 5 0H40C41.375 0 42.5525 0.49 43.5325 1.47C44.5125 2.45 45.0017 3.62667 45 5V40C45 41.375 44.5108 42.5525 43.5325 43.5325C42.5542 44.5125 41.3767 45.0017 40 45H5ZM17.5 35H27.5C28.875 35 30.0525 34.5108 31.0325 33.5325C32.0125 32.5542 32.5017 31.3767 32.5 30V20H22.5V25H27.5V30H17.5V15H32.5C32.5 13.625 32.0108 12.4483 31.0325 11.47C30.0542 10.4917 28.8767 10.0017 27.5 10H17.5C16.125 10 14.9483 10.49 13.97 11.47C12.9917 12.45 12.5017 13.6267 12.5 15V30C12.5 31.375 12.99 32.5525 13.97 33.5325C14.95 34.5125 16.1267 35.0017 17.5 35Z"/></svg>' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Inline menu icon.
208
209 add_menu_page(
210 __( 'GutSlider', 'slider-blocks' ),
211 __( 'GutSlider', 'slider-blocks' ),
212 'manage_options',
213 self::MENU_SLUG,
214 array( $this, 'render_dashboard_page' ),
215 $icon,
216 100
217 );
218
219 add_submenu_page(
220 self::MENU_SLUG,
221 __( 'Dashboard', 'slider-blocks' ),
222 __( 'Dashboard', 'slider-blocks' ),
223 'manage_options',
224 self::MENU_SLUG,
225 array( $this, 'render_dashboard_page' )
226 );
227
228 add_submenu_page(
229 self::MENU_SLUG,
230 __( 'Blocks', 'slider-blocks' ),
231 __( 'Blocks', 'slider-blocks' ),
232 'manage_options',
233 self::BLOCKS_SLUG,
234 array( $this, 'render_blocks_page' )
235 );
236
237 add_submenu_page(
238 self::MENU_SLUG,
239 __( 'Settings', 'slider-blocks' ),
240 __( 'Settings', 'slider-blocks' ),
241 'manage_options',
242 self::SETTINGS_SLUG,
243 array( $this, 'render_settings_page' )
244 );
245 }
246
247 /**
248 * Screen IDs that should receive the dashboard assets.
249 *
250 * @return array<int, string> Screen IDs.
251 */
252 public static function screen_ids() {
253 return array(
254 'toplevel_page_' . self::MENU_SLUG,
255 'gutslider_page_' . self::BLOCKS_SLUG,
256 'gutslider_page_' . self::SETTINGS_SLUG,
257 'gutslider_page_gutslider-license',
258 );
259 }
260
261 /**
262 * Whether the current request is a GutSlider dashboard screen.
263 *
264 * @param string $screen Current screen ID.
265 * @return bool True on a dashboard screen.
266 */
267 public static function is_dashboard_screen( $screen = '' ) {
268 if ( '' === $screen ) {
269 $current = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
270 $screen = $current ? $current->id : '';
271 }
272
273 return in_array( $screen, self::screen_ids(), true );
274 }
275
276 /**
277 * Enqueue the dashboard stylesheet and script.
278 *
279 * @param string $screen Current screen ID.
280 * @return void
281 */
282 public function admin_assets( $screen ) {
283 if ( ! self::is_dashboard_screen( $screen ) ) {
284 return;
285 }
286
287 wp_enqueue_style(
288 'gutslider-admin',
289 GUTSLIDER_URL . 'admin/css/admin.css',
290 array(),
291 GUTSLIDER_VERSION
292 );
293
294 wp_enqueue_script(
295 'gutslider-admin',
296 GUTSLIDER_URL . 'admin/js/admin.js',
297 array( 'wp-i18n' ),
298 GUTSLIDER_VERSION,
299 true
300 );
301
302 wp_localize_script(
303 'gutslider-admin',
304 'gutslider',
305 array(
306 'version' => GUTSLIDER_VERSION,
307 'isPro' => defined( 'GUTSLIDER_PRO_VERSION' ),
308 'proVersion' => defined( 'GUTSLIDER_PRO_VERSION' ) ? GUTSLIDER_PRO_VERSION : '',
309 'restUrl' => esc_url_raw( rest_url( 'gutslider/v1/' ) ),
310 'nonce' => wp_create_nonce( 'wp_rest' ),
311 'pricingUrl' => 'https://gutslider.com/pricing',
312 'i18n' => array(
313 'saved' => __( 'Changes saved', 'slider-blocks' ),
314 'saveFailed' => __( 'Could not save. Please try again.', 'slider-blocks' ),
315 'discarded' => __( 'Changes discarded', 'slider-blocks' ),
316 'cacheCleared' => __( 'Style cache cleared', 'slider-blocks' ),
317 /* translators: %d: number of unsaved changes. */
318 'changeSingle' => __( '%d unsaved change', 'slider-blocks' ),
319 /* translators: %d: number of unsaved changes. */
320 'changePlural' => __( '%d unsaved changes', 'slider-blocks' ),
321 'leaveWarning' => __( 'You have unsaved changes.', 'slider-blocks' ),
322 /* translators: 1: enabled block count, 2: total block count. */
323 'enabledCount' => __( '%1$d of %2$d enabled', 'slider-blocks' ),
324 /* translators: 1: free block count, 2: pro block count, 3: enabled count, 4: total count. */
325 'blockSummary' => __( '%1$d free blocks · %2$d pro blocks · %3$d of %4$d enabled', 'slider-blocks' ),
326 'allEnabled' => __( 'All blocks enabled', 'slider-blocks' ),
327 /* translators: %d: number of disabled blocks. */
328 'someDisabled' => __( '%d turned off', 'slider-blocks' ),
329 ),
330 )
331 );
332 }
333
334 /**
335 * Add a marker class so the stylesheet can reset the admin chrome.
336 *
337 * @param string $classes Existing body classes.
338 * @return string Filtered body classes.
339 */
340 public function admin_body_class( $classes ) {
341 if ( self::is_dashboard_screen() ) {
342 $classes .= ' gutslider-admin-page ';
343 }
344
345 return $classes;
346 }
347
348 /* ---------------------------------------------------------------
349 * Dashboard data
350 * ------------------------------------------------------------- */
351
352 /**
353 * Scan post content for GutSlider blocks.
354 *
355 * Counts how many slider blocks are in use and how many posts they
356 * live on. The result is cached because it is a full-text scan.
357 *
358 * @return array{sliders:int, posts:int, capped:bool} Usage figures.
359 */
360 public static function get_usage_stats() {
361 $cached = get_transient( self::USAGE_TRANSIENT );
362
363 if ( is_array( $cached ) ) {
364 return $cached;
365 }
366
367 global $wpdb;
368
369 $needle = '<!-- wp:gutsliders/';
370 $like = '%' . $wpdb->esc_like( $needle ) . '%';
371
372 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Content scan, cached in a transient below.
373 $contents = $wpdb->get_col(
374 $wpdb->prepare(
375 "SELECT post_content FROM {$wpdb->posts}
376 WHERE post_type NOT IN ( 'revision', 'nav_menu_item' )
377 AND post_status NOT IN ( 'auto-draft', 'trash', 'inherit' )
378 AND post_content LIKE %s
379 LIMIT %d",
380 $like,
381 self::USAGE_SCAN_LIMIT
382 )
383 );
384
385 $contents = is_array( $contents ) ? $contents : array();
386 $sliders = 0;
387
388 foreach ( $contents as $content ) {
389 $sliders += substr_count( (string) $content, $needle );
390 }
391
392 $stats = array(
393 'sliders' => $sliders,
394 'posts' => count( $contents ),
395 'capped' => count( $contents ) >= self::USAGE_SCAN_LIMIT,
396 );
397
398 set_transient( self::USAGE_TRANSIENT, $stats, 12 * HOUR_IN_SECONDS );
399
400 return $stats;
401 }
402
403 /**
404 * Drop the cached usage scan.
405 *
406 * @return void
407 */
408 public static function flush_usage_cache() {
409 delete_transient( self::USAGE_TRANSIENT );
410 }
411
412 /**
413 * Measure the stylesheets GutSlider has generated.
414 *
415 * @return array{files:int, bytes:int, average:int} Stylesheet figures.
416 */
417 public static function get_css_stats() {
418 $upload = wp_upload_dir();
419 $dir = trailingslashit( $upload['basedir'] ) . 'gutslider-styles';
420 $files = is_dir( $dir ) ? glob( $dir . '/*.css' ) : array();
421 $files = is_array( $files ) ? $files : array();
422 $bytes = 0;
423
424 foreach ( $files as $file ) {
425 $bytes += (int) filesize( $file );
426 }
427
428 $count = count( $files );
429
430 return array(
431 'files' => $count,
432 'bytes' => $bytes,
433 'average' => $count > 0 ? (int) round( $bytes / $count ) : 0,
434 );
435 }
436
437 /**
438 * Get the Pro licence status.
439 *
440 * The free plugin has no licence data of its own, so the Pro plugin
441 * supplies it through the `gutslider_license_status` filter. Anything
442 * missing simply degrades to a quieter pill.
443 *
444 * @return array{state:string, label:string}|null Licence data, or null when unavailable.
445 */
446 public static function get_license() {
447 if ( ! defined( 'GUTSLIDER_PRO_VERSION' ) ) {
448 return null;
449 }
450
451 /**
452 * Filter the licence status shown in the dashboard header.
453 *
454 * @param array|null $status Associative array with `state` (active|expiring|inactive)
455 * and either a ready-made `label` or an `expires` timestamp.
456 */
457 $status = apply_filters( 'gutslider_license_status', null );
458
459 if ( ! is_array( $status ) ) {
460 return array(
461 'state' => 'active',
462 'label' => __( 'GutSlider Pro active', 'slider-blocks' ),
463 );
464 }
465
466 $state = isset( $status['state'] ) ? (string) $status['state'] : 'active';
467 $label = isset( $status['label'] ) ? (string) $status['label'] : '';
468
469 if ( '' === $label && ! empty( $status['expires'] ) ) {
470 $expires = is_numeric( $status['expires'] ) ? (int) $status['expires'] : strtotime( (string) $status['expires'] );
471
472 if ( $expires ) {
473 $label = sprintf(
474 /* translators: %s: licence renewal date. */
475 'expiring' === $state ? __( 'License expires %s', 'slider-blocks' ) : __( 'License active · renews %s', 'slider-blocks' ),
476 date_i18n( 'j M Y', $expires )
477 );
478 }
479 }
480
481 if ( '' === $label ) {
482 $label = 'inactive' === $state
483 ? __( 'License inactive', 'slider-blocks' )
484 : __( 'GutSlider Pro active', 'slider-blocks' );
485 }
486
487 return array(
488 'state' => in_array( $state, array( 'active', 'expiring', 'inactive' ), true ) ? $state : 'active',
489 'label' => $label,
490 );
491 }
492
493 /**
494 * Get the banner shown across the top of the dashboard.
495 *
496 * The free plugin owns the markup and styling; anything with
497 * something to say — the Pro plugin's licence prompt, for example —
498 * supplies the copy through the filter.
499 *
500 * @return array{tone:string, title:string, text:string, action_label:string, action_url:string}|null
501 * Normalised notice, or null when there is nothing to show.
502 */
503 public static function get_dashboard_notice() {
504 /**
505 * Filter the dashboard banner.
506 *
507 * Return an empty array for no banner, or an array with:
508 * `tone` (info|warning|error), `title`, `text`, `action_label`
509 * and `action_url`. A notice without a title is discarded.
510 *
511 * @param array $notice Notice definition.
512 */
513 $notice = apply_filters( 'gutslider_dashboard_notice', array() );
514
515 if ( ! is_array( $notice ) || empty( $notice['title'] ) ) {
516 return null;
517 }
518
519 $tone = isset( $notice['tone'] ) ? (string) $notice['tone'] : 'info';
520
521 return array(
522 'tone' => in_array( $tone, array( 'info', 'warning', 'error' ), true ) ? $tone : 'info',
523 'title' => (string) $notice['title'],
524 'text' => isset( $notice['text'] ) ? (string) $notice['text'] : '',
525 'action_label' => isset( $notice['action_label'] ) ? (string) $notice['action_label'] : '',
526 'action_url' => isset( $notice['action_url'] ) ? (string) $notice['action_url'] : '',
527 );
528 }
529
530 /**
531 * Read the current release notes out of readme.txt.
532 *
533 * @param int $limit Maximum number of entries to return.
534 * @return array<int, array{tag:string, label:string, text:string}> Release notes.
535 */
536 public static function get_changelog( $limit = 4 ) {
537 $readme = GUTSLIDER_DIR . '/readme.txt';
538
539 if ( ! is_readable( $readme ) ) {
540 return array();
541 }
542
543 $contents = file_get_contents( $readme ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local plugin file.
544
545 if ( ! is_string( $contents ) ) {
546 return array();
547 }
548
549 // Grab the block of lines under the heading for the running version.
550 $pattern = '/^=\s*' . preg_quote( GUTSLIDER_VERSION, '/' ) . '\s*=\s*$(.*?)(?=^=\s|\z)/ms';
551
552 if ( ! preg_match( $pattern, $contents, $matches ) ) {
553 return array();
554 }
555
556 $tags = array(
557 'added' => 'new',
558 'new' => 'new',
559 'fixed' => 'fixed',
560 'fix' => 'fixed',
561 'improved' => 'improved',
562 'updated' => 'improved',
563 'optimized' => 'improved',
564 'enhanced' => 'improved',
565 'changed' => 'improved',
566 'checked' => 'improved',
567 );
568
569 $labels = array(
570 'new' => __( 'New', 'slider-blocks' ),
571 'improved' => __( 'Improved', 'slider-blocks' ),
572 'fixed' => __( 'Fixed', 'slider-blocks' ),
573 );
574
575 $entries = array();
576
577 foreach ( preg_split( '/\R/', $matches[1] ) as $line ) {
578 $line = trim( $line );
579
580 if ( '' === $line || '*' !== $line[0] ) {
581 continue;
582 }
583
584 $text = trim( ltrim( $line, '*' ) );
585 $tag = 'new';
586
587 if ( preg_match( '/^([A-Za-z]+)\s*:\s*(.+)$/', $text, $prefix ) ) {
588 $key = strtolower( $prefix[1] );
589
590 if ( isset( $tags[ $key ] ) ) {
591 $tag = $tags[ $key ];
592 $text = trim( $prefix[2] );
593 }
594 } else {
595 $first = strtolower( strtok( $text, ' ' ) );
596 $tag = isset( $tags[ $first ] ) ? $tags[ $first ] : 'new';
597 }
598
599 $entries[] = array(
600 'tag' => $tag,
601 'label' => $labels[ $tag ],
602 'text' => $text,
603 );
604
605 if ( count( $entries ) >= $limit ) {
606 break;
607 }
608 }
609
610 return $entries;
611 }
612
613 /**
614 * Mark an onboarding step as done.
615 *
616 * @param string $step Step key.
617 * @return void
618 */
619 public static function complete_onboarding_step( $step ) {
620 $state = get_option( self::ONBOARDING_OPTION, array() );
621
622 if ( ! is_array( $state ) ) {
623 $state = array();
624 }
625
626 if ( empty( $state[ $step ] ) ) {
627 $state[ $step ] = true;
628 update_option( self::ONBOARDING_OPTION, $state );
629 }
630 }
631
632 /**
633 * Whether an onboarding step has been completed.
634 *
635 * @param string $step Step key.
636 * @return bool True when done.
637 */
638 public static function onboarding_done( $step ) {
639 $state = get_option( self::ONBOARDING_OPTION, array() );
640
641 return is_array( $state ) && ! empty( $state[ $step ] );
642 }
643
644 /* ---------------------------------------------------------------
645 * Icons
646 * ------------------------------------------------------------- */
647
648 /**
649 * Inline SVG bodies, drawn on a 24x24 grid with a 1.7px stroke.
650 *
651 * @return array<string, string> Map of icon name to SVG children.
652 */
653 private static function icon_paths() {
654 static $paths = null;
655
656 if ( null !== $paths ) {
657 return $paths;
658 }
659
660 $paths = array(
661 'sun' => '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>',
662 'moon' => '<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>',
663 'book-open' => '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
664 'plus' => '<path d="M5 12h14M12 5v14"/>',
665 'sliders-horizontal' => '<path d="M21 4h-7M10 4H3M21 12h-9M8 12H3M21 20h-5M12 20H3M14 2v4M8 10v4M16 18v4"/>',
666 'layout-grid' => '<rect width="7" height="7" x="3" y="3" rx="1.5"/><rect width="7" height="7" x="14" y="3" rx="1.5"/><rect width="7" height="7" x="14" y="14" rx="1.5"/><rect width="7" height="7" x="3" y="14" rx="1.5"/>',
667 'layers' => '<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>',
668 'gauge' => '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
669 'type' => '<path d="M4 7V4h16v3M9 20h6M12 4v16"/>',
670 'search' => '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
671 'arrow-right' => '<path d="M5 12h14M12 5l7 7-7 7"/>',
672 'check' => '<path d="M20 6 9 17l-5-5"/>',
673 'play' => '<path d="M7 4.5v15l13-7.5z" fill="currentColor" stroke="none"/>',
674 'life-buoy' => '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/><path d="m4.93 4.93 4.24 4.24M14.83 9.17l4.24-4.24M14.83 14.83l4.24 4.24M9.17 14.83l-4.24 4.24"/>',
675 'star' => '<path d="m12 2.6 2.9 5.9 6.5.9-4.7 4.6 1.1 6.4-5.8-3-5.8 3 1.1-6.4L2.6 9.4l6.5-.9z"/>',
676 'chevron-right' => '<path d="m9 18 6-6-6-6"/>',
677 'layout-template' => '<rect width="18" height="7" x="3" y="3" rx="1.5"/><rect width="9" height="7" x="3" y="14" rx="1.5"/><rect width="5" height="7" x="16" y="14" rx="1.5"/>',
678 'chevrons-down-up' => '<path d="m7 20 5-5 5 5M7 4l5 5 5-5"/>',
679 'map-pin' => '<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z"/><circle cx="12" cy="10" r="3"/>',
680 'lock' => '<rect width="16" height="11" x="4" y="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/>',
681 'key' => '<circle cx="7.5" cy="15.5" r="4.5"/><path d="m10.7 12.3 9.3-9.3M17 5l2.5 2.5M14 8l2.5 2.5"/>',
682 'alert-triangle' => '<path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3z"/><path d="M12 9v4M12 17h.01"/>',
683 'external' => '<path d="M14 4h6v6M20 4l-8 8M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/>',
684
685 /* Block glyphs. */
686 'gallery-horizontal' => '<path d="M2 7v10M6 5v14"/><rect width="12" height="18" x="10" y="3" rx="2"/>',
687 'move-horizontal' => '<path d="m18 8 4 4-4 4M2 12h20M6 8l-4 4 4 4"/>',
688 'quote' => '<path d="M10 11H6.5A2.5 2.5 0 0 1 4 8.5v-.5A3 3 0 0 1 7 5h1M10 11v3a5 5 0 0 1-5 5"/><path d="M21 11h-3.5A2.5 2.5 0 0 1 15 8.5v-.5a3 3 0 0 1 3-3h1M21 11v3a5 5 0 0 1-5 5"/>',
689 'images' => '<rect width="16" height="16" x="6" y="2" rx="2"/><path d="M18 22H4a2 2 0 0 1-2-2V6"/><circle cx="12" cy="8" r="1.6"/><path d="m22 13-1.3-1.3a2.4 2.4 0 0 0-3.4 0L11 18"/>',
690 'image' => '<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="1.8"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>',
691 'video' => '<path d="m22 8-6 4 6 4z"/><rect width="14" height="12" x="2" y="6" rx="2"/>',
692 'newspaper' => '<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2zm0 0a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h2"/><path d="M18 14h-8M15 18h-5M10 6h8v4h-8z"/>',
693 'gallery-thumbnails' => '<rect width="18" height="14" x="3" y="3" rx="2"/><path d="M4 21h1M9.5 21h1M15 21h1M20 21h1"/>',
694 'columns' => '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/>',
695 'blinds' => '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18M15 3v18"/>',
696 'sparkles' => '<path d="m12 3 1.9 5 5.1 1.9-5.1 1.9L12 17l-1.9-5.2L5 9.9 10.1 8z"/><path d="M19 15.5 19.7 18l2.3.8-2.3.8L19 22l-.7-2.4-2.3-.8 2.3-.8z"/>',
697 'scissors' => '<circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M8.12 8.12 20 20M20 4 8.12 15.88"/>',
698 'shirt' => '<path d="M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z"/>',
699 'rows' => '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M21 9H3M21 15H3"/>',
700 'box' => '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="m3.3 7 8.7 5 8.7-5M12 22V12"/>',
701 'panorama' => '<rect width="20" height="12" x="2" y="6" rx="2"/><path d="m6 14 3-3 3 3 3-4 3 4"/>',
702 'credit-card' => '<rect width="20" height="14" x="2" y="5" rx="2"/><path d="M2 10h20"/>',
703 'heart' => '<path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/>',
704 'pointer' => '<path d="m3.7 3.04 6.5 16a.5.5 0 0 0 .95-.06l1.57-6.09a2 2 0 0 1 1.44-1.48l6.13-1.58a.5.5 0 0 0 .06-.94l-16-6.5a.5.5 0 0 0-.65.65z"/>',
705 'shopping-bag' => '<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18M16 10a4 4 0 0 1-8 0"/>',
706 'tags' => '<path d="M12 2H2v10l9.29 9.29a2 2 0 0 0 2.83 0l6.58-6.58a2 2 0 0 0 0-2.83z"/><path d="M7 7h.01"/>',
707 );
708
709 return $paths;
710 }
711
712 /**
713 * Render an inline icon.
714 *
715 * @param string $name Icon name.
716 * @param string $classes Additional classes.
717 * @return string SVG markup, or an empty string for unknown icons.
718 */
719 public static function get_icon( $name, $classes = '' ) {
720 $paths = self::icon_paths();
721
722 if ( ! isset( $paths[ $name ] ) ) {
723 return '';
724 }
725
726 return sprintf(
727 '<svg class="gs-icon%s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">%s</svg>',
728 $classes ? ' ' . esc_attr( $classes ) : '',
729 $paths[ $name ]
730 );
731 }
732
733 /**
734 * Echo an inline icon.
735 *
736 * @param string $name Icon name.
737 * @param string $classes Additional classes.
738 * @return void
739 */
740 public static function icon( $name, $classes = '' ) {
741 echo self::get_icon( $name, $classes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static inline SVG.
742 }
743
744 /**
745 * Map a block to its glyph.
746 *
747 * @param string $block_name Block directory name.
748 * @return string Icon name.
749 */
750 public static function block_icon( $block_name ) {
751 $map = array(
752 'content-slider' => 'gallery-horizontal',
753 'marquee' => 'move-horizontal',
754 'any-content' => 'layout-template',
755 'testimonial-slider' => 'quote',
756 'photo-carousel' => 'images',
757 'logo-carousel' => 'gallery-thumbnails',
758 'before-after' => 'columns',
759 'videos-carousel' => 'video',
760 'post-slider' => 'newspaper',
761 'shader-slider' => 'sparkles',
762 'shutters-slider' => 'blinds',
763 'slicer-slider' => 'scissors',
764 'fashion-slider' => 'shirt',
765 'triple-slider' => 'rows',
766 'spring-carousel' => 'box',
767 'panorama-carousel' => 'panorama',
768 'three-d-carousel' => 'box',
769 'card-slider' => 'credit-card',
770 'marquee-carousel' => 'move-horizontal',
771 'material-carousel' => 'layout-grid',
772 'tinder-slider' => 'heart',
773 'hover-slider' => 'pointer',
774 'product-carousel' => 'shopping-bag',
775 'product-categories-carousel' => 'tags',
776 );
777
778 return isset( $map[ $block_name ] ) ? $map[ $block_name ] : 'layers';
779 }
780
781 /* ---------------------------------------------------------------
782 * Views
783 * ------------------------------------------------------------- */
784
785 /**
786 * Load a dashboard view.
787 *
788 * @param string $view View file name without extension.
789 * @param array<string, mixed> $args Variables exposed to the view.
790 * @return void
791 */
792 private function view( $view, array $args = array() ) {
793 $file = GUTSLIDER_DIR . '/admin/views/' . $view . '.php';
794
795 if ( ! file_exists( $file ) ) {
796 return;
797 }
798
799 $admin = $this;
800 $has_pro = defined( 'GUTSLIDER_PRO_VERSION' );
801
802 // phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- Controlled, internal view data.
803 extract( $args, EXTR_SKIP );
804
805 include $file;
806 }
807
808 /**
809 * Render the dashboard (overview) screen.
810 *
811 * @return void
812 */
813 public function render_dashboard_page() {
814 $blocks = $this->get_blocks_with_state();
815
816 $this->view(
817 'dashboard',
818 array(
819 'blocks' => $blocks,
820 'total' => count( $blocks ),
821 'enabled' => self::count_enabled( $blocks ),
822 'current' => self::MENU_SLUG,
823 'settings' => self::get_settings(),
824 'usage' => self::get_usage_stats(),
825 'css_stats' => self::get_css_stats(),
826 'changelog' => self::get_changelog(),
827 )
828 );
829 }
830
831 /**
832 * Render the blocks manager screen.
833 *
834 * @return void
835 */
836 public function render_blocks_page() {
837 $blocks = $this->get_blocks_with_state();
838
839 $this->view(
840 'blocks',
841 array(
842 'blocks' => $blocks,
843 'total' => count( $blocks ),
844 'enabled' => self::count_enabled( $blocks ),
845 'current' => self::BLOCKS_SLUG,
846 )
847 );
848 }
849
850 /**
851 * Render the settings screen.
852 *
853 * @return void
854 */
855 public function render_settings_page() {
856 $this->view(
857 'settings',
858 array(
859 'settings' => self::get_settings(),
860 'current' => self::SETTINGS_SLUG,
861 )
862 );
863 }
864
865 /* ---------------------------------------------------------------
866 * REST endpoints
867 * ------------------------------------------------------------- */
868
869 /**
870 * Register the dashboard REST routes.
871 *
872 * @return void
873 */
874 public function register_routes() {
875 register_rest_route(
876 'gutslider/v1',
877 '/dashboard/blocks',
878 array(
879 'methods' => WP_REST_Server::CREATABLE,
880 'callback' => array( $this, 'rest_save_blocks' ),
881 'permission_callback' => array( $this, 'can_manage' ),
882 'args' => array(
883 'blocks' => array(
884 'required' => true,
885 'type' => 'object',
886 ),
887 ),
888 )
889 );
890
891 register_rest_route(
892 'gutslider/v1',
893 '/dashboard/settings',
894 array(
895 'methods' => WP_REST_Server::CREATABLE,
896 'callback' => array( $this, 'rest_save_settings' ),
897 'permission_callback' => array( $this, 'can_manage' ),
898 'args' => array(
899 'settings' => array(
900 'required' => true,
901 'type' => 'object',
902 ),
903 ),
904 )
905 );
906
907 register_rest_route(
908 'gutslider/v1',
909 '/dashboard/clear-cache',
910 array(
911 'methods' => WP_REST_Server::CREATABLE,
912 'callback' => array( $this, 'rest_clear_cache' ),
913 'permission_callback' => array( $this, 'can_manage' ),
914 )
915 );
916 }
917
918 /**
919 * Permission callback for the dashboard routes.
920 *
921 * @return bool True when the user may manage options.
922 */
923 public function can_manage() {
924 return current_user_can( 'manage_options' );
925 }
926
927 /**
928 * Persist the enabled state of one or more blocks.
929 *
930 * Writes both the per-block option consumed by the block registrar
931 * and the `active` flag on the cached block list, keeping the two
932 * storage formats in sync.
933 *
934 * @param WP_REST_Request $request Incoming request.
935 * @return WP_REST_Response|WP_Error Response payload.
936 */
937 public function rest_save_blocks( WP_REST_Request $request ) {
938 $requested = $request->get_param( 'blocks' );
939
940 if ( ! is_array( $requested ) || empty( $requested ) ) {
941 return new WP_Error(
942 'gutslider_invalid_payload',
943 __( 'No block changes were supplied.', 'slider-blocks' ),
944 array( 'status' => 400 )
945 );
946 }
947
948 $has_pro = defined( 'GUTSLIDER_PRO_VERSION' );
949 $known = array();
950
951 foreach ( $this->get_blocks() as $block ) {
952 $known[ $block['name'] ] = ! empty( $block['is_pro'] );
953 }
954
955 $saved = array();
956
957 foreach ( $requested as $name => $enabled ) {
958 $name = sanitize_text_field( (string) $name );
959
960 if ( ! isset( $known[ $name ] ) ) {
961 continue;
962 }
963
964 // Pro blocks cannot be enabled without the Pro plugin.
965 if ( $known[ $name ] && ! $has_pro ) {
966 continue;
967 }
968
969 $enabled = rest_sanitize_boolean( $enabled );
970
971 /*
972 * Store '1'/'0' rather than a boolean: update_option() treats a
973 * `false` value as identical to a missing option and skips the
974 * write, which would silently drop the first "disable" of a
975 * block that has never been toggled before.
976 */
977 update_option( self::block_option_key( $name ), $enabled ? '1' : '0' );
978 $saved[ $name ] = $enabled;
979 }
980
981 $this->sync_block_list( $saved );
982 self::complete_onboarding_step( 'blocks' );
983
984 return rest_ensure_response(
985 array(
986 'success' => true,
987 'blocks' => $saved,
988 )
989 );
990 }
991
992 /**
993 * Mirror enabled states onto the cached `gutslider_blocks` option.
994 *
995 * @param array<string, bool> $saved Map of block name to enabled state.
996 * @return void
997 */
998 private function sync_block_list( array $saved ) {
999 if ( empty( $saved ) ) {
1000 return;
1001 }
1002
1003 $blocks = get_option( 'gutslider_blocks' );
1004
1005 if ( ! is_array( $blocks ) || empty( $blocks ) ) {
1006 return;
1007 }
1008
1009 $changed = false;
1010
1011 foreach ( $blocks as $index => $block ) {
1012 if ( isset( $block['name'], $saved[ $block['name'] ] ) ) {
1013 $blocks[ $index ]['active'] = $saved[ $block['name'] ];
1014 $changed = true;
1015 }
1016 }
1017
1018 if ( $changed ) {
1019 update_option( 'gutslider_blocks', $blocks );
1020 }
1021 }
1022
1023 /**
1024 * Persist the general settings.
1025 *
1026 * @param WP_REST_Request $request Incoming request.
1027 * @return WP_REST_Response|WP_Error Response payload.
1028 */
1029 public function rest_save_settings( WP_REST_Request $request ) {
1030 $incoming = $request->get_param( 'settings' );
1031
1032 if ( ! is_array( $incoming ) ) {
1033 return new WP_Error(
1034 'gutslider_invalid_payload',
1035 __( 'No settings were supplied.', 'slider-blocks' ),
1036 array( 'status' => 400 )
1037 );
1038 }
1039
1040 $settings = self::get_settings();
1041 $previous_delivery = $settings['css_delivery'];
1042
1043 if ( isset( $incoming['css_delivery'] ) ) {
1044 $settings['css_delivery'] = in_array( $incoming['css_delivery'], array( 'file', 'inline' ), true )
1045 ? $incoming['css_delivery']
1046 : 'file';
1047 }
1048
1049 if ( isset( $incoming['google_fonts'] ) ) {
1050 $settings['google_fonts'] = rest_sanitize_boolean( $incoming['google_fonts'] );
1051 }
1052
1053 if ( isset( $incoming['pattern_library'] ) ) {
1054 $settings['pattern_library'] = rest_sanitize_boolean( $incoming['pattern_library'] );
1055 }
1056
1057 if ( isset( $incoming['remove_data'] ) ) {
1058 $settings['remove_data'] = rest_sanitize_boolean( $incoming['remove_data'] );
1059 }
1060
1061 update_option( self::SETTINGS_OPTION, $settings );
1062 self::complete_onboarding_step( 'settings' );
1063
1064 /*
1065 * Switching to inline delivery leaves the previously generated
1066 * stylesheets orphaned in the uploads directory, so clear them out.
1067 */
1068 if ( 'inline' === $settings['css_delivery'] && 'inline' !== $previous_delivery ) {
1069 self::delete_generated_css();
1070 }
1071
1072 return rest_ensure_response(
1073 array(
1074 'success' => true,
1075 'settings' => $settings,
1076 )
1077 );
1078 }
1079
1080 /**
1081 * Delete every generated stylesheet in the uploads directory.
1082 *
1083 * @return WP_REST_Response Response payload.
1084 */
1085 public function rest_clear_cache() {
1086 return rest_ensure_response(
1087 array(
1088 'success' => true,
1089 'deleted' => self::delete_generated_css(),
1090 )
1091 );
1092 }
1093
1094 /**
1095 * Remove all generated stylesheets from the uploads directory.
1096 *
1097 * @return int Number of files deleted.
1098 */
1099 private static function delete_generated_css() {
1100 $upload_dir = wp_upload_dir();
1101 $css_dir = trailingslashit( $upload_dir['basedir'] ) . 'gutslider-styles';
1102 $deleted = 0;
1103
1104 if ( is_dir( $css_dir ) ) {
1105 $files = glob( $css_dir . '/*.css' );
1106
1107 if ( is_array( $files ) ) {
1108 foreach ( $files as $file ) {
1109 if ( is_file( $file ) && wp_delete_file_from_directory( $file, $css_dir ) ) {
1110 ++$deleted;
1111 }
1112 }
1113 }
1114 }
1115
1116 return $deleted;
1117 }
1118 }
1119 }
1120
1121 new GutSlider_Admin();
1122