PluginProbe
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment / 3.1.14
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment v3.1.14
3.1.14 3.1.13 3.1.12 3.1.11 3.1.10 3.1.9 3.1.8 3.1.7 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 All 123 releases
← All changes | Inc/Core/Form/Ajax.php +93 -18 3.1.9 → 3.1.14 View file →
@@ -1,8 +1,8 @@
1 1 <?php
2 2 /**
3 3 * @package FireBox
4 - * @version 3.1.9 Free
4 + * @version 3.1.14 Free
5 5 *
6 6 * @author FirePlugins <[email protected]>
7 7 * @link https://www.fireplugins.com
8 8 * @copyright Copyright © 2026 FirePlugins All Rights Reserved
@@ -20,8 +20,24 @@
20 20 use \FireBox\Core\Helpers\BoxHelper;
21 21
22 22 class Ajax
23 23 {
24 + /**
25 + * Accepted submissions one client may make per minute.
26 + *
27 + * Set well above anything a person produces — the target is a script replaying a
28 + * valid submission, not a fast typist. Only submissions that pass validation count,
29 + * so the headroom also absorbs the cases where several visitors look like one
30 + * client: an office or campus behind a single address, or a proxy that does not
31 + * pass the visitor's address on.
32 + *
33 + * Sites can tune this through the firebox/rate_limit/limit filter, which switches
34 + * the throttle off entirely when it returns 0.
35 + *
36 + * @var int
37 + */
38 + const SUBMISSIONS_PER_MINUTE = 60;
39 +
24 40 public function __construct()
25 41 {
26 42 $this->setupAjax();
27 43
@@ -68,17 +84,28 @@
68 84 ]);
69 85 wp_die();
70 86 }
71 87
72 - $submission_id = isset($_POST['submission_id']) ? sanitize_key(wp_unslash($_POST['submission_id'])) : '';
88 + $submission_id = isset($_POST['submission_id']) ? absint($_POST['submission_id']) : 0;
73 89 $new_state = isset($_POST['new_state']) ? sanitize_key(wp_unslash($_POST['new_state'])) : '';
74 90
75 91 $new_state = $new_state === 'publish' ? 1 : 0;
76 -
92 +
93 + // Confirm the submission exists before reporting anything about it.
94 + if (!$submission_id || !\FireBox\Core\Helpers\Form\Submission::exists($submission_id))
95 + {
96 + echo wp_json_encode([
97 + 'error' => true,
98 + 'message' => 'Submission state couldn\'t be updated.'
99 + ]);
100 + wp_die();
101 + }
102 +
77 103 if (!\FireBox\Core\Helpers\Form\Submission::updateState($submission_id, $new_state))
78 104 {
79 105 echo wp_json_encode([
80 - 'error' => false,
106 + // This branch means the write failed, so report it as an error.
107 + 'error' => true,
81 108 'message' => 'Submission state couldn\'t be updated.'
82 109 ]);
83 110 wp_die();
84 111 }
@@ -101,17 +128,45 @@
101 128
102 129 // verify nonce
103 130 if (!$verify = wp_verify_nonce($nonce, 'fbox_js_nonce'))
104 131 {
132 + /**
133 + * A full-page cache routinely outlives the 12-24h nonce baked into the cached
134 + * HTML. Name the cause so the client can fetch a fresh nonce and replay the
135 + * submission once, instead of losing the lead.
136 + */
105 137 echo wp_json_encode([
138 + 'error' => 'invalid_nonce',
139 + 'message' => firebox()->_('FB_FORM_CANNOT_VERIFY_REQUEST')
140 + ]);
141 + wp_die();
142 + }
143 +
144 + /**
145 + * The nonce above is printed into every page a campaign renders on and is the
146 + * same for every logged-out visitor, so it does not bound how often this endpoint
147 + * can be called. An accepted submission writes several rows and may send mail and
148 + * hit third-party APIs, so throttle per client.
149 + *
150 + * Only the check happens here. The hit is recorded further down, once the
151 + * submission has passed the honeypot, the captcha and validation — see
152 + * RATE LIMIT below. Counting rejected attempts would let a spam bot exhaust an
153 + * allowance that real visitors may be sharing: wherever a proxy leaves many
154 + * visitors looking like one client, junk traffic would lock all of them out of a
155 + * form they are entitled to use. It would also spend a visitor's own allowance on
156 + * their typos.
157 + */
158 + if (\FireBox\Core\Helpers\RateLimit::isLimited('form_submit', self::SUBMISSIONS_PER_MINUTE))
159 + {
160 + echo wp_json_encode([
106 161 'error' => true,
107 - 'message' => 'Cannot verify request.'
162 + 'message' => firebox()->_('FB_FORM_TOO_MANY_SUBMISSIONS')
108 163 ]);
109 164 wp_die();
110 - }
165 + }
111 166
112 167 $form_data = isset($_POST['form_data']) ? sanitize_text_field(wp_unslash($_POST['form_data'])) : '';
113 - $form_data = $form_data ? json_decode(html_entity_decode(stripslashes($form_data)), true) : '';
168 + $form_data = $form_data ? json_decode(stripslashes($form_data), true) : '';
114 169
115 170 if (!$form_data)
116 171 {
117 172 echo wp_json_encode([
@@ -150,8 +205,18 @@
150 205 ]);
151 206 wp_die();
152 207 }
153 208
209 + // Forms live inside campaigns; only a published campaign may accept submissions from the frontend.
210 + if (empty($form['state']) || $form['state'] !== '1')
211 + {
212 + echo wp_json_encode([
213 + 'error' => true,
214 + 'message' => 'This form does not exist.'
215 + ]);
216 + wp_die();
217 + }
218 +
154 219 $form_block = $form['block'];
155 220 $form_fields = $form['fields'];
156 221
157 222 // Get the Campaign ID
@@ -167,9 +232,9 @@
167 232 catch (\Exception $e)
168 233 {
169 234 echo wp_json_encode([
170 235 'error' => true,
171 - 'message' => $e->getMessage()
236 + 'message' => wp_kses_post($e->getMessage())
172 237 ]);
173 238 wp_die();
174 239 }
175 240
@@ -179,9 +244,9 @@
179 244 catch (\Exception $e)
180 245 {
181 246 echo wp_json_encode([
182 247 'error' => true,
183 - 'message' => $e->getMessage()
248 + 'message' => wp_kses_post($e->getMessage())
184 249 ]);
185 250 wp_die();
186 251 }
187 252
@@ -199,8 +264,15 @@
199 264 echo wp_json_encode($payload);
200 265 wp_die();
201 266 }
202 267
268 + /**
269 + * RATE LIMIT: the submission is accepted from here on, and everything below it
270 + * costs something — rows written, mail sent, integrations called. This is the
271 + * point worth rationing, so the hit is recorded here rather than on arrival.
272 + */
273 + \FireBox\Core\Helpers\RateLimit::hit('form_submit', MINUTE_IN_SECONDS);
274 +
203 275 $submission = [];
204 276 $submission_meta_data = [];
205 277
206 278 /**
@@ -238,23 +310,26 @@
238 310 ]);
239 311 wp_die();
240 312 }
241 313
242 - // Track conversion after storing the submission
243 - if ($box_log_id)
314 + /**
315 + * Track conversion after storing the submission.
316 + *
317 + * box_log_id arrives from the client, so it is only trusted once we have
318 + * confirmed it refers to a real impression of this campaign. Otherwise any
319 + * submitter could attribute their conversion to any campaign.
320 + */
321 + if ($box_log_id && firebox()->tables->boxlog->belongsToCampaign($box_log_id, $box_id))
244 322 {
245 - /**
246 - * Track conversion
247 - */
248 323 $factory = new \FPFramework\Base\Factory();
249 324 $data = [
250 - 'log_id' => $box_log_id,
325 + 'log_id' => (int) $box_log_id,
251 326 'event' => 'conversion',
252 327 'event_source' => 'form',
253 - 'event_label' => 'FireBox #' . $box_id . ' Form',
328 + 'event_label' => 'FireBox #' . (int) $box_id . ' Form',
254 329 'date' => $factory->getDate()->format('Y-m-d H:i:s')
255 330 ];
256 -
331 +
257 332 firebox()->tables->boxlogdetails->insert($data);
258 333 }
259 334
260 335 // Replace Smart Tags in form attributes
@@ -272,9 +347,9 @@
272 347 if (!$actions->run())
273 348 {
274 349 echo wp_json_encode([
275 350 'error' => true,
276 - 'message' => $actions->getErrorMessage()
351 + 'message' => wp_kses_post($actions->getErrorMessage())
277 352 ]);
278 353 wp_die();
279 354 }
280 355 }