| 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 |
|