PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 All 138 releases
bit-form / includes / Frontend / Ajax / FrontendAjax.php

FrontendAjax.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 3.3.1, at includes/Frontend/Ajax/FrontendAjax.php

269 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace BitCode\BitForm\Frontend\Ajax;
4
5 if (!defined('ABSPATH')) {
6 exit;
7 }
8
9 use BitCode\BitForm\Admin\Form\Helpers;
10 use BitCode\BitForm\Core\Database\FormEntryLogModel;
11 use BitCode\BitForm\Core\Util\FrontendHelpers;
12 use BitCode\BitForm\Core\Util\Log;
13 use BitCode\BitForm\Core\Util\Utilities;
14 use BitCode\BitForm\Core\WorkFlow\WorkflowExecutor;
15 use BitCode\BitForm\Frontend\Form\FrontendFormManager;
16
17 final class FrontendAjax
18 {
19 public function register()
20 {
21 add_action('wp_ajax_nopriv_bitforms_submit_form', [$this, 'submit_form']);
22 add_action('wp_ajax_bitforms_submit_form', [$this, 'submit_form']);
23 add_action('wp_ajax_bitforms_entry_update', [$this, 'update_entry']);
24 add_action('wp_ajax_nopriv_bitforms_entry_update', [$this, 'update_entry']);
25 add_action('wp_ajax_bitforms_update_form_entry', [$this, 'update_entry']);
26 add_action('wp_ajax_nopriv_bitforms_update_form_entry', [$this, 'update_entry']);
27 add_action('wp_ajax_bitforms_before_submit_validate', [$this, 'beforeSubmittedValidate']);
28 add_action('wp_ajax_nopriv_bitforms_before_submit_validate', [$this, 'beforeSubmittedValidate']);
29 add_action('wp_ajax_nopriv_bitforms_trigger_workflow', [$this, 'triggerWorkFlow']);
30 add_action('wp_ajax_bitforms_trigger_workflow', [$this, 'triggerWorkFlow']);
31 add_action('wp_ajax_bitforms_onload_added_field_and_property', [$this, 'addHiddenFieldAndProperty']);
32 add_action('wp_ajax_nopriv_bitforms_onload_added_field_and_property', [$this, 'addHiddenFieldAndProperty']);
33 }
34
35 public function beforeSubmittedValidate()
36 {
37 // CSRF verified inside FrontendFormManager::handleSubmission() via verifySubmissionNonce() using HMAC-SHA256 token (Helpers::csrfDecrypted).
38 $form_id = isset($_POST['bitforms_id']) ? str_replace('bitforms_', '', sanitize_text_field(wp_unslash($_POST['bitforms_id']))) : '';
39 if (empty($form_id)) {
40 wp_send_json_error(__('Form ID not found', 'bit-form'), 400);
41 }
42 $FrontendFormManager = FrontendFormManager::getInstance($form_id);
43 if (!$FrontendFormManager->checkStatus()) {
44 wp_send_json_error(__('Form is not active', 'bit-form'), 403);
45 }
46 $FrontendFormManager->fieldNameReplaceOfPost();
47 $validateStatus = $FrontendFormManager->beforeSubmittedValidate(false);
48 if (is_wp_error($validateStatus)) {
49 wp_send_json_error($validateStatus->get_error_message(), 400);
50 } else {
51 wp_send_json_success($validateStatus);
52 }
53 }
54
55 public function submit_form()
56 {
57 Utilities::ignoreUserAbort();
58 // CSRF verified inside FrontendFormManager::handleSubmission() via verifySubmissionNonce() using HMAC-SHA256 token (Helpers::csrfDecrypted).
59 $form_id = isset($_POST['bitforms_id']) ? str_replace('bitforms_', '', sanitize_text_field(wp_unslash($_POST['bitforms_id']))) : '';
60 $FrontendFormManager = FrontendFormManager::getInstance($form_id);
61 if (!$FrontendFormManager->checkStatus()) {
62 wp_send_json_error(__('Form is not active', 'bit-form'), 403);
63 }
64 $submitSatus = $FrontendFormManager->handleSubmission();
65 if (is_wp_error($submitSatus)) {
66 do_action('bitform_submit_error', $form_id, $submitSatus);
67 wp_send_json_error($submitSatus->get_error_message(), 400);
68 } elseif (true !== $submitSatus && !\is_array($submitSatus)) {
69 // Validation failed (e.g. OTP) — $submitSatus is the error message string
70 do_action('bitform_submit_error', $form_id, $submitSatus);
71 wp_send_json_error(\is_string($submitSatus) ? $submitSatus : __('Validation failed.', 'bit-form'), 400);
72 } else {
73 wp_send_json_success($submitSatus);
74 }
75 }
76
77 public function update_entry()
78 {
79 Utilities::ignoreUserAbort();
80 // Entry token validated via Helpers::validateEntryTokenAndUser() or capability check; CSRF covered by HMAC-SHA256 token (Helpers::csrfDecrypted).
81 $form_id = isset($_POST['bitforms_id']) ? str_replace('bitforms_', '', sanitize_text_field(wp_unslash($_POST['bitforms_id']))) : '';
82 if (empty($form_id)) {
83 wp_send_json_error(__('Form ID not found', 'bit-form'), 400);
84 }
85 $entryId = isset($_REQUEST['entryID']) ? sanitize_text_field(wp_unslash($_REQUEST['entryID'])) : '';
86 $entryToken = isset($_REQUEST['entryToken']) ? sanitize_text_field(wp_unslash($_REQUEST['entryToken'])) : '';
87 $GLOBALS['bitform_entry_id'] = $entryId;
88 if (Helpers::validateEntryTokenAndUser($entryToken, $entryId) || FrontendHelpers::is_current_user_can_access($form_id, 'entryEditAccess')) {
89 $FrontendFormManager = FrontendFormManager::getInstance($form_id);
90 if (!$FrontendFormManager->checkStatus()) {
91 wp_send_json_error(__('Form is not active', 'bit-form'), 403);
92 }
93 $updateStatus = $FrontendFormManager->handleUpdateEntry();
94 if (is_wp_error($updateStatus)) {
95 do_action('bitform_update_error', $form_id, $updateStatus);
96 wp_send_json_error($updateStatus->get_error_message(), 400);
97 } elseif (true !== $updateStatus && !\is_array($updateStatus)) {
98 // Validation failed (e.g. OTP) — $updateStatus is the error message string
99 do_action('bitform_update_error', $form_id, $updateStatus);
100 wp_send_json_error(\is_string($updateStatus) ? $updateStatus : __('Validation failed.', 'bit-form'), 400);
101 } else {
102 wp_send_json_success($updateStatus);
103 }
104 } else {
105 wp_send_json_error('Entry Token or User is not Authorized', 401);
106 }
107 }
108
109 public function hiddenFields($formId)
110 {
111 $tokens = Helpers::csrfEecrypted();
112 $fields = [
113 [
114 'name' => 'csrf',
115 'value' => $tokens['csrf'],
116 ],
117 [
118 'name' => 't_identity',
119 'value' => $tokens['t_identity'],
120 ]
121 ];
122 $frontendFormManger = FrontendFormManager::getInstance($formId);
123 if ($frontendFormManger->isHoneypotActive()) {
124 $time = time();
125 $honeypodFldName = Helpers::honeypotEncryptedToken("_bitforms_{$formId}_{$time}_");
126 $fields[] = [
127 'name' => 'b_h_t',
128 'value' => $honeypodFldName,
129 ];
130 }
131 return $fields;
132 }
133
134 public function hiddenPropeties($formId)
135 {
136 $properties = [];
137 $properties[] = [
138 'name' => 'nonce',
139 'value' => wp_create_nonce('bitforms_' . $formId),
140 ];
141 return $properties;
142 }
143
144 public function addHiddenFieldAndProperty()
145 {
146 Utilities::ignoreUserAbort();
147 $rawInput = file_get_contents('php://input');
148 if ($rawInput) {
149 $request = is_string($rawInput) ? sanitize_text_field($rawInput) : $rawInput;
150 $data = is_string($request) ? \json_decode($request) : $request;
151 if (!isset($data->formId)) {
152 wp_send_json_error('Form Id not found', 400);
153 } else {
154 $formId = absint($data->formId);
155 $frontendFormManager = FrontendFormManager::getInstance($formId);
156 if (!$frontendFormManager->isExist() || !$frontendFormManager->checkStatus()) {
157 wp_send_json_error(__('Form is not active', 'bit-form'), 403);
158 }
159 $fields = $this->hiddenFields($formId);
160 $properties = $this->hiddenPropeties($formId);
161 wp_send_json_success(['hidden_fields'=>$fields, 'hidden_properties'=>$properties]);
162 }
163 }
164 }
165
166 public function triggerWorkFlow()
167 {
168 Utilities::ignoreUserAbort();
169
170 $rawInput = file_get_contents('php://input');
171
172 if (!$rawInput) {
173 Log::debug_log('No Input data found');
174 wp_send_json_error('Invalid Request', 400);
175 }
176 // The raw body is JSON — sanitize_text_field on the whole string can mangle
177 // the payload. Individual values are validated/absint-ed below instead.
178 $request = Utilities::jsonObj($rawInput);
179 if (!isset($request->id, $request->cronNotOk)) {
180 Log::debug_log('Cron Not Ok data not found');
181 wp_send_json_error('Cron Not Ok data found', 400);
182 }
183 $formID = absint(str_replace('bitforms_', '', sanitize_text_field($request->id)));
184 $frontendFormManager = FrontendFormManager::getInstance($formID);
185 if (!$frontendFormManager->isExist() || !$frontendFormManager->checkStatus()) {
186 Log::debug_log('Inactive or non-existent form for workflow trigger. FormID=' . $formID);
187 wp_send_json_error(['message' => 'Form is not active'], 403);
188 }
189 $cronNotOk = $request->cronNotOk;
190
191 // Validate and sanitize entry ID and log ID
192 if (!isset($cronNotOk[0]) || !is_numeric($cronNotOk[0]) || !isset($cronNotOk[1]) || !is_numeric($cronNotOk[1])) {
193 Log::debug_log('Invalid cronNotOk data for formID=' . $formID);
194 wp_send_json_error(['message' => 'Invalid request data'], 400);
195 }
196
197 $entryID = absint($cronNotOk[0]);
198 $logID = absint($cronNotOk[1]);
199 $queueLogId = isset($cronNotOk[2]) && is_numeric($cronNotOk[2]) ? absint($cronNotOk[2]) : 0;
200 $GLOBALS['bitform_entry_id'] = $entryID;
201
202 // Quick admin check to allow retry of workflows
203 $isAdmin = false;
204 if (is_user_logged_in()) {
205 $user = wp_get_current_user();
206 $isAdmin = in_array('administrator', $user->roles) || current_user_can('manage_bitform');
207 }
208
209 // Check if already picked up (skip for administrators to allow retries)
210 if (!$isAdmin) {
211 if ($queueLogId) {
212 $entryLog = new FormEntryLogModel();
213 $queueudEntry = $entryLog->get('response_obj', ['id' => $queueLogId]);
214 if (!is_wp_error($queueudEntry) && !empty($queueudEntry)) {
215 // status lives in the row JSON; a plain substring check would false-match
216 // user field values stored alongside it in the durable trigger copy
217 $rowObj = json_decode(isset($queueudEntry[0]->response_obj) ? $queueudEntry[0]->response_obj : '', true);
218 $rowStatus = isset($rowObj['status']) ? $rowObj['status'] : '';
219 if (in_array($rowStatus, ['processed', 'processing', 'failed'], true)) {
220 Log::debug_log('Cron Not Ok[2] Already Processed');
221 wp_send_json_error();
222 }
223 } else {
224 Log::debug_log('Cron Not Ok[2] Query Entry data not found');
225 wp_send_json_error();
226 }
227 } else {
228 Log::debug_log('Cron Not Ok[2](Log Id) data not found');
229 wp_send_json_error();
230 }
231 } else {
232 Log::debug_log('Admin bypass: Skipping "already processed" check for workflow retry');
233 }
234
235 // SECURITY CHECK: Validate trigger token using helper function
236 $validation = Helpers::validateWorkflowTriggerToken($request, $formID);
237
238 if (!$validation['valid']) {
239 wp_send_json_error(['message' => $validation['error']], 403);
240 }
241
242 // Use validated trigger data if available (prevents transient overwrite bug);
243 // otherwise fall back to transient -> durable log-row copy -> rebuild
244 $triggerData = !empty($validation['triggerData'])
245 ? (array) $validation['triggerData']
246 : WorkflowExecutor::loadTriggerData($entryID, $formID, $logID, $queueLogId);
247
248 if (empty($triggerData)) {
249 Log::debug_log('No Trigger Data Found');
250 wp_send_json_success();
251 }
252
253 // Atomic claim: if the reclaim cron (or a duplicate request) already picked
254 // this run up, do not execute it a second time. Admin retries bypass.
255 $claimed = WorkflowExecutor::claimQueuedLog($queueLogId, 'browser', $triggerData);
256 if (!$claimed && !$isAdmin) {
257 Log::debug_log('Workflow already claimed by another trigger');
258 wp_send_json_error();
259 }
260
261 $result = WorkflowExecutor::execute($triggerData, $formID, $entryID, $logID, $queueLogId, 'browser');
262 if (is_wp_error($result)) {
263 wp_send_json_error($result->get_error_message(), 411);
264 }
265
266 wp_send_json_success();
267 }
268 }
269