PluginProbe
GutSlider – All in One Slider and Carousel Blocks for Gutenberg / 3.2.0
GutSlider – All in One Slider and Carousel Blocks for Gutenberg v3.2.0
3.2.0 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 All 57 releases
slider-blocks / admin / admin.php

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

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