$value) { if (is_null($value)) { unset($entry->$key); } } if (count((array) $entry)) { $filteredEntries[] = $entry; } } return $filteredEntries; } public static function scriptLoader($src, $id, $instanceObj = null, $selector = '', $attrs = [], $integrity = null, $contentId = '') { $attributes = wp_json_encode($attrs); $instObj = ''; if ($instanceObj) { $instObj .= sprintf( ' script.onload = function () { bfSelect("#{%1$s}").querySelectorAll("{%2$s}").forEach(function(fld){ %3$s; }); } ', $contentId, $selector, $instanceObj ); } return sprintf( ' var script = document.createElement("script"), integrity = "%1$s", attrs = %2$s, id = "%3$s"; script.src = "%4$s"; script.id = id; if(integrity){ script.integrity = integrity; script.crossOrigin = "anonymous"; } if(attrs){ Object.entries(attrs).forEach(function([key, val]){ script.setAttribute(key,val); }) } $instObj; var bodyElm = document.body; var alreadyExistScriptElm = bodyElm ? bodyElm.querySelector("script#$id"):null; if(alreadyExistScriptElm){ bodyElm.removeChild(alreadyExistScriptElm) } if(!(window.recaptcha && id === "g-recaptcha-script")){ bodyElm.appendChild(script); } ', $integrity, $attributes, $id, $src, ); } public static function minifyJs($input) { if ('' === trim($input)) { return $input; } return preg_replace( [ '/ {2,}/', '/\s*=\s*/', '/\s*,\s*/', '/\s+(?=\(|\{|\:|\?)|\t|(?:\r?\n[ \t]*)+/s' ], [' ', '=', ',', ''], $input ); } public static function removeJsSingleLineComments($code) { $length = strlen($code); $result = ''; $inString = false; $inTemplate = false; $inRegex = false; $escapeNext = false; $stringDelimiter = ''; $i = 0; while ($i < $length) { $char = $code[$i]; $nextChar = $i + 1 < $length ? $code[$i + 1] : ''; if ($escapeNext) { $result .= $char; $escapeNext = false; } elseif ($inString) { $result .= $char; if ('\\' === $char) { $escapeNext = true; } elseif ($char === $stringDelimiter) { $inString = false; } } elseif ($inTemplate) { $result .= $char; if ('\\' === $char) { $escapeNext = true; } elseif ('`' === $char) { $inTemplate = false; } } elseif ($inRegex) { $result .= $char; if ('\\' === $char) { $escapeNext = true; } elseif ('/' === $char) { $inRegex = false; } } else { if ('"' === $char || "'" === $char) { $inString = true; $stringDelimiter = $char; $result .= $char; } elseif ('`' === $char) { $inTemplate = true; $result .= $char; } elseif ('/' === $char) { if ('/' === $nextChar) { // Single-line comment found while ($i < $length && "\n" !== $code[$i]) { $i++; } continue; // skip until newline } elseif ('*' === $nextChar) { // Block comment start, just copy it (optional, depending on need) $result .= $char; } else { // Assume division or regex $result .= $char; } } else { $result .= $char; } } $i++; } return $result; } /** * @method name : saveFile * @description : save js/css field to disk * @param : $path => like(dirName/css), $fileName => main.css, $script * @return : boolean */ public static function saveFile($path, $fileName, $script, $fileOpenMode = 'a') { try { $rootDir = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR; $path = trim($path, '/'); $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user] foreach ($pathArr as $d) { $rootDir .= $d . DIRECTORY_SEPARATOR; if (!realpath($rootDir)) { wp_mkdir_p($rootDir); } } $fullPath = $rootDir . $fileName; if ('a' === $fileOpenMode) { $result = FileHandler::appendFile($fullPath, $script); } else { $result = FileHandler::writeFile($fullPath, $script); } if (false === $result) { throw new Exception("Failed to write to file: $fullPath"); } return true; } catch (\Exception $e) { Log::debug_log($e->getMessage()); return false; } } /** * @method name : generatePathDirOrFile * @dscription : generate path for js/css file * @params : $path => like(dirName/css) * @return : a string of full path */ public static function generatePathDirOrFile($path) { $rootDir = BITFORMS_CONTENT_DIR . DIRECTORY_SEPARATOR; $path = trim($path, '/'); $pathArr = explode('/', $path); // like "fieldname/user => [Fieldname, user] foreach ($pathArr as $d) { $rootDir .= $d . DIRECTORY_SEPARATOR; } return rtrim($rootDir, DIRECTORY_SEPARATOR); } public static function fileRead($filePath) { return FileHandler::readFile($filePath); } public static function getDataFromNestedPath($data, $key) { $keys = explode('->', $key); $lastKey = array_pop($keys); $dataType = is_array($data) ? 'array' : (is_object($data) ? 'object' : ''); if ('array' === $dataType) { return self::accessFromArray($data, $keys, $lastKey); } if ('object' === $dataType) { return self::accessFromObject($data, $keys, $lastKey); } } private static function accessFromObject($data, $keys, $lastKey) { foreach ($keys as $k) { if (!property_exists($data, $k)) { return null; } $data = $data->$k; } return isset($data->$lastKey) ? $data->$lastKey : null; } private static function accessFromArray($data, $keys, $lastKey) { foreach ($keys as $k) { if (!array_key_exists($k, $data)) { return null; } $data = $data[$k]; } return isset($data[$lastKey]) ? $data[$lastKey] : null; } public static function setDataToNestedPath($data, $key, $value) { $keys = explode('->', $key); $lastKey = array_pop($keys); foreach ($keys as $k) { if (!array_key_exists($k, $data)) { $data->$k = (object) []; } $data = $data->$k; } $data->$lastKey = json_decode(wp_json_encode($value)); return $data; } public static function property_exists_nested($obj, $path = '', $valToCheck = null, $checkNegativeVal = 0) { $path = explode('->', $path); $current = $obj; foreach ($path as $key) { if (is_object($current)) { if (property_exists($current, $key)) { $current = $current->{$key}; } else { return false; } } else { return false; } } if (isset($valToCheck)) { if ($checkNegativeVal) { return $current !== $valToCheck; } return $current === $valToCheck; } return true; } public static function validateEntryTokenAndUser($entryToken, $entryId) { // check if the user is logged in if (is_user_logged_in()) { $user = wp_get_current_user(); if (in_array('administrator', $user->roles) || current_user_can('manage_bitform')) { return true; } $entryModel = new FormEntryModel(); $entry = $entryModel->get( 'id, user_id, form_id', [ 'id' => $entryId, 'user_id' => $user->ID ] ); if (!is_wp_error($entry) && !empty($entry)) { return true; } } // check if the entry token is valid if (isset($entryToken) && $entryToken) { $decryptEntryId = Cryptography::decrypt($entryToken, self::getBitformSalt()); if ($decryptEntryId === $entryId) { return true; } } return false; } /** * Validate workflow trigger token with proper input sanitization * * @param object $request The AJAX request object * @param string $formID The form ID (already sanitized) * @return array ['valid' => bool, 'error' => string, 'triggerData' => object|null, 'isAdminBypass' => bool] */ public static function validateWorkflowTriggerToken($request, $formID) { // Sanitize and validate cronNotOk array if (!isset($request->cronNotOk) || !is_array($request->cronNotOk)) { return [ 'valid' => false, 'error' => 'Missing or invalid cronNotOk data', 'triggerData' => null, 'isAdminBypass' => false ]; } // Validate and sanitize entry ID and log ID (must be integers) if (!isset($request->cronNotOk[0]) || !is_numeric($request->cronNotOk[0])) { Log::debug_log('Invalid entry ID in cronNotOk[0]'); return ['valid' => false, 'error' => 'Invalid entry ID', 'triggerData' => null, 'isAdminBypass' => false]; } if (!isset($request->cronNotOk[1]) || !is_numeric($request->cronNotOk[1])) { Log::debug_log('Invalid log ID in cronNotOk[1]'); return ['valid' => false, 'error' => 'Invalid log ID', 'triggerData' => null, 'isAdminBypass' => false]; } $entryID = absint($request->cronNotOk[0]); $logID = absint($request->cronNotOk[1]); // Check for administrator bypass $isAdminBypass = false; if (is_user_logged_in()) { $user = wp_get_current_user(); if (in_array('administrator', $user->roles) || current_user_can('manage_bitform')) { Log::debug_log('Admin bypass: Workflow triggered by ' . $user->user_login . ' for entryID=' . $entryID); return [ 'valid' => true, 'error' => '', 'triggerData' => null, 'isAdminBypass' => true ]; } // For logged-in non-admin users: verify nonce if (isset($request->token, $request->id)) { if (!wp_verify_nonce($request->token, $request->id)) { Log::debug_log('Nonce verification failed for logged-in user. FormID=' . $formID); return [ 'valid' => false, 'error' => 'Invalid nonce for logged-in user', 'triggerData' => null, 'isAdminBypass' => false ]; } } else { Log::debug_log('Missing nonce for logged-in user. FormID=' . $formID); return [ 'valid' => false, 'error' => 'Missing nonce', 'triggerData' => null, 'isAdminBypass' => false ]; } } // For non-admin users (both logged-in and anonymous): validate one-time trigger token if (!isset($request->cronNotOk[3]) || empty($request->cronNotOk[3])) { Log::debug_log('Missing trigger token for formID=' . $formID . ', entryID=' . $entryID); return [ 'valid' => false, 'error' => 'Missing trigger token', 'triggerData' => null, 'isAdminBypass' => false ]; } $submittedToken = sanitize_text_field($request->cronNotOk[3]); // Validate trigger token from transient $transientData = get_transient("bitform_trigger_transient_{$entryID}"); if (empty($transientData)) { Log::debug_log('Trigger token transient missing for entryID=' . $entryID . ', logID=' . $logID); return [ 'valid' => false, 'error' => 'Trigger token expired or missing', 'triggerData' => null, 'isAdminBypass' => false ]; } $triggerData = is_string($transientData) ? json_decode($transientData) : $transientData; // Verify token matches and belongs to this entry/log if ( !isset($triggerData['trigger_token']) || !hash_equals($triggerData['trigger_token'], $submittedToken) || (int)$triggerData['entryID'] !== $entryID || (int)$triggerData['logID'] !== $logID ) { Log::debug_log('Invalid trigger token for entryID=' . $entryID . ', logID=' . $logID); return [ 'valid' => false, 'error' => 'Invalid trigger token', 'triggerData' => null, 'isAdminBypass' => false ]; } // Token is valid - delete transient to prevent reuse (single-use token) delete_transient("bitform_trigger_transient_{$entryID}"); Log::debug_log('Valid trigger token consumed for entryID=' . $entryID); return [ 'valid' => true, 'error' => '', 'triggerData' => $triggerData, 'isAdminBypass' => false ]; } public static function validateFormEntryEditPermission($formId, $entryId) { if (is_user_logged_in()) { if (current_user_can('manage_bitform') || current_user_can('bitform_entry_edit') || current_user_can('edit_post')) { return true; } } return false; } public static function honeypotEncryptedToken($str) { $token = base64_encode(base64_encode($str)); return $token; } public static function csrfEecrypted() { $secretKey = get_option('bitform_csrf_secret'); if (!$secretKey) { $secretKey = 'bf-' . time(); update_option('bitform_csrf_secret', $secretKey); } $tIdenty = base64_encode(\random_bytes(32)); $csrf = \base64_encode(\hash_hmac('sha256', $tIdenty, $secretKey, true)); return ['csrf' => $csrf, 't_identity' => $tIdenty]; } public static function csrfDecrypted($identy, $token) { $secretKey = get_option('bitform_csrf_secret'); return \hash_equals( \base64_encode(\hash_hmac('sha256', $identy, $secretKey, true)), $token ); } public static function checkIsIntArr($arr) { $filteredArray = array_filter($arr, 'is_numeric'); $intArray = array_map('intval', $filteredArray); $result = count($arr) === count($intArray); return $result; } public static function getTruncatedEncryptToken($str, $length = 20) { $token = hash_hmac('sha256', $str, self::getBitformSalt()); return substr($token, 0, $length); } public static function getAuthSaltEncryptToken($str, $length = 20) { if (!$str) { return ''; } if (!defined('AUTH_SALT')) { return ''; } return substr(hash_hmac('sha256', $str, AUTH_SALT), 0, $length); } public static function getEncryptedEntryId($entryId) { if (!isset(self::$encryptEntryIds[$entryId])) { self::$encryptEntryIds[$entryId] = self::getTruncatedEncryptToken($entryId); } return self::$encryptEntryIds[$entryId]; } public static function getFullPathWithEncryptedEntryId($formId, $entryId) { $uploadDir = rtrim(BITFORMS_UPLOAD_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $formId . DIRECTORY_SEPARATOR; $encryptDirectoryId = Helpers::getEncryptedEntryId($entryId); $encryptDirectory = $uploadDir . $encryptDirectoryId; if (is_dir($encryptDirectory)) { return $encryptDirectory; } $oldEntriesFileUploadDir = Helpers::getOldEntriesFileUploadDir($uploadDir, $entryId); if (!empty($oldEntriesFileUploadDir) && is_dir($oldEntriesFileUploadDir)) { return $oldEntriesFileUploadDir; } return $encryptDirectory; } public static function getWebPathWithEncryptedEntryId($formId, $entryId) { $serverFileUploadDir = rtrim(BITFORMS_UPLOAD_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $formId . DIRECTORY_SEPARATOR; $webFileDirectory = BITFORMS_UPLOAD_BASE_URL . '/' . 'uploads' . '/' . $formId . '/'; $encryptDirectoryId = Helpers::getEncryptedEntryId($entryId); $encryptDirectory = $serverFileUploadDir . $encryptDirectoryId; if (is_dir($encryptDirectory)) { return $webFileDirectory . $encryptDirectoryId; } $authSaltEncryptedEntryId = Helpers::getAuthSaltEncryptToken($entryId); $authSaltEncryptedDirectory = $serverFileUploadDir . $authSaltEncryptedEntryId; if (!empty($authSaltEncryptedEntryId) && is_dir($authSaltEncryptedDirectory)) { return $webFileDirectory . $authSaltEncryptedEntryId; } $previousEntryDirectory = $serverFileUploadDir . $entryId; if (!empty($previousEntryDirectory) && is_dir($previousEntryDirectory)) { return $webFileDirectory . $entryId; } return $webFileDirectory . $encryptDirectoryId; } public static function getOldEntriesFileUploadDir($uploadDir, $entry_id) { $authSaltEncryptedEntryId = Helpers::getAuthSaltEncryptToken($entry_id); $authSaltEncryptedDirectory = $uploadDir . $authSaltEncryptedEntryId; if (!empty($authSaltEncryptedEntryId) && is_dir($authSaltEncryptedDirectory)) { return $authSaltEncryptedDirectory; } $previousEntryDirectory = $uploadDir . $entry_id; if (!empty($previousEntryDirectory) && is_dir($previousEntryDirectory)) { return $previousEntryDirectory; } return ''; } public static function PDFPassHash($entryId) { return abs(crc32($entryId)); } public static function encryptBinaryData($plaintext) { $iv = openssl_random_pseudo_bytes(16); $encrypted = openssl_encrypt($plaintext, 'AES-256-CBC', BITFORMS_SECRET_KEY, OPENSSL_RAW_DATA, $iv); return bin2hex($iv . $encrypted); } public static function decryptBinaryData($encryptedHex) { $decoded = hex2bin($encryptedHex); $iv = substr($decoded, 0, 16); $cipherText = substr($decoded, 16); return openssl_decrypt($cipherText, 'AES-256-CBC', BITFORMS_SECRET_KEY, OPENSSL_RAW_DATA, $iv); } /** * Sanitize user-provided HTML content by removing dangerous JS code * while allowing all valid HTML/CSS. * * @param string $html Raw HTML from user input * @return string Sanitized safe HTML */ public static function sanitizeUserHTML(string $html): string { // Remove