PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.13
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.13
2.0.13 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
← All changes | blockenberg.php +126 -10 2.0.112.0.13 View file →
@@ -1,9 +1,9 @@
1 1 <?php
2 2 /**
3 - * Plugin Name: Blockenberg
4 - * Description: Advanced Gutenberg Blocks for WordPress Block Editor
5 - * Version: 2.0.11
3 + * Plugin Name: Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor
4 + * Description: Advanced Gutenberg Blocks and an AI Agent for the WordPress Block Editor
5 + * Version: 2.0.13
6 6 * Author: Blockenberg
7 7 * Text Domain: blockenberg
8 8 * Domain Path: /languages
9 9 * License: GPLv2 or later
@@ -24,8 +24,19 @@
24 24 */
25 25 require_once __DIR__ . '/assets/php/google-fonts.php';
26 26
27 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 + * AI Agent — OpenRouter-powered chat panel inside the block editor.
34 + */
35 +require_once __DIR__ . '/assets/php/ai-assistant.php';
36 +require_once __DIR__ . '/assets/php/ai-external.php';
37 +
38 +/**
28 39 * Admin dashboard — Block Manager (enable / disable blocks).
29 40 */
30 41 if ( is_admin() ) {
31 42 require_once __DIR__ . '/assets/php/admin-dashboard.php';
@@ -1094,10 +1105,98 @@
1094 1105 ) );
1095 1106 } );
1096 1107
1097 1108 /**
1109 + * Strip CR/LF and related sequences so values cannot inject mail headers.
1110 + *
1111 + * @param string $value Raw header fragment.
1112 + * @return string
1113 + */
1114 +function bkbg_sanitize_mail_header( $value ) {
1115 + $value = (string) $value;
1116 + $value = str_replace( array( "\r", "\n", '%0a', '%0d', '%0A', '%0D' ), '', $value );
1117 + return trim( sanitize_text_field( $value ) );
1118 +}
1119 +
1120 +/**
1121 + * HMAC signature for a contact-form recipient email.
1122 + * Prevents open-relay abuse: the client may only use a recipient that was
1123 + * signed server-side when the block was rendered.
1124 + *
1125 + * @param string $email Recipient email.
1126 + * @return string Hex HMAC or empty string if invalid.
1127 + */
1128 +function bkbg_contact_recipient_sig( $email ) {
1129 + $email = strtolower( sanitize_email( (string) $email ) );
1130 + if ( ! is_email( $email ) ) {
1131 + return '';
1132 + }
1133 + return hash_hmac( 'sha256', $email, wp_salt( 'auth' ) );
1134 +}
1135 +
1136 +/**
1137 + * Verify a contact-form recipient signature.
1138 + *
1139 + * @param string $email Recipient email from the client.
1140 + * @param string $sig HMAC from data-recipient-sig.
1141 + * @return bool
1142 + */
1143 +function bkbg_verify_contact_recipient( $email, $sig ) {
1144 + $email = strtolower( sanitize_email( (string) $email ) );
1145 + if ( ! is_email( $email ) || ! is_string( $sig ) || '' === $sig ) {
1146 + return false;
1147 + }
1148 + $expected = bkbg_contact_recipient_sig( $email );
1149 + return ( '' !== $expected && hash_equals( $expected, $sig ) );
1150 +}
1151 +
1152 +/**
1153 + * Inject a server-signed recipient HMAC into Contact Form markup on render.
1154 + * Existing posts do not need to be re-saved — the signature is added at runtime.
1155 + */
1156 +add_filter( 'render_block', function ( $block_content, $block ) {
1157 + if ( empty( $block['blockName'] ) || 'blockenberg/contact-form' !== $block['blockName'] ) {
1158 + return $block_content;
1159 + }
1160 + if ( ! is_string( $block_content ) || '' === $block_content ) {
1161 + return $block_content;
1162 + }
1163 +
1164 + $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
1165 + $recipient = isset( $attrs['recipientEmail'] ) ? sanitize_email( (string) $attrs['recipientEmail'] ) : '';
1166 + if ( ! is_email( $recipient ) ) {
1167 + return $block_content;
1168 + }
1169 +
1170 + $sig = bkbg_contact_recipient_sig( $recipient );
1171 + if ( '' === $sig ) {
1172 + return $block_content;
1173 + }
1174 +
1175 + // Replace existing sig if present, otherwise inject onto the first opening tag.
1176 + if ( false !== strpos( $block_content, 'data-recipient-sig=' ) ) {
1177 + $block_content = preg_replace(
1178 + '/\sdata-recipient-sig=(["\'])[^"\']*\1/',
1179 + ' data-recipient-sig="' . esc_attr( $sig ) . '"',
1180 + $block_content,
1181 + 1
1182 + );
1183 + } else {
1184 + $block_content = preg_replace(
1185 + '/^\s*(<[a-zA-Z][^>]*)/',
1186 + '$1 data-recipient-sig="' . esc_attr( $sig ) . '"',
1187 + $block_content,
1188 + 1
1189 + );
1190 + }
1191 +
1192 + return $block_content;
1193 +}, 10, 2 );
1194 +
1195 +/**
1098 1196 * Contact Form endpoint — POST /wp-json/blockenberg/v1/contact
1099 1197 * Sends an email via wp_mail() to the admin or a custom recipient stored in the block.
1198 + * Custom recipients require a valid server-issued HMAC (data-recipient-sig).
1100 1199 */
1101 1200 add_action( 'rest_api_init', function () {
1102 1201 register_rest_route( 'blockenberg/v1', '/contact', array(
1103 1202 'methods' => 'POST',
@@ -1145,15 +1244,23 @@
1145 1244 if ( empty( $message ) ) {
1146 1245 return new WP_Error( 'empty_message', __( 'Message is required.', 'blockenberg' ), array( 'status' => 400 ) );
1147 1246 }
1148 1247
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' );
1248 + // Recipient: only accept a client-supplied address when it carries a
1249 + // valid server HMAC. Otherwise always fall back to admin_email (no open relay).
1250 + $admin_email = sanitize_email( (string) get_option( 'admin_email' ) );
1251 + $recipient = $admin_email;
1252 + $requested = isset( $payload['recipient'] ) ? sanitize_email( (string) $payload['recipient'] ) : '';
1253 + $sig = isset( $payload['recipientSig'] ) ? (string) $payload['recipientSig'] : '';
1254 + if ( is_email( $requested ) && bkbg_verify_contact_recipient( $requested, $sig ) ) {
1255 + $recipient = strtolower( $requested );
1153 1256 }
1154 - $subject = isset( $payload['subject'] ) ? sanitize_text_field( (string) $payload['subject'] ) : __( 'New Contact Form Submission', 'blockenberg' );
1155 1257
1258 + $subject = isset( $payload['subject'] ) ? bkbg_sanitize_mail_header( (string) $payload['subject'] ) : '';
1259 + if ( '' === $subject ) {
1260 + $subject = __( 'New Contact Form Submission', 'blockenberg' );
1261 + }
1262 +
1156 1263 // Build email body.
1157 1264 $body = "Name: {$name}\n";
1158 1265 $body .= "Email: {$email}\n";
1159 1266 if ( ! empty( $phone ) ) {
@@ -1160,11 +1267,20 @@
1160 1267 $body .= "Phone: {$phone}\n";
1161 1268 }
1162 1269 $body .= "\nMessage:\n{$message}\n";
1163 1270
1271 + // Reply-To: strip CR/LF from name; never allow header injection.
1272 + $safe_name = bkbg_sanitize_mail_header( $name );
1273 + $safe_name = str_replace( array( '"', '<', '>' ), '', $safe_name );
1274 + if ( '' !== $safe_name ) {
1275 + $reply_to = sprintf( 'Reply-To: %s <%s>', $safe_name, $email );
1276 + } else {
1277 + $reply_to = 'Reply-To: ' . $email;
1278 + }
1279 +
1164 1280 $headers = array(
1165 1281 'Content-Type: text/plain; charset=UTF-8',
1166 - "Reply-To: {$name} <{$email}>",
1282 + $reply_to,
1167 1283 );
1168 1284
1169 1285 $sent = wp_mail( $recipient, $subject, $body, $headers );
1170 1286
@@ -1174,5 +1290,5 @@
1174 1290
1175 1291 return rest_ensure_response( array( 'ok' => true ) );
1176 1292 },
1177 1293 ) );
1178 -} );
1294 +} );