PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.14.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.14.0
2.14.0 2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 All 81 releases
ablocks / includes / helper.php

helper.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.14.0, at includes/helper.php

1,141 lines 37.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocks;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit; // Exit if accessed directly.
6 }
7
8 use ABlocks\traits\Importer;
9
10 class Helper {
11
12 use Importer;
13
14 /** Memoized responsive device list (see get_responsive_devices). */
15 private static $responsive_devices_cache = null;
16
17 public static function get_time() {
18 return time() + ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS );
19 }
20
21 public static function get_settings( $key, $default = null ) {
22 global $ablocks_settings;
23
24 if ( isset( $ablocks_settings->{$key} ) ) {
25 return $ablocks_settings->{$key};
26 }
27
28 return $default;
29 }
30
31 /**
32 * Responsive breakpoint widths (px) for the whole plugin. User-configurable
33 * via Settings; defaults preserve the historical 800/480 values. Every CSS
34 * generator and the block editor reads these so breakpoints stay in sync.
35 */
36 /**
37 * Make one CSS property or declaration value safe to write into a stylesheet.
38 *
39 * Block attributes reach the compiled CSS verbatim and that CSS is echoed
40 * inside a `<style>` element, so a value carrying `</style>` closes the
41 * element and everything after it is parsed as HTML — a stored XSS
42 * available to anyone who can set a block attribute, which includes a
43 * Contributor editing their own draft. `{` and `}` are the same problem one
44 * level down: they close the rule and let the value choose its own
45 * selector.
46 *
47 * Only those four characters are removed. A declaration value never needs
48 * them, and everything a real one does need survives: data URIs (which
49 * carry `;`), gradients, `calc()` and `var()` (parentheses and commas),
50 * font stacks (quotes), `content` escapes (backslashes), and shorthand
51 * slashes such as `font: 12px/1.5`.
52 *
53 * `;` is deliberately kept. With the braces gone, an injected `;` can only
54 * add declarations to the same rule — which targets the block's own
55 * element, exactly what its style controls already allow — so removing it
56 * would break data URIs to buy nothing.
57 *
58 * @param mixed $value A CSS property name or declaration value.
59 * @return string The value with the escape characters removed.
60 */
61 public static function esc_css_value( $value ) {
62 if ( ! is_scalar( $value ) ) {
63 return '';
64 }
65 return str_replace( [ '<', '>', '{', '}' ], '', (string) $value );
66 }
67
68 public static function get_breakpoints() {
69 $tablet = (int) self::get_settings( 'breakpoint_tablet', 800 );
70 $mobile = (int) self::get_settings( 'breakpoint_mobile', 480 );
71
72 // Guard against nonsensical config (mobile must be below tablet).
73 if ( $tablet < 1 ) {
74 $tablet = 800;
75 }
76 if ( $mobile < 1 || $mobile >= $tablet ) {
77 $mobile = min( 480, $tablet - 1 );
78 }
79
80 return array(
81 'tablet' => $tablet,
82 'mobile' => $mobile,
83 );
84 }
85
86 /**
87 * The full ordered list of responsive devices the atomic style system emits
88 * for: the base (Desktop, width 0 = no media query), the two built-in
89 * breakpoints, then any user-registered custom breakpoints. Each entry:
90 * id - stable identifier (also the WP device name for the built-ins)
91 * label - shown in the editor device switcher
92 * suffix - appended to responsive attribute keys (e.g. fontSize + suffix)
93 * width - max-width px for the @media rule (0 = base, no media query)
94 *
95 * Ordering is load-bearing, not cosmetic: the base comes first and every
96 * other device follows widest-first, so narrower breakpoints emit later and
97 * win in `cascade` mode. Precedence therefore follows the breakpoint's own
98 * bounds rather than the order a custom breakpoint happened to be
99 * registered in.
100 */
101 public static function get_responsive_devices() {
102 if ( null !== self::$responsive_devices_cache ) {
103 return self::$responsive_devices_cache;
104 }
105
106 $bp = self::get_breakpoints();
107 // Built-ins are max-width only (min 0). `width` is the sort key
108 // (max-width, or a large value for min-only so it sorts widest).
109 $devices = array(
110 array( 'id' => 'Desktop', 'label' => 'Desktop', 'suffix' => '', 'width' => 0, 'min' => 0, 'max' => 0 ),
111 array( 'id' => 'Tablet', 'label' => 'Tablet', 'suffix' => 'Tablet', 'width' => $bp['tablet'], 'min' => 0, 'max' => $bp['tablet'] ),
112 array( 'id' => 'Mobile', 'label' => 'Mobile', 'suffix' => 'Mobile', 'width' => $bp['mobile'], 'min' => 0, 'max' => $bp['mobile'] ),
113 );
114
115 $custom = self::get_settings( 'breakpoint_custom', array() );
116 if ( is_array( $custom ) ) {
117 foreach ( $custom as $c ) {
118 $c = (array) $c;
119 // Advanced breakpoints support a min and/or max width. `width` is
120 // kept as a legacy alias for max-width.
121 $max = isset( $c['maxWidth'] ) ? (int) $c['maxWidth'] : ( isset( $c['width'] ) ? (int) $c['width'] : 0 );
122 $min = isset( $c['minWidth'] ) ? (int) $c['minWidth'] : 0;
123 if ( $max < 1 && $min < 1 ) {
124 continue; // needs at least one bound
125 }
126 // Stable, alphanumeric suffix so stored values survive label edits.
127 $key = ! empty( $c['key'] ) ? preg_replace( '/[^a-zA-Z0-9]/', '', $c['key'] ) : (string) ( $max ? $max : $min );
128 $suffix = 'Bp' . ucfirst( $key );
129 $label = ! empty( $c['label'] ) ? $c['label'] : self::breakpoint_auto_label( $min, $max );
130 $devices[] = array(
131 'id' => $suffix,
132 'label' => $label,
133 'suffix' => $suffix,
134 'width' => $max > 0 ? $max : 999999, // sort key (min-only = widest)
135 'min' => $min,
136 'max' => $max,
137 );
138 }
139 }
140
141 self::$responsive_devices_cache = self::sort_responsive_devices( $devices );
142 return self::$responsive_devices_cache;
143 }
144
145 /**
146 * Base first, then widest-first. Ties break on id so the order is stable
147 * regardless of the PHP version's sort stability.
148 */
149 private static function sort_responsive_devices( $devices ) {
150 $base = array();
151 $rest = array();
152 foreach ( $devices as $d ) {
153 if ( empty( $d['min'] ) && empty( $d['max'] ) ) {
154 $base[] = $d;
155 } else {
156 $rest[] = $d;
157 }
158 }
159
160 usort(
161 $rest,
162 function ( $a, $b ) {
163 $cmp = (int) $b['width'] - (int) $a['width'];
164 return 0 !== $cmp ? $cmp : strcmp( (string) $a['id'], (string) $b['id'] );
165 }
166 );
167
168 return array_merge( $base, $rest );
169 }
170
171 /** Drop the memoized device list (settings changed mid-request). */
172 public static function flush_responsive_devices_cache() {
173 self::$responsive_devices_cache = null;
174 }
175
176 /** A readable fallback label for a min/max breakpoint. */
177 public static function breakpoint_auto_label( $min, $max ) {
178 if ( $min > 0 && $max > 0 ) {
179 return $min . '–' . $max . 'px';
180 }
181 if ( $max > 0 ) {
182 return '≤ ' . $max . 'px';
183 }
184 return '≥ ' . $min . 'px';
185 }
186
187 /** Compose a CSS media condition (no `@media` keyword) from min/max px. */
188 public static function breakpoint_media_condition( $min, $max ) {
189 $parts = array();
190 if ( $min > 0 ) {
191 $parts[] = '(min-width:' . (int) $min . 'px)';
192 }
193 if ( $max > 0 ) {
194 $parts[] = '(max-width:' . (int) $max . 'px)';
195 }
196 return implode( ' and ', $parts );
197 }
198
199 /**
200 * How breakpoint queries relate to each other, site-wide.
201 *
202 * cascade (default) - max-width envelopes. A Tablet value still applies at
203 * Mobile widths unless Mobile overrides it. This is how
204 * aBlocks v1/v2 blocks behave, so a page mixing block
205 * versions stays consistent.
206 * strict - exclusive bands. A Tablet value applies only between
207 * the Mobile bound and the Tablet bound, matching
208 * WordPress core and block themes.
209 */
210 public static function get_breakpoint_mode() {
211 return 'strict' === self::get_settings( 'breakpoint_mode', 'cascade' ) ? 'strict' : 'cascade';
212 }
213
214 /**
215 * The single place an atomic media query is built. Returns the complete
216 * `@media …` prelude for a device entry, or '' for the base device (which
217 * needs no query at all).
218 *
219 * Both bounds are honoured, so a custom breakpoint declared with only a
220 * `minWidth` produces a real min-width query instead of being skipped —
221 * animations already behaved this way, style rules did not.
222 */
223 public static function breakpoint_media_query( $device ) {
224 list( $min, $max ) = self::breakpoint_bounds( $device );
225
226 if ( $min < 1 && $max < 1 ) {
227 return '';
228 }
229
230 $condition = self::breakpoint_media_condition( $min, $max );
231 return '' === $condition ? '' : '@media screen and ' . $condition;
232 }
233
234 /**
235 * The [ min, max ] a device's media query actually covers. Mirror of the
236 * JS `effectiveBounds()`.
237 *
238 * @param array $device A device entry.
239 * @return int[] [ min, max ], 0 meaning unbounded.
240 */
241 public static function breakpoint_bounds( $device ) {
242 $device = (array) $device;
243 $min = isset( $device['min'] ) ? (int) $device['min'] : 0;
244 $max = isset( $device['max'] ) ? (int) $device['max'] : 0;
245
246 /*
247 * Strict mode bounds a max-width breakpoint from below with the next
248 * narrower breakpoint, turning overlapping envelopes into exclusive
249 * bands. A breakpoint that already declares its own min is left alone —
250 * the author has stated the band explicitly.
251 */
252 if ( 'strict' === self::get_breakpoint_mode() && $max > 0 && $min < 1 ) {
253 $narrower = self::next_narrower_max( $max );
254 if ( $narrower > 0 ) {
255 $min = $narrower + 1;
256 }
257 }
258
259 return [ $min, $max ];
260 }
261
262 /** The largest max-width bound narrower than $max, or 0 if none. */
263 private static function next_narrower_max( $max ) {
264 $best = 0;
265 foreach ( self::get_responsive_devices() as $d ) {
266 $dmax = isset( $d['max'] ) ? (int) $d['max'] : 0;
267 if ( $dmax > 0 && $dmax < $max && $dmax > $best ) {
268 $best = $dmax;
269 }
270 }
271 return $best;
272 }
273
274 public static function get_page_permalink( $page, $fallback = null ) {
275 $page_id = self::get_settings( $page );
276 $permalink = 0 < $page_id ? get_permalink( $page_id ) : '';
277 if ( ! $permalink ) {
278 $permalink = is_null( $fallback ) ? get_home_url() : $fallback;
279 }
280
281 return apply_filters( 'ablocks/get_' . $page . '_permalink', $permalink );
282 }
283
284 public static function get_logout_url( $redirect = '' ) {
285 $redirect = $redirect ? $redirect : apply_filters( 'ablocks/logout_default_redirect_url', self::get_page_permalink( 'dashboard_page' ) );
286
287 return wp_logout_url( $redirect );
288 }
289 public static function is_enabled_block( $block_name, $parent_block_name = '' ) {
290 global $ablocks_blocks;
291 $block_name = ! empty( $parent_block_name ) ? $parent_block_name : $block_name;
292 if ( isset( $ablocks_blocks->{$block_name} ) ) {
293 return (bool) $ablocks_blocks->{$block_name};
294 }
295 return false;
296 }
297
298
299 public static function is_plugin_installed( $path ) {
300 $installed_plugins = get_plugins();
301 return isset( $installed_plugins[ $path ] );
302 }
303
304 public static function is_active_academy() {
305 $academy = 'academy/academy.php';
306 return self::is_plugin_active( $academy );
307 }
308
309
310 public static function is_active_storeengine() {
311 $storeengine = 'storeengine/storeengine.php';
312 return self::is_plugin_active( $storeengine );
313 }
314
315 public static function is_active_ablocks_pro() {
316 $ablocks = 'ablocks-pro/ablocks-pro.php';
317 return self::is_plugin_active( $ablocks );
318 }
319 public static function is_active_wp_map_block() {
320 $wp_map_block = 'wp-map-block/wp-map-block.php';
321 return self::is_plugin_active( $wp_map_block );
322 }
323 public static function is_active_quizpress() {
324 return class_exists( 'QuizPress' );
325 }
326 public static function is_active_zencommunity() {
327 $zencommunity = 'zencommunity/zencommunity.php';
328 return self::is_plugin_active( $zencommunity );
329 }
330 public static function is_active_gemboards() {
331 $gemboards = 'gemboards/gemboards.php';
332 return self::is_plugin_active( $gemboards );
333 }
334 public static function is_active_easy_content_manager() {
335 return class_exists( 'EasyContentManager' );
336 }
337
338 public static function is_enabled_assets_generation() {
339 // Default OFF — combining/generating per-page asset files churns while a
340 // site is still being built, so it's recommended (via the Performance tab
341 // notice) once the site is complete rather than forced on. When enabled it
342 // merges every block's CSS/JS into one per-page file, inlined when small
343 // (see Assets::enqueue_frontend_assets), removing the per-block
344 // render-blocking stylesheets.
345 $flag = (bool) self::get_settings( 'enabled_assets_file_generation', false );
346 return apply_filters( 'ablocks/is_enabled_assets_generation', $flag );
347 }
348
349 public static function is_plugin_active( $basename ) {
350 if ( ! function_exists( 'get_plugins' ) ) {
351 include_once ABSPATH . '/wp-admin/includes/plugin.php';
352 }
353 return is_plugin_active( $basename );
354 }
355
356 public static function is_dev_mode_enable() {
357 $environment = wp_get_environment_type();
358 if ( 'local' === $environment || 'development' === $environment ) {
359 return true;
360 }
361 }
362
363 /**
364 * The aBlocks submenu.
365 *
366 * Each item declares the aBlocks capability that owns it rather than
367 * manage_options, so a site can hand somebody the Theme Builder without
368 * handing them the whole of WordPress. Administrators hold every one of
369 * these, so nothing changes for them. See Permissions.
370 */
371 public static function get_admin_menu_list() {
372 $menu = [];
373 $menu[ ABLOCKS_PLUGIN_SLUG ] = [
374 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
375 'title' => __( 'Dashboard', 'ablocks' ),
376 'capability' => Permissions::ACCESS,
377 ];
378 if ( self::is_enabled_block( 'form-builder' ) ) {
379 $menu[ ABLOCKS_PLUGIN_SLUG . '-submissions' ] = [
380 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
381 'title' => __( 'Submissions', 'ablocks' ),
382 'capability' => 'ablocks_view_submissions',
383 ];
384 }
385 if ( self::get_addon_active_status( 'theme-builder' ) ) {
386 $menu[ ABLOCKS_PLUGIN_SLUG . '-theme-builder' ] = [
387 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
388 'title' => __( 'Theme Builder', 'ablocks' ),
389 'capability' => 'ablocks_manage_theme_builder',
390 ];
391 }
392 $menu[ ABLOCKS_PLUGIN_SLUG . '-addons' ] = [
393 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
394 'title' => __( 'Add-ons', 'ablocks' ),
395 'capability' => 'ablocks_manage_addons',
396 ];
397 $menu[ ABLOCKS_PLUGIN_SLUG . '-scanner' ] = [
398 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
399 'title' => __( 'Site Scanner', 'ablocks' ),
400 'capability' => 'ablocks_run_scanner',
401 ];
402 $menu[ ABLOCKS_PLUGIN_SLUG . '-settings' ] = [
403 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
404 'title' => __( 'Settings', 'ablocks' ),
405 'capability' => Permissions::SAVE_SETTINGS,
406 ];
407 if ( ! defined( 'ABLOCKS_PRO_VERSION' ) ) {
408 $menu[ ABLOCKS_PLUGIN_SLUG . '-get-pro' ] = [
409 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
410 'title' => '<span class="dashicons dashicons-awards academy-blue-color"></span> ' . __( 'Get Pro', 'ablocks' ),
411 'capability' => Permissions::ACCESS,
412 ];
413 }
414 return apply_filters( 'ablocks/admin_menu_list', $menu );
415 }
416
417 public static function get_preloader_html() {
418 ob_start();
419 ?>
420 <div class="ablocks-initial-preloader"><?php esc_html_e( 'Loading...', 'ablocks' ); ?></div>
421 <?php
422 return ob_get_clean();
423 }
424 public static function has_value( $value ) {
425 return isset( $value ) && ! empty( $value );
426 }
427
428 public static function get_array_value( $array, $key, $default ) {
429 return ( isset( $array[ $key ] ) && ! empty( $array[ $key ] ) ) ? $array[ $key ] : $default;
430 }
431
432 /**
433 * Whether a stored responsive value is set — the mirror of the editor's
434 * hasValue(). 0 and '0' are set; null, '' and blank strings are unset (they
435 * inherit); arrays/objects are set when non-empty. Unlike has_value(), which
436 * many unrelated callers rely on, this never treats 0 as missing.
437 *
438 * @param mixed $value Stored value.
439 * @return bool
440 */
441 public static function has_responsive_value( $value ) {
442 if ( null === $value ) {
443 return false;
444 }
445 if ( is_string( $value ) ) {
446 return '' !== trim( $value );
447 }
448 if ( is_array( $value ) ) {
449 return ! empty( $value );
450 }
451 if ( is_object( $value ) ) {
452 return ! empty( get_object_vars( $value ) );
453 }
454 return true;
455 }
456
457 /**
458 * The devices a device inherits from, nearest first, ending with Desktop ('')
459 * — the mirror of the editor's getDeviceAncestors().
460 *
461 * A wider device is an ancestor only when its range contains the device's own
462 * width, i.e. exactly when the frontend cascade applies its rule there, so a
463 * min-only (≥1400) or banded (900–1200) breakpoint is never inherited by
464 * Tablet. With the built-in devices this is Tablet ← Desktop, Mobile ← Tablet.
465 * Desktop has no ancestors; an unknown suffix (e.g. the phantom probe)
466 * inherits Desktop only.
467 *
468 * @param string $device Device suffix ('' | 'Desktop' | 'Tablet' | 'Bp…').
469 * @return string[] Ancestor suffixes, nearest first.
470 */
471 public static function get_responsive_ancestors( $device ) {
472 $target = 'Desktop' === $device ? '' : (string) $device;
473 if ( '' === $target ) {
474 return [];
475 }
476 $devices = array_values( self::get_responsive_devices() );
477 $index = array_search( $target, array_column( $devices, 'suffix' ), true );
478 if ( false === $index ) {
479 return [ '' ];
480 }
481 $ancestors = [];
482 for ( $i = $index - 1; $i >= 0; $i-- ) {
483 if ( empty( $devices[ $i ]['min'] ) && empty( $devices[ $i ]['max'] ) ) {
484 continue; // the base is always last
485 }
486 if ( self::breakpoint_contains( $devices[ $i ], $devices[ $index ] ) ) {
487 $ancestors[] = $devices[ $i ]['suffix'];
488 }
489 }
490 $ancestors[] = '';
491 return $ancestors;
492 }
493
494 /**
495 * Whether breakpoint $outer's range contains breakpoint $inner, judged at
496 * $inner's own width (its max, else its min) — i.e. whether $outer's rule
497 * still applies where $inner starts. The base device contains everything.
498 * Shared by inheritance (get_responsive_ancestors) and CSS dedupe.
499 *
500 * @param array $outer Breakpoint with `min` / `max` bounds.
501 * @param array $inner Breakpoint with `min` / `max` bounds.
502 * @return bool
503 */
504 /**
505 * Run a per-device CSS builder for a custom-breakpoint (or phantom probe)
506 * suffix. Block builders read `$attributes[ 'x' . $device ]` directly, and
507 * attribute defaults only declare the built-in suffixes, so a custom suffix
508 * key is missing — which is exactly "unset" for the caller
509 * (custom_device_map() subtracts the phantom baseline). Only those
510 * missing-key warnings are silenced, and only while the builder runs.
511 *
512 * @param callable $builder `( $device ) => styles`.
513 * @param string $device Device suffix.
514 * @return array Styles.
515 */
516 public static function call_device_builder( $builder, $device ) {
517 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
518 set_error_handler(
519 function ( $errno, $errstr ) {
520 return (bool) preg_match( '/^Undefined (array key|index|offset)/', $errstr );
521 },
522 E_WARNING | E_NOTICE
523 );
524 try {
525 return (array) call_user_func( $builder, $device );
526 } finally {
527 restore_error_handler();
528 }
529 }
530
531 public static function breakpoint_contains( $outer, $inner ) {
532 $o_min = isset( $outer['min'] ) ? (int) $outer['min'] : 0;
533 $o_max = isset( $outer['max'] ) ? (int) $outer['max'] : 0;
534 $i_min = isset( $inner['min'] ) ? (int) $inner['min'] : 0;
535 $i_max = isset( $inner['max'] ) ? (int) $inner['max'] : 0;
536 $width = $i_max ? $i_max : $i_min;
537 return ( ! $o_min || $width >= $o_min ) && ( ! $o_max || $width <= $o_max );
538 }
539
540 /**
541 * A device's value for a key: its own, else the nearest containing wider
542 * device's (see get_responsive_ancestors()), else Desktop's — the mirror of
543 * the editor's getResponsiveValue(). Stored values win over declared
544 * defaults per device. Returns false when nothing is set anywhere.
545 */
546 public static function get_responsive_value( $attribute, $attribute_object_key, $device, $attribute_default_value = [] ) {
547 $target = 'Desktop' === $device ? '' : (string) $device;
548 $suffixes = array_merge( [ $target ], self::get_responsive_ancestors( $target ) );
549 foreach ( $suffixes as $suffix ) {
550 $key = $attribute_object_key . $suffix;
551 if ( isset( $attribute[ $key ] ) && self::has_responsive_value( $attribute[ $key ] ) ) {
552 return $attribute[ $key ];
553 }
554 if ( isset( $attribute_default_value[ $key ] ) && self::has_responsive_value( $attribute_default_value[ $key ] ) ) {
555 return $attribute_default_value[ $key ];
556 }
557 }
558 return false;
559 }
560
561 public static function is_gutenberg_editor() {
562 global $pagenow;
563 if ( $pagenow === 'post.php' || $pagenow === 'post-new.php' ) {
564 return true;
565 }
566 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
567 return ( isset( $_GET['context'] ) && 'edit' === $_GET['context'] ) || ( isset( $_GET['action'] ) && 'edit' === $_GET['action'] );
568 }
569 public static function attr_shortcode( $attr_array ) {
570 $html_attr = '';
571 foreach ( $attr_array as $attr_name => $attr_val ) {
572 if ( empty( $attr_val ) ) {
573 continue;
574 }
575 if ( is_array( $attr_val ) ) {
576 $html_attr .= $attr_name . '="' . implode( ',', $attr_val ) . '" ';
577 } else {
578 $html_attr .= $attr_name . '="' . $attr_val . '" ';
579 }
580 }
581 return $html_attr;
582 }
583
584 public static function get_attribute_value( $attributes, $attribute_name ) {
585 return isset( $attributes[ $attribute_name ] ) ? $attributes[ $attribute_name ] : '';
586 }
587
588 public static function get_terms_list( $taxonomy = 'category' ) {
589 $options = [];
590 $terms = get_terms( [
591 'taxonomy' => $taxonomy,
592 'hide_empty' => true,
593 ] );
594
595 if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
596 foreach ( $terms as $term ) {
597 $options[] = [
598 'label' => $term->name,
599 'value' => $term->term_id,
600 ];
601 }
602 }
603
604 return $options;
605 }
606
607 public static function get_author_data( $post_id, $author_id = false ) {
608 $author_id = $author_id ?: get_post_field( 'post_author', $post_id );
609 $user = get_userdata( $author_id );
610
611 if ( ! $user ) {
612 wp_send_json_error( 'Author not found.', 404 );
613 }
614
615 $data = [
616 'author_id' => $author_id,
617 'author_name' => sanitize_text_field( $user->display_name ),
618 'author_posts_count' => count_user_posts( $author_id ),
619 'author_posts_url' => esc_url( get_author_posts_url( $author_id ) ),
620 'author_profile_picture_url' => esc_url( get_avatar_url( $author_id ) ),
621 'author_bio' => sanitize_text_field( get_user_meta( $author_id, 'description', true ) ),
622 'author_email' => sanitize_email( $user->user_email ),
623 'author_website' => esc_url( $user->user_url ),
624 'author_first_name' => sanitize_text_field( get_user_meta( $author_id, 'first_name', true ) ),
625 'author_last_name' => sanitize_text_field( get_user_meta( $author_id, 'last_name', true ) )
626 ];
627
628 return $data;
629 }
630
631 public static function get_icon_picker_attribute( $attributePrefix = 'icon', $defaultValue = [] ) {
632 $svgPathKey = $attributePrefix . 'SvgPath';
633 $svgViewBoxKey = $attributePrefix . 'SvgViewBox';
634 $svgClassKey = $attributePrefix . 'Class';
635
636 $attribute = [
637 $svgPathKey => [
638 'type' => 'string',
639 'source' => 'attribute',
640 'selector' => 'svg.ablocks-svg-icon path',
641 'attribute' => 'd',
642 ],
643 $svgViewBoxKey => [
644 'type' => 'string',
645 'source' => 'attribute',
646 'selector' => 'svg.ablocks-svg-icon',
647 'attribute' => 'viewBox',
648 ],
649 $svgClassKey => [
650 'type' => 'string',
651 ],
652 ];
653
654 if ( isset( $defaultValue['path'] ) && isset( $defaultValue['viewBox'] ) ) {
655 $attribute[ $svgPathKey ]['default'] = $defaultValue['path'];
656 $attribute[ $svgViewBoxKey ]['default'] = $defaultValue['viewBox'];
657 }
658 if ( isset( $defaultValue['className'] ) ) {
659 $attribute[ $svgClassKey ]['default'] = $defaultValue['className'];
660 }
661 return $attribute;
662 }
663
664 public static function get_terms_for_post( $taxonomy, $post_id ) {
665 $terms = wp_get_object_terms( $post_id, $taxonomy, [ 'fields' => 'all' ] );
666
667 if ( is_wp_error( $terms ) ) {
668 return [];
669 }
670
671 return array_map( function( $term ) {
672 return [
673 'id' => $term->term_id,
674 'name' => $term->name,
675 'slug' => $term->slug,
676 ];
677 }, $terms );
678 }
679
680 public static function get_taxonomies_data_for_post_type( $post_type ) {
681 $all_taxonomies = get_object_taxonomies( $post_type, 'objects' );
682 return array_values(array_map( function( $taxonomy ) {
683 return [
684 'value' => $taxonomy->name,
685 'label' => $taxonomy->label,
686 ];
687 }, $all_taxonomies ));
688 }
689
690 public static function get_post_excerpt( $post_id, $length = false ) {
691 $excerpt = get_the_excerpt( $post_id );
692 if ( $length ) {
693 return wp_trim_words( $excerpt, $length, '...' );
694 }
695 return $excerpt;
696 }
697
698 public static function get_post_terms_as_string( $post_id, $taxonomy, $separator = ', ' ) {
699 $terms = wp_get_post_terms( $post_id, $taxonomy );
700
701 if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
702 $term_names = wp_list_pluck( $terms, 'name' );
703 return implode( $separator, $term_names );
704 }
705
706 return '';
707 }
708
709 public static function get_post_time_date( $post_id, $dynamicContentAttribute, $is_time = false ) {
710 $dateTimeType = $dynamicContentAttribute['dateTimeType'];
711 $dateTimeFormat = $dynamicContentAttribute['dateTimeFormat'];
712 $customDateTimeFormat = $dynamicContentAttribute['customDateTimeFormat'];
713
714 $format = $dateTimeFormat;
715 $defaultFormat = $is_time ? 'g:i a' : 'j M, Y';
716 if ( ! $dateTimeFormat ) {
717 $format = $defaultFormat;
718 } elseif ( 'custom' === $dateTimeFormat ) {
719 $format = $customDateTimeFormat || $defaultFormat;
720 }
721
722 $date_time = '';
723 if ( ! $dateTimeType || 'post_published' === $dateTimeType ) {
724 $date_time = get_the_date( $format, $post_id );
725 } elseif ( 'post_modified' === $dateTimeType ) {
726 $date_time = get_the_modified_date( $format, $post_id );
727 }
728
729 return $date_time;
730 }
731
732 public static function is_fse_theme() {
733 return function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
734 }
735
736 public static function check_post_type_from_admin( $post_type ) {
737 global $post;
738 if ( is_admin() ) {
739 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
740 if ( $post && get_post_type( $post ) === $post_type ) {
741 return true;
742 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
743 } elseif ( isset( $_GET['post_type'] ) && $_GET['post_type'] === $post_type ) {
744 return true;
745 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
746 } elseif ( isset( $_GET['post'] ) ) {
747 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
748 $queried_post_type = get_post_type( sanitize_text_field( wp_unslash( $_GET['post'] ) ) );
749 if ( $queried_post_type === $post_type ) {
750 return true;
751 }
752 }
753 }
754 return false;
755 }
756
757 public static function get_content_by_object_id( string $id_or_fse_slug ) : ?string {
758 if ( is_numeric( $id_or_fse_slug ) ) {
759 if (
760 ! current_user_can( 'edit_post', $id_or_fse_slug ) &&
761 get_post_status( $id_or_fse_slug ) !== 'publish'
762 ) {
763 return null;
764 }
765 return get_post_field( 'post_content', intval( $id_or_fse_slug ) );
766 } elseif (
767 ! empty( $template = get_block_template( $id_or_fse_slug, 'wp_template_part' ) ) ||
768 ! empty( $template = get_block_template( $id_or_fse_slug ) )
769 ) {
770 return $template->content;
771 }
772 return null;
773 }
774
775 public static function get_block_attributes( string $post_id, string $block_id, string $block_name ) : array {
776 // Cache parsed blocks per object id for the request — this is called once
777 // per loop/REST lookup and would otherwise re-fetch + re-parse the whole
778 // post content every time.
779 static $parsed_cache = [];
780 if ( ! array_key_exists( $post_id, $parsed_cache ) ) {
781 $post_content = self::get_content_by_object_id( $post_id );
782 $parsed_cache[ $post_id ] = ( ! is_null( $post_content ) && is_array( $blocks = parse_blocks( $post_content ) ) )
783 ? $blocks
784 : null;
785 }
786 if ( is_array( $parsed_cache[ $post_id ] ) ) {
787 return self::get_block_attributes_recursive( $block_id, $block_name, $parsed_cache[ $post_id ] );
788 }
789 return [];
790 }
791
792 public static function get_block_attributes_recursive( string $block_id, string $block_name, array $blocks ) : array {
793 foreach ( $blocks as $block ) {
794
795 if (
796 ( $block['attrs']['block_id'] ?? '' ) === $block_id &&
797 ( $block['blockName'] ?? '' ) === $block_name
798 ) {
799 return [
800 'parentAttributes' => $block['attrs'] ?? [],
801 'innerBlocks' => self::extract_inner_blocks( $block['innerBlocks'] ?? [] ),
802 ];
803 }
804
805 if (
806 array_key_exists( 'innerBlocks', $block ) &&
807 is_array( $block['innerBlocks'] ) &&
808 count( $block['innerBlocks'] ) > 0
809 ) {
810 $data = self::get_block_attributes_recursive(
811 $block_id,
812 $block_name,
813 $block['innerBlocks']
814 );
815 if ( ! empty( $data ) ) {
816 return $data;
817 }
818 }
819
820 if (
821 isset( $block['blockName'] ) &&
822 $block['blockName'] === 'core/template-part' &&
823 ! empty( $block['attrs']['slug'] )
824 ) {
825 $part_slug = $block['attrs']['theme'] . '//' . $block['attrs']['slug'];
826 $data = self::get_block_attributes( $part_slug, $block_id, $block_name );
827 if ( ! empty( $data ) ) {
828 return $data;
829 }
830 }
831 }//end foreach
832 return [];
833 }
834
835 public static function extract_inner_blocks( $innerBlocks ) {
836 return array_map(function ( $inner_block ) {
837 $block_data = [
838 'blockName' => $inner_block['blockName'],
839 'attributes' => $inner_block['attrs'],
840 ];
841 if ( ! empty( $inner_block['innerBlocks'] ) ) {
842 $block_data['innerBlocks'] = self::extract_inner_blocks( $inner_block['innerBlocks'] );
843 }
844 return $block_data;
845 }, $innerBlocks);
846 }
847
848 public static function generate_schema_using_form_data( $customFields ) {
849 $schema = [];
850
851 foreach ( $customFields as $inputField ) {
852 // Check if 'name' exists in the input field
853 if ( isset( $inputField['name'] ) ) {
854 $field_name = $inputField['name'];
855 $input_type = $inputField['inputType'] ?? 'text'; // Default to 'text' if not specified
856
857 // Map the input types to schema types
858 switch ( $input_type ) {
859 case 'Text':
860 $schema[ $field_name ] = 'string';
861 break;
862 case 'Email':
863 $schema[ $field_name ] = 'email';
864 break;
865 case 'Password':
866 $schema[ $field_name ] = 'string';
867 break;
868 case 'Number':
869 $schema[ $field_name ] = 'number';
870 break;
871 case 'Url':
872 $schema[ $field_name ] = 'url';
873 break;
874 case 'Boolean':
875 $schema[ $field_name ] = 'boolean';
876 break;
877 case 'Textarea':
878 $schema[ $field_name ] = 'textarea';
879 break;
880 default:
881 $schema[ $field_name ] = 'string'; // Default to string for unknown types
882 break;
883 }//end switch
884 }//end if
885 }//end foreach
886
887 return $schema;
888 }
889
890 public static function sorted_input_fields_by_input_type( $blockData ) {
891 $sorted_custom_fields = [];
892 foreach ( $blockData as $block ) {
893 $name = $block['attributes']['name'] ?? null;
894 $inputType = $block['attributes']['inputType'] ?? null;
895
896 if ( $name && isset( $custom_fields[ $name ] ) ) {
897 $sorted_custom_fields[] = [
898 'name' => $name,
899 'inputType' => $inputType,
900 'value' => $custom_fields[ $name ]
901 ];
902 }
903 }
904
905 return $sorted_custom_fields;
906 }
907
908
909 public static function render_svg_icon_using_attr( $attributes = array() ) {
910 $default_attributes = array(
911 'path' => '',
912 'viewBox' => '0 0 24 24',
913 'className' => 'icon-class',
914 'width' => '24',
915 'height' => '24',
916 );
917
918 // Merge passed attributes with default values
919 $attributes = array_merge( $default_attributes, $attributes );
920
921 // Sanitize attributes for safety
922 $path = esc_attr( $attributes['path'] );
923 $viewBox = esc_attr( $attributes['viewBox'] );
924 $className = esc_attr( $attributes['className'] );
925 $width = esc_attr( $attributes['width'] );
926 $height = esc_attr( $attributes['height'] );
927
928 // Output the SVG
929 return '
930 <svg
931 xmlns="http://www.w3.org/2000/svg"
932 viewBox="' . $viewBox . '"
933 class="' . $className . '"
934 width="' . $width . '"
935 height="' . $height . '">
936 <path d="' . $path . '"></path>
937 </svg>';
938 }
939
940 public static function get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
941 $template = false;
942
943 if ( ! $template ) {
944 $template = self::locate_template( $template_name, $template_path, $default_path );
945 }
946
947 // Allow 3rd party plugin filter template file from their plugin.
948 $filter_template = apply_filters( 'ablocks/get_template', $template, $template_name, $args, $template_path, $default_path );
949
950 if ( $filter_template !== $template ) {
951 if ( ! file_exists( $filter_template ) ) {
952 /* translators: %s template */
953 wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'ablocks' ), '<code>' . $filter_template . '</code>' ), '1.0.0' );
954
955 return;
956 }
957 $template = $filter_template;
958 }
959
960 $action_args = array(
961 'template_name' => $template_name,
962 'template_path' => $template_path,
963 'located' => $template,
964 'args' => $args,
965 );
966
967 if ( ! empty( $args ) && is_array( $args ) ) {
968 if ( isset( $args['action_args'] ) ) {
969 wc_doing_it_wrong(
970 __FUNCTION__,
971 __( 'action_args should not be overwritten when calling ablocks/get_template.', 'ablocks' ),
972 '1.0.0'
973 );
974 unset( $args['action_args'] );
975 }
976 extract( $args ); // @codingStandardsIgnoreLine
977 }
978
979 do_action( 'ablocks/before_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] );
980 include $action_args['located'];
981
982 do_action( 'ablocks/after_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] );
983 }
984
985 public static function locate_template( $template_name, $template_path = '', $default_path = '' ) {
986 if ( ! $template_path ) {
987 $template_path = self::template_path();
988 }
989
990 if ( ! $default_path ) {
991 $default_path = self::plugin_path() . 'templates/';
992 }
993
994 if ( empty( $template ) ) {
995 $template = locate_template(
996 array(
997 trailingslashit( $template_path ) . $template_name,
998 $template_name,
999 )
1000 );
1001 }
1002 if ( ! $template ) {
1003 $template = $default_path . $template_name;
1004 }
1005
1006 // Return what we found.
1007 return apply_filters( 'ablocks/locate_template', $template, $template_name, $template_path );
1008 }
1009
1010 public static function template_path() {
1011 return apply_filters( 'ablocks/template_path', 'ablocks/' );
1012 }
1013 public static function plugin_path() {
1014 return apply_filters( 'ablocks/plugin_path', ABLOCKS_ROOT_DIR_PATH );
1015 }
1016
1017 public static function get_addon_active_status( $addon_name, $is_pro = false ) {
1018 global $ablocks_addons;
1019 if ( $is_pro && ! self::is_active_ablocks_pro() ) {
1020 return false;
1021 }
1022 if ( isset( $ablocks_addons->{$addon_name} ) ) {
1023 return (bool) $ablocks_addons->{$addon_name};
1024 }
1025
1026 return false;
1027 }
1028
1029 public static function sanitize_checkbox_field( $boolean ) {
1030 return filter_var( sanitize_text_field( $boolean ), FILTER_VALIDATE_BOOLEAN );
1031 }
1032
1033 public static function is_valid_site_url( string $url ): bool {
1034 return str_starts_with( $url, get_option( 'siteurl' ) );
1035 }
1036
1037 public static function get_script_loading_strategy() {
1038 return 'defer';
1039 }
1040
1041 public static function is_static_front_page( $post_id ) {
1042 $show_on_front = get_option( 'show_on_front' );
1043 $page_on_front = (int) get_option( 'page_on_front' );
1044 return ( $show_on_front === 'page' && $page_on_front === (int) $post_id );
1045 }
1046
1047 public static function get_public_post_type_options() {
1048 $args = [
1049 'public' => true,
1050 '_builtin' => true,
1051 ];
1052 $post_types = get_post_types( $args, 'objects' );
1053 unset( $post_types['attachment'] ); // Remove 'attachment' if present
1054
1055 // Get custom post types
1056 $args['_builtin'] = false;
1057 $custom_post_types = get_post_types( $args, 'objects' );
1058
1059 // Allow filters to modify the combined post types
1060 $all_post_types = apply_filters(
1061 'ablocks_theme_builder/location_rule_post_types',
1062 array_merge( $post_types, $custom_post_types )
1063 );
1064
1065 // Format result
1066 $result = [];
1067 foreach ( $all_post_types as $post_type => $post_type_obj ) {
1068 $result[] = [
1069 'label' => $post_type_obj->label,
1070 'value' => $post_type,
1071 ];
1072 }
1073
1074 return $result;
1075 }
1076
1077 public static function clear_third_party_plugin_cache() {
1078
1079 // Breeze
1080 try {
1081 if ( class_exists( 'Breeze_Purge' ) ) {
1082 \Breeze_Purge::breeze_cache_flush();
1083 }
1084 } catch ( \Throwable $e ) {
1085 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1086 error_log( 'Breeze Cache Clear Failed: ' . $e->getMessage() );
1087 }
1088
1089 // W3 Total Cache
1090 try {
1091 if ( function_exists( 'w3tc_flush_all' ) ) {
1092 \w3tc_flush_all();
1093 }
1094 } catch ( \Throwable $e ) {
1095 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1096 error_log( 'W3 Total Cache Clear Failed: ' . $e->getMessage() );
1097 }
1098
1099 // WP Super Cache
1100 try {
1101 if ( function_exists( 'wp_cache_clear_cache' ) ) {
1102 \wp_cache_clear_cache();
1103 }
1104 } catch ( \Throwable $e ) {
1105 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1106 error_log( 'WP Super Cache Clear Failed: ' . $e->getMessage() );
1107 }
1108
1109 // LiteSpeed Cache
1110 try {
1111 if ( class_exists( 'LiteSpeed_Cache_API' ) ) {
1112 \LiteSpeed_Cache_API::purge_all();
1113 }
1114 } catch ( \Throwable $e ) {
1115 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1116 error_log( 'LiteSpeed Cache Clear Failed: ' . $e->getMessage() );
1117 }
1118
1119 // WP Fastest Cache
1120 try {
1121 if ( class_exists( 'WpFastestCache' ) ) {
1122 $wpfc = new \WpFastestCache();
1123 $wpfc->deleteCache();
1124 }
1125 } catch ( \Throwable $e ) {
1126 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1127 error_log( 'WP Fastest Cache Clear Failed: ' . $e->getMessage() );
1128 }
1129
1130 // Autoptimize
1131 try {
1132 if ( function_exists( 'autoptimize_clearall' ) ) {
1133 \autoptimize_clearall();
1134 }
1135 } catch ( \Throwable $e ) {
1136 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1137 error_log( 'Autoptimize Clear Failed: ' . $e->getMessage() );
1138 }
1139 }
1140 }
1141