| @@ -4,10 +4,8 @@ | ||
| 4 | 4 | if ( ! defined( 'ABSPATH' ) ) { |
| 5 | 5 | exit; // Exit if accessed directly. |
| 6 | 6 | } |
| 7 | 7 | |
| 8 | -use ABlocks\Classes\FileUpload; | |
| 9 | -use ABlocks\Classes\BlockGlobal; | |
| 10 | 8 | use ABlocks\traits\Importer; |
| 11 | 9 | |
| 12 | 10 | class Helper { |
| 13 | 11 | |
| @@ -12,8 +10,11 @@ | ||
| 12 | 10 | class Helper { |
| 13 | 11 | |
| 14 | 12 | use Importer; |
| 15 | 13 | |
| 14 | + /** Memoized responsive device list (see get_responsive_devices). */ | |
| 15 | + private static $responsive_devices_cache = null; | |
| 16 | + | |
| 16 | 17 | public static function get_time() { |
| 17 | 18 | return time() + ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ); |
| 18 | 19 | } |
| 19 | 20 | |
| @@ -26,8 +27,266 @@ | ||
| 26 | 27 | |
| 27 | 28 | return $default; |
| 28 | 29 | } |
| 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 | + } | |
| 30 | 289 | public static function is_enabled_block( $block_name, $parent_block_name = '' ) { |
| 31 | 290 | global $ablocks_blocks; |
| 32 | 291 | $block_name = ! empty( $parent_block_name ) ? $parent_block_name : $block_name; |
| 33 | 292 | if ( isset( $ablocks_blocks->{$block_name} ) ) { |
| @@ -60,14 +319,31 @@ | ||
| 60 | 319 | public static function is_active_wp_map_block() { |
| 61 | 320 | $wp_map_block = 'wp-map-block/wp-map-block.php'; |
| 62 | 321 | return self::is_plugin_active( $wp_map_block ); |
| 63 | 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 | + } | |
| 64 | 337 | |
| 65 | 338 | public static function is_enabled_assets_generation() { |
| 66 | - $flag = false; | |
| 67 | - if ( (bool) self::get_settings( 'enabled_assets_file_generation' ) ) { | |
| 68 | - $flag = function_exists( 'wp_is_block_theme' ) ? ! wp_is_block_theme() : true; | |
| 69 | - } | |
| 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 ); | |
| 70 | 346 | return apply_filters( 'ablocks/is_enabled_assets_generation', $flag ); |
| 71 | 347 | } |
| 72 | 348 | |
| 73 | 349 | public static function is_plugin_active( $basename ) { |
| @@ -83,20 +359,28 @@ | ||
| 83 | 359 | return true; |
| 84 | 360 | } |
| 85 | 361 | } |
| 86 | 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 | + */ | |
| 87 | 371 | public static function get_admin_menu_list() { |
| 88 | 372 | $menu = []; |
| 89 | 373 | $menu[ ABLOCKS_PLUGIN_SLUG ] = [ |
| 90 | 374 | 'parent_slug' => ABLOCKS_PLUGIN_SLUG, |
| 91 | 375 | 'title' => __( 'Dashboard', 'ablocks' ), |
| 92 | - 'capability' => 'manage_options', | |
| 376 | + 'capability' => Permissions::ACCESS, | |
| 93 | 377 | ]; |
| 94 | 378 | if ( self::is_enabled_block( 'form-builder' ) ) { |
| 95 | 379 | $menu[ ABLOCKS_PLUGIN_SLUG . '-submissions' ] = [ |
| 96 | 380 | 'parent_slug' => ABLOCKS_PLUGIN_SLUG, |
| 97 | 381 | 'title' => __( 'Submissions', 'ablocks' ), |
| 98 | - 'capability' => 'manage_options', | |
| 382 | + 'capability' => 'ablocks_view_submissions', | |
| 99 | 383 | ]; |
| 100 | 384 | } |
| 101 | 385 | if ( self::get_addon_active_status( 'theme-builder' ) ) { |
| 102 | 386 | $menu[ ABLOCKS_PLUGIN_SLUG . '-theme-builder' ] = [ |
| @@ -101,26 +385,31 @@ | ||
| 101 | 385 | if ( self::get_addon_active_status( 'theme-builder' ) ) { |
| 102 | 386 | $menu[ ABLOCKS_PLUGIN_SLUG . '-theme-builder' ] = [ |
| 103 | 387 | 'parent_slug' => ABLOCKS_PLUGIN_SLUG, |
| 104 | 388 | 'title' => __( 'Theme Builder', 'ablocks' ), |
| 105 | - 'capability' => 'manage_options', | |
| 389 | + 'capability' => 'ablocks_manage_theme_builder', | |
| 106 | 390 | ]; |
| 107 | 391 | } |
| 108 | 392 | $menu[ ABLOCKS_PLUGIN_SLUG . '-addons' ] = [ |
| 109 | 393 | 'parent_slug' => ABLOCKS_PLUGIN_SLUG, |
| 110 | 394 | 'title' => __( 'Add-ons', 'ablocks' ), |
| 111 | - 'capability' => 'manage_options', | |
| 395 | + 'capability' => 'ablocks_manage_addons', | |
| 112 | 396 | ]; |
| 397 | + $menu[ ABLOCKS_PLUGIN_SLUG . '-scanner' ] = [ | |
| 398 | + 'parent_slug' => ABLOCKS_PLUGIN_SLUG, | |
| 399 | + 'title' => __( 'Site Scanner', 'ablocks' ), | |
| 400 | + 'capability' => 'ablocks_run_scanner', | |
| 401 | + ]; | |
| 113 | 402 | $menu[ ABLOCKS_PLUGIN_SLUG . '-settings' ] = [ |
| 114 | 403 | 'parent_slug' => ABLOCKS_PLUGIN_SLUG, |
| 115 | 404 | 'title' => __( 'Settings', 'ablocks' ), |
| 116 | - 'capability' => 'manage_options', | |
| 405 | + 'capability' => Permissions::SAVE_SETTINGS, | |
| 117 | 406 | ]; |
| 118 | 407 | if ( ! defined( 'ABLOCKS_PRO_VERSION' ) ) { |
| 119 | 408 | $menu[ ABLOCKS_PLUGIN_SLUG . '-get-pro' ] = [ |
| 120 | 409 | 'parent_slug' => ABLOCKS_PLUGIN_SLUG, |
| 121 | 410 | 'title' => '<span class="dashicons dashicons-awards academy-blue-color"></span> ' . __( 'Get Pro', 'ablocks' ), |
| 122 | - 'capability' => 'manage_options', | |
| 411 | + 'capability' => Permissions::ACCESS, | |
| 123 | 412 | ]; |
| 124 | 413 | } |
| 125 | 414 | return apply_filters( 'ablocks/admin_menu_list', $menu ); |
| 126 | 415 | } |
| @@ -134,8 +423,142 @@ | ||
| 134 | 423 | } |
| 135 | 424 | public static function has_value( $value ) { |
| 136 | 425 | return isset( $value ) && ! empty( $value ); |
| 137 | 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 | + | |
| 138 | 561 | public static function is_gutenberg_editor() { |
| 139 | 562 | global $pagenow; |
| 140 | 563 | if ( $pagenow === 'post.php' || $pagenow === 'post-new.php' ) { |
| 141 | 564 | return true; |
| @@ -332,8 +755,14 @@ | ||
| 332 | 755 | } |
| 333 | 756 | |
| 334 | 757 | public static function get_content_by_object_id( string $id_or_fse_slug ) : ?string { |
| 335 | 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 | + } | |
| 336 | 765 | return get_post_field( 'post_content', intval( $id_or_fse_slug ) ); |
| 337 | 766 | } elseif ( |
| 338 | 767 | ! empty( $template = get_block_template( $id_or_fse_slug, 'wp_template_part' ) ) || |
| 339 | 768 | ! empty( $template = get_block_template( $id_or_fse_slug ) ) |
| @@ -343,13 +772,21 @@ | ||
| 343 | 772 | return null; |
| 344 | 773 | } |
| 345 | 774 | |
| 346 | 775 | public static function get_block_attributes( string $post_id, string $block_id, string $block_name ) : array { |
| 347 | - if ( ! is_null( $post_content = self::get_content_by_object_id( $post_id ) ) ) { | |
| 348 | - if ( is_array( $blocks = parse_blocks( $post_content ) ) ) { | |
| 349 | - return self::get_block_attributes_recursive( $block_id, $block_name, $blocks ); | |
| 350 | - } | |
| 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; | |
| 351 | 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 | + } | |
| 352 | 789 | return []; |
| 353 | 790 | } |
| 354 | 791 | |
| 355 | 792 | public static function get_block_attributes_recursive( string $block_id, string $block_name, array $blocks ) : array { |
| @@ -378,8 +815,20 @@ | ||
| 378 | 815 | if ( ! empty( $data ) ) { |
| 379 | 816 | return $data; |
| 380 | 817 | } |
| 381 | 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 | + } | |
| 382 | 831 | }//end foreach |
| 383 | 832 | return []; |
| 384 | 833 | } |
| 385 | 834 | |
| @@ -584,5 +1033,108 @@ | ||
| 584 | 1033 | public static function is_valid_site_url( string $url ): bool { |
| 585 | 1034 | return str_starts_with( $url, get_option( 'siteurl' ) ); |
| 586 | 1035 | } |
| 587 | 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 | + } | |
| 588 | 1140 | } |