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 / popup-optin / PopupOptinHandler.php

PopupOptinHandler.php in bBlocks – Essential Gutenberg Blocks & Patterns Collection 2.1.8, at includes/blocks/popup-optin/PopupOptinHandler.php

158 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Popup Optin Block — AJAX submission handler.
4 *
5 * Receives lead submissions from the Popup Optin block frontend
6 * (src/popup-optin/view.js) and emails the subscriber details to the
7 * configured recipient. MVP scope: wp_mail() only — no ESP integration,
8 * no DB storage.
9 *
10 * SECURITY: the mail recipient is NEVER taken from the client POST. It is
11 * resolved server-side by parsing the saved block attributes of the post the
12 * popup lives on (per-block `adminEmail` override), falling back to the site
13 * admin email. This deliberately avoids the arbitrary-recipient / open
14 * mail-relay pattern where a POST parameter sets wp_mail()'s recipient.
15 *
16 * @package bBlocks
17 */
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 if ( ! class_exists( 'BBlocksPopupOptinHandler' ) ) {
24
25 class BBlocksPopupOptinHandler {
26
27 public function __construct() {
28 add_action( 'wp_ajax_bb_popup_optin_submit', [ $this, 'handle_submit' ] );
29 add_action( 'wp_ajax_nopriv_bb_popup_optin_submit', [ $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/popup-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 popup.
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 popup-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/popup-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 (dies with -1 / 403 on failure).
104 check_ajax_referer( 'bb_popup_optin_submit', '_wpnonce' );
105
106 // Transient-based per-IP rate limit (max 5 submissions per 60 seconds).
107 BBlocksOptinRateLimit::check( 'bb_po' );
108
109 // Validate email.
110 $email = isset( $_POST['bb_po_email'] ) ? sanitize_email( wp_unslash( $_POST['bb_po_email'] ) ) : '';
111 if ( '' === $email || ! is_email( $email ) ) {
112 wp_send_json_error( [ 'message' => __( 'Please enter a valid email address.', 'b-blocks' ) ] );
113 }
114
115 // Optional name.
116 $name = isset( $_POST['bb_po_name'] ) ? sanitize_text_field( wp_unslash( $_POST['bb_po_name'] ) ) : '';
117
118 // Resolve recipient SERVER-SIDE from saved block config — never POST.
119 $post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;
120 $recipient = $this->resolve_recipient( $post_id );
121
122 if ( '' === $recipient || ! is_email( $recipient ) ) {
123 wp_send_json_error( [ 'message' => __( 'Subscription could not be processed. Please try again later.', 'b-blocks' ) ] );
124 }
125
126 // Strip HTML entities, then strip any CRLF that could inject mail headers.
127 $site_name = wp_strip_all_tags( wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
128 $site_name = str_replace( [ "\r", "\n" ], ' ', $site_name );
129
130 /* translators: %s: site name */
131 $subject = sprintf( __( 'New popup lead on %s', 'b-blocks' ), $site_name );
132
133 $lines = [];
134 $lines[] = __( 'You have a new lead from a popup optin.', 'b-blocks' );
135 $lines[] = '';
136 if ( '' !== $name ) {
137 /* translators: %s: subscriber name */
138 $lines[] = sprintf( __( 'Name: %s', 'b-blocks' ), $name );
139 }
140 /* translators: %s: subscriber email */
141 $lines[] = sprintf( __( 'Email: %s', 'b-blocks' ), $email );
142
143 $message = implode( "\n", $lines );
144 $headers = [ 'Reply-To: ' . $email ];
145
146 $sent = wp_mail( $recipient, $subject, $message, $headers );
147
148 if ( ! $sent ) {
149 wp_send_json_error( [ 'message' => __( 'Subscription could not be processed. Please try again later.', 'b-blocks' ) ] );
150 }
151
152 wp_send_json_success( [ 'message' => __( "Thanks! You're on the list.", 'b-blocks' ) ] );
153 }
154 }
155
156 new BBlocksPopupOptinHandler();
157 }
158