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
← All changes | includes/helper.php +421 -43 2.8.1 → 2.14.0 View file →
@@ -10,8 +10,11 @@
10 10 class Helper {
11 11
12 12 use Importer;
13 13
14 + /** Memoized responsive device list (see get_responsive_devices). */
15 + private static $responsive_devices_cache = null;
16 +
14 17 public static function get_time() {
15 18 return time() + ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS );
16 19 }
17 20
@@ -24,8 +27,251 @@
24 27
25 28 return $default;
26 29 }
27 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 +
28 274 public static function get_page_permalink( $page, $fallback = null ) {
29 275 $page_id = self::get_settings( $page );
30 276 $permalink = 0 < $page_id ? get_permalink( $page_id ) : '';
31 277 if ( ! $permalink ) {
@@ -76,15 +322,28 @@
76 322 }
77 323 public static function is_active_quizpress() {
78 324 return class_exists( 'QuizPress' );
79 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 + }
80 334 public static function is_active_easy_content_manager() {
81 - $easy_content_manager = 'easy-content-manager/easy-content-manager.php';
82 - return self::is_plugin_active( $easy_content_manager );
335 + return class_exists( 'EasyContentManager' );
83 336 }
84 337
85 338 public static function is_enabled_assets_generation() {
86 - $flag = (bool) self::get_settings( 'enabled_assets_file_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 );
87 346 return apply_filters( 'ablocks/is_enabled_assets_generation', $flag );
88 347 }
89 348
90 349 public static function is_plugin_active( $basename ) {
@@ -100,20 +359,28 @@
100 359 return true;
101 360 }
102 361 }
103 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 + */
104 371 public static function get_admin_menu_list() {
105 372 $menu = [];
106 373 $menu[ ABLOCKS_PLUGIN_SLUG ] = [
107 374 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
108 375 'title' => __( 'Dashboard', 'ablocks' ),
109 - 'capability' => 'manage_options',
376 + 'capability' => Permissions::ACCESS,
110 377 ];
111 378 if ( self::is_enabled_block( 'form-builder' ) ) {
112 379 $menu[ ABLOCKS_PLUGIN_SLUG . '-submissions' ] = [
113 380 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
114 381 'title' => __( 'Submissions', 'ablocks' ),
115 - 'capability' => 'manage_options',
382 + 'capability' => 'ablocks_view_submissions',
116 383 ];
117 384 }
118 385 if ( self::get_addon_active_status( 'theme-builder' ) ) {
119 386 $menu[ ABLOCKS_PLUGIN_SLUG . '-theme-builder' ] = [
@@ -118,26 +385,31 @@
118 385 if ( self::get_addon_active_status( 'theme-builder' ) ) {
119 386 $menu[ ABLOCKS_PLUGIN_SLUG . '-theme-builder' ] = [
120 387 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
121 388 'title' => __( 'Theme Builder', 'ablocks' ),
122 - 'capability' => 'manage_options',
389 + 'capability' => 'ablocks_manage_theme_builder',
123 390 ];
124 391 }
125 392 $menu[ ABLOCKS_PLUGIN_SLUG . '-addons' ] = [
126 393 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
127 394 'title' => __( 'Add-ons', 'ablocks' ),
128 - 'capability' => 'manage_options',
395 + 'capability' => 'ablocks_manage_addons',
129 396 ];
397 + $menu[ ABLOCKS_PLUGIN_SLUG . '-scanner' ] = [
398 + 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
399 + 'title' => __( 'Site Scanner', 'ablocks' ),
400 + 'capability' => 'ablocks_run_scanner',
401 + ];
130 402 $menu[ ABLOCKS_PLUGIN_SLUG . '-settings' ] = [
131 403 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
132 404 'title' => __( 'Settings', 'ablocks' ),
133 - 'capability' => 'manage_options',
405 + 'capability' => Permissions::SAVE_SETTINGS,
134 406 ];
135 407 if ( ! defined( 'ABLOCKS_PRO_VERSION' ) ) {
136 408 $menu[ ABLOCKS_PLUGIN_SLUG . '-get-pro' ] = [
137 409 'parent_slug' => ABLOCKS_PLUGIN_SLUG,
138 410 'title' => '<span class="dashicons dashicons-awards academy-blue-color"></span> ' . __( 'Get Pro', 'ablocks' ),
139 - 'capability' => 'manage_options',
411 + 'capability' => Permissions::ACCESS,
140 412 ];
141 413 }
142 414 return apply_filters( 'ablocks/admin_menu_list', $menu );
143 415 }
@@ -156,43 +428,135 @@
156 428 public static function get_array_value( $array, $key, $default ) {
157 429 return ( isset( $array[ $key ] ) && ! empty( $array[ $key ] ) ) ? $array[ $key ] : $default;
158 430 }
159 431
160 - public static function get_responsive_value( $attribute, $attribute_object_key, $device, $attribute_default_value = [] ) {
161 - // Closure to "clean" a value (similar to your JS clean() helper)
162 - $clean = function( $value ) {
163 - return self::has_value( $value ) ? $value : null;
164 - };
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 + }
165 456
166 - // Desktop value (fallback to default, or false if nothing found)
167 - $desktop_value =
168 - $clean( $attribute[ $attribute_object_key ] ?? null ) ??
169 - $clean( $attribute_default_value[ $attribute_object_key ] ?? null ) ??
170 - false;
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 + }
171 493
172 - // Tablet value (fallback to desktop if nothing found)
173 - $tablet_key = $attribute_object_key . 'Tablet';
174 - $tablet_value =
175 - $clean( $attribute[ $tablet_key ] ?? null ) ??
176 - $clean( $attribute_default_value[ $tablet_key ] ?? null ) ??
177 - $desktop_value;
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 + }
178 530
179 - // Mobile value (fallback to tablet if nothing found)
180 - $mobile_key = $attribute_object_key . 'Mobile';
181 - $mobile_value =
182 - $clean( $attribute[ $mobile_key ] ?? null ) ??
183 - $clean( $attribute_default_value[ $mobile_key ] ?? null ) ??
184 - $tablet_value;
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 + }
185 539
186 - // Return based on device
187 - switch ( $device ) {
188 - case 'Mobile':
189 - return $mobile_value;
190 - case 'Tablet':
191 - return $tablet_value;
192 - default:
193 - return $desktop_value;
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 + }
194 557 }
558 + return false;
195 559 }
196 560
197 561 public static function is_gutenberg_editor() {
198 562 global $pagenow;
@@ -391,8 +755,14 @@
391 755 }
392 756
393 757 public static function get_content_by_object_id( string $id_or_fse_slug ) : ?string {
394 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 + }
395 765 return get_post_field( 'post_content', intval( $id_or_fse_slug ) );
396 766 } elseif (
397 767 ! empty( $template = get_block_template( $id_or_fse_slug, 'wp_template_part' ) ) ||
398 768 ! empty( $template = get_block_template( $id_or_fse_slug ) )
@@ -402,12 +772,20 @@
402 772 return null;
403 773 }
404 774
405 775 public static function get_block_attributes( string $post_id, string $block_id, string $block_name ) : array {
406 - if ( ! is_null( $post_content = self::get_content_by_object_id( $post_id ) ) ) {
407 - if ( is_array( $blocks = parse_blocks( $post_content ) ) ) {
408 - return self::get_block_attributes_recursive( $block_id, $block_name, $blocks );
409 - }
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 ] );
410 788 }
411 789 return [];
412 790 }
413 791