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