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
← All changes | includes/Admin/Form/Helpers.php +665 -76 2.03.3.1 View file →
@@ -1,57 +1,178 @@
1 1 <?php
2 2
3 3 namespace BitCode\BitForm\Admin\Form;
4 4
5 -class Helpers {
6 - public static function scriptLoader($src, $id, $instanceObj = null, $selector = '', $attrs = [], $integrity = null, $contentId = '') {
7 - $attributes = json_encode($attrs);
5 +use BitCode\BitForm\Core\Cryptography\Cryptography;
6 +use BitCode\BitForm\Core\Database\FormEntryModel;
7 +use BitCode\BitForm\Core\Util\FileHandler;
8 +use BitCode\BitForm\Core\Util\Log;
9 +use BitCode\BitForm\Core\WorkFlow\WorkflowExecutor;
10 +use Exception;
11 +use WP_Error;
12 +
13 +class Helpers
14 +{
15 + private static $encryptEntryIds = [];
16 +
17 + public static $file_upload_types = ['file-up', 'advanced-file-up'];
18 +
19 + public static $repeated_array_type_data_fields = ['check', 'image-select'];
20 +
21 + public static function filterNullEntries($entries)
22 + {
23 + $filteredEntries = [];
24 + foreach ($entries as $entry) {
25 + foreach ($entry as $key => $value) {
26 + if (is_null($value)) {
27 + unset($entry->$key);
28 + }
29 + }
30 + if (count((array) $entry)) {
31 + $filteredEntries[] = $entry;
32 + }
33 + }
34 + return $filteredEntries;
35 + }
36 +
37 + public static function scriptLoader($src, $id, $instanceObj = null, $selector = '', $attrs = [], $integrity = null, $contentId = '')
38 + {
39 + $attributes = wp_json_encode($attrs);
8 40 $instObj = '';
9 41 if ($instanceObj) {
10 - $instObj .= <<<INST
11 -script.onload = function () {
12 - bfSelect('#{$contentId}').querySelectorAll('{$selector}').forEach(function(fld){
13 - $instanceObj;
14 - });
15 -}
16 -INST;
42 + $instObj .= sprintf(
43 + '
44 + script.onload = function () {
45 + bfSelect("#{%1$s}").querySelectorAll("{%2$s}").forEach(function(fld){
46 + %3$s;
47 + });
48 + }
49 + ',
50 + $contentId,
51 + $selector,
52 + $instanceObj
53 + );
17 54 }
18 - return <<<LOAD_SECRIPT
19 -var script = document.createElement('script'), integrity = '$integrity', attrs = $attributes;
20 -script.src = '$src';
21 -script.id = '$id';
22 -if(integrity){
23 - script.integrity = integrity;
24 - script.crossOrigin = 'anonymous';
25 -}
26 -if(attrs){
27 - Object.entries(attrs).forEach(function([key, val]){
28 - script.setAttribute(key,val);
29 - })
30 -}
31 -$instObj;
32 -var bodyElm = document.body;
33 -var alreadyExistScriptElm = bodyElm ? bodyElm.querySelector('script#$id'):null;
34 -if(alreadyExistScriptElm){
35 - bodyElm.removeChild(alreadyExistScriptElm)
36 -}
37 -bodyElm.appendChild(script);
38 -LOAD_SECRIPT;
55 + return sprintf(
56 + '
57 + var script = document.createElement("script"), integrity = "%1$s", attrs = %2$s, id = "%3$s";
58 + script.src = "%4$s";
59 + script.id = id;
60 + if(integrity){
61 + script.integrity = integrity;
62 + script.crossOrigin = "anonymous";
63 + }
64 + if(attrs){
65 + Object.entries(attrs).forEach(function([key, val]){
66 + script.setAttribute(key,val);
67 + })
68 + }
69 + $instObj;
70 + var bodyElm = document.body;
71 + var alreadyExistScriptElm = bodyElm ? bodyElm.querySelector("script#$id"):null;
72 + if(alreadyExistScriptElm){
73 + bodyElm.removeChild(alreadyExistScriptElm)
74 + }
75 + if(!(window.recaptcha && id === "g-recaptcha-script")){
76 + bodyElm.appendChild(script);
77 + }
78 + ',
79 + $integrity,
80 + $attributes,
81 + $id,
82 + $src,
83 + );
39 84 }
40 85
41 - public static function minifyJs($content) {
42 - $output = preg_replace(
86 + public static function minifyJs($input)
87 + {
88 + if ('' === trim($input)) {
89 + return $input;
90 + }
91 + // Line-boundary whitespace only. The previous regexes also rewrote the
92 + // inside of string and template literals, corrupting generated config and
93 + // custom JS. Inputs are already terser-minified, so the aggressive pass
94 + // saved ~0.1% anyway.
95 + return preg_replace(
43 96 [
44 - '/ {2,}/',
45 - '/\s*=\s*/',
46 - '/\s*,\s*/',
47 - '/\s+(?=\(|\{|\:|\?)|\t|(?:\r?\n[ \t]*)+/s'
97 + '/^[ \t]+/m', // leading indentation
98 + '/[ \t]+$/m', // trailing whitespace
99 + '/(?:\r?\n){3,}/', // 3+ consecutive newlines -> 2
48 100 ],
49 - [' ', '=', ',', ''],
50 - $content
101 + ['', '', "\n\n"],
102 + $input
51 103 );
104 + }
52 105
53 - return $output;
106 + public static function removeJsSingleLineComments($code)
107 + {
108 + $length = strlen($code);
109 + $result = '';
110 + $inString = false;
111 + $inTemplate = false;
112 + $inRegex = false;
113 + $escapeNext = false;
114 + $stringDelimiter = '';
115 + $i = 0;
116 +
117 + while ($i < $length) {
118 + $char = $code[$i];
119 + $nextChar = $i + 1 < $length ? $code[$i + 1] : '';
120 +
121 + if ($escapeNext) {
122 + $result .= $char;
123 + $escapeNext = false;
124 + } elseif ($inString) {
125 + $result .= $char;
126 + if ('\\' === $char) {
127 + $escapeNext = true;
128 + } elseif ($char === $stringDelimiter) {
129 + $inString = false;
130 + }
131 + } elseif ($inTemplate) {
132 + $result .= $char;
133 + if ('\\' === $char) {
134 + $escapeNext = true;
135 + } elseif ('`' === $char) {
136 + $inTemplate = false;
137 + }
138 + } elseif ($inRegex) {
139 + $result .= $char;
140 + if ('\\' === $char) {
141 + $escapeNext = true;
142 + } elseif ('/' === $char) {
143 + $inRegex = false;
144 + }
145 + } else {
146 + if ('"' === $char || "'" === $char) {
147 + $inString = true;
148 + $stringDelimiter = $char;
149 + $result .= $char;
150 + } elseif ('`' === $char) {
151 + $inTemplate = true;
152 + $result .= $char;
153 + } elseif ('/' === $char) {
154 + if ('/' === $nextChar) {
155 + // Single-line comment found
156 + while ($i < $length && "\n" !== $code[$i]) {
157 + $i++;
158 + }
159 + continue; // skip until newline
160 + } elseif ('*' === $nextChar) {
161 + // Block comment start, just copy it (optional, depending on need)
162 + $result .= $char;
163 + } else {
164 + // Assume division or regex
165 + $result .= $char;
166 + }
167 + } else {
168 + $result .= $char;
169 + }
170 + }
171 + $i++;
172 + }
173 +
174 + return $result;
54 175 }
55 176
56 177 /**
57 178 * @method name : saveFile
@@ -58,23 +179,34 @@
58 179 * @description : save js/css field to disk
59 180 * @param : $path => like(dirName/css), $fileName => main.css, $script
60 181 * @return : boolean
61 182 */
62 - public static function saveFile($path, $fileName, $script, $fileOpenMode = 'a') {
63 - $rootDir = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR;
64 - $path = trim($path, '/');
65 - $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user]
66 - foreach ($pathArr as $d) {
67 - $rootDir .= $d . DIRECTORY_SEPARATOR;
68 - if (!realpath($rootDir)) {
69 - mkdir($rootDir);
183 + public static function saveFile($path, $fileName, $script, $fileOpenMode = 'a')
184 + {
185 + try {
186 + $rootDir = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR;
187 + $path = trim($path, '/');
188 + $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user]
189 + foreach ($pathArr as $d) {
190 + $rootDir .= $d . DIRECTORY_SEPARATOR;
191 + if (!realpath($rootDir)) {
192 + wp_mkdir_p($rootDir);
193 + }
70 194 }
195 + $fullPath = $rootDir . $fileName;
196 + if ('a' === $fileOpenMode) {
197 + $result = FileHandler::appendFile($fullPath, $script);
198 + } else {
199 + $result = FileHandler::writeFile($fullPath, $script);
200 + }
201 + if (false === $result) {
202 + throw new Exception("Failed to write to file: $fullPath");
203 + }
204 + return true;
205 + } catch (\Exception $e) {
206 + Log::debug_log($e->getMessage());
207 + return false;
71 208 }
72 - $fullPath = $rootDir . $fileName;
73 - $file = fopen($fullPath, $fileOpenMode);
74 - fwrite($file, $script);
75 - fclose($file);
76 - return true;
77 209 }
78 210
79 211 /**
80 212 * @method name : generatePathDirOrFile
@@ -81,9 +213,10 @@
81 213 * @dscription : generate path for js/css file
82 214 * @params : $path => like(dirName/css)
83 215 * @return : a string of full path
84 216 */
85 - public static function generatePathDirOrFile($path) {
217 + public static function generatePathDirOrFile($path)
218 + {
86 219 $rootDir = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR;
87 220 $path = trim($path, '/');
88 221 $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user]
89 222 foreach ($pathArr as $d) {
@@ -91,19 +224,15 @@
91 224 }
92 225 return rtrim($rootDir, DIRECTORY_SEPARATOR);
93 226 }
94 227
95 - public static function fileRead($filePath) {
96 - $fileContent = '';
97 - if (file_exists($filePath)) {
98 - $file = fopen($filePath, 'r');
99 - $fileContent .= fread($file, filesize($filePath));
100 - fclose($file);
101 - }
102 - return $fileContent;
228 + public static function fileRead($filePath)
229 + {
230 + return FileHandler::readFile($filePath);
103 231 }
104 232
105 - public static function getDataFromNestedPath($data, $key) {
233 + public static function getDataFromNestedPath($data, $key)
234 + {
106 235 $keys = explode('->', $key);
107 236 $lastKey = array_pop($keys);
108 237 $dataType = is_array($data) ? 'array' : (is_object($data) ? 'object' : '');
109 238 if ('array' === $dataType) {
@@ -113,9 +242,10 @@
113 242 return self::accessFromObject($data, $keys, $lastKey);
114 243 }
115 244 }
116 245
117 - private static function accessFromObject($data, $keys, $lastKey) {
246 + private static function accessFromObject($data, $keys, $lastKey)
247 + {
118 248 foreach ($keys as $k) {
119 249 if (!property_exists($data, $k)) {
120 250 return null;
121 251 }
@@ -123,9 +253,10 @@
123 253 }
124 254 return isset($data->$lastKey) ? $data->$lastKey : null;
125 255 }
126 256
127 - private static function accessFromArray($data, $keys, $lastKey) {
257 + private static function accessFromArray($data, $keys, $lastKey)
258 + {
128 259 foreach ($keys as $k) {
129 260 if (!array_key_exists($k, $data)) {
130 261 return null;
131 262 }
@@ -133,9 +264,10 @@
133 264 }
134 265 return isset($data[$lastKey]) ? $data[$lastKey] : null;
135 266 }
136 267
137 - public static function setDataToNestedPath($data, $key, $value) {
268 + public static function setDataToNestedPath($data, $key, $value)
269 + {
138 270 $keys = explode('->', $key);
139 271 $lastKey = array_pop($keys);
140 272 foreach ($keys as $k) {
141 273 if (!array_key_exists($k, $data)) {
@@ -142,13 +274,14 @@
142 274 $data->$k = (object) [];
143 275 }
144 276 $data = $data->$k;
145 277 }
146 - $data->$lastKey = json_decode(json_encode($value)); ;
278 + $data->$lastKey = json_decode(wp_json_encode($value));
147 279 return $data;
148 280 }
149 281
150 - public static function property_exists_nested($obj, $path = '', $valToCheck = null, $checkNegativeVal = 0) {
282 + public static function property_exists_nested($obj, $path = '', $valToCheck = null, $checkNegativeVal = 0)
283 + {
151 284 $path = explode('->', $path);
152 285 $current = $obj;
153 286 foreach ($path as $key) {
154 287 if (is_object($current)) {
@@ -169,28 +302,484 @@
169 302 }
170 303 return true;
171 304 }
172 305
173 - public static function honeypotEncryptedToken($str) {
306 + public static function validateEntryTokenAndUser($entryToken, $entryId)
307 + {
308 + // check if the user is logged in
309 + if (is_user_logged_in()) {
310 + $user = wp_get_current_user();
311 + if (in_array('administrator', $user->roles) || current_user_can('manage_bitform')) {
312 + return true;
313 + }
314 + $entryModel = new FormEntryModel();
315 + $entry = $entryModel->get(
316 + 'id, user_id, form_id',
317 + [
318 + 'id' => $entryId,
319 + 'user_id' => $user->ID
320 + ]
321 + );
322 + if (!is_wp_error($entry) && !empty($entry)) {
323 + return true;
324 + }
325 + }
326 + // check if the entry token is valid
327 + if (isset($entryToken) && $entryToken) {
328 + $decryptEntryId = Cryptography::decrypt($entryToken, self::getBitformSalt());
329 + if ($decryptEntryId === $entryId) {
330 + return true;
331 + }
332 + }
333 +
334 + return false;
335 + }
336 +
337 + /**
338 + * Validate workflow trigger token with proper input sanitization
339 + *
340 + * @param object $request The AJAX request object
341 + * @param string $formID The form ID (already sanitized)
342 + * @return array ['valid' => bool, 'error' => string, 'triggerData' => object|null, 'isAdminBypass' => bool]
343 + */
344 + public static function validateWorkflowTriggerToken($request, $formID)
345 + {
346 + // Sanitize and validate cronNotOk array
347 + if (!isset($request->cronNotOk) || !is_array($request->cronNotOk)) {
348 + return [
349 + 'valid' => false,
350 + 'error' => 'Missing or invalid cronNotOk data',
351 + 'triggerData' => null,
352 + 'isAdminBypass' => false
353 + ];
354 + }
355 +
356 + // Validate and sanitize entry ID and log ID (must be integers)
357 + if (!isset($request->cronNotOk[0]) || !is_numeric($request->cronNotOk[0])) {
358 + Log::debug_log('Invalid entry ID in cronNotOk[0]');
359 + return ['valid' => false, 'error' => 'Invalid entry ID', 'triggerData' => null, 'isAdminBypass' => false];
360 + }
361 +
362 + if (!isset($request->cronNotOk[1]) || !is_numeric($request->cronNotOk[1])) {
363 + Log::debug_log('Invalid log ID in cronNotOk[1]');
364 + return ['valid' => false, 'error' => 'Invalid log ID', 'triggerData' => null, 'isAdminBypass' => false];
365 + }
366 +
367 + $entryID = absint($request->cronNotOk[0]);
368 + $logID = absint($request->cronNotOk[1]);
369 +
370 + // Check for administrator bypass
371 + $isAdminBypass = false;
372 + if (is_user_logged_in()) {
373 + $user = wp_get_current_user();
374 + if (in_array('administrator', $user->roles) || current_user_can('manage_bitform')) {
375 + Log::debug_log('Admin bypass: Workflow triggered by ' . $user->user_login . ' for entryID=' . $entryID);
376 + return [
377 + 'valid' => true,
378 + 'error' => '',
379 + 'triggerData' => null,
380 + 'isAdminBypass' => true
381 + ];
382 + }
383 +
384 + // For logged-in non-admin users: verify nonce
385 + if (isset($request->token, $request->id)) {
386 + if (!wp_verify_nonce($request->token, $request->id)) {
387 + Log::debug_log('Nonce verification failed for logged-in user. FormID=' . $formID);
388 + return [
389 + 'valid' => false,
390 + 'error' => 'Invalid nonce for logged-in user',
391 + 'triggerData' => null,
392 + 'isAdminBypass' => false
393 + ];
394 + }
395 + } else {
396 + Log::debug_log('Missing nonce for logged-in user. FormID=' . $formID);
397 + return [
398 + 'valid' => false,
399 + 'error' => 'Missing nonce',
400 + 'triggerData' => null,
401 + 'isAdminBypass' => false
402 + ];
403 + }
404 + }
405 +
406 + // For non-admin users (both logged-in and anonymous): validate one-time trigger token
407 + if (!isset($request->cronNotOk[3]) || empty($request->cronNotOk[3])) {
408 + Log::debug_log('Missing trigger token for formID=' . $formID . ', entryID=' . $entryID);
409 + return [
410 + 'valid' => false,
411 + 'error' => 'Missing trigger token',
412 + 'triggerData' => null,
413 + 'isAdminBypass' => false
414 + ];
415 + }
416 +
417 + $submittedToken = sanitize_text_field($request->cronNotOk[3]);
418 +
419 + // Validate trigger token from transient
420 + $transientData = get_transient("bitform_trigger_transient_{$entryID}");
421 +
422 + // Object caches (W3TC, SG Optimizer, Redis) can drop the transient before the
423 + // browser trigger arrives; the queue log row holds the same payload durably.
424 + if (empty($transientData)) {
425 + $queueLogId = isset($request->cronNotOk[2]) && is_numeric($request->cronNotOk[2]) ? absint($request->cronNotOk[2]) : 0;
426 + $transientData = WorkflowExecutor::loadTriggerDataFromLog($queueLogId);
427 + Log::debug_log('Trigger token transient missing for entryID=' . $entryID . ', logID=' . $logID . '; durable copy ' . (empty($transientData) ? 'not found' : 'used'));
428 + }
429 +
430 + if (empty($transientData)) {
431 + return [
432 + 'valid' => false,
433 + 'error' => 'Trigger token expired or missing',
434 + 'triggerData' => null,
435 + 'isAdminBypass' => false
436 + ];
437 + }
438 +
439 + $triggerData = is_string($transientData) ? json_decode($transientData, true) : (array) $transientData;
440 + // Verify token matches and belongs to this entry/log
441 + if (
442 + !isset($triggerData['trigger_token'], $triggerData['entryID'], $triggerData['logID'])
443 + || !hash_equals($triggerData['trigger_token'], $submittedToken)
444 + || (int)$triggerData['entryID'] !== $entryID
445 + || (int)$triggerData['logID'] !== $logID
446 + ) {
447 + Log::debug_log('Invalid trigger token for entryID=' . $entryID . ', logID=' . $logID);
448 + return [
449 + 'valid' => false,
450 + 'error' => 'Invalid trigger token',
451 + 'triggerData' => null,
452 + 'isAdminBypass' => false
453 + ];
454 + }
455 +
456 + // Token is valid - delete transient to prevent reuse (single-use token)
457 + delete_transient("bitform_trigger_transient_{$entryID}");
458 + Log::debug_log('Valid trigger token consumed for entryID=' . $entryID);
459 +
460 + return [
461 + 'valid' => true,
462 + 'error' => '',
463 + 'triggerData' => $triggerData,
464 + 'isAdminBypass' => false
465 + ];
466 + }
467 +
468 + public static function validateFormEntryEditPermission($formId, $entryId)
469 + {
470 + if (is_user_logged_in()) {
471 + if (current_user_can('manage_bitform') || current_user_can('bitform_entry_edit') || current_user_can('edit_post')) {
472 + return true;
473 + }
474 + }
475 + return false;
476 + }
477 +
478 + public static function honeypotEncryptedToken($str)
479 + {
174 480 $token = base64_encode(base64_encode($str));
175 481 return $token;
176 482 }
177 483
178 - public static function csrfEecrypted() {
179 - $secretKey = get_option('bf_csrf_secret');
484 + public static function csrfEecrypted()
485 + {
486 + $secretKey = get_option('bitform_csrf_secret');
180 487 if (!$secretKey) {
181 488 $secretKey = 'bf-' . time();
182 - update_option('bf_csrf_secret', $secretKey);
489 + update_option('bitform_csrf_secret', $secretKey);
183 490 }
184 - $tIdenty = base64_encode(random_bytes(32));
491 + $tIdenty = base64_encode(\random_bytes(32));
185 492 $csrf = \base64_encode(\hash_hmac('sha256', $tIdenty, $secretKey, true));
186 - return ['csrf' => $csrf, 't_identy' => $tIdenty];
493 + return ['csrf' => $csrf, 't_identity' => $tIdenty];
187 494 }
188 495
189 - public static function csrfDecrypted($identy, $token) {
190 - $secretKey = get_option('bf_csrf_secret');
496 + public static function csrfDecrypted($identy, $token)
497 + {
498 + $secretKey = get_option('bitform_csrf_secret');
191 499 return \hash_equals(
192 500 \base64_encode(\hash_hmac('sha256', $identy, $secretKey, true)),
193 501 $token
194 502 );
503 + }
504 +
505 + public static function checkIsIntArr($arr)
506 + {
507 + $filteredArray = array_filter($arr, 'is_numeric');
508 + $intArray = array_map('intval', $filteredArray);
509 + $result = count($arr) === count($intArray);
510 +
511 + return $result;
512 + }
513 +
514 + public static function getTruncatedEncryptToken($str, $length = 20)
515 + {
516 + $token = hash_hmac('sha256', $str, self::getBitformSalt());
517 + return substr($token, 0, $length);
518 + }
519 +
520 + public static function getAuthSaltEncryptToken($str, $length = 20)
521 + {
522 + if (!$str) {
523 + return '';
524 + }
525 + if (!defined('AUTH_SALT')) {
526 + return '';
527 + }
528 +
529 + return substr(hash_hmac('sha256', $str, AUTH_SALT), 0, $length);
530 + }
531 +
532 + public static function getEncryptedEntryId($entryId)
533 + {
534 + if (!isset(self::$encryptEntryIds[$entryId])) {
535 + self::$encryptEntryIds[$entryId] = self::getTruncatedEncryptToken($entryId);
536 + }
537 + return self::$encryptEntryIds[$entryId];
538 + }
539 +
540 + public static function getFullPathWithEncryptedEntryId($formId, $entryId)
541 + {
542 + $uploadDir = rtrim(BITFORMS_UPLOAD_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $formId . DIRECTORY_SEPARATOR;
543 + $encryptDirectoryId = Helpers::getEncryptedEntryId($entryId);
544 + $encryptDirectory = $uploadDir . $encryptDirectoryId;
545 + if (is_dir($encryptDirectory)) {
546 + return $encryptDirectory;
547 + }
548 +
549 + $oldEntriesFileUploadDir = Helpers::getOldEntriesFileUploadDir($uploadDir, $entryId);
550 + if (!empty($oldEntriesFileUploadDir) && is_dir($oldEntriesFileUploadDir)) {
551 + return $oldEntriesFileUploadDir;
552 + }
553 +
554 + return $encryptDirectory;
555 + }
556 +
557 + public static function getWebPathWithEncryptedEntryId($formId, $entryId)
558 + {
559 + $serverFileUploadDir = rtrim(BITFORMS_UPLOAD_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $formId . DIRECTORY_SEPARATOR;
560 + $webFileDirectory = BITFORMS_UPLOAD_BASE_URL . '/' . 'uploads' . '/' . $formId . '/';
561 + $encryptDirectoryId = Helpers::getEncryptedEntryId($entryId);
562 + $encryptDirectory = $serverFileUploadDir . $encryptDirectoryId;
563 + if (is_dir($encryptDirectory)) {
564 + return $webFileDirectory . $encryptDirectoryId;
565 + }
566 +
567 + $authSaltEncryptedEntryId = Helpers::getAuthSaltEncryptToken($entryId);
568 + $authSaltEncryptedDirectory = $serverFileUploadDir . $authSaltEncryptedEntryId;
569 + if (!empty($authSaltEncryptedEntryId) && is_dir($authSaltEncryptedDirectory)) {
570 + return $webFileDirectory . $authSaltEncryptedEntryId;
571 + }
572 + $previousEntryDirectory = $serverFileUploadDir . $entryId;
573 + if (!empty($previousEntryDirectory) && is_dir($previousEntryDirectory)) {
574 + return $webFileDirectory . $entryId;
575 + }
576 +
577 + return $webFileDirectory . $encryptDirectoryId;
578 + }
579 +
580 + public static function getOldEntriesFileUploadDir($uploadDir, $entry_id)
581 + {
582 + $authSaltEncryptedEntryId = Helpers::getAuthSaltEncryptToken($entry_id);
583 + $authSaltEncryptedDirectory = $uploadDir . $authSaltEncryptedEntryId;
584 + if (!empty($authSaltEncryptedEntryId) && is_dir($authSaltEncryptedDirectory)) {
585 + return $authSaltEncryptedDirectory;
586 + }
587 + $previousEntryDirectory = $uploadDir . $entry_id;
588 + if (!empty($previousEntryDirectory) && is_dir($previousEntryDirectory)) {
589 + return $previousEntryDirectory;
590 + }
591 + return '';
592 + }
593 +
594 + public static function PDFPassHash($entryId)
595 + {
596 + return abs(crc32($entryId));
597 + }
598 +
599 + public static function encryptBinaryData($plaintext)
600 + {
601 + $iv = openssl_random_pseudo_bytes(16);
602 + $encrypted = openssl_encrypt($plaintext, 'AES-256-CBC', BITFORMS_SECRET_KEY, OPENSSL_RAW_DATA, $iv);
603 +
604 + return bin2hex($iv . $encrypted);
605 + }
606 +
607 + public static function decryptBinaryData($encryptedHex)
608 + {
609 + $decoded = hex2bin($encryptedHex);
610 + $iv = substr($decoded, 0, 16);
611 + $cipherText = substr($decoded, 16);
612 +
613 + return openssl_decrypt($cipherText, 'AES-256-CBC', BITFORMS_SECRET_KEY, OPENSSL_RAW_DATA, $iv);
614 + }
615 +
616 + /**
617 + * Sanitize user-provided HTML content by removing dangerous JS code
618 + * while allowing all valid HTML/CSS.
619 + *
620 + * @param string $html Raw HTML from user input
621 + * @return string Sanitized safe HTML
622 + */
623 + public static function sanitizeUserHTML(string $html): string
624 + {
625 + // Remove <script> tags entirely
626 + $html = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $html);
627 +
628 + // Remove event handler attributes (like onclick, onload, etc.)
629 + $html = preg_replace_callback('/<[^>]+>/i', function ($matches) {
630 + return preg_replace('/\s*on\w+\s*=\s*"[^"]*"/i', '', $matches[0]); // on*=""
631 + }, $html);
632 +
633 + $html = preg_replace_callback('/<[^>]+>/i', function ($matches) {
634 + return preg_replace("/\s*on\w+\s*=\s*'[^']*'/i", '', $matches[0]); // on*=''
635 + }, $html);
636 +
637 + // Remove javascript: from href or src
638 + $html = preg_replace('/(href|src)\s*=\s*([\'"])\s*javascript:[^\'"]*\2/i', '', $html);
639 +
640 + return $html;
641 + }
642 +
643 + public static function sanitizeUrlParam($param)
644 + {
645 + if (preg_match('/\.\.?\//', $param)) {
646 + return new WP_Error('parameter_error', 'Invalid URL parameter');
647 + }
648 +
649 + $param = htmlspecialchars(trim($param), ENT_QUOTES, 'UTF-8');
650 + return sanitize_text_field($param);
651 + }
652 +
653 + public static function replaceFieldsDefaultErrorMsg($fields)
654 + {
655 + try {
656 + $appSettings = get_option('bitform_app_settings', (object) []);
657 + if (!isset($appSettings->globalMessages) || !isset($appSettings->globalMessages->err)) {
658 + return $fields;
659 + }
660 +
661 + $globalErrMsg = $appSettings->globalMessages->err;
662 + $templateCache = []; // [type_errKey] => compiled template
663 +
664 + foreach ($fields as $fieldKey => $field) {
665 + if (!isset($field->err) || !is_object($field->err)) {
666 + continue;
667 + }
668 +
669 + foreach ($field->err as $errKey => $errObj) {
670 + if (!isset($errObj->dflt)) {
671 + continue;
672 + }
673 +
674 + $cacheKey = $field->typ . '_' . $errKey;
675 + $template = null;
676 +
677 + // 1. Check Cache First
678 + if (isset($templateCache[$cacheKey])) {
679 + $template = $templateCache[$cacheKey];
680 + } else {
681 + // 2. Lookup from globalErrMsg
682 + if (isset($globalErrMsg->{$field->typ}->{$errKey})) {
683 + $template = $globalErrMsg->{$field->typ}->{$errKey};
684 + } elseif (isset($globalErrMsg->{$errKey}) && !is_object($globalErrMsg->{$errKey})) {
685 + $template = $globalErrMsg->{$errKey};
686 + }
687 +
688 + // 3. Cache it
689 + if ($template) {
690 + $templateCache[$cacheKey] = $template;
691 + }
692 + }
693 +
694 + // 4. Apply Template if Found
695 + if ($template) {
696 + $finalMsg = self::replaceShortcodeInErrorMsg($template, $field);
697 + // 5. Sanitize final output
698 + $field->err->{$errKey}->dflt = wp_kses_post($finalMsg);
699 + }
700 + }
701 +
702 + $fields->{$fieldKey} = $field;
703 + }
704 + } catch (Exception $e) {
705 + Log::debug_log('Error In Replacing Fields Default Error messages: ' . $e->getMessage());
706 + }
707 + return $fields;
708 + }
709 +
710 + //replace shortcode in error message
711 + public static function replaceShortcodeInErrorMsg($msg, $field)
712 + {
713 + $shortcodes = [
714 + '${field.label}' => isset($field->lbl) ? $field->lbl : '',
715 + '${field.minimum}' => isset($field->mn) ? $field->mn : '',
716 + '${field.maximum}' => isset($field->mx) ? $field->mx : '',
717 + '${field.minimum_file}' => isset($field->config->minFile) ? $field->config->minFile : '',
718 + '${field.maximum_file}' => isset($field->config->maxFile) ? $field->config->maxFile : '',
719 + '${field.maximum_size}' => isset($field->config->maxSize) ? $field->config->maxSize : '',
720 + '${field.minimum_amount}' => isset($field->config->minValue) ? $field->config->minValue : '',
721 + '${field.maximum_amount}' => isset($field->config->maxValue) ? $field->config->maxValue : '',
722 + ];
723 + $msg = str_replace(array_keys($shortcodes), array_values($shortcodes), $msg);
724 + return $msg;
725 + }
726 +
727 + public static function getDefaultGlobalMessages()
728 + {
729 + $defaultGlobalMessages = [
730 + 'err' => [
731 + 'req' => '<p style="margin:0">' . __('This field is required', 'bit-form') . '</p>',
732 + 'email' => [
733 + 'invalid' => '<p style="margin:0">' . __('Please, enter a valid email address', 'bit-form') . '</p>',
734 + ],
735 + 'url' => [
736 + 'invalid' => '<p style="margin:0">' . __('Please, enter a valid URL', 'bit-form') . '</p>',
737 + ],
738 + 'mn' => '<p style="margin:0">' . __('Minimum ${field.minimum} is required', 'bit-form') . '</p>',
739 + 'mx' => '<p style="margin:0">' . __('Maximum ${field.maximum} is allowed', 'bit-form') . '</p>',
740 + 'number' => [
741 + 'invalid' => '<p style="margin:0">' . __('Please, enter only numbers', 'bit-form') . '</p>',
742 + ],
743 + 'phone-number' => [
744 + 'invalid' => '<p style="margin:0">' . __('Please, enter a valid phone number', 'bit-form') . '</p>',
745 + ],
746 + 'check' => [
747 + 'mn' => '<p style="margin:0">' . __('Select at least ${field.minimum} option(s)', 'bit-form') . '</p>',
748 + 'mx' => '<p style="margin:0">' . __('Please, select no more than ${field.maximum} option(s)', 'bit-form') . '</p>',
749 + ],
750 + 'select' => [
751 + 'mn' => '<p style="margin:0">' . __('Select at least ${field.minimum} option(s)', 'bit-form') . '</p>',
752 + 'mx' => '<p style="margin:0">' . __('Please, select no more than ${field.maximum} option(s)', 'bit-form') . '</p>',
753 + ],
754 + 'image-select' => [
755 + 'mn' => '<p style="margin:0">' . __('Select at least ${field.minimum} option(s)', 'bit-form') . '</p>',
756 + 'mx' => '<p style="margin:0">' . __('Please, select no more than ${field.maximum} option(s)', 'bit-form') . '</p>',
757 + ],
758 + 'inputMask' => '<p style="margin:0">' . __('Input does not match the required pattern', 'bit-form') . '</p>',
759 + 'regexr' => '<p style="margin:0">' . __('Input does not match the required pattern', 'bit-form') . '</p>',
760 + 'minFile' => '<p style="margin:0">' . __('Minimum ${field.minimum_file} file(s) required', 'bit-form') . '</p>',
761 + 'maxFile' => '<p style="margin:0">' . __('Maximum ${field.maximum_file} file(s) allowed', 'bit-form') . '</p>',
762 + 'maxSize' => '<p style="margin:0">' . __('Maximum file size exceeded. (Max: ${field.maximum_size}MB)', 'bit-form') . '</p>',
763 + 'fileType' => '<p style="margin:0">' . __('File type is not supported', 'bit-form') . '</p>',
764 + 'entryUnique' => '<p style="margin:0">' . __('This value is already taken. Please, choose a different one.', 'bit-form') . '</p>',
765 + 'userUnique' => '<p style="margin:0">' . __('This username or email is already registered. Please, use another.', 'bit-form') . '</p>',
766 + 'otherOptReq' => '<p style="margin:0">' . __('Custom Option Required', 'bit-form') . '</p>',
767 + 'minValue' => '<p style="margin:0">' . __('Minimum amount of ${field.minimum_amount} is required', 'bit-form') . '</p>',
768 + 'maxValue' => '<p style="margin:0">' . __('Maximum amount of ${field.maximum_amount} is allowed', 'bit-form') . '</p>',
769 + ],
770 + ];
771 +
772 + // Convert array to object recursively
773 + return json_decode(json_encode($defaultGlobalMessages));
774 + }
775 +
776 + public static function getBitformSalt()
777 + {
778 + $salt = get_option('bitforms_salt');
779 + if (!$salt) {
780 + $salt = bin2hex(\random_bytes(32));
781 + update_option('bitforms_salt', $salt);
782 + }
783 + return $salt;
195 784 }
196 785 }