PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 2.15.3
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v2.15.3
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 +61 -540 3.3.12.15.3 View file →
@@ -3,22 +3,17 @@
3 3 namespace BitCode\BitForm\Admin\Form;
4 4
5 5 use BitCode\BitForm\Core\Cryptography\Cryptography;
6 6 use BitCode\BitForm\Core\Database\FormEntryModel;
7 -use BitCode\BitForm\Core\Util\FileHandler;
8 7 use BitCode\BitForm\Core\Util\Log;
9 -use BitCode\BitForm\Core\WorkFlow\WorkflowExecutor;
10 8 use Exception;
11 -use WP_Error;
12 9
13 10 class Helpers
14 11 {
15 - private static $encryptEntryIds = [];
16 12
17 13 public static $file_upload_types = ['file-up', 'advanced-file-up'];
18 14
19 15 public static $repeated_array_type_data_fields = ['check', 'image-select'];
20 -
21 16 public static function filterNullEntries($entries)
22 17 {
23 18 $filteredEntries = [];
24 19 foreach ($entries as $entry) {
@@ -38,50 +33,39 @@
38 33 {
39 34 $attributes = wp_json_encode($attrs);
40 35 $instObj = '';
41 36 if ($instanceObj) {
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 - );
37 + $instObj .= <<<INST
38 +script.onload = function () {
39 + bfSelect('#{$contentId}').querySelectorAll('{$selector}').forEach(function(fld){
40 + $instanceObj;
41 + });
42 +}
43 +INST;
54 44 }
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 - );
45 + return <<<LOAD_SECRIPT
46 +var script = document.createElement('script'), integrity = '$integrity', attrs = $attributes, id = '$id';
47 +script.src = '$src';
48 +script.id = id;
49 +if(integrity){
50 + script.integrity = integrity;
51 + script.crossOrigin = 'anonymous';
52 +}
53 +if(attrs){
54 + Object.entries(attrs).forEach(function([key, val]){
55 + script.setAttribute(key,val);
56 + })
57 +}
58 +$instObj;
59 +var bodyElm = document.body;
60 +var alreadyExistScriptElm = bodyElm ? bodyElm.querySelector('script#$id'):null;
61 +if(alreadyExistScriptElm){
62 + bodyElm.removeChild(alreadyExistScriptElm)
63 +}
64 +if(!(window.recaptcha && id === 'g-recaptcha-script')){
65 + bodyElm.appendChild(script);
66 +}
67 +LOAD_SECRIPT;
84 68 }
85 69
86 70 public static function minifyJs($input)
87 71 {
@@ -87,94 +71,20 @@
87 71 {
88 72 if ('' === trim($input)) {
89 73 return $input;
90 74 }
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 75 return preg_replace(
96 76 [
97 - '/^[ \t]+/m', // leading indentation
98 - '/[ \t]+$/m', // trailing whitespace
99 - '/(?:\r?\n){3,}/', // 3+ consecutive newlines -> 2
77 + '/ {2,}/',
78 + '/\s*=\s*/',
79 + '/\s*,\s*/',
80 + '/\s+(?=\(|\{|\:|\?)|\t|(?:\r?\n[ \t]*)+/s'
100 81 ],
101 - ['', '', "\n\n"],
82 + [' ', '=', ',', ''],
102 83 $input
103 84 );
104 85 }
105 86
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;
175 - }
176 -
177 87 /**
178 88 * @method name : saveFile
179 89 * @description : save js/css field to disk
180 90 * @param : $path => like(dirName/css), $fileName => main.css, $script
@@ -188,20 +98,22 @@
188 98 $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user]
189 99 foreach ($pathArr as $d) {
190 100 $rootDir .= $d . DIRECTORY_SEPARATOR;
191 101 if (!realpath($rootDir)) {
192 - wp_mkdir_p($rootDir);
102 + mkdir($rootDir);
193 103 }
194 104 }
195 105 $fullPath = $rootDir . $fileName;
196 - if ('a' === $fileOpenMode) {
197 - $result = FileHandler::appendFile($fullPath, $script);
198 - } else {
199 - $result = FileHandler::writeFile($fullPath, $script);
106 + $file = fopen($fullPath, $fileOpenMode);
107 + if (false === $file) {
108 + throw new Exception("Failed to open file: $fullPath");
200 109 }
201 - if (false === $result) {
110 + if (false === fwrite($file, $script)) {
202 111 throw new Exception("Failed to write to file: $fullPath");
203 112 }
113 + if (false === fclose($file)) {
114 + throw new Exception("Failed to close file: $fullPath");
115 + }
204 116 return true;
205 117 } catch (\Exception $e) {
206 118 Log::debug_log($e->getMessage());
207 119 return false;
@@ -226,9 +138,15 @@
226 138 }
227 139
228 140 public static function fileRead($filePath)
229 141 {
230 - return FileHandler::readFile($filePath);
142 + $fileContent = '';
143 + if (file_exists($filePath)) {
144 + $file = fopen($filePath, 'r');
145 + $fileContent .= fread($file, filesize($filePath));
146 + fclose($file);
147 + }
148 + return $fileContent;
231 149 }
232 150
233 151 public static function getDataFromNestedPath($data, $key)
234 152 {
@@ -275,8 +193,9 @@
275 193 }
276 194 $data = $data->$k;
277 195 }
278 196 $data->$lastKey = json_decode(wp_json_encode($value));
197 + ;
279 198 return $data;
280 199 }
281 200
282 201 public static function property_exists_nested($obj, $path = '', $valToCheck = null, $checkNegativeVal = 0)
@@ -324,9 +243,9 @@
324 243 }
325 244 }
326 245 // check if the entry token is valid
327 246 if (isset($entryToken) && $entryToken) {
328 - $decryptEntryId = Cryptography::decrypt($entryToken, self::getBitformSalt());
247 + $decryptEntryId = Cryptography::decrypt($entryToken, AUTH_SALT);
329 248 if ($decryptEntryId === $entryId) {
330 249 return true;
331 250 }
332 251 }
@@ -333,146 +252,20 @@
333 252
334 253 return false;
335 254 }
336 255
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)
256 + public static function validateEormEntryEditPermission($formId, $entryId)
345 257 {
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 258 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 - ];
259 + if(current_user_can('prevent_bitform_entry_edit')){
260 + return false;
382 261 }
383 262
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 - ];
263 + if(current_user_can('manage_bitform')||current_user_can('bitform_entry_edit') || current_user_can('edit_post')){
264 + return true;
403 265 }
404 - }
405 266
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 267 }
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 268 return false;
476 269 }
477 270
478 271 public static function honeypotEncryptedToken($str)
@@ -482,14 +275,14 @@
482 275 }
483 276
484 277 public static function csrfEecrypted()
485 278 {
486 - $secretKey = get_option('bitform_csrf_secret');
279 + $secretKey = get_option('bf_csrf_secret');
487 280 if (!$secretKey) {
488 281 $secretKey = 'bf-' . time();
489 - update_option('bitform_csrf_secret', $secretKey);
282 + update_option('bf_csrf_secret', $secretKey);
490 283 }
491 - $tIdenty = base64_encode(\random_bytes(32));
284 + $tIdenty = base64_encode(random_bytes(32));
492 285 $csrf = \base64_encode(\hash_hmac('sha256', $tIdenty, $secretKey, true));
493 286 return ['csrf' => $csrf, 't_identity' => $tIdenty];
494 287 }
495 288
@@ -494,9 +287,9 @@
494 287 }
495 288
496 289 public static function csrfDecrypted($identy, $token)
497 290 {
498 - $secretKey = get_option('bitform_csrf_secret');
291 + $secretKey = get_option('bf_csrf_secret');
499 292 return \hash_equals(
500 293 \base64_encode(\hash_hmac('sha256', $identy, $secretKey, true)),
501 294 $token
502 295 );
@@ -508,278 +301,6 @@
508 301 $intArray = array_map('intval', $filteredArray);
509 302 $result = count($arr) === count($intArray);
510 303
511 304 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;
784 305 }
785 306 }