PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.8
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.8
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / gutenberg / class-gutenberg-enhancements-controller.php
superb-blocks / src / gutenberg Last commit date
block-api 1 week ago form 1 week ago import 1 week ago popup 1 week ago templates 1 week ago class-gutenberg-block-styles.php 1 week ago class-gutenberg-cache-util.php 1 week ago class-gutenberg-conditions-controller.php 1 week ago class-gutenberg-controller.php 1 week ago class-gutenberg-dynamic-content-controller.php 1 week ago class-gutenberg-enhancements-controller.php 1 week ago class-gutenberg-social-icons-controller.php 1 week ago class-gutenberg-sticky-controller.php 1 week ago class-gutenberg-z-index-controller.php 1 week ago
class-gutenberg-enhancements-controller.php
754 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\Controllers;
4
5 defined('ABSPATH') || exit();
6
7 use Exception;
8 use SuperbAddons\Config\Capabilities;
9 use SuperbAddons\Data\Controllers\LogController;
10 use SuperbAddons\Data\Controllers\Option;
11 use SuperbAddons\Data\Controllers\RestController;
12 use SuperbAddons\Data\Utils\Engagement;
13
14 use WP_Block_Type_Registry;
15 use WP_Error;
16 use WP_REST_Server;
17 use WP_HTML_Tag_Processor;
18
19 class GutenbergEnhancementsController
20 {
21 const ENHANCEMENTS_OPTION = 'superb-blocks-enhancements';
22
23 const HIGHLIGHTS_KEY = 'superb-blocks-highlights';
24 const HIGHLIGHTS_QUICKOPTIONS_KEY = 'superb-blocks-highlights-qo';
25 const HIGHLIGHTS_QUICKOPTIONS_BOTTOM_KEY = 'superb-blocks-highlights-qo-b';
26 const RESPONSIVE_KEY = 'superb-blocks-responsive';
27 const ANIMATIONS_KEY = 'superb-blocks-animations';
28 const CONDITIONS_KEY = 'superb-blocks-conditions';
29 const DYNAMIC_CONTENT_KEY = 'superb-blocks-dynamic-content';
30 const NAVIGATION_KEY = 'superb-blocks-navigation';
31 const RICHTEXT_KEY = 'superb-blocks-richtext';
32 const SOCIAL_ICONS_KEY = 'superb-blocks-social-icons';
33 const DASHBOARD_SHORTCUTS_KEY = 'superb-blocks-dashboard-shortcuts';
34 const STICKY_KEY = 'superb-blocks-sticky';
35 const Z_INDEX_KEY = 'superb-blocks-z-index';
36 const PANEL_DEFAULT_STATE_KEY = 'superb-blocks-panel-default-state';
37
38 private static $global_keys = array(
39 self::RESPONSIVE_KEY,
40 self::ANIMATIONS_KEY,
41 self::CONDITIONS_KEY,
42 self::DYNAMIC_CONTENT_KEY,
43 self::NAVIGATION_KEY,
44 self::RICHTEXT_KEY,
45 self::SOCIAL_ICONS_KEY,
46 self::DASHBOARD_SHORTCUTS_KEY,
47 self::STICKY_KEY,
48 self::Z_INDEX_KEY,
49 );
50
51 private static $entrance_animations = array(
52 'fadeIn',
53 'fadeInUp',
54 'fadeInDown',
55 'fadeInLeft',
56 'fadeInRight',
57 'slideInUp',
58 'slideInDown',
59 'slideInLeft',
60 'slideInRight',
61 'zoomIn',
62 'zoomInUp',
63 'zoomInDown',
64 'zoomInLeft',
65 'zoomInRight',
66 'bounceIn',
67 'bounceInUp',
68 'bounceInDown',
69 'bounceInLeft',
70 'bounceInRight',
71 'flipInX',
72 'flipInY',
73 'rotateIn',
74 'rotateInUpLeft',
75 'rotateInUpRight',
76 'rotateInDownLeft',
77 'rotateInDownRight',
78 'revealltr',
79 'revealrtl',
80 'revealbtt',
81 'revealttb'
82 );
83
84 private static $letter_animations = array(
85 'letterZoomIn',
86 'letterFadeIn',
87 'letterSlideUp',
88 'letterSpiralUp',
89 'letterBounceIn',
90 'letterFlipIn',
91 'letterSlideInRight',
92 'letterRiseUp',
93 'letterDropDown',
94 'letterFlyInRight',
95 'letterBounce',
96 'letterGentleFade'
97 );
98
99 public static function Initialize()
100 {
101 self::InitializeEnhancementEndpoints();
102 self::InitializeEditorEnhancements();
103 GutenbergBlockStyles::Initialize();
104 }
105
106 private static function InitializeEnhancementEndpoints()
107 {
108 RestController::AddRoute('/options/enhancements', array(
109 'methods' => WP_REST_Server::READABLE,
110 'permission_callback' => array(self::class, 'OptionsCallbackPermissionCheck'),
111 'callback' => array(self::class, 'OptionsCallback'),
112 ));
113 RestController::AddRoute('/options/enhancements', array(
114 'methods' => WP_REST_Server::EDITABLE,
115 'permission_callback' => array(self::class, 'OptionsCallbackPermissionCheck'),
116 'callback' => array(self::class, 'OptionsSaveCallback'),
117 ));
118 }
119
120 private static function InitializeEditorEnhancements()
121 {
122 // Use global (site-wide) settings for frontend rendering, not per-user.
123 // This ensures consistent output regardless of who is logged in.
124 $options = self::GetGlobalEnhancementsOptions();
125
126 if (isset($options[self::CONDITIONS_KEY]) && $options[self::CONDITIONS_KEY]) {
127 GutenbergConditionsController::Initialize();
128 }
129 if (isset($options[self::DYNAMIC_CONTENT_KEY]) && $options[self::DYNAMIC_CONTENT_KEY]) {
130 GutenbergDynamicContentController::Initialize();
131 }
132 if (isset($options[self::SOCIAL_ICONS_KEY]) && $options[self::SOCIAL_ICONS_KEY]) {
133 GutenbergSocialIconsController::Initialize();
134 }
135 if (isset($options[self::STICKY_KEY]) && $options[self::STICKY_KEY]) {
136 GutenbergStickyController::Initialize();
137 }
138 if (isset($options[self::Z_INDEX_KEY]) && $options[self::Z_INDEX_KEY]) {
139 GutenbergZIndexController::Initialize();
140 }
141 add_filter('render_block', [__CLASS__, 'FilterEnhancementsRender'], 10, 2);
142 if (isset($options[self::NAVIGATION_KEY]) && $options[self::NAVIGATION_KEY]) {
143 add_filter('render_block', [__CLASS__, 'NavigationEnhancementsRender'], 10, 2);
144 }
145 add_filter('wp_enqueue_scripts', [__CLASS__, 'EnhancementsEnqueue']);
146
147 add_filter('rest_pre_dispatch', array(__CLASS__, 'rest_pre_dispatch'), 10, 3);
148 }
149
150 /*
151 * This function is used to remove the attributes that are not allowed for the block during the server side rendering.
152 * Solves the issue of editor server side rendering not working if blocks have custom attributes registered during runtime.
153 */
154 public static function rest_pre_dispatch($result, $server, $request)
155 {
156 if (strpos($request->get_route(), '/wp/v2/block-renderer') === false || !isset($request['context']) || $request['context'] !== 'edit') {
157 return $result;
158 }
159
160 if (!isset($request['attributes']) || !class_exists('WP_Block_Type_Registry')) {
161 return $result;
162 }
163
164 $block_type = str_replace('/wp/v2/block-renderer/', '', $request->get_route());
165 $registry = WP_Block_Type_Registry::get_instance();
166 $block = $registry->get_registered($block_type);
167 if (!$block) {
168 return $result;
169 }
170
171 $allowed_attributes = $block->get_attributes();
172 $attributes = $request['attributes'];
173
174 foreach ($attributes as $key => $value) {
175 if (!isset($allowed_attributes[$key])) {
176 unset($attributes[$key]);
177 }
178 }
179
180 $request['attributes'] = $attributes;
181
182 return $result;
183 }
184
185 public static function FilterEnhancementsRender($block_content, $block)
186 {
187 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, array(
188 'spbaddHideOnMobile' => array(
189 'class' => 'superb-addons-hide-on-mobile',
190 ),
191 'spbaddHideOnTablet' => array(
192 'class' => 'superb-addons-hide-on-tablet',
193 ),
194 'spbaddHideOnDesktop' => array(
195 'class' => 'superb-addons-hide-on-desktop',
196 )
197 ));
198
199 // Process spbaddResponsive object attribute for per-feature CSS classes + custom properties
200 if (isset($block['attrs']['spbaddResponsive']) && is_array($block['attrs']['spbaddResponsive'])) {
201 $responsive = $block['attrs']['spbaddResponsive'];
202 $classes = array();
203 $style_parts = array();
204
205 self::BuildResponsiveStyles($responsive, $classes, $style_parts);
206
207 if (!empty($classes) || !empty($style_parts)) {
208 $processor = new WP_HTML_Tag_Processor($block_content);
209 if ($processor->next_tag()) {
210 foreach ($classes as $cls) {
211 $processor->add_class($cls);
212 }
213 if (!empty($style_parts)) {
214 $styles = $processor->get_attribute("style");
215 $styles = isset($styles) && $styles !== null ? $styles : "";
216 if (!empty($styles) && substr($styles, -1) !== ";") {
217 $styles .= ";";
218 }
219 $processor->set_attribute("style", $styles . implode(" ", $style_parts));
220 }
221 $block_content = $processor->get_updated_html();
222 }
223 }
224 }
225
226 $isPopupBlock = isset($block['blockName']) && $block['blockName'] === 'superb-addons/popup';
227
228 if (isset($block['attrs']['spbaddAnimationsEnabled']) && $block['attrs']['spbaddAnimationsEnabled']) {
229 $animation_id = isset($block['attrs']['spbaddAnimationId']) ? $block['attrs']['spbaddAnimationId'] : '';
230 wp_enqueue_script('superb-addons-animations', SUPERBADDONS_ASSETS_PATH . '/js/dynamic-blocks/block-animations.js', array(), SUPERBADDONS_VERSION, true);
231 $is_letter_animation = in_array($animation_id, self::$letter_animations, true);
232
233 // Letter animations don't apply to popup blocks
234 if ($is_letter_animation && $isPopupBlock) {
235 // Skip — letter animations don't make sense on a popup dialog
236 } elseif ($is_letter_animation) {
237 // Letter animation — output letter-specific data attribute
238 $animation_modifications = array(
239 'spbaddAnimationId' => array(
240 'data-spbadd-anim-letter' => array("attribute" => "spbaddAnimationId"),
241 )
242 );
243 // All letter animations except letterBounce are entrance animations
244 if ($animation_id !== 'letterBounce') {
245 $animation_modifications['spbaddAnimationId']['data-spbadd-entrance'] = '';
246 }
247
248 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, $animation_modifications);
249 } elseif ($isPopupBlock) {
250 // Popup block — add animation attributes to the dialog element (not the wrapper)
251 // so the MutationObserver in block-animations.js detects visibility changes via the overlay parent
252 $animation_attrs = array(
253 'superb-addons-animation' => $animation_id,
254 );
255 if (in_array($animation_id, self::$entrance_animations, true)) {
256 $animation_attrs['data-spbadd-entrance'] = '';
257 }
258 if (isset($block['attrs']['spbaddAnimationSpeed']) && $block['attrs']['spbaddAnimationSpeed'] != 1) {
259 $animation_attrs['data-spbadd-animation-speed'] = strval($block['attrs']['spbaddAnimationSpeed']);
260 }
261 if (isset($block['attrs']['spbaddAnimationDelay']) && $block['attrs']['spbaddAnimationDelay'] > 0) {
262 $animation_attrs['data-spbadd-animation-delay'] = strval($block['attrs']['spbaddAnimationDelay']);
263 }
264 if (isset($block['attrs']['spbaddAnimationLoop']) && $block['attrs']['spbaddAnimationLoop']) {
265 $animation_attrs['data-spbadd-animation-loop'] = 'true';
266 }
267 $block_content = self::AddPopupDialogAnimationAttributes($block_content, $animation_attrs);
268 } else {
269 // Block animation — output block-specific data attributes
270 $animation_modifications = array(
271 'spbaddAnimationId' => array(
272 'superb-addons-animation' => array("attribute" => "spbaddAnimationId"),
273 )
274 );
275
276 // Entrance animations get a data attribute that CSS uses to set opacity:0 with a safety fallback
277 if (in_array($animation_id, self::$entrance_animations, true)) {
278 $animation_modifications['spbaddAnimationId']['data-spbadd-entrance'] = '';
279 }
280
281 // Shared: speed/delay/loop output for both block and letter animations
282 if (isset($block['attrs']['spbaddAnimationSpeed']) && $block['attrs']['spbaddAnimationSpeed'] != 1) {
283 $animation_modifications['spbaddAnimationSpeed'] = array(
284 'data-spbadd-animation-speed' => array("attribute" => "spbaddAnimationSpeed"),
285 );
286 }
287 if (isset($block['attrs']['spbaddAnimationDelay']) && $block['attrs']['spbaddAnimationDelay'] > 0) {
288 $animation_modifications['spbaddAnimationDelay'] = array(
289 'data-spbadd-animation-delay' => array("attribute" => "spbaddAnimationDelay"),
290 );
291 }
292 if (isset($block['attrs']['spbaddAnimationLoop']) && $block['attrs']['spbaddAnimationLoop']) {
293 $animation_modifications['spbaddAnimationLoop'] = array(
294 'data-spbadd-animation-loop' => 'true',
295 );
296 }
297
298 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, $animation_modifications);
299 }
300 }
301
302 // Typing Animation (inline RichText format - detected in content)
303 if (strpos($block_content, 'spbadd-anim-typing') !== false) {
304 wp_enqueue_script('superb-addons-typing-animations', SUPERBADDONS_ASSETS_PATH . '/js/dynamic-blocks/typing-animations.js', array(), SUPERBADDONS_VERSION, true);
305 }
306
307 // Count Animation (inline RichText format - detected in content)
308 if (strpos($block_content, 'spbadd-anim-count') !== false) {
309 wp_enqueue_script('superb-addons-count-animations', SUPERBADDONS_ASSETS_PATH . '/js/dynamic-blocks/count-animations.js', array(), SUPERBADDONS_VERSION, true);
310 }
311
312 return $block_content;
313 }
314
315 public static function NavigationEnhancementsRender($block_content, $block)
316 {
317 if (!isset($block['blockName']) || $block['blockName'] !== 'core/navigation') {
318 return $block_content;
319 }
320
321 if (
322 isset($block['attrs']['spbaddMobileMenuJustification']) &&
323 !empty($block['attrs']['spbaddMobileMenuJustification']) &&
324 $block['attrs']['spbaddMobileMenuJustification'] !== 'default'
325 ) {
326 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, array(
327 'spbaddMobileMenuJustification' => array(
328 'class' => 'has-superb-addons-overlay-menu-justification',
329 'required-values' => array(
330 'left' => array(
331 'class' => 'superb-addons-overlay-menu-justification-left'
332 ),
333 'center' => array(
334 'class' => 'superb-addons-overlay-menu-justification-center'
335 ),
336 'right' => array(
337 'class' => 'superb-addons-overlay-menu-justification-right'
338 ),
339 'stretch' => array(
340 'class' => 'superb-addons-overlay-menu-justification-stretch'
341 ),
342 )
343 )
344 ));
345 }
346 if (
347 isset($block['attrs']['spbaddSubmenuLayout']) &&
348 !empty($block['attrs']['spbaddSubmenuLayout']) &&
349 $block['attrs']['spbaddSubmenuLayout'] !== 'default'
350 ) {
351 $block_content = self::MaybeAddBlockTagModifications($block_content, $block, array(
352 'spbaddSubmenuLayout' => array(
353 'required-values' => array(
354 'card' => array(
355 'class' => 'is-superb-addons-submenu-layout-card'
356 ),
357 )
358 )
359 ));
360 }
361
362
363 return $block_content;
364 }
365
366 public static function EnhancementsEnqueue()
367 {
368 wp_enqueue_style('superb-addons-enhancements', SUPERBADDONS_ASSETS_PATH . '/css/enhancements.min.css', array(), SUPERBADDONS_VERSION);
369 }
370
371 public static function MaybeAddBlockTagModifications($block_content, $block, $classes)
372 {
373 if (!is_array($classes) || empty($classes)) {
374 return $block_content;
375 }
376
377 $added_html_classes = array();
378 $added_html_styles = array();
379 $added_html_attributes = array();
380 foreach ($classes as $required_attribute => $modification) {
381 if (!isset($block['attrs'][$required_attribute]) || !$block['attrs'][$required_attribute]) {
382 continue;
383 }
384 foreach ($modification as $modification_key => $value) {
385 if ($modification_key === 'required-values') {
386 foreach ($value as $required_value => $conditional_modifications) {
387 if ($block['attrs'][$required_attribute] !== $required_value) {
388 continue;
389 }
390 foreach ($conditional_modifications as $conditional_modification_key => $conditional_value) {
391 self::AppendModificationArrays($block, $conditional_modification_key, $conditional_value, $added_html_classes, $added_html_styles, $added_html_attributes);
392 }
393 }
394 continue;
395 }
396
397 self::AppendModificationArrays($block, $modification_key, $value, $added_html_classes, $added_html_styles, $added_html_attributes);
398 }
399 }
400
401 if (empty($added_html_classes) && empty($added_html_styles) && empty($added_html_attributes)) {
402 return $block_content;
403 }
404
405 $processor = new WP_HTML_Tag_Processor($block_content);
406 if (!$processor->next_tag()) {
407 return $block_content;
408 }
409 if (!empty($added_html_attributes)) {
410 foreach ($added_html_attributes as $attribute => $value) {
411 $processor->set_attribute($attribute, $value);
412 }
413 }
414 if (!empty($added_html_styles)) {
415 $styles = $processor->get_attribute("style");
416 $styles = isset($styles) ? $styles : "";
417 if (!empty($styles) && substr($styles, -1) !== ";") {
418 $styles .= ";";
419 }
420 $processor->set_attribute("style", $styles . join(" ", $added_html_styles));
421 }
422 if (!empty($added_html_classes)) {
423 $processor->add_class(join(" ", $added_html_classes));
424 }
425 return $processor->get_updated_html();
426 }
427
428 /**
429 * Add animation attributes to the .superb-popup-dialog element instead of the block wrapper.
430 * This allows the block-animations.js MutationObserver to detect visibility changes
431 * via the overlay parent when the popup opens.
432 */
433 private static function AddPopupDialogAnimationAttributes($block_content, $animation_attrs)
434 {
435 $processor = new WP_HTML_Tag_Processor($block_content);
436 while ($processor->next_tag('div')) {
437 $class = $processor->get_attribute('class');
438 if ($class !== null && strpos($class, 'superb-popup-dialog') !== false) {
439 foreach ($animation_attrs as $attr => $val) {
440 $processor->set_attribute($attr, $val);
441 }
442 return $processor->get_updated_html();
443 }
444 }
445 return $block_content;
446 }
447
448 private static function AppendModificationArrays($block, $modification_key, $value, &$added_html_classes, &$added_html_styles, &$added_html_attributes)
449 {
450 if (is_array($value)) {
451 $key = key($value);
452 $dynamic_value = $value[$key];
453 switch ($key) {
454 case 'attribute':
455 $value = isset($block['attrs'][$dynamic_value]) ? $block['attrs'][$dynamic_value] : (isset($value['default']) ? $value['default'] : '');
456 break;
457 case 'json-attribute':
458 $raw = isset($block['attrs'][$dynamic_value]) ? $block['attrs'][$dynamic_value] : (isset($value['default']) ? $value['default'] : array());
459 $value = wp_json_encode($raw);
460 break;
461 default:
462 $value = "";
463 break;
464 }
465 }
466 switch ($modification_key) {
467 case 'class':
468 $added_html_classes[] = $value;
469 break;
470 case 'style':
471 $added_html_styles[] = $value;
472 break;
473 default:
474 $added_html_attributes[$modification_key] = $value;
475 break;
476 }
477 }
478
479 /**
480 * Map of feature keys to their CSS class and CSS var prefix.
481 * Box properties (padding, margin) use per-side vars.
482 * Simple properties use a single var.
483 */
484 private static $responsive_feature_map = array(
485 'padding' => array(
486 'class' => 'superb-addons-has-padding',
487 'type' => 'box',
488 'prefix' => '--spbadd-padding',
489 ),
490 'margin' => array(
491 'class' => 'superb-addons-has-margin',
492 'type' => 'box',
493 'prefix' => '--spbadd-margin',
494 ),
495 'fontSize' => array(
496 'class' => 'superb-addons-has-font-size',
497 'type' => 'simple',
498 'prefix' => '--spbadd-font-size',
499 ),
500 'lineHeight' => array(
501 'class' => 'superb-addons-has-line-height',
502 'type' => 'simple',
503 'prefix' => '--spbadd-line-height',
504 ),
505 'textAlign' => array(
506 'class' => 'superb-addons-has-text-align',
507 'type' => 'simple',
508 'prefix' => '--spbadd-text-align',
509 ),
510 'maxWidth' => array(
511 'class' => 'superb-addons-has-max-width',
512 'type' => 'simple',
513 'prefix' => '--spbadd-max-width',
514 ),
515 'gap' => array(
516 'class' => 'superb-addons-has-gap',
517 'type' => 'simple',
518 'prefix' => '--spbadd-gap',
519 ),
520 'flexDir' => array(
521 'class' => 'superb-addons-has-flex-dir',
522 'type' => 'simple',
523 'prefix' => '--spbadd-flex-dir',
524 ),
525 'justifyContent' => array(
526 'class' => 'superb-addons-has-justify-content',
527 'type' => 'simple',
528 'prefix' => '--spbadd-justify-content',
529 ),
530 'alignItems' => array(
531 'class' => 'superb-addons-has-align-items',
532 'type' => 'simple',
533 'prefix' => '--spbadd-align-items',
534 ),
535 'order' => array(
536 'class' => 'superb-addons-has-order',
537 'type' => 'simple',
538 'prefix' => '--spbadd-order',
539 ),
540 'colCount' => array(
541 'class' => 'superb-addons-has-col-count',
542 'type' => 'simple',
543 'prefix' => '--spbadd-col-count',
544 ),
545 );
546
547 private static $responsive_devices = array('desktop', 'tablet', 'mobile');
548 private static $responsive_box_sides = array('top', 'right', 'bottom', 'left');
549
550 /**
551 * Build per-feature CSS classes and inline CSS custom property style strings.
552 *
553 * @param array $responsive The spbaddResponsive object from block attributes.
554 * @param array &$classes Array to append CSS class strings to.
555 * @param array &$styles Array to append CSS style strings to.
556 */
557 /**
558 * Convert WP spacing preset format to CSS var.
559 * e.g. "var:preset|spacing|30" → "var(--wp--preset--spacing--30)"
560 */
561 private static function ToCssValue($value)
562 {
563 if (!is_string($value) || strpos($value, 'var:') !== 0) {
564 return $value;
565 }
566 return 'var(--wp--' . str_replace('|', '--', substr($value, 4)) . ')';
567 }
568
569 private static function BuildResponsiveStyles($responsive, &$classes, &$styles)
570 {
571 foreach (self::$responsive_feature_map as $feature_key => $config) {
572 if (!isset($responsive[$feature_key]) || !is_array($responsive[$feature_key])) {
573 continue;
574 }
575
576 $feature_data = $responsive[$feature_key];
577 $has_values = false;
578
579 if ($config['type'] === 'box') {
580 // Box properties: per-side + per-device classes and CSS vars.
581 // Each device that has a value gets its own class to avoid IACVT
582 // when only some devices are set (e.g. mobile but not desktop).
583 foreach (self::$responsive_box_sides as $side) {
584 foreach (self::$responsive_devices as $device) {
585 if (isset($feature_data[$device]) && is_array($feature_data[$device]) && isset($feature_data[$device][$side]) && $feature_data[$device][$side] !== '') {
586 $val = self::ToCssValue(sanitize_text_field($feature_data[$device][$side]));
587 $suffix = $device === 'desktop' ? '' : '-' . $device;
588 $styles[] = $config['prefix'] . '-' . $side . $suffix . ':' . $val . ';';
589 $classes[] = $config['class'] . '-' . $side . '-' . $device;
590 $has_values = true;
591 }
592 }
593 }
594 } else {
595 // Simple properties: per-device classes + CSS vars to avoid IACVT.
596 foreach (self::$responsive_devices as $device) {
597 if (!isset($feature_data[$device]) || $feature_data[$device] === '') {
598 continue;
599 }
600 $suffix = $device === 'desktop' ? '' : '-' . $device;
601 $val = self::ToCssValue(sanitize_text_field($feature_data[$device]));
602 $styles[] = $config['prefix'] . $suffix . ':' . $val . ';';
603 $classes[] = $config['class'] . '-' . $device;
604 $has_values = true;
605 }
606 }
607 }
608 }
609
610 public static function OptionsCallbackPermissionCheck()
611 {
612 // Restrict endpoint to only users who have the proper capability.
613 if (!current_user_can(Capabilities::CONTRIBUTOR)) {
614 return new WP_Error('rest_forbidden', esc_html__('Unauthorized. Please check user permissions.', "superb-blocks"), array('status' => 401));
615 }
616
617 return true;
618 }
619
620 public static function OptionsCallback()
621 {
622 try {
623 return rest_ensure_response(self::GetEnhancementsOptions(get_current_user_id()));
624 } catch (Exception $ex) {
625 LogController::HandleException($ex);
626 return new \WP_Error('internal_error_plugin', 'Internal Plugin Error', array('status' => 500));
627 }
628 }
629
630 public static function OptionsSaveCallback($request)
631 {
632 try {
633 $key = sanitize_title($request['action']);
634 $is_global = self::IsGlobalKey($key);
635
636 // Global settings require admin capability
637 if ($is_global && !current_user_can(Capabilities::ADMIN)) {
638 return new \WP_Error('rest_forbidden', esc_html__('Unauthorized. Only administrators can change site-wide enhancement settings.', "superb-blocks"), array('status' => 403));
639 }
640
641 switch ($key) {
642 // Per-user boolean settings
643 case self::HIGHLIGHTS_KEY:
644 case self::HIGHLIGHTS_QUICKOPTIONS_KEY:
645 case self::HIGHLIGHTS_QUICKOPTIONS_BOTTOM_KEY:
646 $userOptions = self::GetUserOnlyOptions(get_current_user_id());
647 $userOptions[$key] = isset($request['value']) && $request['value'] === 'true';
648 if (update_user_meta(get_current_user_id(), self::ENHANCEMENTS_OPTION, $userOptions) === false) {
649 throw new Exception('Failed to update user meta');
650 }
651 break;
652 // Per-user string setting
653 case self::PANEL_DEFAULT_STATE_KEY:
654 $allowed = array('open', 'closed', 'dynamic');
655 $value = isset($request['value']) ? sanitize_text_field($request['value']) : '';
656 if (!in_array($value, $allowed, true)) {
657 return new \WP_Error('invalid_request', 'Invalid value', array('status' => 400));
658 }
659 $userOptions = self::GetUserOnlyOptions(get_current_user_id());
660 $userOptions[$key] = $value;
661 if (update_user_meta(get_current_user_id(), self::ENHANCEMENTS_OPTION, $userOptions) === false) {
662 throw new Exception('Failed to update user meta');
663 }
664 break;
665 // Global boolean settings
666 case self::RESPONSIVE_KEY:
667 case self::ANIMATIONS_KEY:
668 case self::CONDITIONS_KEY:
669 case self::DYNAMIC_CONTENT_KEY:
670 case self::NAVIGATION_KEY:
671 case self::RICHTEXT_KEY:
672 case self::SOCIAL_ICONS_KEY:
673 case self::DASHBOARD_SHORTCUTS_KEY:
674 case self::STICKY_KEY:
675 case self::Z_INDEX_KEY:
676 $value = isset($request['value']) && $request['value'] === 'true';
677 if (self::SaveGlobalEnhancementOption($key, $value) === false) {
678 throw new Exception('Failed to update global enhancement option');
679 }
680 break;
681 default:
682 return new \WP_Error('invalid_request', 'Invalid Request', array('status' => 400));
683 }
684
685 Engagement::MarkUsed(Engagement::FEATURE_ENHANCEMENT);
686
687 return rest_ensure_response(array('success' => true));
688 } catch (Exception $ex) {
689 LogController::HandleException($ex);
690 return new \WP_Error('internal_error_plugin', 'Internal Plugin Error', array('status' => 500));
691 }
692 }
693
694 public static function IsGlobalKey($key)
695 {
696 return in_array($key, self::$global_keys, true);
697 }
698
699 public static function GetGlobalEnhancementsOptions()
700 {
701 $defaults = array(
702 self::RESPONSIVE_KEY => true,
703 self::ANIMATIONS_KEY => true,
704 self::CONDITIONS_KEY => true,
705 self::DYNAMIC_CONTENT_KEY => true,
706 self::NAVIGATION_KEY => true,
707 self::RICHTEXT_KEY => true,
708 self::SOCIAL_ICONS_KEY => true,
709 self::DASHBOARD_SHORTCUTS_KEY => true,
710 self::STICKY_KEY => true,
711 self::Z_INDEX_KEY => true,
712 );
713 $options = get_option(Option::GLOBAL_ENHANCEMENTS, array());
714 if (!is_array($options)) {
715 return $defaults;
716 }
717 return wp_parse_args($options, $defaults);
718 }
719
720 private static function SaveGlobalEnhancementOption($key, $value)
721 {
722 $options = self::GetGlobalEnhancementsOptions();
723 $options[$key] = $value;
724 return update_option(Option::GLOBAL_ENHANCEMENTS, $options);
725 }
726
727 private static function GetUserOnlyOptions($user_id)
728 {
729 $defaults = array(
730 self::HIGHLIGHTS_KEY => true,
731 self::HIGHLIGHTS_QUICKOPTIONS_KEY => false,
732 self::HIGHLIGHTS_QUICKOPTIONS_BOTTOM_KEY => false,
733 self::PANEL_DEFAULT_STATE_KEY => 'open',
734 );
735 $options = get_user_meta($user_id, self::ENHANCEMENTS_OPTION, true);
736 if (!is_array($options)) {
737 return $defaults;
738 }
739 // Only return per-user keys, ignore any stale global keys in user_meta
740 $result = array();
741 foreach ($defaults as $k => $default_val) {
742 $result[$k] = isset($options[$k]) ? $options[$k] : $default_val;
743 }
744 return $result;
745 }
746
747 public static function GetEnhancementsOptions($user_id)
748 {
749 $global = self::GetGlobalEnhancementsOptions();
750 $per_user = self::GetUserOnlyOptions($user_id);
751 return array_merge($global, $per_user);
752 }
753 }
754