PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 2.21.4
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v2.21.4
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 2.10.2 All 137 releases
bit-form / includes / Admin / Form / Helpers.php

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

584 lines 18.5 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\Log;
8 use Exception;
9 use WP_Error;
10
11 class Helpers
12 {
13 private static $encryptEntryIds = [];
14
15 public static $file_upload_types = ['file-up', 'advanced-file-up'];
16
17 public static $repeated_array_type_data_fields = ['check', 'image-select'];
18
19 public static function filterNullEntries($entries)
20 {
21 $filteredEntries = [];
22 foreach ($entries as $entry) {
23 foreach ($entry as $key => $value) {
24 if (is_null($value)) {
25 unset($entry->$key);
26 }
27 }
28 if (count((array) $entry)) {
29 $filteredEntries[] = $entry;
30 }
31 }
32 return $filteredEntries;
33 }
34
35 public static function scriptLoader($src, $id, $instanceObj = null, $selector = '', $attrs = [], $integrity = null, $contentId = '')
36 {
37 $attributes = wp_json_encode($attrs);
38 $instObj = '';
39 if ($instanceObj) {
40 $instObj .= <<<INST
41 script.onload = function () {
42 bfSelect('#{$contentId}').querySelectorAll('{$selector}').forEach(function(fld){
43 $instanceObj;
44 });
45 }
46 INST;
47 }
48 return <<<LOAD_SECRIPT
49 var script = document.createElement('script'), integrity = '$integrity', attrs = $attributes, id = '$id';
50 script.src = '$src';
51 script.id = id;
52 if(integrity){
53 script.integrity = integrity;
54 script.crossOrigin = 'anonymous';
55 }
56 if(attrs){
57 Object.entries(attrs).forEach(function([key, val]){
58 script.setAttribute(key,val);
59 })
60 }
61 $instObj;
62 var bodyElm = document.body;
63 var alreadyExistScriptElm = bodyElm ? bodyElm.querySelector('script#$id'):null;
64 if(alreadyExistScriptElm){
65 bodyElm.removeChild(alreadyExistScriptElm)
66 }
67 if(!(window.recaptcha && id === 'g-recaptcha-script')){
68 bodyElm.appendChild(script);
69 }
70 LOAD_SECRIPT;
71 }
72
73 public static function minifyJs($input)
74 {
75 if ('' === trim($input)) {
76 return $input;
77 }
78 return preg_replace(
79 [
80 '/ {2,}/',
81 '/\s*=\s*/',
82 '/\s*,\s*/',
83 '/\s+(?=\(|\{|\:|\?)|\t|(?:\r?\n[ \t]*)+/s'
84 ],
85 [' ', '=', ',', ''],
86 $input
87 );
88 }
89
90 public static function removeJsSingleLineComments($code)
91 {
92 $length = strlen($code);
93 $result = '';
94 $inString = false;
95 $inTemplate = false;
96 $inRegex = false;
97 $escapeNext = false;
98 $stringDelimiter = '';
99 $i = 0;
100
101 while ($i < $length) {
102 $char = $code[$i];
103 $nextChar = $i + 1 < $length ? $code[$i + 1] : '';
104
105 if ($escapeNext) {
106 $result .= $char;
107 $escapeNext = false;
108 } elseif ($inString) {
109 $result .= $char;
110 if ('\\' === $char) {
111 $escapeNext = true;
112 } elseif ($char === $stringDelimiter) {
113 $inString = false;
114 }
115 } elseif ($inTemplate) {
116 $result .= $char;
117 if ('\\' === $char) {
118 $escapeNext = true;
119 } elseif ('`' === $char) {
120 $inTemplate = false;
121 }
122 } elseif ($inRegex) {
123 $result .= $char;
124 if ('\\' === $char) {
125 $escapeNext = true;
126 } elseif ('/' === $char) {
127 $inRegex = false;
128 }
129 } else {
130 if ('"' === $char || "'" === $char) {
131 $inString = true;
132 $stringDelimiter = $char;
133 $result .= $char;
134 } elseif ('`' === $char) {
135 $inTemplate = true;
136 $result .= $char;
137 } elseif ('/' === $char) {
138 if ('/' === $nextChar) {
139 // Single-line comment found
140 while ($i < $length && "\n" !== $code[$i]) {
141 $i++;
142 }
143 continue; // skip until newline
144 } elseif ('*' === $nextChar) {
145 // Block comment start, just copy it (optional, depending on need)
146 $result .= $char;
147 } else {
148 // Assume division or regex
149 $result .= $char;
150 }
151 } else {
152 $result .= $char;
153 }
154 }
155 $i++;
156 }
157
158 return $result;
159 }
160
161 /**
162 * @method name : saveFile
163 * @description : save js/css field to disk
164 * @param : $path => like(dirName/css), $fileName => main.css, $script
165 * @return : boolean
166 */
167 public static function saveFile($path, $fileName, $script, $fileOpenMode = 'a')
168 {
169 try {
170 $rootDir = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR;
171 $path = trim($path, '/');
172 $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user]
173 foreach ($pathArr as $d) {
174 $rootDir .= $d . DIRECTORY_SEPARATOR;
175 if (!realpath($rootDir)) {
176 mkdir($rootDir);
177 }
178 }
179 $fullPath = $rootDir . $fileName;
180 $file = fopen($fullPath, $fileOpenMode);
181 if (false === $file) {
182 throw new Exception("Failed to open file: $fullPath");
183 }
184 if (false === fwrite($file, $script)) {
185 throw new Exception("Failed to write to file: $fullPath");
186 }
187 if (false === fclose($file)) {
188 throw new Exception("Failed to close file: $fullPath");
189 }
190 return true;
191 } catch (\Exception $e) {
192 Log::debug_log($e->getMessage());
193 return false;
194 }
195 }
196
197 /**
198 * @method name : generatePathDirOrFile
199 * @dscription : generate path for js/css file
200 * @params : $path => like(dirName/css)
201 * @return : a string of full path
202 */
203 public static function generatePathDirOrFile($path)
204 {
205 $rootDir = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR;
206 $path = trim($path, '/');
207 $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user]
208 foreach ($pathArr as $d) {
209 $rootDir .= $d . DIRECTORY_SEPARATOR;
210 }
211 return rtrim($rootDir, DIRECTORY_SEPARATOR);
212 }
213
214 public static function fileRead($filePath)
215 {
216 $fileContent = '';
217 if (file_exists($filePath)) {
218 $file = fopen($filePath, 'r');
219 $fileContent .= fread($file, filesize($filePath));
220 fclose($file);
221 }
222 return $fileContent;
223 }
224
225 public static function getDataFromNestedPath($data, $key)
226 {
227 $keys = explode('->', $key);
228 $lastKey = array_pop($keys);
229 $dataType = is_array($data) ? 'array' : (is_object($data) ? 'object' : '');
230 if ('array' === $dataType) {
231 return self::accessFromArray($data, $keys, $lastKey);
232 }
233 if ('object' === $dataType) {
234 return self::accessFromObject($data, $keys, $lastKey);
235 }
236 }
237
238 private static function accessFromObject($data, $keys, $lastKey)
239 {
240 foreach ($keys as $k) {
241 if (!property_exists($data, $k)) {
242 return null;
243 }
244 $data = $data->$k;
245 }
246 return isset($data->$lastKey) ? $data->$lastKey : null;
247 }
248
249 private static function accessFromArray($data, $keys, $lastKey)
250 {
251 foreach ($keys as $k) {
252 if (!array_key_exists($k, $data)) {
253 return null;
254 }
255 $data = $data[$k];
256 }
257 return isset($data[$lastKey]) ? $data[$lastKey] : null;
258 }
259
260 public static function setDataToNestedPath($data, $key, $value)
261 {
262 $keys = explode('->', $key);
263 $lastKey = array_pop($keys);
264 foreach ($keys as $k) {
265 if (!array_key_exists($k, $data)) {
266 $data->$k = (object) [];
267 }
268 $data = $data->$k;
269 }
270 $data->$lastKey = json_decode(wp_json_encode($value));
271 ;
272 return $data;
273 }
274
275 public static function property_exists_nested($obj, $path = '', $valToCheck = null, $checkNegativeVal = 0)
276 {
277 $path = explode('->', $path);
278 $current = $obj;
279 foreach ($path as $key) {
280 if (is_object($current)) {
281 if (property_exists($current, $key)) {
282 $current = $current->{$key};
283 } else {
284 return false;
285 }
286 } else {
287 return false;
288 }
289 }
290 if (isset($valToCheck)) {
291 if ($checkNegativeVal) {
292 return $current !== $valToCheck;
293 }
294 return $current === $valToCheck;
295 }
296 return true;
297 }
298
299 public static function validateEntryTokenAndUser($entryToken, $entryId)
300 {
301 // check if the user is logged in
302 if (is_user_logged_in()) {
303 $user = wp_get_current_user();
304 if (in_array('administrator', $user->roles) || current_user_can('manage_bitform')) {
305 return true;
306 }
307 $entryModel = new FormEntryModel();
308 $entry = $entryModel->get(
309 'id, user_id, form_id',
310 [
311 'id' => $entryId,
312 'user_id' => $user->ID
313 ]
314 );
315 if (!is_wp_error($entry) && !empty($entry)) {
316 return true;
317 }
318 }
319 // check if the entry token is valid
320 if (isset($entryToken) && $entryToken) {
321 $decryptEntryId = Cryptography::decrypt($entryToken, AUTH_SALT);
322 if ($decryptEntryId === $entryId) {
323 return true;
324 }
325 }
326
327 return false;
328 }
329
330 public static function validateFormEntryEditPermission($formId, $entryId)
331 {
332 if (is_user_logged_in()) {
333 if (current_user_can('manage_bitform') || current_user_can('bitform_entry_edit') || current_user_can('edit_post')) {
334 return true;
335 }
336 }
337 return false;
338 }
339
340 public static function honeypotEncryptedToken($str)
341 {
342 $token = base64_encode(base64_encode($str));
343 return $token;
344 }
345
346 public static function csrfEecrypted()
347 {
348 $secretKey = get_option('bf_csrf_secret');
349 if (!$secretKey) {
350 $secretKey = 'bf-' . time();
351 update_option('bf_csrf_secret', $secretKey);
352 }
353 $tIdenty = base64_encode(random_bytes(32));
354 $csrf = \base64_encode(\hash_hmac('sha256', $tIdenty, $secretKey, true));
355 return ['csrf' => $csrf, 't_identity' => $tIdenty];
356 }
357
358 public static function csrfDecrypted($identy, $token)
359 {
360 $secretKey = get_option('bf_csrf_secret');
361 return \hash_equals(
362 \base64_encode(\hash_hmac('sha256', $identy, $secretKey, true)),
363 $token
364 );
365 }
366
367 public static function checkIsIntArr($arr)
368 {
369 $filteredArray = array_filter($arr, 'is_numeric');
370 $intArray = array_map('intval', $filteredArray);
371 $result = count($arr) === count($intArray);
372
373 return $result;
374 }
375
376 public static function getTruncatedEncryptToken($str, $length = 20)
377 {
378 $token = hash_hmac('sha256', $str, AUTH_SALT);
379 return substr($token, 0, $length);
380 }
381
382 public static function getEncryptedEntryId($entryId)
383 {
384 if (!isset(self::$encryptEntryIds[$entryId])) {
385 self::$encryptEntryIds[$entryId] = self::getTruncatedEncryptToken($entryId);
386 }
387 return self::$encryptEntryIds[$entryId];
388 }
389
390 public static function getFullPathWithEncryptedEntryId($formId, $entryId)
391 {
392 $encryptDirectory = Helpers::getEncryptedEntryId($entryId);
393 return BITFORMS_UPLOAD_DIR . DIRECTORY_SEPARATOR . $formId . DIRECTORY_SEPARATOR . $encryptDirectory;
394 }
395
396 public static function getWebPathWithEncryptedEntryId($formId, $entryId)
397 {
398 $encryptDirectory = Helpers::getEncryptedEntryId($entryId);
399 return BITFORMS_UPLOAD_BASE_URL . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . $formId . DIRECTORY_SEPARATOR . $encryptDirectory;
400 }
401
402 public static function PDFPassHash($entryId)
403 {
404 return abs(crc32($entryId));
405 }
406
407 public static function encryptBinaryData($plaintext)
408 {
409 $iv = openssl_random_pseudo_bytes(16);
410 $encrypted = openssl_encrypt($plaintext, 'AES-256-CBC', BITFORMS_SECRET_KEY, OPENSSL_RAW_DATA, $iv);
411
412 return bin2hex($iv . $encrypted);
413 }
414
415 public static function decryptBinaryData($encryptedHex)
416 {
417 $decoded = hex2bin($encryptedHex);
418 $iv = substr($decoded, 0, 16);
419 $cipherText = substr($decoded, 16);
420
421 return openssl_decrypt($cipherText, 'AES-256-CBC', BITFORMS_SECRET_KEY, OPENSSL_RAW_DATA, $iv);
422 }
423
424 /**
425 * Sanitize user-provided HTML content by removing dangerous JS code
426 * while allowing all valid HTML/CSS.
427 *
428 * @param string $html Raw HTML from user input
429 * @return string Sanitized safe HTML
430 */
431 public static function sanitizeUserHTML(string $html): string
432 {
433 // Remove <script> tags entirely
434 $html = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $html);
435
436 // Remove event handler attributes (like onclick, onload, etc.)
437 $html = preg_replace_callback('/<[^>]+>/i', function ($matches) {
438 return preg_replace('/\s*on\w+\s*=\s*"[^"]*"/i', '', $matches[0]); // on*=""
439 }, $html);
440
441 $html = preg_replace_callback('/<[^>]+>/i', function ($matches) {
442 return preg_replace("/\s*on\w+\s*=\s*'[^']*'/i", '', $matches[0]); // on*=''
443 }, $html);
444
445 // Remove javascript: from href or src
446 $html = preg_replace('/(href|src)\s*=\s*([\'"])\s*javascript:[^\'"]*\2/i', '', $html);
447
448 return $html;
449 }
450
451 public static function sanitizeUrlParam($param)
452 {
453 if (preg_match('/\.\.?\//', $param)) {
454 return new WP_Error('parameter_error', 'Invalid URL parameter');
455 }
456
457 $param = htmlspecialchars(trim($param), ENT_QUOTES, 'UTF-8');
458 return sanitize_text_field($param);
459 }
460
461 public static function replaceFieldsDefaultErrorMsg($fields)
462 {
463 try {
464 $appSettings = get_option('bitform_app_settings', (object) []);
465 if (!isset($appSettings->globalMessages) || !isset($appSettings->globalMessages->err)) {
466 return $fields;
467 }
468
469 $globalErrMsg = $appSettings->globalMessages->err;
470 $templateCache = []; // [type_errKey] => compiled template
471
472 foreach ($fields as $fieldKey => $field) {
473 if (!isset($field->err) || !is_object($field->err)) {
474 continue;
475 }
476
477 foreach ($field->err as $errKey => $errObj) {
478 if (!isset($errObj->dflt)) {
479 continue;
480 }
481
482 $cacheKey = $field->typ . '_' . $errKey;
483 $template = null;
484
485 // 1. Check Cache First
486 if (isset($templateCache[$cacheKey])) {
487 $template = $templateCache[$cacheKey];
488 } else {
489 // 2. Lookup from globalErrMsg
490 if (isset($globalErrMsg->{$field->typ}->{$errKey})) {
491 $template = $globalErrMsg->{$field->typ}->{$errKey};
492 } elseif (isset($globalErrMsg->{$errKey}) && !is_object($globalErrMsg->{$errKey})) {
493 $template = $globalErrMsg->{$errKey};
494 }
495
496 // 3. Cache it
497 if ($template) {
498 $templateCache[$cacheKey] = $template;
499 }
500 }
501
502 // 4. Apply Template if Found
503 if ($template) {
504 $finalMsg = self::replaceShortcodeInErrorMsg($template, $field);
505 // 5. Sanitize final output
506 $field->err->{$errKey}->dflt = wp_kses_post($finalMsg);
507 }
508 }
509
510 $fields->{$fieldKey} = $field;
511 }
512 } catch (Exception $e) {
513 Log::debug_log('Error In Replacing Fields Default Error messages: ' . $e->getMessage());
514 }
515 return $fields;
516 }
517
518 //replace shortcode in error message
519 public static function replaceShortcodeInErrorMsg($msg, $field)
520 {
521 $shortcodes = [
522 '${field.label}' => isset($field->lbl) ? $field->lbl : '',
523 '${field.minimum}' => isset($field->mn) ? $field->mn : '',
524 '${field.maximum}' => isset($field->mx) ? $field->mx : '',
525 '${field.minimum_file}' => isset($field->config->minFile) ? $field->config->minFile : '',
526 '${field.maximum_file}' => isset($field->config->maxFile) ? $field->config->maxFile : '',
527 '${field.maximum_size}' => isset($field->config->maxSize) ? $field->config->maxSize : '',
528 '${field.minimum_amount}' => isset($field->config->minValue) ? $field->config->minValue : '',
529 '${field.maximum_amount}' => isset($field->config->maxValue) ? $field->config->maxValue : '',
530 ];
531 $msg = str_replace(array_keys($shortcodes), array_values($shortcodes), $msg);
532 return $msg;
533 }
534
535 public static function getDefaultGlobalMessages()
536 {
537 $defaultGlobalMessages = [
538 'err' => [
539 'req' => '<p style="margin:0">' . __('This field is required', 'bit-form') . '</p>',
540 'email' => [
541 'invalid' => '<p style="margin:0">' . __('Please, enter a valid email address', 'bit-form') . '</p>',
542 ],
543 'url' => [
544 'invalid' => '<p style="margin:0">' . __('Please, enter a valid URL', 'bit-form') . '</p>',
545 ],
546 'mn' => '<p style="margin:0">' . __('Minimum ${field.minimum} is required', 'bit-form') . '</p>',
547 'mx' => '<p style="margin:0">' . __('Maximum ${field.maximum} is allowed', 'bit-form') . '</p>',
548 'number' => [
549 'invalid' => '<p style="margin:0">' . __('Please, enter only numbers', 'bit-form') . '</p>',
550 ],
551 'phone-number' => [
552 'invalid' => '<p style="margin:0">' . __('Please, enter a valid phone number', 'bit-form') . '</p>',
553 ],
554 'check' => [
555 'mn' => '<p style="margin:0">' . __('Select at least ${field.minimum} option(s)', 'bit-form') . '</p>',
556 'mx' => '<p style="margin:0">' . __('Please, select no more than ${field.maximum} option(s)', 'bit-form') . '</p>',
557 ],
558 'select' => [
559 'mn' => '<p style="margin:0">' . __('Select at least ${field.minimum} option(s)', 'bit-form') . '</p>',
560 'mx' => '<p style="margin:0">' . __('Please, select no more than ${field.maximum} option(s)', 'bit-form') . '</p>',
561 ],
562 'image-select' => [
563 'mn' => '<p style="margin:0">' . __('Select at least ${field.minimum} option(s)', 'bit-form') . '</p>',
564 'mx' => '<p style="margin:0">' . __('Please, select no more than ${field.maximum} option(s)', 'bit-form') . '</p>',
565 ],
566 'inputMask' => '<p style="margin:0">' . __('Input does not match the required pattern', 'bit-form') . '</p>',
567 'regexr' => '<p style="margin:0">' . __('Input does not match the required pattern', 'bit-form') . '</p>',
568 'minFile' => '<p style="margin:0">' . __('Minimum ${field.minimum_file} file(s) required', 'bit-form') . '</p>',
569 'maxFile' => '<p style="margin:0">' . __('Maximum ${field.maximum_file} file(s) allowed', 'bit-form') . '</p>',
570 'maxSize' => '<p style="margin:0">' . __('Maximum file size exceeded. (Max: ${field.maximum_size}MB)', 'bit-form') . '</p>',
571 'fileType' => '<p style="margin:0">' . __('File type is not supported', 'bit-form') . '</p>',
572 'entryUnique' => '<p style="margin:0">' . __('This value is already taken. Please, choose a different one.', 'bit-form') . '</p>',
573 'userUnique' => '<p style="margin:0">' . __('This username or email is already registered. Please, use another.', 'bit-form') . '</p>',
574 'otherOptReq' => '<p style="margin:0">' . __('Custom Option Required', 'bit-form') . '</p>',
575 'minValue' => '<p style="margin:0">' . __('Minimum amount of ${field.minimum_amount} is required', 'bit-form') . '</p>',
576 'maxValue' => '<p style="margin:0">' . __('Maximum amount of ${field.maximum_amount} is allowed', 'bit-form') . '</p>',
577 ],
578 ];
579
580 // Convert array to object recursively
581 return json_decode(json_encode($defaultGlobalMessages));
582 }
583 }
584