PluginProbe
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment / 3.1.13
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment v3.1.13
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 1.1.1 All 122 releases
firebox / Inc / Core / Form / Ajax.php

Ajax.php in FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment 3.1.13, at Inc/Core/Form/Ajax.php

380 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package FireBox
4 * @version 3.1.13 Free
5 *
6 * @author FirePlugins <info@fireplugins.com>
7 * @link https://www.fireplugins.com
8 * @copyright Copyright © 2026 FirePlugins All Rights Reserved
9 * @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
10 */
11
12 namespace FireBox\Core\Form;
13
14 if (!defined('ABSPATH'))
15 {
16 exit; // Exit if accessed directly.
17 }
18
19 use \FireBox\Core\Helpers\Form\Form;
20 use \FireBox\Core\Helpers\BoxHelper;
21
22 class Ajax
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
40 public function __construct()
41 {
42 $this->setupAjax();
43
44 new Actions\Ajax();
45 }
46
47 /**
48 * Setup ajax requests
49 *
50 * @return void
51 */
52 public function setupAjax()
53 {
54 add_action('wp_ajax_fb_form_submission_status_change', [$this, 'fb_form_submission_status_change']);
55
56 add_action('wp_ajax_fb_form_submit', [$this, 'fb_form_submit']);
57 add_action('wp_ajax_nopriv_fb_form_submit', [$this, 'fb_form_submit']);
58 }
59
60 /**
61 * Update submission status.
62 *
63 * @return void
64 */
65 public function fb_form_submission_status_change()
66 {
67 if (!current_user_can('edit_fireboxes'))
68 {
69 echo wp_json_encode([
70 'error' => true,
71 'message' => 'You are not allowed to do this.'
72 ]);
73 wp_die();
74 }
75
76 $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
77
78 // verify nonce
79 if (!$verify = wp_verify_nonce($nonce, 'fb_form_submission_action'))
80 {
81 echo wp_json_encode([
82 'error' => true,
83 'message' => 'Cannot verify request.'
84 ]);
85 wp_die();
86 }
87
88 $submission_id = isset($_POST['submission_id']) ? absint($_POST['submission_id']) : 0;
89 $new_state = isset($_POST['new_state']) ? sanitize_key(wp_unslash($_POST['new_state'])) : '';
90
91 $new_state = $new_state === 'publish' ? 1 : 0;
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
103 if (!\FireBox\Core\Helpers\Form\Submission::updateState($submission_id, $new_state))
104 {
105 echo wp_json_encode([
106 // This branch means the write failed, so report it as an error.
107 'error' => true,
108 'message' => 'Submission state couldn\'t be updated.'
109 ]);
110 wp_die();
111 }
112
113 echo wp_json_encode([
114 'error' => false,
115 'message' => 'Submission state updated successfully.'
116 ]);
117 wp_die();
118 }
119
120 /**
121 * Form submit.
122 *
123 * @return void
124 */
125 public function fb_form_submit()
126 {
127 $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
128
129 // verify nonce
130 if (!$verify = wp_verify_nonce($nonce, 'fbox_js_nonce'))
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 */
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([
161 'error' => true,
162 'message' => firebox()->_('FB_FORM_TOO_MANY_SUBMISSIONS')
163 ]);
164 wp_die();
165 }
166
167 $form_data = isset($_POST['form_data']) ? sanitize_text_field(wp_unslash($_POST['form_data'])) : '';
168 $form_data = $form_data ? json_decode(stripslashes($form_data), true) : '';
169
170 if (!$form_data)
171 {
172 echo wp_json_encode([
173 'error' => true,
174 'message' => 'Cannot submit form.'
175 ]);
176 wp_die();
177 }
178
179 $form_id = isset($form_data['form_id']) ? $form_data['form_id'] : false;
180 if (!$form_id)
181 {
182 echo wp_json_encode([
183 'error' => true,
184 'message' => 'Missing Form ID.'
185 ]);
186 wp_die();
187 }
188
189 $values = isset($form_data['fields']) ? $form_data['fields'] : false;
190 if (!$values || !is_array($values))
191 {
192 echo wp_json_encode([
193 'error' => true,
194 'message' => 'Missing submission data.'
195 ]);
196 wp_die();
197 }
198
199 $form_id = str_replace('form-', '', $form_id);
200 if (!$form = Form::getFormByID($form_id))
201 {
202 echo wp_json_encode([
203 'error' => true,
204 'message' => 'This form does not exist.'
205 ]);
206 wp_die();
207 }
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
219 $form_block = $form['block'];
220 $form_fields = $form['fields'];
221
222 // Get the Campaign ID
223 $box_id = isset($_POST['box_id']) ? sanitize_key(wp_unslash($_POST['box_id'])) : false;
224
225 // Get box
226 $box = firebox()->box->get($box_id);
227
228 // Allow to hook into the form submission process and validate the submission data
229 try {
230 $values = apply_filters('firebox/form/process', $values, $box, $form_id);
231 }
232 catch (\Exception $e)
233 {
234 echo wp_json_encode([
235 'error' => true,
236 'message' => wp_kses_post($e->getMessage())
237 ]);
238 wp_die();
239 }
240
241 try {
242 $validated_fields = Form::validate($form_fields, $values);
243 }
244 catch (\Exception $e)
245 {
246 echo wp_json_encode([
247 'error' => true,
248 'message' => wp_kses_post($e->getMessage())
249 ]);
250 wp_die();
251 }
252
253 if (isset($validated_fields['error']))
254 {
255 $payload = [
256 'error' => true,
257 'message' => isset($validated_fields['message']) ? $validated_fields['message'] : 'Form is invalid.'
258 ];
259
260 if (is_array($validated_fields['error']))
261 {
262 $payload['validation'] = $validated_fields['error'];
263 }
264 echo wp_json_encode($payload);
265 wp_die();
266 }
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
275 $submission = [];
276 $submission_meta_data = [];
277
278 /**
279 * Also set the popup log id in field values.
280 *
281 * This is useful for analytics purposes, i.e. to track form conversions.
282 */
283 $box_log_id = isset($_POST['box_log_id']) && !empty($_POST['box_log_id']) ? sanitize_key(wp_unslash($_POST['box_log_id'])) : false;
284 if ($box_log_id)
285 {
286 $submission_meta_data['box_log_id'] = $box_log_id;
287 }
288
289 // $submission_meta_data is the raw submitted data that are saved in the database
290 foreach ($validated_fields as $field)
291 {
292 $field_id = $field->getOptionValue('id');
293 $field_name = $field->getOptionValue('name');
294
295 if (!isset($values[$field_name]))
296 {
297 continue;
298 }
299
300 $submission_meta_data[$field_id] = $values[$field_name];
301 }
302
303 // Determine whether to store the submission and store it
304 $storeSubmissions = isset($form_block['attrs']['storeSubmissions']) ? $form_block['attrs']['storeSubmissions'] : true;
305 if (!$submission = Form::storeSubmission($form_id, $form_block, $validated_fields, $submission_meta_data, $storeSubmissions))
306 {
307 echo wp_json_encode([
308 'error' => true,
309 'message' => 'Could not save submission. Please try again.'
310 ]);
311 wp_die();
312 }
313
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))
322 {
323 $factory = new \FPFramework\Base\Factory();
324 $data = [
325 'log_id' => (int) $box_log_id,
326 'event' => 'conversion',
327 'event_source' => 'form',
328 'event_label' => 'FireBox #' . (int) $box_id . ' Form',
329 'date' => $factory->getDate()->format('Y-m-d H:i:s')
330 ];
331
332 firebox()->tables->boxlogdetails->insert($data);
333 }
334
335 // Replace Smart Tags in form attributes
336 Form::replaceSmartTags($form_block['attrs'], $values, $submission);
337
338 // Determine whether to run actions and run them
339 if (isset($form_block['attrs']['actions']) && is_array($form_block['attrs']['actions']) && count($form_block['attrs']['actions']))
340 {
341 if ($box_id)
342 {
343 $submission['box_id'] = (int) $box_id;
344 }
345
346 $actions = new \FireBox\Core\Form\Actions\Actions($form_block, $submission);
347 if (!$actions->run())
348 {
349 echo wp_json_encode([
350 'error' => true,
351 'message' => wp_kses_post($actions->getErrorMessage())
352 ]);
353 wp_die();
354 }
355 }
356
357 $action = Form::getSubmissionAction($form_block['attrs']);
358
359 /**
360 * Fires after a successful form submission.
361 *
362 * @param array $box The campaign settings
363 * @param array $values The form values
364 * @param array $submission The submission
365 */
366 do_action('firebox/form/success', $box, $values, $submission);
367
368 /**
369 * Allow to hook into the success action and customize it.
370 *
371 * @param array $action
372 */
373 $action = apply_filters('firebox/form/submit_action', $action);
374
375 echo wp_json_encode(array_merge([
376 'error' => false
377 ], $action));
378 wp_die();
379 }
380 }