'blockenberg', 'title' => __( 'General (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-layout', 'title' => __( 'Layout & Structure (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-content', 'title' => __( 'Content & Typography (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-media', 'title' => __( 'Media & Images (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-marketing', 'title' => __( 'Marketing & Conversion (Blockenberg)','blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-business', 'title' => __( 'Business & Services (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-blog', 'title' => __( 'Blog & Editorial (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-interactive', 'title' => __( 'Interactive & Games (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-charts', 'title' => __( 'Charts & Data (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-calculators', 'title' => __( 'Calculators & Tools (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-effects', 'title' => __( 'Effects & Animation (Blockenberg)', 'blockenberg' ), 'icon' => null ), array( 'slug' => 'bkbg-dev', 'title' => __( 'Developer Tools (Blockenberg)', 'blockenberg' ), 'icon' => null ), ); foreach ( array_reverse( $bkbg_categories ) as $cat ) { array_unshift( $categories, $cat ); } return $categories; }, 10, 2 ); /** * Load Google Fonts on the frontend for Blockenberg blocks. * Scans block attributes for 'headerTypo', 'contentTypo', and any other * attribute ending in 'Typo' that contains a non-empty 'family' key. */ add_action( 'wp_enqueue_scripts', function () { if ( ! is_singular() ) { return; } $post = get_post(); if ( ! $post || ! has_blocks( $post->post_content ) ) { return; } $system_fonts = array( 'Arial', 'Georgia', 'Helvetica', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana' ); $queued = array(); $blocks = parse_blocks( $post->post_content ); // Recursive walker for nested blocks $collect = null; $collect = function ( $blocks ) use ( &$collect, $system_fonts, &$queued ) { foreach ( $blocks as $block ) { if ( strpos( (string) $block['blockName'], 'blockenberg/' ) !== 0 ) { if ( ! empty( $block['innerBlocks'] ) ) { $collect( $block['innerBlocks'] ); } continue; } $attrs = $block['attrs'] ?? array(); foreach ( $attrs as $key => $val ) { // Any typography attribute (legacy *Typo suffix OR new typo* prefix) // that is an array with a non-empty 'family' key. $is_typo_key = ( substr( $key, -4 ) === 'Typo' ) || ( strpos( $key, 'typo' ) === 0 ); if ( $is_typo_key && is_array( $val ) && ! empty( $val['family'] ) ) { $family = sanitize_text_field( $val['family'] ); if ( ! in_array( $family, $system_fonts, true ) && ! isset( $queued[ $family ] ) ) { $queued[ $family ] = true; $handle = 'bkbg-gf-' . sanitize_title( $family ); $url = 'https://fonts.googleapis.com/css2?family=' . urlencode( $family ) . ':wght@300;400;500;600;700;800;900&display=swap'; wp_enqueue_style( $handle, $url, array(), null ); } } } if ( ! empty( $block['innerBlocks'] ) ) { $collect( $block['innerBlocks'] ); } } }; $collect( $blocks ); } ); /** * Register advanced layout attributes on the SERVER side for every Blockenberg block. * Without this, WordPress strips unknown attributes during server-side parsing * (array_intersect_key in WP_Block_Type::prepare_attributes_for_render). */ add_filter( 'register_block_type_args', function ( $args, $block_type ) { if ( strpos( $block_type, 'blockenberg/' ) !== 0 ) { return $args; } // Blockenberg has its own Advanced spacing controls. // Disable core Gutenberg "Dimensions" (spacing) UI to avoid duplicates. if ( isset( $args['supports'] ) && is_array( $args['supports'] ) ) { unset( $args['supports']['spacing'] ); unset( $args['supports']['__experimentalSpacing'] ); unset( $args['supports']['dimensions'] ); unset( $args['supports']['__experimentalDimensions'] ); } $sides = array( 'Top', 'Right', 'Bottom', 'Left' ); $devices = array( '', 'Tablet', 'Mobile' ); $extra = array(); foreach ( array( 'bkbgMargin', 'bkbgPadding' ) as $prefix ) { foreach ( $sides as $side ) { foreach ( $devices as $device ) { $key = $prefix . $side . $device; $extra[ $key ] = array( 'type' => 'string', 'default' => '' ); $extra[ $key . 'Unit' ] = array( 'type' => 'string', 'default' => 'px' ); } } foreach ( $devices as $device ) { $extra[ $prefix . 'Linked' . $device ] = array( 'type' => 'boolean', 'default' => true ); } } foreach ( $devices as $device ) { $extra[ 'bkbgZIndex' . $device ] = array( 'type' => 'string', 'default' => '' ); } $extra['bkbgCssId'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgCssClasses'] = array( 'type' => 'string', 'default' => '' ); // ── Background attributes ── $extra['bkbgBgType'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgBgHoverType'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgBgColor'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgBgHoverColor']= array( 'type' => 'string', 'default' => '' ); // Classic image (responsive) — normal & hover foreach ( array( 'bkbgBgImage', 'bkbgBgHoverImage' ) as $img_prefix ) { foreach ( $devices as $device ) { $extra[ $img_prefix . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ $img_prefix . 'Id' . $device ] = array( 'type' => 'number', 'default' => 0 ); } } // Classic image settings — normal & hover (position/repeat/size responsive, attachment global) foreach ( array( 'bkbgBg', 'bkbgBgHover' ) as $s_prefix ) { foreach ( $devices as $device ) { $extra[ $s_prefix . 'Position' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ $s_prefix . 'PositionCustomX' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ $s_prefix . 'PositionCustomY' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ $s_prefix . 'Repeat' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ $s_prefix . 'Size' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ $s_prefix . 'SizeCustomW' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ $s_prefix . 'SizeCustomH' . $device ] = array( 'type' => 'string', 'default' => '' ); } $extra[ $s_prefix . 'Attachment' ] = array( 'type' => 'string', 'default' => '' ); } // Gradient — normal $extra['bkbgBgGradColor1'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgBgGradColor2'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgBgGradType'] = array( 'type' => 'string', 'default' => 'linear' ); foreach ( $devices as $device ) { $extra[ 'bkbgBgGradLoc1' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ 'bkbgBgGradLoc2' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ 'bkbgBgGradAngle' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ 'bkbgBgGradPosition' . $device ] = array( 'type' => 'string', 'default' => '' ); } // Gradient — hover $extra['bkbgBgHoverGradColor1'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgBgHoverGradColor2'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgBgHoverGradType'] = array( 'type' => 'string', 'default' => 'linear' ); foreach ( $devices as $device ) { $extra[ 'bkbgBgHoverGradLoc1' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ 'bkbgBgHoverGradLoc2' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ 'bkbgBgHoverGradAngle' . $device ] = array( 'type' => 'string', 'default' => '' ); $extra[ 'bkbgBgHoverGradPosition' . $device ] = array( 'type' => 'string', 'default' => '' ); } // ── Border attributes ── // Border Type (normal & hover) $extra['bkbgBorderType'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgBorderHoverType'] = array( 'type' => 'string', 'default' => '' ); // Border Width — per side, per device, with unit (normal & hover) foreach ( array( 'bkbgBorderWidth', 'bkbgBorderHoverWidth' ) as $prefix ) { foreach ( $sides as $side ) { foreach ( $devices as $device ) { $key = $prefix . $side . $device; $extra[ $key ] = array( 'type' => 'string', 'default' => '' ); $extra[ $key . 'Unit' ] = array( 'type' => 'string', 'default' => 'px' ); } } foreach ( $devices as $device ) { $extra[ $prefix . 'Linked' . $device ] = array( 'type' => 'boolean', 'default' => true ); } } // Border Color (normal & hover) $extra['bkbgBorderColor'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgBorderHoverColor'] = array( 'type' => 'string', 'default' => '' ); // Border Radius — per corner, per device, with unit (normal & hover) foreach ( array( 'bkbgBorderRadius', 'bkbgBorderHoverRadius' ) as $prefix ) { foreach ( $sides as $side ) { foreach ( $devices as $device ) { $key = $prefix . $side . $device; $extra[ $key ] = array( 'type' => 'string', 'default' => '' ); $extra[ $key . 'Unit' ] = array( 'type' => 'string', 'default' => 'px' ); } } foreach ( $devices as $device ) { $extra[ $prefix . 'Linked' . $device ] = array( 'type' => 'boolean', 'default' => true ); } } // Box Shadow (normal) $extra['bkbgShadowColor'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowH'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowV'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowBlur'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowSpread'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowPosition'] = array( 'type' => 'string', 'default' => '' ); // Box Shadow (hover) $extra['bkbgShadowHoverColor'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowHoverH'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowHoverV'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowHoverBlur'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowHoverSpread'] = array( 'type' => 'string', 'default' => '' ); $extra['bkbgShadowHoverPosition'] = array( 'type' => 'string', 'default' => '' ); // Responsive visibility $extra['bkbgHideDesktop'] = array( 'type' => 'boolean', 'default' => false ); $extra['bkbgHideTablet'] = array( 'type' => 'boolean', 'default' => false ); $extra['bkbgHideMobile'] = array( 'type' => 'boolean', 'default' => false ); if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) { $args['attributes'] = array(); } $args['attributes'] = array_merge( $args['attributes'], $extra ); return $args; }, 10, 2 ); /** * Render ALL advanced layout CSS (desktop + tablet + mobile) for Blockenberg blocks. * Desktop styles are output as inline styles on the wrapper. * Responsive styles use '; } return $block_content; }, 10, 2 ); /** * Register REST API endpoint for Post Grid block. * * Route: /wp-json/blockenberg/v1/post-grid * Method: GET * Params: * - type: string (posts|pages|any CPT) * - orderby: string (date|title|comment_count|...) * - order: string (asc|desc) * - per_page: int * - offset: int * - page: int (1-based) * - excerpt_len: int (optional) */ add_action( 'rest_api_init', function () { register_rest_route( 'blockenberg/v1', '/post-grid', array( 'methods' => 'GET', 'permission_callback' => '__return_true', 'args' => array( 'type' => array( 'sanitize_callback' => 'sanitize_key' ), 'orderby' => array( 'sanitize_callback' => 'sanitize_key' ), 'order' => array( 'sanitize_callback' => 'sanitize_text_field' ), 'per_page' => array( 'sanitize_callback' => 'absint' ), 'offset' => array( 'sanitize_callback' => 'absint' ), 'page' => array( 'sanitize_callback' => 'absint' ), 'excerpt_len' => array( 'sanitize_callback' => 'absint' ), ), 'callback' => function ( WP_REST_Request $request ) { $requested_type = sanitize_key( $request->get_param( 'type' ) ?: 'post' ); if ( 'posts' === $requested_type ) { $requested_type = 'post'; } elseif ( 'pages' === $requested_type ) { $requested_type = 'page'; } $public_post_types = get_post_types( array( 'public' => true ), 'names' ); $post_type = in_array( $requested_type, $public_post_types, true ) ? $requested_type : 'post'; $requested_orderby = sanitize_key( $request->get_param( 'orderby' ) ?: 'date' ); $allowed_orderby = array( 'date', 'title', 'modified', 'comment_count', 'rand', 'menu_order' ); $orderby = in_array( $requested_orderby, $allowed_orderby, true ) ? $requested_orderby : 'date'; $order = strtolower( (string) $request->get_param( 'order' ) ) === 'asc' ? 'ASC' : 'DESC'; $per_page = max( 1, absint( $request->get_param( 'per_page' ) ?: 6 ) ); $per_page = min( 50, $per_page ); $offset = max( 0, absint( $request->get_param( 'offset' ) ?: 0 ) ); $offset = min( 5000, $offset ); $page = max( 1, absint( $request->get_param( 'page' ) ?: 1 ) ); $page = min( 200, $page ); $excerpt_len = max( 5, absint( $request->get_param( 'excerpt_len' ) ?: 18 ) ); $excerpt_len = min( 80, $excerpt_len ); // Short cache for public non-product queries. $is_product_query = ( 'product' === $post_type ); $cache_key = ''; if ( ! $is_product_query ) { $cache_key = 'bkbg_post_grid_' . md5( wp_json_encode( array( 'type' => $post_type, 'orderby' => $orderby, 'order' => $order, 'per_page' => $per_page, 'offset' => $offset, 'page' => $page, 'excerpt_len' => $excerpt_len, ) ) ); $cached = get_transient( $cache_key ); if ( is_array( $cached ) ) { return rest_ensure_response( $cached ); } } $q = new WP_Query( array( 'post_type' => $post_type, 'post_status' => 'publish', 'orderby' => $orderby, 'order' => $order, 'posts_per_page' => $per_page, 'offset' => $offset + ( ( $page - 1 ) * $per_page ), 'ignore_sticky_posts' => true, 'no_found_rows' => true, ) ); $posts = array(); foreach ( $q->posts as $p ) { $post_id = $p->ID; $title = wp_strip_all_tags( get_the_title( $post_id ) ); $link = esc_url_raw( get_permalink( $post_id ) ); $image = esc_url_raw( (string) get_the_post_thumbnail_url( $post_id, 'medium_large' ) ); $date = wp_strip_all_tags( (string) get_the_date( '', $post_id ) ); $author = sanitize_text_field( (string) get_the_author_meta( 'display_name', $p->post_author ) ); $meta = trim( $date . ' · ' . $author ); $raw = get_post_field( 'post_excerpt', $post_id ); if ( '' === $raw ) { $raw = get_post_field( 'post_content', $post_id ); } $excerpt = wp_strip_all_tags( wp_trim_words( wp_strip_all_tags( $raw ), $excerpt_len, '…' ) ); $item = array( 'id' => $post_id, 'title' => $title, 'link' => $link, 'image' => $image, 'meta' => $meta, 'excerpt' => $excerpt, ); if ( 'product' === $post_type && function_exists( 'wc_get_product' ) ) { $product = wc_get_product( $post_id ); if ( $product ) { $item['price_html'] = wp_kses_post( $product->get_price_html() ); $item['add_to_cart'] = esc_url_raw( $product->add_to_cart_url() ); } } $posts[] = $item; } $payload = array( 'posts' => $posts ); if ( ! $is_product_query && '' !== $cache_key ) { set_transient( $cache_key, $payload, 60 ); } return rest_ensure_response( $payload ); }, ) ); } ); /** * Simple local newsletter subscribe endpoint. * Stores emails in an option array; extend as needed (e.g., to a custom table). */ add_action( 'rest_api_init', function () { register_rest_route( 'blockenberg/v1', '/subscribe', array( 'methods' => 'POST', 'permission_callback' => '__return_true', 'args' => array( 'email' => array( 'sanitize_callback' => 'sanitize_email' ), 'website' => array( 'sanitize_callback' => 'sanitize_text_field' ), ), 'callback' => function ( WP_REST_Request $request ) { $payload = $request->get_json_params(); if ( ! is_array( $payload ) ) { $payload = array(); } // Basic rate limiting by IP to reduce spam / abuse. $ip = ''; if ( isset( $_SERVER['REMOTE_ADDR'] ) ) { $ip = sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) ); } if ( '' !== $ip ) { $key = 'bkbg_subscribe_' . md5( $ip ); $rl = get_transient( $key ); if ( is_array( $rl ) && isset( $rl['count'], $rl['start'] ) ) { if ( $rl['count'] >= 5 ) { return new WP_Error( 'rate_limited', __( 'Too many requests. Please try again later.', 'blockenberg' ), array( 'status' => 429 ) ); } $rl['count']++; set_transient( $key, $rl, 60 ); } else { set_transient( $key, array( 'count' => 1, 'start' => time() ), 60 ); } } // Optional honeypot field (bots tend to fill it). $honeypot = isset( $payload['website'] ) ? sanitize_text_field( (string) $payload['website'] ) : ''; if ( '' !== $honeypot ) { return rest_ensure_response( array( 'ok' => true ) ); } $email = isset( $payload['email'] ) ? sanitize_email( (string) $payload['email'] ) : ''; if ( empty( $email ) || ! is_email( $email ) ) { return new WP_Error( 'invalid_email', __( 'Invalid email address', 'blockenberg' ), array( 'status' => 400 ) ); } $list = get_option( 'blockenberg_newsletter_subscribers', array() ); if ( ! is_array( $list ) ) { $list = array(); } // Prevent unbounded option growth. if ( count( $list ) >= 5000 ) { return new WP_Error( 'storage_full', __( 'Subscriber list is full.', 'blockenberg' ), array( 'status' => 503 ) ); } if ( ! in_array( $email, $list, true ) ) { $list[] = $email; update_option( 'blockenberg_newsletter_subscribers', $list, false ); } return rest_ensure_response( array( 'ok' => true ) ); }, ) ); } ); /** * Strip CR/LF and related sequences so values cannot inject mail headers. * * @param string $value Raw header fragment. * @return string */ function bkbg_sanitize_mail_header( $value ) { $value = (string) $value; $value = str_replace( array( "\r", "\n", '%0a', '%0d', '%0A', '%0D' ), '', $value ); return trim( sanitize_text_field( $value ) ); } /** * HMAC signature for a contact-form recipient email. * Prevents open-relay abuse: the client may only use a recipient that was * signed server-side when the block was rendered. * * @param string $email Recipient email. * @return string Hex HMAC or empty string if invalid. */ function bkbg_contact_recipient_sig( $email ) { $email = strtolower( sanitize_email( (string) $email ) ); if ( ! is_email( $email ) ) { return ''; } return hash_hmac( 'sha256', $email, wp_salt( 'auth' ) ); } /** * Verify a contact-form recipient signature. * * @param string $email Recipient email from the client. * @param string $sig HMAC from data-recipient-sig. * @return bool */ function bkbg_verify_contact_recipient( $email, $sig ) { $email = strtolower( sanitize_email( (string) $email ) ); if ( ! is_email( $email ) || ! is_string( $sig ) || '' === $sig ) { return false; } $expected = bkbg_contact_recipient_sig( $email ); return ( '' !== $expected && hash_equals( $expected, $sig ) ); } /** * Inject a server-signed recipient HMAC into Contact Form markup on render. * Existing posts do not need to be re-saved — the signature is added at runtime. */ add_filter( 'render_block', function ( $block_content, $block ) { if ( empty( $block['blockName'] ) || 'blockenberg/contact-form' !== $block['blockName'] ) { return $block_content; } if ( ! is_string( $block_content ) || '' === $block_content ) { return $block_content; } $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array(); $recipient = isset( $attrs['recipientEmail'] ) ? sanitize_email( (string) $attrs['recipientEmail'] ) : ''; if ( ! is_email( $recipient ) ) { return $block_content; } $sig = bkbg_contact_recipient_sig( $recipient ); if ( '' === $sig ) { return $block_content; } // Replace existing sig if present, otherwise inject onto the first opening tag. if ( false !== strpos( $block_content, 'data-recipient-sig=' ) ) { $block_content = preg_replace( '/\sdata-recipient-sig=(["\'])[^"\']*\1/', ' data-recipient-sig="' . esc_attr( $sig ) . '"', $block_content, 1 ); } else { $block_content = preg_replace( '/^\s*(<[a-zA-Z][^>]*)/', '$1 data-recipient-sig="' . esc_attr( $sig ) . '"', $block_content, 1 ); } return $block_content; }, 10, 2 ); /** * Contact Form endpoint — POST /wp-json/blockenberg/v1/contact * Sends an email via wp_mail() to the admin or a custom recipient stored in the block. * Custom recipients require a valid server-issued HMAC (data-recipient-sig). */ add_action( 'rest_api_init', function () { register_rest_route( 'blockenberg/v1', '/contact', array( 'methods' => 'POST', 'permission_callback' => '__return_true', 'callback' => function ( WP_REST_Request $request ) { $payload = $request->get_json_params(); if ( ! is_array( $payload ) ) { $payload = array(); } // Rate-limit by IP: max 5 requests per 60 s. $ip = ''; if ( isset( $_SERVER['REMOTE_ADDR'] ) ) { $ip = sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) ); } if ( '' !== $ip ) { $rl_key = 'bkbg_contact_' . md5( $ip ); $rl = get_transient( $rl_key ); if ( is_array( $rl ) && isset( $rl['count'] ) && $rl['count'] >= 5 ) { return new WP_Error( 'rate_limited', __( 'Too many requests. Please try again later.', 'blockenberg' ), array( 'status' => 429 ) ); } if ( is_array( $rl ) ) { $rl['count']++; set_transient( $rl_key, $rl, 60 ); } else { set_transient( $rl_key, array( 'count' => 1 ), 60 ); } } // Honeypot check (bots fill the hidden website field). $honeypot = isset( $payload['website'] ) ? sanitize_text_field( (string) $payload['website'] ) : ''; if ( '' !== $honeypot ) { return rest_ensure_response( array( 'ok' => true ) ); // silently accept. } // Validate required fields. $name = isset( $payload['name'] ) ? sanitize_text_field( (string) $payload['name'] ) : ''; $email = isset( $payload['email'] ) ? sanitize_email( (string) $payload['email'] ) : ''; $phone = isset( $payload['phone'] ) ? sanitize_text_field( (string) $payload['phone'] ) : ''; $message = isset( $payload['message'] ) ? sanitize_textarea_field( (string) $payload['message'] ) : ''; if ( ! is_email( $email ) ) { return new WP_Error( 'invalid_email', __( 'Invalid email address.', 'blockenberg' ), array( 'status' => 400 ) ); } if ( empty( $message ) ) { return new WP_Error( 'empty_message', __( 'Message is required.', 'blockenberg' ), array( 'status' => 400 ) ); } // Recipient: only accept a client-supplied address when it carries a // valid server HMAC. Otherwise always fall back to admin_email (no open relay). $admin_email = sanitize_email( (string) get_option( 'admin_email' ) ); $recipient = $admin_email; $requested = isset( $payload['recipient'] ) ? sanitize_email( (string) $payload['recipient'] ) : ''; $sig = isset( $payload['recipientSig'] ) ? (string) $payload['recipientSig'] : ''; if ( is_email( $requested ) && bkbg_verify_contact_recipient( $requested, $sig ) ) { $recipient = strtolower( $requested ); } $subject = isset( $payload['subject'] ) ? bkbg_sanitize_mail_header( (string) $payload['subject'] ) : ''; if ( '' === $subject ) { $subject = __( 'New Contact Form Submission', 'blockenberg' ); } // Build email body. $body = "Name: {$name}\n"; $body .= "Email: {$email}\n"; if ( ! empty( $phone ) ) { $body .= "Phone: {$phone}\n"; } $body .= "\nMessage:\n{$message}\n"; // Reply-To: strip CR/LF from name; never allow header injection. $safe_name = bkbg_sanitize_mail_header( $name ); $safe_name = str_replace( array( '"', '<', '>' ), '', $safe_name ); if ( '' !== $safe_name ) { $reply_to = sprintf( 'Reply-To: %s <%s>', $safe_name, $email ); } else { $reply_to = 'Reply-To: ' . $email; } $headers = array( 'Content-Type: text/plain; charset=UTF-8', $reply_to, ); $sent = wp_mail( $recipient, $subject, $body, $headers ); if ( ! $sent ) { return new WP_Error( 'mail_failed', __( 'Failed to send email. Please try again.', 'blockenberg' ), array( 'status' => 500 ) ); } return rest_ensure_response( array( 'ok' => true ) ); }, ) ); } );