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 +596 -57 2.10.13.3.1 View file →
@@ -1,13 +1,24 @@
1 1 <?php
2 2
3 3 namespace BitCode\BitForm\Admin\Form;
4 4
5 +use BitCode\BitForm\Core\Cryptography\Cryptography;
6 +use BitCode\BitForm\Core\Database\FormEntryModel;
7 +use BitCode\BitForm\Core\Util\FileHandler;
5 8 use BitCode\BitForm\Core\Util\Log;
9 +use BitCode\BitForm\Core\WorkFlow\WorkflowExecutor;
6 10 use Exception;
11 +use WP_Error;
7 12
8 13 class Helpers
9 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 +
10 21 public static function filterNullEntries($entries)
11 22 {
12 23 $filteredEntries = [];
13 24 foreach ($entries as $entry) {
@@ -27,39 +38,50 @@
27 38 {
28 39 $attributes = wp_json_encode($attrs);
29 40 $instObj = '';
30 41 if ($instanceObj) {
31 - $instObj .= <<<INST
32 -script.onload = function () {
33 - bfSelect('#{$contentId}').querySelectorAll('{$selector}').forEach(function(fld){
34 - $instanceObj;
35 - });
36 -}
37 -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 + );
38 54 }
39 - return <<<LOAD_SECRIPT
40 -var script = document.createElement('script'), integrity = '$integrity', attrs = $attributes, id = '$id';
41 -script.src = '$src';
42 -script.id = id;
43 -if(integrity){
44 - script.integrity = integrity;
45 - script.crossOrigin = 'anonymous';
46 -}
47 -if(attrs){
48 - Object.entries(attrs).forEach(function([key, val]){
49 - script.setAttribute(key,val);
50 - })
51 -}
52 -$instObj;
53 -var bodyElm = document.body;
54 -var alreadyExistScriptElm = bodyElm ? bodyElm.querySelector('script#$id'):null;
55 -if(alreadyExistScriptElm){
56 - bodyElm.removeChild(alreadyExistScriptElm)
57 -}
58 -if(!(window.recaptcha && id === 'g-recaptcha-script')){
59 - bodyElm.appendChild(script);
60 -}
61 -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 + );
62 84 }
63 85
64 86 public static function minifyJs($input)
65 87 {
@@ -65,20 +87,94 @@
65 87 {
66 88 if ('' === trim($input)) {
67 89 return $input;
68 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.
69 95 return preg_replace(
70 96 [
71 - '/ {2,}/',
72 - '/\s*=\s*/',
73 - '/\s*,\s*/',
74 - '/\s+(?=\(|\{|\:|\?)|\t|(?:\r?\n[ \t]*)+/s'
97 + '/^[ \t]+/m', // leading indentation
98 + '/[ \t]+$/m', // trailing whitespace
99 + '/(?:\r?\n){3,}/', // 3+ consecutive newlines -> 2
75 100 ],
76 - [' ', '=', ',', ''],
101 + ['', '', "\n\n"],
77 102 $input
78 103 );
79 104 }
80 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 +
81 177 /**
82 178 * @method name : saveFile
83 179 * @description : save js/css field to disk
84 180 * @param : $path => like(dirName/css), $fileName => main.css, $script
@@ -85,9 +181,9 @@
85 181 * @return : boolean
86 182 */
87 183 public static function saveFile($path, $fileName, $script, $fileOpenMode = 'a')
88 184 {
89 - try{
185 + try {
90 186 $rootDir = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR;
91 187 $path = trim($path, '/');
92 188 $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user]
93 189 foreach ($pathArr as $d) {
@@ -92,28 +188,25 @@
92 188 $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user]
93 189 foreach ($pathArr as $d) {
94 190 $rootDir .= $d . DIRECTORY_SEPARATOR;
95 191 if (!realpath($rootDir)) {
96 - mkdir($rootDir);
192 + wp_mkdir_p($rootDir);
97 193 }
98 194 }
99 195 $fullPath = $rootDir . $fileName;
100 - $file = fopen($fullPath, $fileOpenMode);
101 - if ($file === false) {
102 - throw new Exception("Failed to open file: $fullPath");
196 + if ('a' === $fileOpenMode) {
197 + $result = FileHandler::appendFile($fullPath, $script);
198 + } else {
199 + $result = FileHandler::writeFile($fullPath, $script);
103 200 }
104 - if (fwrite($file, $script) === false) {
201 + if (false === $result) {
105 202 throw new Exception("Failed to write to file: $fullPath");
106 203 }
107 - if (fclose($file) === false) {
108 - throw new Exception("Failed to close file: $fullPath");
109 - }
110 204 return true;
111 205 } catch (\Exception $e) {
112 206 Log::debug_log($e->getMessage());
113 207 return false;
114 208 }
115 -
116 209 }
117 210
118 211 /**
119 212 * @method name : generatePathDirOrFile
@@ -133,15 +226,9 @@
133 226 }
134 227
135 228 public static function fileRead($filePath)
136 229 {
137 - $fileContent = '';
138 - if (file_exists($filePath)) {
139 - $file = fopen($filePath, 'r');
140 - $fileContent .= fread($file, filesize($filePath));
141 - fclose($file);
142 - }
143 - return $fileContent;
230 + return FileHandler::readFile($filePath);
144 231 }
145 232
146 233 public static function getDataFromNestedPath($data, $key)
147 234 {
@@ -188,9 +275,8 @@
188 275 }
189 276 $data = $data->$k;
190 277 }
191 278 $data->$lastKey = json_decode(wp_json_encode($value));
192 - ;
193 279 return $data;
194 280 }
195 281
196 282 public static function property_exists_nested($obj, $path = '', $valToCheck = null, $checkNegativeVal = 0)
@@ -216,8 +302,180 @@
216 302 }
217 303 return true;
218 304 }
219 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 +
220 478 public static function honeypotEncryptedToken($str)
221 479 {
222 480 $token = base64_encode(base64_encode($str));
223 481 return $token;
@@ -224,14 +482,14 @@
224 482 }
225 483
226 484 public static function csrfEecrypted()
227 485 {
228 - $secretKey = get_option('bf_csrf_secret');
486 + $secretKey = get_option('bitform_csrf_secret');
229 487 if (!$secretKey) {
230 488 $secretKey = 'bf-' . time();
231 - update_option('bf_csrf_secret', $secretKey);
489 + update_option('bitform_csrf_secret', $secretKey);
232 490 }
233 - $tIdenty = base64_encode(random_bytes(32));
491 + $tIdenty = base64_encode(\random_bytes(32));
234 492 $csrf = \base64_encode(\hash_hmac('sha256', $tIdenty, $secretKey, true));
235 493 return ['csrf' => $csrf, 't_identity' => $tIdenty];
236 494 }
237 495
@@ -236,11 +494,292 @@
236 494 }
237 495
238 496 public static function csrfDecrypted($identy, $token)
239 497 {
240 - $secretKey = get_option('bf_csrf_secret');
498 + $secretKey = get_option('bitform_csrf_secret');
241 499 return \hash_equals(
242 500 \base64_encode(\hash_hmac('sha256', $identy, $secretKey, true)),
243 501 $token
244 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;
245 784 }
246 785 }