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 / Admin / Form / Helpers.php

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

786 lines 26.2 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\Admin\Form;
4
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);
40 $instObj = '';
41 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 );
54 }
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 );
84 }
85
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(
96 [
97 '/^[ \t]+/m', // leading indentation
98 '/[ \t]+$/m', // trailing whitespace
99 '/(?:\r?\n){3,}/', // 3+ consecutive newlines -> 2
100 ],
101 ['', '', "\n\n"],
102 $input
103 );
104 }
105
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 /**
178 * @method name : saveFile
179 * @description : save js/css field to disk
180 * @param : $path => like(dirName/css), $fileName => main.css, $script
181 * @return : boolean
182 */
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 }
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;
208 }
209 }
210
211 /**
212 * @method name : generatePathDirOrFile
213 * @dscription : generate path for js/css file
214 * @params : $path => like(dirName/css)
215 * @return : a string of full path
216 */
217 public static function generatePathDirOrFile($path)
218 {
219 $rootDir = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR;
220 $path = trim($path, '/');
221 $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user]
222 foreach ($pathArr as $d) {
223 $rootDir .= $d . DIRECTORY_SEPARATOR;
224 }
225 return rtrim($rootDir, DIRECTORY_SEPARATOR);
226 }
227
228 public static function fileRead($filePath)
229 {
230 return FileHandler::readFile($filePath);
231 }
232
233 public static function getDataFromNestedPath($data, $key)
234 {
235 $keys = explode('->', $key);
236 $lastKey = array_pop($keys);
237 $dataType = is_array($data) ? 'array' : (is_object($data) ? 'object' : '');
238 if ('array' === $dataType) {
239 return self::accessFromArray($data, $keys, $lastKey);
240 }
241 if ('object' === $dataType) {
242 return self::accessFromObject($data, $keys, $lastKey);
243 }
244 }
245
246 private static function accessFromObject($data, $keys, $lastKey)
247 {
248 foreach ($keys as $k) {
249 if (!property_exists($data, $k)) {
250 return null;
251 }
252 $data = $data->$k;
253 }
254 return isset($data->$lastKey) ? $data->$lastKey : null;
255 }
256
257 private static function accessFromArray($data, $keys, $lastKey)
258 {
259 foreach ($keys as $k) {
260 if (!array_key_exists($k, $data)) {
261 return null;
262 }
263 $data = $data[$k];
264 }
265 return isset($data[$lastKey]) ? $data[$lastKey] : null;
266 }
267
268 public static function setDataToNestedPath($data, $key, $value)
269 {
270 $keys = explode('->', $key);
271 $lastKey = array_pop($keys);
272 foreach ($keys as $k) {
273 if (!array_key_exists($k, $data)) {
274 $data->$k = (object) [];
275 }
276 $data = $data->$k;
277 }
278 $data->$lastKey = json_decode(wp_json_encode($value));
279 return $data;
280 }
281
282 public static function property_exists_nested($obj, $path = '', $valToCheck = null, $checkNegativeVal = 0)
283 {
284 $path = explode('->', $path);
285 $current = $obj;
286 foreach ($path as $key) {
287 if (is_object($current)) {
288 if (property_exists($current, $key)) {
289 $current = $current->{$key};
290 } else {
291 return false;
292 }
293 } else {
294 return false;
295 }
296 }
297 if (isset($valToCheck)) {
298 if ($checkNegativeVal) {
299 return $current !== $valToCheck;
300 }
301 return $current === $valToCheck;
302 }
303 return true;
304 }
305
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 {
480 $token = base64_encode(base64_encode($str));
481 return $token;
482 }
483
484 public static function csrfEecrypted()
485 {
486 $secretKey = get_option('bitform_csrf_secret');
487 if (!$secretKey) {
488 $secretKey = 'bf-' . time();
489 update_option('bitform_csrf_secret', $secretKey);
490 }
491 $tIdenty = base64_encode(\random_bytes(32));
492 $csrf = \base64_encode(\hash_hmac('sha256', $tIdenty, $secretKey, true));
493 return ['csrf' => $csrf, 't_identity' => $tIdenty];
494 }
495
496 public static function csrfDecrypted($identy, $token)
497 {
498 $secretKey = get_option('bitform_csrf_secret');
499 return \hash_equals(
500 \base64_encode(\hash_hmac('sha256', $identy, $secretKey, true)),
501 $token
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;
784 }
785 }
786