PluginProbe
bBlocks – Essential Gutenberg Blocks & Patterns Collection / 2.1.8
bBlocks – Essential Gutenberg Blocks & Patterns Collection v2.1.8
2.1.8 2.1.7 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.43 2.0.42 2.0.41 2.0.40 2.0.39 2.0.38 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.5.1 1.5.2 1.5.3 All 108 releases
b-blocks / includes / blocks / newsletter / NewsletterHandler.php

NewsletterHandler.php in bBlocks – Essential Gutenberg Blocks & Patterns Collection 2.1.8, at includes/blocks/newsletter/NewsletterHandler.php

161 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Newsletter Optin Block — AJAX submission handler.
4 *
5 * Receives subscribe submissions from the Newsletter Optin block frontend
6 * (src/newsletter-optin/view.js) and emails the subscriber details to the
7 * configured recipient.
8 *
9 * SECURITY: the mail recipient is NEVER taken from the client POST. It is
10 * resolved server-side by parsing the saved block attributes of the post that
11 * contains the newsletter form (per-block `adminEmail` override), falling back
12 * to the site admin email. This mirrors the open-mail-relay fix applied to the
13 * popup-optin handler and prevents an unauthenticated caller from redirecting
14 * notification emails to an arbitrary address.
15 *
16 * @package bBlocks
17 */
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 if ( ! class_exists( 'BBlocksNewsletterHandler' ) ) {
24
25 class BBlocksNewsletterHandler {
26
27 public function __construct() {
28 add_action( 'wp_ajax_bb_newsletter_optin', [ $this, 'handle_submit' ] );
29 add_action( 'wp_ajax_nopriv_bb_newsletter_optin', [ $this, 'handle_submit' ] );
30 }
31
32 /**
33 * Resolve the mail recipient from saved block content, never from POST.
34 *
35 * Walks the blocks of the given post, finds the first
36 * `b-blocks/newsletter-optin` block with a non-empty, valid `adminEmail`
37 * attribute, and returns it. Falls back to the site admin email.
38 *
39 * @param int $post_id Post that contained the form.
40 * @return string A valid recipient email, or '' if none can be resolved.
41 */
42 private function resolve_recipient( $post_id ) {
43 $fallback = get_option( 'admin_email' );
44 $fallback = is_email( $fallback ) ? $fallback : '';
45
46 if ( $post_id <= 0 ) {
47 return $fallback;
48 }
49
50 $post = get_post( $post_id );
51 if ( ! $post || empty( $post->post_content ) ) {
52 return $fallback;
53 }
54
55 // Only consider published/viewable posts to avoid leaking config.
56 if ( ! in_array( $post->post_status, [ 'publish', 'private' ], true ) ) {
57 return $fallback;
58 }
59
60 if ( ! has_blocks( $post->post_content ) ) {
61 return $fallback;
62 }
63
64 $blocks = parse_blocks( $post->post_content );
65 $override = $this->find_admin_email( $blocks );
66
67 if ( '' !== $override && is_email( $override ) ) {
68 return $override;
69 }
70
71 return $fallback;
72 }
73
74 /**
75 * Recursively search parsed blocks for a newsletter-optin adminEmail override.
76 *
77 * @param array $blocks Parsed block tree.
78 * @return string The first valid override found, or ''.
79 */
80 private function find_admin_email( $blocks ) {
81 foreach ( $blocks as $block ) {
82 if ( isset( $block['blockName'] ) && 'b-blocks/newsletter-optin' === $block['blockName'] ) {
83 if ( ! empty( $block['attrs']['adminEmail'] ) ) {
84 $candidate = sanitize_email( (string) $block['attrs']['adminEmail'] );
85 if ( '' !== $candidate && is_email( $candidate ) ) {
86 return $candidate;
87 }
88 }
89 }
90
91 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
92 $nested = $this->find_admin_email( $block['innerBlocks'] );
93 if ( '' !== $nested ) {
94 return $nested;
95 }
96 }
97 }
98
99 return '';
100 }
101
102 public function handle_submit() {
103 // Verify nonce.
104 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
105 if ( '' === $nonce || ! wp_verify_nonce( $nonce, 'bb_newsletter_optin' ) ) {
106 wp_send_json_error( [ 'message' => __( 'Security check failed. Please reload the page and try again.', 'b-blocks' ) ] );
107 }
108
109 // Transient-based per-IP rate limit (max 5 submissions per 60 seconds).
110 BBlocksOptinRateLimit::check( 'bb_no' );
111
112 // Validate email.
113 $email_raw = isset( $_POST['bb_no_email'] ) ? sanitize_email( wp_unslash( $_POST['bb_no_email'] ) ) : '';
114 if ( '' === $email_raw || ! is_email( $email_raw ) ) {
115 wp_send_json_error( [ 'message' => __( 'Please enter a valid email address.', 'b-blocks' ) ] );
116 }
117
118 // Optional name.
119 $name = isset( $_POST['bb_no_name'] ) ? sanitize_text_field( wp_unslash( $_POST['bb_no_name'] ) ) : '';
120
121 // Resolve recipient SERVER-SIDE from saved block config — never POST.
122 $post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;
123 $recipient = $this->resolve_recipient( $post_id );
124
125 if ( '' === $recipient || ! is_email( $recipient ) ) {
126 wp_send_json_error( [ 'message' => __( 'Subscription could not be processed. Please try again later.', 'b-blocks' ) ] );
127 }
128
129 // Strip HTML tags and CRLF from site name to prevent mail-header injection.
130 $site_name = wp_strip_all_tags( wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
131 $site_name = str_replace( [ "\r", "\n" ], ' ', $site_name );
132
133 /* translators: %s: site name */
134 $subject = sprintf( __( 'New newsletter subscriber on %s', 'b-blocks' ), $site_name );
135
136 $lines = [];
137 $lines[] = __( 'You have a new newsletter subscriber.', 'b-blocks' );
138 $lines[] = '';
139 if ( '' !== $name ) {
140 /* translators: %s: subscriber name */
141 $lines[] = sprintf( __( 'Name: %s', 'b-blocks' ), $name );
142 }
143 /* translators: %s: subscriber email */
144 $lines[] = sprintf( __( 'Email: %s', 'b-blocks' ), $email_raw );
145
146 $message = implode( "\n", $lines );
147 $headers = [ 'Reply-To: ' . $email_raw ];
148
149 $sent = wp_mail( $recipient, $subject, $message, $headers );
150
151 if ( ! $sent ) {
152 wp_send_json_error( [ 'message' => __( 'Subscription could not be processed. Please try again later.', 'b-blocks' ) ] );
153 }
154
155 wp_send_json_success( [ 'message' => __( 'Thank you for subscribing!', 'b-blocks' ) ] );
156 }
157 }
158
159 new BBlocksNewsletterHandler();
160 }
161