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