PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks for WordPress Block Editor / 2.0.10
Blockenberg — 600+ Advanced Gutenberg Blocks for WordPress Block Editor v2.0.10
2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blockenberg.php

blockenberg.php in Blockenberg — 600+ Advanced Gutenberg Blocks for WordPress Block Editor 2.0.10, at blockenberg.php

1,178 lines 51.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Blockenberg
4 * Description: Advanced Gutenberg Blocks for WordPress Block Editor
5 * Version: 2.0.10
6 * Author: Blockenberg
7 * Text Domain: blockenberg
8 * Domain Path: /languages
9 * License: GPLv2 or later
10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 */
12
13 defined( 'ABSPATH' ) || exit;
14
15 /**
16 * Ensure enough PHP memory to register 600+ blocks with layout attributes.
17 * The register_block_type_args filter adds ~400 extra attributes per block,
18 * which requires significantly more memory than the WordPress default 128M.
19 */
20 @ini_set( 'memory_limit', '512M' );
21
22 /**
23 * Google Fonts list for the Typography Control.
24 */
25 require_once __DIR__ . '/assets/php/google-fonts.php';
26
27 /**
28 * Admin dashboard — Block Manager (enable / disable blocks).
29 */
30 if ( is_admin() ) {
31 require_once __DIR__ . '/assets/php/admin-dashboard.php';
32 }
33
34 /**
35 * Enqueue common editor styles and scripts for all Blockenberg blocks
36 */
37 add_action( 'enqueue_block_editor_assets', function() {
38 wp_enqueue_style( 'dashicons' );
39 // Ensure Media Library modal assets are available for blocks using MediaUpload.
40 wp_enqueue_media();
41
42 // Common editor scripts (branded icons)
43 $editor_js = __DIR__ . '/assets/js/editor.js';
44 if ( file_exists( $editor_js ) ) {
45 wp_enqueue_script(
46 'bkbg-editor-common',
47 plugins_url( 'assets/js/editor.js', __FILE__ ),
48 array( 'wp-blocks', 'wp-dom-ready', 'wp-element' ),
49 filemtime( $editor_js ),
50 true
51 );
52 }
53
54 // Inspector tabs (General / Advanced) for all Blockenberg blocks
55 $inspector_tabs_js = __DIR__ . '/assets/js/inspector-tabs.js';
56 if ( file_exists( $inspector_tabs_js ) ) {
57 wp_enqueue_script(
58 'bkbg-inspector-tabs',
59 plugins_url( 'assets/js/inspector-tabs.js', __FILE__ ),
60 array( 'wp-blocks', 'wp-element', 'wp-compose', 'wp-hooks', 'wp-block-editor', 'wp-components', 'wp-i18n', 'wp-data' ),
61 filemtime( $inspector_tabs_js ),
62 true
63 );
64 }
65
66 // Typography Control — shared Elementor-like popover for all blocks
67 // Registered on init (so block scripts can safely depend on it).
68 wp_enqueue_script( 'bkbg-typography-control' );
69 });
70
71 /**
72 * Register shared editor scripts early (so other scripts can list them as deps).
73 */
74 add_action( 'init', function () {
75 $typo_js = __DIR__ . '/assets/js/typography-control.js';
76 if ( ! file_exists( $typo_js ) ) {
77 return;
78 }
79
80 wp_register_script(
81 'bkbg-typography-control',
82 plugins_url( 'assets/js/typography-control.js', __FILE__ ),
83 array( 'wp-element', 'wp-components', 'wp-i18n' ),
84 filemtime( $typo_js ),
85 true
86 );
87
88 // Pass Google Fonts list to JS as window.bkbgGoogleFonts
89 wp_localize_script(
90 'bkbg-typography-control',
91 'bkbgGoogleFonts',
92 function_exists( 'bkbg_google_fonts_list' ) ? bkbg_google_fonts_list() : array()
93 );
94
95 // Icon Picker — shared icon type selector + dashicon picker for all blocks
96 $icon_picker_js = __DIR__ . '/assets/js/icon-picker.js';
97 if ( file_exists( $icon_picker_js ) ) {
98 // Editor handle (needs WP component deps for UI)
99 wp_register_script(
100 'bkbg-icon-picker',
101 plugins_url( 'assets/js/icon-picker.js', __FILE__ ),
102 array( 'wp-element', 'wp-components', 'wp-i18n' ),
103 filemtime( $icon_picker_js ),
104 true
105 );
106 // Frontend handle (same file, no WP deps — only data + DOM builder)
107 wp_register_script(
108 'bkbg-icon-picker-frontend',
109 plugins_url( 'assets/js/icon-picker.js', __FILE__ ),
110 array(),
111 filemtime( $icon_picker_js ),
112 true
113 );
114 }
115 } );
116
117 /**
118 * Enqueue editor styles in a way compatible with the iframe-based editor canvas.
119 */
120 add_action( 'enqueue_block_assets', function () {
121 // Avoid loading editor-only CSS on the frontend.
122 if ( ! is_admin() ) {
123 return;
124 }
125
126 // Layout system CSS (shared variables and utilities)
127 $layout_css = __DIR__ . '/assets/css/layout.css';
128 if ( file_exists( $layout_css ) ) {
129 wp_enqueue_style(
130 'bkbg-layout-system',
131 plugins_url( 'assets/css/layout.css', __FILE__ ),
132 array(),
133 filemtime( $layout_css )
134 );
135 }
136
137 // Common editor styles for all blocks
138 $editor_css = __DIR__ . '/assets/css/editor.css';
139 if ( file_exists( $editor_css ) ) {
140 wp_enqueue_style(
141 'bkbg-editor-common',
142 plugins_url( 'assets/css/editor.css', __FILE__ ),
143 array( 'bkbg-layout-system' ),
144 filemtime( $editor_css )
145 );
146 }
147 } );
148
149 /**
150 * Register block scripts with dependencies (but don't enqueue them yet)
151 */
152 add_action( 'init', function () {
153 $blocks_dir = __DIR__ . '/blocks/';
154
155 // Standard WordPress script dependencies for blocks
156 $script_dependencies = array(
157 'wp-blocks',
158 'wp-element',
159 'wp-i18n',
160 'wp-block-editor',
161 'wp-components',
162 'wp-dom-ready',
163 'wp-data',
164 'bkbg-inspector-tabs',
165 'bkbg-typography-control',
166 'bkbg-icon-picker'
167 );
168
169 // Standard WordPress style dependencies for blocks
170 $style_dependencies = array(
171 'dashicons'
172 );
173
174 // Register layout system CSS for frontend
175 $layout_css = __DIR__ . '/assets/css/layout.css';
176 if ( file_exists( $layout_css ) ) {
177 wp_register_style(
178 'bkbg-layout-system',
179 plugins_url( 'assets/css/layout.css', __FILE__ ),
180 array(),
181 filemtime( $layout_css )
182 );
183 }
184
185 // Get disabled blocks list to skip asset registration.
186 $disabled_blocks = get_option( 'blockenberg_disabled_blocks', array() );
187 if ( ! is_array( $disabled_blocks ) ) {
188 $disabled_blocks = array();
189 }
190
191 // Automatically register scripts for all blocks
192 foreach ( glob( $blocks_dir . '*', GLOB_ONLYDIR ) as $block_dir ) {
193 $block_name = basename( $block_dir );
194
195 // Skip disabled blocks.
196 if ( in_array( 'blockenberg/' . $block_name, $disabled_blocks, true ) ) {
197 continue;
198 }
199
200 $script_file = $block_dir . '/index.js';
201
202 $style_file = $block_dir . '/style.css';
203 $frontend_file = $block_dir . '/frontend.js';
204
205 // Check if block has a JavaScript file
206 if ( file_exists( $script_file ) ) {
207 wp_register_script(
208 'bkbg-' . $block_name . '-editor',
209 plugins_url( 'blocks/' . $block_name . '/index.js', __FILE__ ),
210 $script_dependencies,
211 filemtime( $script_file ),
212 true
213 );
214 }
215
216 // Check if block has a CSS file
217 if ( file_exists( $style_file ) ) {
218 // Layout blocks need the layout system CSS
219 $block_style_deps = $style_dependencies;
220 if ( in_array( $block_name, array( 'section', 'row', 'column' ), true ) ) {
221 $block_style_deps[] = 'bkbg-layout-system';
222 }
223
224 wp_register_style(
225 'bkbg-' . $block_name . '-style',
226 plugins_url( 'blocks/' . $block_name . '/style.css', __FILE__ ),
227 $block_style_deps,
228 filemtime( $style_file )
229 );
230 }
231
232 // Check if block has a frontend JavaScript file
233 if ( file_exists( $frontend_file ) ) {
234 wp_register_script(
235 'bkbg-' . $block_name . '-frontend',
236 plugins_url( 'blocks/' . $block_name . '/frontend.js', __FILE__ ),
237 array( 'wp-dom-ready', 'bkbg-icon-picker-frontend' ),
238 filemtime( $frontend_file ),
239 true
240 );
241 }
242 }
243
244 // Automatically register all blocks in the /blocks directory
245 // Skip blocks the admin has disabled via the Blockenberg dashboard.
246 $disabled_blocks = get_option( 'blockenberg_disabled_blocks', array() );
247 if ( ! is_array( $disabled_blocks ) ) {
248 $disabled_blocks = array();
249 }
250
251 foreach ( glob( __DIR__ . '/blocks/*/block.json' ) as $metadata ) {
252 // Read block name from block.json to check against disabled list.
253 $raw_json = file_get_contents( $metadata );
254 $block_meta = $raw_json ? json_decode( $raw_json, true ) : null;
255 $block_name = is_array( $block_meta ) && isset( $block_meta['name'] ) ? $block_meta['name'] : '';
256
257 if ( '' !== $block_name && in_array( $block_name, $disabled_blocks, true ) ) {
258 continue; // Block is disabled — skip registration.
259 }
260
261 register_block_type( dirname( $metadata ) );
262 }
263 } );
264
265 // Register custom block category and ensure Blockenberg blocks appear in it.
266 add_filter( 'block_categories_all', function( $categories, $block_editor_context ) {
267 // Prepend Blockenberg sub-categories in reverse order so they appear in the right order.
268 $bkbg_categories = array(
269 array( 'slug' => 'blockenberg', 'title' => __( 'General (Blockenberg)', 'blockenberg' ), 'icon' => null ),
270 array( 'slug' => 'bkbg-layout', 'title' => __( 'Layout & Structure (Blockenberg)', 'blockenberg' ), 'icon' => null ),
271 array( 'slug' => 'bkbg-content', 'title' => __( 'Content & Typography (Blockenberg)', 'blockenberg' ), 'icon' => null ),
272 array( 'slug' => 'bkbg-media', 'title' => __( 'Media & Images (Blockenberg)', 'blockenberg' ), 'icon' => null ),
273 array( 'slug' => 'bkbg-marketing', 'title' => __( 'Marketing & Conversion (Blockenberg)','blockenberg' ), 'icon' => null ),
274 array( 'slug' => 'bkbg-business', 'title' => __( 'Business & Services (Blockenberg)', 'blockenberg' ), 'icon' => null ),
275 array( 'slug' => 'bkbg-blog', 'title' => __( 'Blog & Editorial (Blockenberg)', 'blockenberg' ), 'icon' => null ),
276 array( 'slug' => 'bkbg-interactive', 'title' => __( 'Interactive & Games (Blockenberg)', 'blockenberg' ), 'icon' => null ),
277 array( 'slug' => 'bkbg-charts', 'title' => __( 'Charts & Data (Blockenberg)', 'blockenberg' ), 'icon' => null ),
278 array( 'slug' => 'bkbg-calculators', 'title' => __( 'Calculators & Tools (Blockenberg)', 'blockenberg' ), 'icon' => null ),
279 array( 'slug' => 'bkbg-effects', 'title' => __( 'Effects & Animation (Blockenberg)', 'blockenberg' ), 'icon' => null ),
280 array( 'slug' => 'bkbg-dev', 'title' => __( 'Developer Tools (Blockenberg)', 'blockenberg' ), 'icon' => null ),
281 );
282 foreach ( array_reverse( $bkbg_categories ) as $cat ) {
283 array_unshift( $categories, $cat );
284 }
285 return $categories;
286 }, 10, 2 );
287
288 /**
289 * Load Google Fonts on the frontend for Blockenberg blocks.
290 * Scans block attributes for 'headerTypo', 'contentTypo', and any other
291 * attribute ending in 'Typo' that contains a non-empty 'family' key.
292 */
293 add_action( 'wp_enqueue_scripts', function () {
294 if ( ! is_singular() ) {
295 return;
296 }
297 $post = get_post();
298 if ( ! $post || ! has_blocks( $post->post_content ) ) {
299 return;
300 }
301
302 $system_fonts = array( 'Arial', 'Georgia', 'Helvetica', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana' );
303 $queued = array();
304
305 $blocks = parse_blocks( $post->post_content );
306
307 // Recursive walker for nested blocks
308 $collect = null;
309 $collect = function ( $blocks ) use ( &$collect, $system_fonts, &$queued ) {
310 foreach ( $blocks as $block ) {
311 if ( strpos( (string) $block['blockName'], 'blockenberg/' ) !== 0 ) {
312 if ( ! empty( $block['innerBlocks'] ) ) {
313 $collect( $block['innerBlocks'] );
314 }
315 continue;
316 }
317 $attrs = $block['attrs'] ?? array();
318 foreach ( $attrs as $key => $val ) {
319 // Any typography attribute (legacy *Typo suffix OR new typo* prefix)
320 // that is an array with a non-empty 'family' key.
321 $is_typo_key = ( substr( $key, -4 ) === 'Typo' ) || ( strpos( $key, 'typo' ) === 0 );
322 if ( $is_typo_key && is_array( $val ) && ! empty( $val['family'] ) ) {
323 $family = sanitize_text_field( $val['family'] );
324 if ( ! in_array( $family, $system_fonts, true ) && ! isset( $queued[ $family ] ) ) {
325 $queued[ $family ] = true;
326 $handle = 'bkbg-gf-' . sanitize_title( $family );
327 $url = 'https://fonts.googleapis.com/css2?family=' .
328 urlencode( $family ) .
329 ':wght@300;400;500;600;700;800;900&display=swap';
330 wp_enqueue_style( $handle, $url, array(), null );
331 }
332 }
333 }
334 if ( ! empty( $block['innerBlocks'] ) ) {
335 $collect( $block['innerBlocks'] );
336 }
337 }
338 };
339 $collect( $blocks );
340 } );
341
342 /**
343 * Register advanced layout attributes on the SERVER side for every Blockenberg block.
344 * Without this, WordPress strips unknown attributes during server-side parsing
345 * (array_intersect_key in WP_Block_Type::prepare_attributes_for_render).
346 */
347 add_filter( 'register_block_type_args', function ( $args, $block_type ) {
348 if ( strpos( $block_type, 'blockenberg/' ) !== 0 ) {
349 return $args;
350 }
351
352 // Blockenberg has its own Advanced spacing controls.
353 // Disable core Gutenberg "Dimensions" (spacing) UI to avoid duplicates.
354 if ( isset( $args['supports'] ) && is_array( $args['supports'] ) ) {
355 unset( $args['supports']['spacing'] );
356 unset( $args['supports']['__experimentalSpacing'] );
357 unset( $args['supports']['dimensions'] );
358 unset( $args['supports']['__experimentalDimensions'] );
359 }
360
361 $sides = array( 'Top', 'Right', 'Bottom', 'Left' );
362 $devices = array( '', 'Tablet', 'Mobile' );
363 $extra = array();
364
365 foreach ( array( 'bkbgMargin', 'bkbgPadding' ) as $prefix ) {
366 foreach ( $sides as $side ) {
367 foreach ( $devices as $device ) {
368 $key = $prefix . $side . $device;
369 $extra[ $key ] = array( 'type' => 'string', 'default' => '' );
370 $extra[ $key . 'Unit' ] = array( 'type' => 'string', 'default' => 'px' );
371 }
372 }
373 foreach ( $devices as $device ) {
374 $extra[ $prefix . 'Linked' . $device ] = array( 'type' => 'boolean', 'default' => true );
375 }
376 }
377
378 foreach ( $devices as $device ) {
379 $extra[ 'bkbgZIndex' . $device ] = array( 'type' => 'string', 'default' => '' );
380 }
381
382 $extra['bkbgCssId'] = array( 'type' => 'string', 'default' => '' );
383 $extra['bkbgCssClasses'] = array( 'type' => 'string', 'default' => '' );
384
385 // ── Background attributes ──
386 $extra['bkbgBgType'] = array( 'type' => 'string', 'default' => '' );
387 $extra['bkbgBgHoverType'] = array( 'type' => 'string', 'default' => '' );
388 $extra['bkbgBgColor'] = array( 'type' => 'string', 'default' => '' );
389 $extra['bkbgBgHoverColor']= array( 'type' => 'string', 'default' => '' );
390
391 // Classic image (responsive) — normal & hover
392 foreach ( array( 'bkbgBgImage', 'bkbgBgHoverImage' ) as $img_prefix ) {
393 foreach ( $devices as $device ) {
394 $extra[ $img_prefix . $device ] = array( 'type' => 'string', 'default' => '' );
395 $extra[ $img_prefix . 'Id' . $device ] = array( 'type' => 'number', 'default' => 0 );
396 }
397 }
398
399 // Classic image settings — normal & hover (position/repeat/size responsive, attachment global)
400 foreach ( array( 'bkbgBg', 'bkbgBgHover' ) as $s_prefix ) {
401 foreach ( $devices as $device ) {
402 $extra[ $s_prefix . 'Position' . $device ] = array( 'type' => 'string', 'default' => '' );
403 $extra[ $s_prefix . 'PositionCustomX' . $device ] = array( 'type' => 'string', 'default' => '' );
404 $extra[ $s_prefix . 'PositionCustomY' . $device ] = array( 'type' => 'string', 'default' => '' );
405 $extra[ $s_prefix . 'Repeat' . $device ] = array( 'type' => 'string', 'default' => '' );
406 $extra[ $s_prefix . 'Size' . $device ] = array( 'type' => 'string', 'default' => '' );
407 $extra[ $s_prefix . 'SizeCustomW' . $device ] = array( 'type' => 'string', 'default' => '' );
408 $extra[ $s_prefix . 'SizeCustomH' . $device ] = array( 'type' => 'string', 'default' => '' );
409 }
410 $extra[ $s_prefix . 'Attachment' ] = array( 'type' => 'string', 'default' => '' );
411 }
412
413 // Gradient — normal
414 $extra['bkbgBgGradColor1'] = array( 'type' => 'string', 'default' => '' );
415 $extra['bkbgBgGradColor2'] = array( 'type' => 'string', 'default' => '' );
416 $extra['bkbgBgGradType'] = array( 'type' => 'string', 'default' => 'linear' );
417 foreach ( $devices as $device ) {
418 $extra[ 'bkbgBgGradLoc1' . $device ] = array( 'type' => 'string', 'default' => '' );
419 $extra[ 'bkbgBgGradLoc2' . $device ] = array( 'type' => 'string', 'default' => '' );
420 $extra[ 'bkbgBgGradAngle' . $device ] = array( 'type' => 'string', 'default' => '' );
421 $extra[ 'bkbgBgGradPosition' . $device ] = array( 'type' => 'string', 'default' => '' );
422 }
423
424 // Gradient — hover
425 $extra['bkbgBgHoverGradColor1'] = array( 'type' => 'string', 'default' => '' );
426 $extra['bkbgBgHoverGradColor2'] = array( 'type' => 'string', 'default' => '' );
427 $extra['bkbgBgHoverGradType'] = array( 'type' => 'string', 'default' => 'linear' );
428 foreach ( $devices as $device ) {
429 $extra[ 'bkbgBgHoverGradLoc1' . $device ] = array( 'type' => 'string', 'default' => '' );
430 $extra[ 'bkbgBgHoverGradLoc2' . $device ] = array( 'type' => 'string', 'default' => '' );
431 $extra[ 'bkbgBgHoverGradAngle' . $device ] = array( 'type' => 'string', 'default' => '' );
432 $extra[ 'bkbgBgHoverGradPosition' . $device ] = array( 'type' => 'string', 'default' => '' );
433 }
434
435
436
437 // ── Border attributes ──
438
439 // Border Type (normal & hover)
440 $extra['bkbgBorderType'] = array( 'type' => 'string', 'default' => '' );
441 $extra['bkbgBorderHoverType'] = array( 'type' => 'string', 'default' => '' );
442
443 // Border Width — per side, per device, with unit (normal & hover)
444 foreach ( array( 'bkbgBorderWidth', 'bkbgBorderHoverWidth' ) as $prefix ) {
445 foreach ( $sides as $side ) {
446 foreach ( $devices as $device ) {
447 $key = $prefix . $side . $device;
448 $extra[ $key ] = array( 'type' => 'string', 'default' => '' );
449 $extra[ $key . 'Unit' ] = array( 'type' => 'string', 'default' => 'px' );
450 }
451 }
452 foreach ( $devices as $device ) {
453 $extra[ $prefix . 'Linked' . $device ] = array( 'type' => 'boolean', 'default' => true );
454 }
455 }
456
457 // Border Color (normal & hover)
458 $extra['bkbgBorderColor'] = array( 'type' => 'string', 'default' => '' );
459 $extra['bkbgBorderHoverColor'] = array( 'type' => 'string', 'default' => '' );
460
461 // Border Radius — per corner, per device, with unit (normal & hover)
462 foreach ( array( 'bkbgBorderRadius', 'bkbgBorderHoverRadius' ) as $prefix ) {
463 foreach ( $sides as $side ) {
464 foreach ( $devices as $device ) {
465 $key = $prefix . $side . $device;
466 $extra[ $key ] = array( 'type' => 'string', 'default' => '' );
467 $extra[ $key . 'Unit' ] = array( 'type' => 'string', 'default' => 'px' );
468 }
469 }
470 foreach ( $devices as $device ) {
471 $extra[ $prefix . 'Linked' . $device ] = array( 'type' => 'boolean', 'default' => true );
472 }
473 }
474
475 // Box Shadow (normal)
476 $extra['bkbgShadowColor'] = array( 'type' => 'string', 'default' => '' );
477 $extra['bkbgShadowH'] = array( 'type' => 'string', 'default' => '' );
478 $extra['bkbgShadowV'] = array( 'type' => 'string', 'default' => '' );
479 $extra['bkbgShadowBlur'] = array( 'type' => 'string', 'default' => '' );
480 $extra['bkbgShadowSpread'] = array( 'type' => 'string', 'default' => '' );
481 $extra['bkbgShadowPosition'] = array( 'type' => 'string', 'default' => '' );
482
483 // Box Shadow (hover)
484 $extra['bkbgShadowHoverColor'] = array( 'type' => 'string', 'default' => '' );
485 $extra['bkbgShadowHoverH'] = array( 'type' => 'string', 'default' => '' );
486 $extra['bkbgShadowHoverV'] = array( 'type' => 'string', 'default' => '' );
487 $extra['bkbgShadowHoverBlur'] = array( 'type' => 'string', 'default' => '' );
488 $extra['bkbgShadowHoverSpread'] = array( 'type' => 'string', 'default' => '' );
489 $extra['bkbgShadowHoverPosition'] = array( 'type' => 'string', 'default' => '' );
490
491
492 // Responsive visibility
493 $extra['bkbgHideDesktop'] = array( 'type' => 'boolean', 'default' => false );
494 $extra['bkbgHideTablet'] = array( 'type' => 'boolean', 'default' => false );
495 $extra['bkbgHideMobile'] = array( 'type' => 'boolean', 'default' => false );
496
497 if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) {
498 $args['attributes'] = array();
499 }
500 $args['attributes'] = array_merge( $args['attributes'], $extra );
501
502 return $args;
503 }, 10, 2 );
504
505 /**
506 * Render ALL advanced layout CSS (desktop + tablet + mobile) for Blockenberg blocks.
507 * Desktop styles are output as inline styles on the wrapper.
508 * Responsive styles use <style> tags with media queries.
509 * CSS ID / CSS Classes are also injected here for reliability.
510 */
511 add_filter( 'render_block', function ( $block_content, $block ) {
512 if ( empty( $block['blockName'] ) || strpos( $block['blockName'], 'blockenberg/' ) !== 0 ) {
513 return $block_content;
514 }
515
516 $attrs = $block['attrs'] ?? array();
517 if ( empty( $attrs ) ) {
518 return $block_content;
519 }
520
521 $sides = array( 'Top', 'Right', 'Bottom', 'Left' );
522 $devices = array( 'desktop', 'tablet', 'mobile' );
523
524 // Collect CSS rules per device
525 $css = array( 'desktop' => array(), 'tablet' => array(), 'mobile' => array() );
526
527 foreach ( array( 'bkbgMargin' => 'margin', 'bkbgPadding' => 'padding' ) as $prefix => $prop ) {
528 foreach ( $sides as $side ) {
529 foreach ( $devices as $dev ) {
530 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
531 $val = isset( $attrs[ $prefix . $side . $suffix ] ) ? $attrs[ $prefix . $side . $suffix ] : '';
532 $unit = isset( $attrs[ $prefix . $side . $suffix . 'Unit' ] ) ? $attrs[ $prefix . $side . $suffix . 'Unit' ] : 'px';
533 if ( '' !== $val && '' !== trim( (string) $val ) ) {
534 $css[ $dev ][] = $prop . '-' . strtolower( $side ) . ':' . $val . $unit . ' !important';
535 }
536 }
537 }
538 }
539
540 // Z-Index per device
541 foreach ( $devices as $dev ) {
542 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
543 $zi = isset( $attrs[ 'bkbgZIndex' . $suffix ] ) ? $attrs[ 'bkbgZIndex' . $suffix ] : '';
544 if ( '' !== $zi && '' !== trim( (string) $zi ) ) {
545 $css[ $dev ][] = 'z-index:' . intval( $zi );
546 $css[ $dev ][] = 'position:relative';
547 }
548 }
549
550 // ── Background CSS ──
551 $hover_css = array( 'desktop' => array(), 'tablet' => array(), 'mobile' => array() );
552
553 $bg_type = ! empty( $attrs['bkbgBgType'] ) ? $attrs['bkbgBgType'] : '';
554 $hover_type = ! empty( $attrs['bkbgBgHoverType'] ) ? $attrs['bkbgBgHoverType'] : '';
555
556 // Helper: build gradient value
557 $build_gradient = function ( $attrs, $prefix, $suffix ) {
558 $c1 = ! empty( $attrs[ $prefix . 'GradColor1' ] ) ? $attrs[ $prefix . 'GradColor1' ] : '';
559 $c2 = ! empty( $attrs[ $prefix . 'GradColor2' ] ) ? $attrs[ $prefix . 'GradColor2' ] : '';
560 if ( '' === $c1 && '' === $c2 ) return '';
561 if ( '' === $c1 ) $c1 = 'transparent';
562 if ( '' === $c2 ) $c2 = 'transparent';
563
564 // Location 1 — with fallback to desktop
565 $loc1 = '';
566 if ( '' !== $suffix && ! empty( $attrs[ $prefix . 'GradLoc1' . $suffix ] ) ) {
567 $loc1 = $attrs[ $prefix . 'GradLoc1' . $suffix ];
568 } elseif ( ! empty( $attrs[ $prefix . 'GradLoc1' ] ) ) {
569 $loc1 = $attrs[ $prefix . 'GradLoc1' ];
570 }
571
572 // Location 2
573 $loc2 = '';
574 if ( '' !== $suffix && ! empty( $attrs[ $prefix . 'GradLoc2' . $suffix ] ) ) {
575 $loc2 = $attrs[ $prefix . 'GradLoc2' . $suffix ];
576 } elseif ( ! empty( $attrs[ $prefix . 'GradLoc2' ] ) ) {
577 $loc2 = $attrs[ $prefix . 'GradLoc2' ];
578 }
579
580 // Angle
581 $angle = '';
582 if ( '' !== $suffix && isset( $attrs[ $prefix . 'GradAngle' . $suffix ] ) && '' !== $attrs[ $prefix . 'GradAngle' . $suffix ] ) {
583 $angle = $attrs[ $prefix . 'GradAngle' . $suffix ];
584 } elseif ( isset( $attrs[ $prefix . 'GradAngle' ] ) && '' !== $attrs[ $prefix . 'GradAngle' ] ) {
585 $angle = $attrs[ $prefix . 'GradAngle' ];
586 }
587
588 $type = ! empty( $attrs[ $prefix . 'GradType' ] ) ? $attrs[ $prefix . 'GradType' ] : 'linear';
589
590 $stop1 = esc_attr( $c1 ) . ( '' !== $loc1 ? ' ' . intval( $loc1 ) . '%' : '' );
591 $stop2 = esc_attr( $c2 ) . ( '' !== $loc2 ? ' ' . intval( $loc2 ) . '%' : '' );
592
593 if ( 'radial' === $type ) {
594 // Radial position
595 $pos = '';
596 if ( '' !== $suffix && ! empty( $attrs[ $prefix . 'GradPosition' . $suffix ] ) ) {
597 $pos = $attrs[ $prefix . 'GradPosition' . $suffix ];
598 } elseif ( ! empty( $attrs[ $prefix . 'GradPosition' ] ) ) {
599 $pos = $attrs[ $prefix . 'GradPosition' ];
600 }
601 $at_part = '' !== $pos ? ' at ' . esc_attr( $pos ) : '';
602 return 'radial-gradient(circle' . $at_part . ',' . $stop1 . ',' . $stop2 . ')';
603 }
604
605 $angle_part = '' !== $angle ? intval( $angle ) . 'deg,' : '';
606 return 'linear-gradient(' . $angle_part . $stop1 . ',' . $stop2 . ')';
607 };
608
609 // Helper: resolve responsive classic image settings (with desktop fallback)
610 $get_img_setting = function ( $attrs, $prefix, $prop, $suffix ) {
611 $val = ! empty( $attrs[ $prefix . $prop . $suffix ] ) ? $attrs[ $prefix . $prop . $suffix ] : '';
612 if ( '' === $val && '' !== $suffix ) {
613 $val = ! empty( $attrs[ $prefix . $prop ] ) ? $attrs[ $prefix . $prop ] : '';
614 }
615 return $val;
616 };
617
618 $render_classic_image_css = function ( $attrs, $prefix, &$target_css, $devices ) use ( $get_img_setting ) {
619 // Background color
620 if ( ! empty( $attrs[ $prefix . 'Color' ] ) ) {
621 $target_css['desktop'][] = 'background-color:' . esc_attr( $attrs[ $prefix . 'Color' ] ) . ' !important';
622 }
623 // Attachment (global, not responsive)
624 $attach = ! empty( $attrs[ $prefix . 'Attachment' ] ) ? esc_attr( $attrs[ $prefix . 'Attachment' ] ) : '';
625 if ( '' !== $attach ) {
626 $target_css['desktop'][] = 'background-attachment:' . $attach . ' !important';
627 }
628
629 foreach ( $devices as $dev ) {
630 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
631 $img = ! empty( $attrs[ $prefix . 'Image' . $suffix ] ) ? $attrs[ $prefix . 'Image' . $suffix ] : '';
632 if ( '' === $img && '' !== $suffix ) {
633 $img = ! empty( $attrs[ $prefix . 'Image' ] ) ? $attrs[ $prefix . 'Image' ] : '';
634 }
635 if ( '' !== $img ) {
636 $target_css[ $dev ][] = 'background-image:url(' . esc_url( $img ) . ') !important';
637
638 // Position
639 $pos = $get_img_setting( $attrs, $prefix, 'Position', $suffix );
640 if ( 'custom' === $pos ) {
641 $cx = $get_img_setting( $attrs, $prefix, 'PositionCustomX', $suffix );
642 $cy = $get_img_setting( $attrs, $prefix, 'PositionCustomY', $suffix );
643 $cx = '' !== $cx ? intval( $cx ) . '%' : '50%';
644 $cy = '' !== $cy ? intval( $cy ) . '%' : '50%';
645 $target_css[ $dev ][] = 'background-position:' . $cx . ' ' . $cy . ' !important';
646 } elseif ( '' !== $pos ) {
647 $target_css[ $dev ][] = 'background-position:' . esc_attr( $pos ) . ' !important';
648 } else {
649 $target_css[ $dev ][] = 'background-position:center !important';
650 }
651
652 // Repeat
653 $repeat = $get_img_setting( $attrs, $prefix, 'Repeat', $suffix );
654 if ( '' !== $repeat ) {
655 $target_css[ $dev ][] = 'background-repeat:' . esc_attr( $repeat ) . ' !important';
656 }
657
658 // Size
659 $size = $get_img_setting( $attrs, $prefix, 'Size', $suffix );
660 if ( 'custom' === $size ) {
661 $sw = $get_img_setting( $attrs, $prefix, 'SizeCustomW', $suffix );
662 $sh = $get_img_setting( $attrs, $prefix, 'SizeCustomH', $suffix );
663 $sw = '' !== $sw ? intval( $sw ) . 'px' : 'auto';
664 $sh = '' !== $sh ? intval( $sh ) . 'px' : 'auto';
665 $target_css[ $dev ][] = 'background-size:' . $sw . ' ' . $sh . ' !important';
666 } elseif ( '' !== $size ) {
667 $target_css[ $dev ][] = 'background-size:' . esc_attr( $size ) . ' !important';
668 } else {
669 $target_css[ $dev ][] = 'background-size:cover !important';
670 }
671 }
672 }
673 };
674
675 // Normal — Classic
676 if ( 'classic' === $bg_type ) {
677 $render_classic_image_css( $attrs, 'bkbgBg', $css, $devices );
678 }
679
680 // Normal — Gradient
681 if ( 'gradient' === $bg_type ) {
682 foreach ( $devices as $dev ) {
683 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
684 $grad = $build_gradient( $attrs, 'bkbgBg', $suffix );
685 if ( '' !== $grad ) {
686 $css[ $dev ][] = 'background-image:' . $grad . ' !important';
687 }
688 }
689 }
690
691 // Hover — Classic
692 if ( 'classic' === $hover_type ) {
693 $render_classic_image_css( $attrs, 'bkbgBgHover', $hover_css, $devices );
694 }
695
696 // Hover — Gradient
697 if ( 'gradient' === $hover_type ) {
698 foreach ( $devices as $dev ) {
699 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
700 $grad = $build_gradient( $attrs, 'bkbgBgHover', $suffix );
701 if ( '' !== $grad ) {
702 $hover_css[ $dev ][] = 'background-image:' . $grad . ' !important';
703 }
704 }
705 }
706
707 // ── Border CSS ──
708
709 // Helper: build box-shadow value
710 $build_shadow = function ( $attrs, $prefix ) {
711 $h = isset( $attrs[ $prefix . 'H' ] ) && '' !== $attrs[ $prefix . 'H' ] ? intval( $attrs[ $prefix . 'H' ] ) : '';
712 $v = isset( $attrs[ $prefix . 'V' ] ) && '' !== $attrs[ $prefix . 'V' ] ? intval( $attrs[ $prefix . 'V' ] ) : '';
713 $blur = isset( $attrs[ $prefix . 'Blur' ] ) && '' !== $attrs[ $prefix . 'Blur' ] ? intval( $attrs[ $prefix . 'Blur' ] ) : '';
714 $spread = isset( $attrs[ $prefix . 'Spread' ] ) && '' !== $attrs[ $prefix . 'Spread' ] ? intval( $attrs[ $prefix . 'Spread' ] ) : '';
715 $color = ! empty( $attrs[ $prefix . 'Color' ] ) ? $attrs[ $prefix . 'Color' ] : '';
716 $pos = ! empty( $attrs[ $prefix . 'Position' ] ) ? $attrs[ $prefix . 'Position' ] : '';
717
718 if ( '' === $h && '' === $v && '' === $blur && '' === $spread && '' === $color ) return '';
719
720 $h = '' !== $h ? $h . 'px' : '0px';
721 $v = '' !== $v ? $v . 'px' : '0px';
722 $blur = '' !== $blur ? $blur . 'px' : '0px';
723 $spread = '' !== $spread ? $spread . 'px' : '0px';
724 $color = '' !== $color ? esc_attr( $color ) : 'rgba(0,0,0,0.5)';
725
726 $val = $h . ' ' . $v . ' ' . $blur . ' ' . $spread . ' ' . $color;
727 if ( 'inset' === $pos ) {
728 $val = 'inset ' . $val;
729 }
730 return $val;
731 };
732
733 // Helper: render border CSS for a prefix (normal or hover)
734 $render_border_css = function ( $attrs, $prefix_type, $prefix_width, $prefix_radius, $shadow_prefix, &$target_css, $devices ) use ( $build_shadow ) {
735 $border_type = ! empty( $attrs[ $prefix_type ] ) ? $attrs[ $prefix_type ] : '';
736
737 // Border type + width + color
738 if ( '' !== $border_type && 'none' !== $border_type ) {
739 $target_css['desktop'][] = 'border-style:' . esc_attr( $border_type ) . ' !important';
740
741 // Border color
742 $color_key = str_replace( 'Type', 'Color', $prefix_type );
743 if ( ! empty( $attrs[ $color_key ] ) ) {
744 $target_css['desktop'][] = 'border-color:' . esc_attr( $attrs[ $color_key ] ) . ' !important';
745 }
746
747 // Border width (responsive, per side)
748 foreach ( $devices as $dev ) {
749 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
750 foreach ( array( 'Top', 'Right', 'Bottom', 'Left' ) as $side ) {
751 $val = isset( $attrs[ $prefix_width . $side . $suffix ] ) ? $attrs[ $prefix_width . $side . $suffix ] : '';
752 $unit = isset( $attrs[ $prefix_width . $side . $suffix . 'Unit' ] ) ? $attrs[ $prefix_width . $side . $suffix . 'Unit' ] : 'px';
753 if ( '' !== $val && '' !== trim( (string) $val ) ) {
754 $target_css[ $dev ][] = 'border-' . strtolower( $side ) . '-width:' . $val . $unit . ' !important';
755 }
756 }
757 }
758 } elseif ( 'none' === $border_type ) {
759 $target_css['desktop'][] = 'border:none !important';
760 }
761
762 // Border radius (responsive, per corner)
763 foreach ( $devices as $dev ) {
764 $suffix = 'desktop' === $dev ? '' : ( 'tablet' === $dev ? 'Tablet' : 'Mobile' );
765 $radius_parts = array();
766 foreach ( array( 'Top', 'Right', 'Bottom', 'Left' ) as $side ) {
767 $val = isset( $attrs[ $prefix_radius . $side . $suffix ] ) ? $attrs[ $prefix_radius . $side . $suffix ] : '';
768 $unit = isset( $attrs[ $prefix_radius . $side . $suffix . 'Unit' ] ) ? $attrs[ $prefix_radius . $side . $suffix . 'Unit' ] : 'px';
769 if ( '' !== $val && '' !== trim( (string) $val ) ) {
770 $radius_parts[ $side ] = $val . $unit;
771 }
772 }
773 if ( ! empty( $radius_parts ) ) {
774 // Map Top/Right/Bottom/Left to border-radius corners: TL TR BR BL
775 $tl = isset( $radius_parts['Top'] ) ? $radius_parts['Top'] : '0px';
776 $tr = isset( $radius_parts['Right'] ) ? $radius_parts['Right'] : '0px';
777 $br = isset( $radius_parts['Bottom'] ) ? $radius_parts['Bottom'] : '0px';
778 $bl = isset( $radius_parts['Left'] ) ? $radius_parts['Left'] : '0px';
779 $target_css[ $dev ][] = 'border-radius:' . $tl . ' ' . $tr . ' ' . $br . ' ' . $bl . ' !important';
780 }
781 }
782
783 // Box shadow
784 $shadow_val = $build_shadow( $attrs, $shadow_prefix );
785 if ( '' !== $shadow_val ) {
786 $target_css['desktop'][] = 'box-shadow:' . $shadow_val . ' !important';
787 }
788 };
789
790 // Normal border
791 $render_border_css( $attrs, 'bkbgBorderType', 'bkbgBorderWidth', 'bkbgBorderRadius', 'bkbgShadow', $css, $devices );
792
793 // Hover border
794 $render_border_css( $attrs, 'bkbgBorderHoverType', 'bkbgBorderHoverWidth', 'bkbgBorderHoverRadius', 'bkbgShadowHover', $hover_css, $devices );
795
796 // ── Responsive visibility ──
797 $hide_desktop = ! empty( $attrs['bkbgHideDesktop'] );
798 $hide_tablet = ! empty( $attrs['bkbgHideTablet'] );
799 $hide_mobile = ! empty( $attrs['bkbgHideMobile'] );
800
801 // Check if hover styles exist
802 $has_hover = ! empty( $hover_css['desktop'] ) || ! empty( $hover_css['tablet'] ) || ! empty( $hover_css['mobile'] );
803
804 // Any styles to output?
805 $has_responsive = $hide_desktop || $hide_tablet || $hide_mobile;
806 $has_styles = ! empty( $css['desktop'] ) || ! empty( $css['tablet'] ) || ! empty( $css['mobile'] ) || $has_hover || $has_responsive;
807 $has_id = ! empty( $attrs['bkbgCssId'] );
808 $has_cls = ! empty( $attrs['bkbgCssClasses'] );
809
810 if ( ! $has_styles && ! $has_id && ! $has_cls ) {
811 return $block_content;
812 }
813
814 // Generate a unique class for targeting this specific block instance
815 $unique = 'bkbg-adv-' . substr( md5( serialize( $attrs ) . wp_rand() ), 0, 8 );
816
817 // Inject unique class into the first HTML tag
818 $block_content = preg_replace(
819 '/(^\s*<[a-zA-Z][^>]*\bclass\s*=\s*")/',
820 '$1' . esc_attr( $unique ) . ' ',
821 $block_content,
822 1,
823 $count
824 );
825 if ( ! $count ) {
826 // No class attribute found — add one
827 $block_content = preg_replace(
828 '/(^\s*<[a-zA-Z][^\s>]*)/',
829 '$1 class="' . esc_attr( $unique ) . '"',
830 $block_content,
831 1
832 );
833 }
834
835 // Inject CSS ID
836 if ( $has_id ) {
837 $safe_id = esc_attr( $attrs['bkbgCssId'] );
838 $block_content = preg_replace(
839 '/(^\s*<[a-zA-Z][^>]*)/',
840 '$1 id="' . $safe_id . '"',
841 $block_content,
842 1
843 );
844 }
845
846 // Inject CSS Classes
847 if ( $has_cls ) {
848 $safe_cls = esc_attr( $attrs['bkbgCssClasses'] );
849 $block_content = preg_replace(
850 '/(^\s*<[a-zA-Z][^>]*\bclass\s*=\s*")/',
851 '$1' . $safe_cls . ' ',
852 $block_content,
853 1
854 );
855 }
856
857 // Build <style> tag
858 if ( $has_styles ) {
859 $sel = '.' . $unique;
860 $style = '';
861
862 if ( ! empty( $css['desktop'] ) ) {
863 $style .= $sel . '{' . implode( ';', $css['desktop'] ) . '}';
864 }
865 if ( ! empty( $css['tablet'] ) ) {
866 $style .= '@media(max-width:1024px){' . $sel . '{' . implode( ';', $css['tablet'] ) . '}}';
867 }
868 if ( ! empty( $css['mobile'] ) ) {
869 $style .= '@media(max-width:767px){' . $sel . '{' . implode( ';', $css['mobile'] ) . '}}';
870 }
871
872 // Hover rules
873 if ( ! empty( $hover_css['desktop'] ) ) {
874 $style .= $sel . ':hover{' . implode( ';', $hover_css['desktop'] ) . '}';
875 }
876 if ( ! empty( $hover_css['tablet'] ) ) {
877 $style .= '@media(max-width:1024px){' . $sel . ':hover{' . implode( ';', $hover_css['tablet'] ) . '}}';
878 }
879 if ( ! empty( $hover_css['mobile'] ) ) {
880 $style .= '@media(max-width:767px){' . $sel . ':hover{' . implode( ';', $hover_css['mobile'] ) . '}}';
881 }
882
883 // Responsive visibility: hide on specific devices
884 if ( $hide_desktop ) {
885 // Hide on desktop (>1024px)
886 $style .= '@media(min-width:1025px){' . $sel . '{display:none !important}}';
887 }
888 if ( $hide_tablet ) {
889 // Hide on tablet (768–1024px)
890 $style .= '@media(min-width:768px) and (max-width:1024px){' . $sel . '{display:none !important}}';
891 }
892 if ( $hide_mobile ) {
893 // Hide on mobile (≤767px)
894 $style .= '@media(max-width:767px){' . $sel . '{display:none !important}}';
895 }
896
897 $block_content .= '<style>' . $style . '</style>';
898 }
899
900 return $block_content;
901 }, 10, 2 );
902
903 /**
904 * Register REST API endpoint for Post Grid block.
905 *
906 * Route: /wp-json/blockenberg/v1/post-grid
907 * Method: GET
908 * Params:
909 * - type: string (posts|pages|any CPT)
910 * - orderby: string (date|title|comment_count|...)
911 * - order: string (asc|desc)
912 * - per_page: int
913 * - offset: int
914 * - page: int (1-based)
915 * - excerpt_len: int (optional)
916 */
917 add_action( 'rest_api_init', function () {
918 register_rest_route( 'blockenberg/v1', '/post-grid', array(
919 'methods' => 'GET',
920 'permission_callback' => '__return_true',
921 'args' => array(
922 'type' => array( 'sanitize_callback' => 'sanitize_key' ),
923 'orderby' => array( 'sanitize_callback' => 'sanitize_key' ),
924 'order' => array( 'sanitize_callback' => 'sanitize_text_field' ),
925 'per_page' => array( 'sanitize_callback' => 'absint' ),
926 'offset' => array( 'sanitize_callback' => 'absint' ),
927 'page' => array( 'sanitize_callback' => 'absint' ),
928 'excerpt_len' => array( 'sanitize_callback' => 'absint' ),
929 ),
930 'callback' => function ( WP_REST_Request $request ) {
931 $requested_type = sanitize_key( $request->get_param( 'type' ) ?: 'post' );
932 if ( 'posts' === $requested_type ) {
933 $requested_type = 'post';
934 } elseif ( 'pages' === $requested_type ) {
935 $requested_type = 'page';
936 }
937
938 $public_post_types = get_post_types( array( 'public' => true ), 'names' );
939 $post_type = in_array( $requested_type, $public_post_types, true ) ? $requested_type : 'post';
940
941 $requested_orderby = sanitize_key( $request->get_param( 'orderby' ) ?: 'date' );
942 $allowed_orderby = array( 'date', 'title', 'modified', 'comment_count', 'rand', 'menu_order' );
943 $orderby = in_array( $requested_orderby, $allowed_orderby, true ) ? $requested_orderby : 'date';
944 $order = strtolower( (string) $request->get_param( 'order' ) ) === 'asc' ? 'ASC' : 'DESC';
945 $per_page = max( 1, absint( $request->get_param( 'per_page' ) ?: 6 ) );
946 $per_page = min( 50, $per_page );
947 $offset = max( 0, absint( $request->get_param( 'offset' ) ?: 0 ) );
948 $offset = min( 5000, $offset );
949 $page = max( 1, absint( $request->get_param( 'page' ) ?: 1 ) );
950 $page = min( 200, $page );
951 $excerpt_len = max( 5, absint( $request->get_param( 'excerpt_len' ) ?: 18 ) );
952 $excerpt_len = min( 80, $excerpt_len );
953
954 // Short cache for public non-product queries.
955 $is_product_query = ( 'product' === $post_type );
956 $cache_key = '';
957 if ( ! $is_product_query ) {
958 $cache_key = 'bkbg_post_grid_' . md5( wp_json_encode( array(
959 'type' => $post_type,
960 'orderby' => $orderby,
961 'order' => $order,
962 'per_page' => $per_page,
963 'offset' => $offset,
964 'page' => $page,
965 'excerpt_len' => $excerpt_len,
966 ) ) );
967 $cached = get_transient( $cache_key );
968 if ( is_array( $cached ) ) {
969 return rest_ensure_response( $cached );
970 }
971 }
972
973 $q = new WP_Query( array(
974 'post_type' => $post_type,
975 'post_status' => 'publish',
976 'orderby' => $orderby,
977 'order' => $order,
978 'posts_per_page' => $per_page,
979 'offset' => $offset + ( ( $page - 1 ) * $per_page ),
980 'ignore_sticky_posts' => true,
981 'no_found_rows' => true,
982 ) );
983
984 $posts = array();
985 foreach ( $q->posts as $p ) {
986 $post_id = $p->ID;
987 $title = wp_strip_all_tags( get_the_title( $post_id ) );
988 $link = esc_url_raw( get_permalink( $post_id ) );
989 $image = esc_url_raw( (string) get_the_post_thumbnail_url( $post_id, 'medium_large' ) );
990 $date = wp_strip_all_tags( (string) get_the_date( '', $post_id ) );
991 $author = sanitize_text_field( (string) get_the_author_meta( 'display_name', $p->post_author ) );
992 $meta = trim( $date . ' · ' . $author );
993 $raw = get_post_field( 'post_excerpt', $post_id );
994 if ( '' === $raw ) {
995 $raw = get_post_field( 'post_content', $post_id );
996 }
997 $excerpt = wp_strip_all_tags( wp_trim_words( wp_strip_all_tags( $raw ), $excerpt_len, '' ) );
998
999 $item = array(
1000 'id' => $post_id,
1001 'title' => $title,
1002 'link' => $link,
1003 'image' => $image,
1004 'meta' => $meta,
1005 'excerpt' => $excerpt,
1006 );
1007
1008 if ( 'product' === $post_type && function_exists( 'wc_get_product' ) ) {
1009 $product = wc_get_product( $post_id );
1010 if ( $product ) {
1011 $item['price_html'] = wp_kses_post( $product->get_price_html() );
1012 $item['add_to_cart'] = esc_url_raw( $product->add_to_cart_url() );
1013 }
1014 }
1015
1016 $posts[] = $item;
1017 }
1018
1019 $payload = array( 'posts' => $posts );
1020
1021 if ( ! $is_product_query && '' !== $cache_key ) {
1022 set_transient( $cache_key, $payload, 60 );
1023 }
1024
1025 return rest_ensure_response( $payload );
1026 },
1027 ) );
1028 } );
1029
1030 /**
1031 * Simple local newsletter subscribe endpoint.
1032 * Stores emails in an option array; extend as needed (e.g., to a custom table).
1033 */
1034 add_action( 'rest_api_init', function () {
1035 register_rest_route( 'blockenberg/v1', '/subscribe', array(
1036 'methods' => 'POST',
1037 'permission_callback' => '__return_true',
1038 'args' => array(
1039 'email' => array( 'sanitize_callback' => 'sanitize_email' ),
1040 'website' => array( 'sanitize_callback' => 'sanitize_text_field' ),
1041 ),
1042 'callback' => function ( WP_REST_Request $request ) {
1043 $payload = $request->get_json_params();
1044 if ( ! is_array( $payload ) ) {
1045 $payload = array();
1046 }
1047
1048 // Basic rate limiting by IP to reduce spam / abuse.
1049 $ip = '';
1050 if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
1051 $ip = sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) );
1052 }
1053 if ( '' !== $ip ) {
1054 $key = 'bkbg_subscribe_' . md5( $ip );
1055 $rl = get_transient( $key );
1056 if ( is_array( $rl ) && isset( $rl['count'], $rl['start'] ) ) {
1057 if ( $rl['count'] >= 5 ) {
1058 return new WP_Error( 'rate_limited', __( 'Too many requests. Please try again later.', 'blockenberg' ), array( 'status' => 429 ) );
1059 }
1060 $rl['count']++;
1061 set_transient( $key, $rl, 60 );
1062 } else {
1063 set_transient( $key, array( 'count' => 1, 'start' => time() ), 60 );
1064 }
1065 }
1066
1067 // Optional honeypot field (bots tend to fill it).
1068 $honeypot = isset( $payload['website'] ) ? sanitize_text_field( (string) $payload['website'] ) : '';
1069 if ( '' !== $honeypot ) {
1070 return rest_ensure_response( array( 'ok' => true ) );
1071 }
1072
1073 $email = isset( $payload['email'] ) ? sanitize_email( (string) $payload['email'] ) : '';
1074 if ( empty( $email ) || ! is_email( $email ) ) {
1075 return new WP_Error( 'invalid_email', __( 'Invalid email address', 'blockenberg' ), array( 'status' => 400 ) );
1076 }
1077
1078 $list = get_option( 'blockenberg_newsletter_subscribers', array() );
1079 if ( ! is_array( $list ) ) {
1080 $list = array();
1081 }
1082
1083 // Prevent unbounded option growth.
1084 if ( count( $list ) >= 5000 ) {
1085 return new WP_Error( 'storage_full', __( 'Subscriber list is full.', 'blockenberg' ), array( 'status' => 503 ) );
1086 }
1087
1088 if ( ! in_array( $email, $list, true ) ) {
1089 $list[] = $email;
1090 update_option( 'blockenberg_newsletter_subscribers', $list, false );
1091 }
1092 return rest_ensure_response( array( 'ok' => true ) );
1093 },
1094 ) );
1095 } );
1096
1097 /**
1098 * Contact Form endpoint — POST /wp-json/blockenberg/v1/contact
1099 * Sends an email via wp_mail() to the admin or a custom recipient stored in the block.
1100 */
1101 add_action( 'rest_api_init', function () {
1102 register_rest_route( 'blockenberg/v1', '/contact', array(
1103 'methods' => 'POST',
1104 'permission_callback' => '__return_true',
1105 'callback' => function ( WP_REST_Request $request ) {
1106 $payload = $request->get_json_params();
1107 if ( ! is_array( $payload ) ) {
1108 $payload = array();
1109 }
1110
1111 // Rate-limit by IP: max 5 requests per 60 s.
1112 $ip = '';
1113 if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
1114 $ip = sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) );
1115 }
1116 if ( '' !== $ip ) {
1117 $rl_key = 'bkbg_contact_' . md5( $ip );
1118 $rl = get_transient( $rl_key );
1119 if ( is_array( $rl ) && isset( $rl['count'] ) && $rl['count'] >= 5 ) {
1120 return new WP_Error( 'rate_limited', __( 'Too many requests. Please try again later.', 'blockenberg' ), array( 'status' => 429 ) );
1121 }
1122 if ( is_array( $rl ) ) {
1123 $rl['count']++;
1124 set_transient( $rl_key, $rl, 60 );
1125 } else {
1126 set_transient( $rl_key, array( 'count' => 1 ), 60 );
1127 }
1128 }
1129
1130 // Honeypot check (bots fill the hidden website field).
1131 $honeypot = isset( $payload['website'] ) ? sanitize_text_field( (string) $payload['website'] ) : '';
1132 if ( '' !== $honeypot ) {
1133 return rest_ensure_response( array( 'ok' => true ) ); // silently accept.
1134 }
1135
1136 // Validate required fields.
1137 $name = isset( $payload['name'] ) ? sanitize_text_field( (string) $payload['name'] ) : '';
1138 $email = isset( $payload['email'] ) ? sanitize_email( (string) $payload['email'] ) : '';
1139 $phone = isset( $payload['phone'] ) ? sanitize_text_field( (string) $payload['phone'] ) : '';
1140 $message = isset( $payload['message'] ) ? sanitize_textarea_field( (string) $payload['message'] ) : '';
1141
1142 if ( ! is_email( $email ) ) {
1143 return new WP_Error( 'invalid_email', __( 'Invalid email address.', 'blockenberg' ), array( 'status' => 400 ) );
1144 }
1145 if ( empty( $message ) ) {
1146 return new WP_Error( 'empty_message', __( 'Message is required.', 'blockenberg' ), array( 'status' => 400 ) );
1147 }
1148
1149 // Determine recipient and subject.
1150 $recipient = isset( $payload['recipient'] ) ? sanitize_email( (string) $payload['recipient'] ) : '';
1151 if ( ! is_email( $recipient ) ) {
1152 $recipient = get_option( 'admin_email' );
1153 }
1154 $subject = isset( $payload['subject'] ) ? sanitize_text_field( (string) $payload['subject'] ) : __( 'New Contact Form Submission', 'blockenberg' );
1155
1156 // Build email body.
1157 $body = "Name: {$name}\n";
1158 $body .= "Email: {$email}\n";
1159 if ( ! empty( $phone ) ) {
1160 $body .= "Phone: {$phone}\n";
1161 }
1162 $body .= "\nMessage:\n{$message}\n";
1163
1164 $headers = array(
1165 'Content-Type: text/plain; charset=UTF-8',
1166 "Reply-To: {$name} <{$email}>",
1167 );
1168
1169 $sent = wp_mail( $recipient, $subject, $body, $headers );
1170
1171 if ( ! $sent ) {
1172 return new WP_Error( 'mail_failed', __( 'Failed to send email. Please try again.', 'blockenberg' ), array( 'status' => 500 ) );
1173 }
1174
1175 return rest_ensure_response( array( 'ok' => true ) );
1176 },
1177 ) );
1178 } );