| @@ -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 | +} ); | |