From a2a96f9752ac50514560e3226a2243ddae232c04 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Tue, 14 Jul 2026 16:32:45 -0400 Subject: [PATCH] Removed unused legacy functions and split the functions into their own file and use function.php to call them --- functions.php | 2320 +--------------------------------------- functions/app.php | 341 ++++++ functions/auth.php | 142 +++ functions/db.php | 224 ++++ functions/domain.php | 228 ++++ functions/files.php | 168 +++ functions/format.php | 332 ++++++ functions/logging.php | 65 ++ functions/request.php | 100 ++ functions/sanitize.php | 148 +++ functions/security.php | 217 ++++ 11 files changed, 1978 insertions(+), 2307 deletions(-) create mode 100644 functions/app.php create mode 100644 functions/auth.php create mode 100644 functions/db.php create mode 100644 functions/domain.php create mode 100644 functions/files.php create mode 100644 functions/format.php create mode 100644 functions/logging.php create mode 100644 functions/request.php create mode 100644 functions/sanitize.php create mode 100644 functions/security.php diff --git a/functions.php b/functions.php index 42f29514..5abd11e9 100644 --- a/functions.php +++ b/functions.php @@ -3,2310 +3,16 @@ // Role check failed wording DEFINE("WORDING_ROLECHECK_FAILED", "You are not permitted to do that!"); -// Function to generate both crypto & URL safe random strings -function randomString(int $length = 16): string { - $bytes = random_bytes((int) ceil($length * 3 / 4)); - return substr( - rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='), - 0, - $length - ); -} - -// Older keygen function - only used for TOTP currently -function key32gen() { - $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - $chars .= "234567"; - while (1) { - $key = ''; - srand((float) microtime() * 1000000); - for ($i = 0; $i < 32; $i++) { - $key .= substr($chars, (rand() % (strlen($chars))), 1); - } - break; - } - return $key; -} - -function nullable_htmlentities($unsanitizedInput) { - //return htmlentities($unsanitizedInput ?? ''); - return htmlspecialchars($unsanitizedInput ?? '', ENT_QUOTES, 'UTF-8'); -} - -function initials($string) { - if (!empty($string)) { - $return = ''; - foreach (explode(' ', $string) as $word) { - $return .= mb_strtoupper($word[0], 'UTF-8'); // Use mb_strtoupper for UTF-8 support - } - $return = substr($return, 0, 2); - return $return; - } -} - -function removeDirectory($path) { - if (!file_exists($path)) { - return; - } - - $files = glob($path . '/*'); - foreach ($files as $file) { - is_dir($file) ? removeDirectory($file) : unlink($file); - } - rmdir($path); -} - -function copyDirectory($src, $dst) { - if (!is_dir($src)) { - return; - } - - if (!is_dir($dst)) { - mkdir($dst, 0775, true); - } - - $items = scandir($src); - foreach ($items as $item) { - if ($item === '.' || $item === '..') { - continue; - } - - $srcPath = $src . '/' . $item; - $dstPath = $dst . '/' . $item; - - if (is_dir($srcPath)) { - copyDirectory($srcPath, $dstPath); - } else { - copy($srcPath, $dstPath); - } - } -} - -function getUserAgent() { - return $_SERVER['HTTP_USER_AGENT']; -} - -function getIP() { - - // Default way to get IP - $ip = $_SERVER['REMOTE_ADDR']; - - // Allow overrides via config.php in-case we use a proxy - https://docs.itflow.org/config_php - if (defined("CONST_GET_IP_METHOD") && CONST_GET_IP_METHOD == "HTTP_X_FORWARDED_FOR") { - $ip = explode(',', getenv('HTTP_X_FORWARDED_FOR'))[0] ?? $_SERVER['REMOTE_ADDR']; - } elseif (defined("CONST_GET_IP_METHOD") && CONST_GET_IP_METHOD == "HTTP_CF_CONNECTING_IP") { - $ip = $_SERVER["HTTP_CF_CONNECTING_IP"] ?? $_SERVER['REMOTE_ADDR']; - } - - // Abort if something isn't right - if (!filter_var($ip, FILTER_VALIDATE_IP)) { - error_log("ITFlow - Could not validate remote IP address"); - error_log("ITFlow - IP was [$ip] using method " . CONST_GET_IP_METHOD); - exit("Potential Security Violation"); - } - - return $ip; -} - -function getWebBrowser($user_browser) { - $browser = "-"; - $browser_array = array( - '/msie/i' => " Internet Explorer", - '/firefox/i' => " Firefox", - '/safari/i' => " Safari", - '/chrome/i' => " Chrome", - '/edg/i' => " Edge", - '/opr/i' => " Opera", - '/ddg/i' => " DuckDuckGo" - ); - foreach ($browser_array as $regex => $value) { - if (preg_match($regex, $user_browser)) { - $browser = $value; - } - } - return $browser; -} - -function getOS($user_os) { - $os_platform = "-"; - $os_array = array( - '/windows/i' => " Windows", - '/macintosh|mac os x/i' => " MacOS", - '/linux/i' => " Linux", - '/ubuntu/i' => " Ubuntu", - '/fedora/i' => " Fedora", - '/iphone/i' => " iPhone", - '/ipad/i' => " iPad", - '/android/i' => " Android" - ); - foreach ($os_array as $regex => $value) { - if (preg_match($regex, $user_os)) { - $os_platform = $value; - } - } - return $os_platform; -} - -function getDevice() { - $tablet_browser = 0; - $mobile_browser = 0; - if (preg_match('/(tablet|ipad|playbook)|(android(?!.*(mobi|opera mini)))/i', strtolower($_SERVER['HTTP_USER_AGENT']))) { - $tablet_browser++; - } - if (preg_match('/(up.browser|up.link|mmp|symbian|smartphone|midp|wap|phone|android|iemobile)/i', strtolower($_SERVER['HTTP_USER_AGENT']))) { - $mobile_browser++; - } - if ((strpos(strtolower($_SERVER['HTTP_ACCEPT']), 'application/vnd.wap.xhtml+xml') > 0) || ((isset($_SERVER['HTTP_X_WAP_PROFILE']) || isset($_SERVER['HTTP_PROFILE'])))) { - $mobile_browser++; - } - $mobile_ua = strtolower(substr(getUserAgent(), 0, 4)); - $mobile_agents = array( - 'w3c ', 'acs-', 'alav', 'alca', 'amoi', 'audi', 'avan', 'benq', 'bird', 'blac', - 'blaz', 'brew', 'cell', 'cldc', 'cmd-', 'dang', 'doco', 'eric', 'hipt', 'inno', - 'ipaq', 'java', 'jigs', 'kddi', 'keji', 'leno', 'lg-c', 'lg-d', 'lg-g', 'lge-', - 'maui', 'maxo', 'midp', 'mits', 'mmef', 'mobi', 'mot-', 'moto', 'mwbp', 'nec-', - 'newt', 'noki', 'palm', 'pana', 'pant', 'phil', 'play', 'port', 'prox', - 'qwap', 'sage', 'sams', 'sany', 'sch-', 'sec-', 'send', 'seri', 'sgh-', 'shar', - 'sie-', 'siem', 'smal', 'smar', 'sony', 'sph-', 'symb', 't-mo', 'teli', 'tim-', - 'tosh', 'tsm-', 'upg1', 'upsi', 'vk-v', 'voda', 'wap-', 'wapa', 'wapi', 'wapp', - 'wapr', 'webc', 'winw', 'winw', 'xda ', 'xda-' - ); - if (in_array($mobile_ua, $mobile_agents)) { - $mobile_browser++; - } - if (strpos(strtolower(getUserAgent()), 'opera mini') > 0) { - $mobile_browser++; - //Check for tablets on Opera Mini alternative headers - $stock_ua = strtolower(isset($_SERVER['HTTP_X_OPERAMINI_PHONE_UA']) ? $_SERVER['HTTP_X_OPERAMINI_PHONE_UA'] : (isset($_SERVER['HTTP_DEVICE_STOCK_UA']) ? $_SERVER['HTTP_DEVICE_STOCK_UA'] : '')); - if (preg_match('/(tablet|ipad|playbook)|(android(?!.*mobile))/i', $stock_ua)) { - $tablet_browser++; - } - } - if ($tablet_browser > 0) { - //do something for tablet devices - return 'Tablet'; - } else if ($mobile_browser > 0) { - //do something for mobile devices - return 'Mobile'; - } else { - //do something for everything else - return 'Computer'; - } -} - -function truncate($text, $chars) { - if (strlen($text) <= $chars) { - return $text; - } - $text = $text . " "; - $text = substr($text, 0, $chars); - $lastSpacePos = strrpos($text, ' '); - if ($lastSpacePos !== false) { - $text = substr($text, 0, $lastSpacePos); - } - return $text . "..."; -} - -function formatPhoneNumber($phoneNumber, $country_code = '', $show_country_code = false) { - // Remove all non-digit characters - $digits = preg_replace('/\D/', '', $phoneNumber ?? ''); - $formatted = ''; - - // If no digits at all, fallback early - if (strlen($digits) === 0) { - return $phoneNumber; - } - - // Helper function to safely check the first digit - $startsWith = function($str, $char) { - return isset($str[0]) && $str[0] === $char; - }; - - switch ($country_code) { - case '1': // USA/Canada - if (strlen($digits) === 10) { - $formatted = '(' . substr($digits, 0, 3) . ') ' . substr($digits, 3, 3) . '-' . substr($digits, 6); - } - break; - - case '44': // UK - if ($startsWith($digits, '0')) { - $digits = substr($digits, 1); - } - if (strlen($digits) === 10) { - $formatted = '0' . substr($digits, 0, 4) . ' ' . substr($digits, 4, 3) . ' ' . substr($digits, 7); - } - break; - - case '61': // Australia - if ($startsWith($digits, '0')) { - $digits = substr($digits, 1); - } - if (strlen($digits) === 9) { - $formatted = '0' . substr($digits, 0, 4) . ' ' . substr($digits, 4, 3) . ' ' . substr($digits, 7); - } - break; - - case '91': // India - if (strlen($digits) === 10) { - $formatted = substr($digits, 0, 5) . ' ' . substr($digits, 5); - } - break; - - case '81': // Japan - if ($startsWith($digits, '0')) { - $digits = substr($digits, 1); - } - if (strlen($digits) >= 9 && strlen($digits) <= 10) { - $formatted = '0' . substr($digits, 0, 2) . '-' . substr($digits, 2, 4) . '-' . substr($digits, 6); - } - break; - - case '49': // Germany - if ($startsWith($digits, '0')) { - $digits = substr($digits, 1); - } - if (strlen($digits) >= 10) { - $formatted = '0' . substr($digits, 0, 3) . ' ' . substr($digits, 3); - } - break; - - case '33': // France - if ($startsWith($digits, '0')) { - $digits = substr($digits, 1); - } - if (strlen($digits) === 9) { - $formatted = '0' . implode(' ', str_split($digits, 2)); - } - break; - - case '34': // Spain - if (strlen($digits) === 9) { - $formatted = substr($digits, 0, 3) . ' ' . substr($digits, 3, 3) . ' ' . substr($digits, 6); - } - break; - - case '39': // Italy - if ($startsWith($digits, '0')) { - $digits = substr($digits, 1); - } - $formatted = '0' . implode(' ', str_split($digits, 3)); - break; - - case '55': // Brazil - if (strlen($digits) === 11) { - $formatted = '(' . substr($digits, 0, 2) . ') ' . substr($digits, 2, 5) . '-' . substr($digits, 7); - } - break; - - case '7': // Russia - if ($startsWith($digits, '8')) { - $digits = substr($digits, 1); - } - if (strlen($digits) === 10) { - $formatted = '8 (' . substr($digits, 0, 3) . ') ' . substr($digits, 3, 3) . '-' . substr($digits, 6, 2) . '-' . substr($digits, 8); - } - break; - - case '86': // China - if (strlen($digits) === 11) { - $formatted = substr($digits, 0, 3) . ' ' . substr($digits, 3, 4) . ' ' . substr($digits, 7); - } - break; - - case '82': // South Korea - if (strlen($digits) === 11) { - $formatted = substr($digits, 0, 3) . '-' . substr($digits, 3, 4) . '-' . substr($digits, 7); - } - break; - - case '62': // Indonesia - if (!$startsWith($digits, '0')) { - $digits = '0' . $digits; - } - if (strlen($digits) === 12) { - $formatted = substr($digits, 0, 4) . ' ' . substr($digits, 4, 4) . ' ' . substr($digits, 8); - } - break; - - case '63': // Philippines - if (strlen($digits) === 11) { - $formatted = substr($digits, 0, 4) . ' ' . substr($digits, 4, 3) . ' ' . substr($digits, 7); - } - break; - - case '234': // Nigeria - if (!$startsWith($digits, '0')) { - $digits = '0' . $digits; - } - if (strlen($digits) === 11) { - $formatted = substr($digits, 0, 4) . ' ' . substr($digits, 4, 3) . ' ' . substr($digits, 7); - } - break; - - case '27': // South Africa - if (strlen($digits) >= 9 && strlen($digits) <= 10) { - $formatted = substr($digits, 0, 3) . ' ' . substr($digits, 3, 3) . ' ' . substr($digits, 6); - } - break; - - case '971': // UAE - if (strlen($digits) === 9) { - $formatted = substr($digits, 0, 3) . ' ' . substr($digits, 3, 3) . ' ' . substr($digits, 6); - } - break; - - default: - // fallback — do nothing, use raw digits later - break; - } - - if (!$formatted) { - $formatted = $digits ?: $phoneNumber; - } - - return $show_country_code && $country_code ? "+$country_code $formatted" : $formatted; -} - -function mkdirMissing($dir) { - if (!is_dir($dir)) { - mkdir($dir); - } -} - -// Called during initial setup -// Encrypts the master key with the user's password -function setupFirstUserSpecificKey($user_password, $site_encryption_master_key) { - $iv = randomString(); - $salt = randomString(); - - //Generate 128-bit (16 byte/char) kdhash of the users password - $user_password_kdhash = hash_pbkdf2('sha256', $user_password, $salt, 100000, 16); - - //Encrypt the master key with the users kdf'd hash and the IV - $ciphertext = openssl_encrypt($site_encryption_master_key, 'aes-128-cbc', $user_password_kdhash, 0, $iv); - - return $salt . $iv . $ciphertext; -} - -/* - * For additional users / password changes (and now the API) - * New Users: Requires the admin setting up their account have a Specific/Session key configured - * Password Changes: Will use the current info in the session. -*/ -function encryptUserSpecificKey($user_password) { - $iv = randomString(); - $salt = randomString(); - - // Get the session info. - $user_encryption_session_ciphertext = $_SESSION['user_encryption_session_ciphertext']; - $user_encryption_session_iv = $_SESSION['user_encryption_session_iv']; - $user_encryption_session_key = $_COOKIE['user_encryption_session_key']; - - // Decrypt the session key to get the master key - $site_encryption_master_key = openssl_decrypt($user_encryption_session_ciphertext, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv); - - // Generate 128-bit (16 byte/char) kdhash of the users (new) password - $user_password_kdhash = hash_pbkdf2('sha256', $user_password, $salt, 100000, 16); - - // Encrypt the master key with the users kdf'd hash and the IV - $ciphertext = openssl_encrypt($site_encryption_master_key, 'aes-128-cbc', $user_password_kdhash, 0, $iv); - - return $salt . $iv . $ciphertext; -} - -// Given a ciphertext (incl. IV) and the user's (or API key) password, returns the site master key -// Ran at login, to facilitate generateUserSessionKey -function decryptUserSpecificKey($user_encryption_ciphertext, $user_password) -{ - //Get the IV, salt and ciphertext - $salt = substr($user_encryption_ciphertext, 0, 16); - $iv = substr($user_encryption_ciphertext, 16, 16); - $ciphertext = substr($user_encryption_ciphertext, 32); - - //Generate 128-bit (16 byte/char) kdhash of the users password - $user_password_kdhash = hash_pbkdf2('sha256', $user_password, $salt, 100000, 16); - - //Use this hash to get the original/master key - return openssl_decrypt($ciphertext, 'aes-128-cbc', $user_password_kdhash, 0, $iv); -} - -/* -Generates what is probably best described as a session key (ephemeral-ish) -- Allows us to store the master key on the server whilst the user is using the application, without prompting to type their password everytime they want to decrypt a credential -- Ciphertext/IV is stored on the server in the users' session, encryption key is controlled/provided by the user as a cookie -- Only the user can decrypt their session ciphertext to get the master key -- Encryption key never hits the disk in cleartext -*/ -function generateUserSessionKey($site_encryption_master_key) { - $user_encryption_session_key = randomString(); - $user_encryption_session_iv = randomString(); - $user_encryption_session_ciphertext = openssl_encrypt($site_encryption_master_key, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv); - - // Store ciphertext in the user's session - $_SESSION['user_encryption_session_ciphertext'] = $user_encryption_session_ciphertext; - $_SESSION['user_encryption_session_iv'] = $user_encryption_session_iv; - - // Give the user "their" key as a cookie - include 'config.php'; - - if ($config_https_only) { - setcookie("user_encryption_session_key", "$user_encryption_session_key", ['path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'None']); - } else { - setcookie("user_encryption_session_key", $user_encryption_session_key, 0, "/"); - $_SESSION['alert_message'] = "Unencrypted connection flag set: Using non-secure cookies."; - } -} - -// Decrypts an encrypted password (website/asset credentials), returns it as a string -function decryptCredentialEntry($credential_password_ciphertext) { - - // Split the credential into IV and Ciphertext - $credential_iv = substr($credential_password_ciphertext, 0, 16); - $credential_ciphertext = $salt = substr($credential_password_ciphertext, 16); - - // Get the user session info. - $user_encryption_session_ciphertext = $_SESSION['user_encryption_session_ciphertext']; - $user_encryption_session_iv = $_SESSION['user_encryption_session_iv']; - $user_encryption_session_key = $_COOKIE['user_encryption_session_key']; - - // Decrypt the session key to get the master key - $site_encryption_master_key = openssl_decrypt($user_encryption_session_ciphertext, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv); - - // Decrypt the credential password using the master key - return openssl_decrypt($credential_ciphertext, 'aes-128-cbc', $site_encryption_master_key, 0, $credential_iv); -} - -// Encrypts a website/asset credential password -function encryptCredentialEntry($credential_password_cleartext) { - $iv = randomString(); - - // Get the user session info. - $user_encryption_session_ciphertext = $_SESSION['user_encryption_session_ciphertext']; - $user_encryption_session_iv = $_SESSION['user_encryption_session_iv']; - $user_encryption_session_key = $_COOKIE['user_encryption_session_key']; - - //Decrypt the session key to get the master key - $site_encryption_master_key = openssl_decrypt($user_encryption_session_ciphertext, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv); - - //Encrypt the website/asset credential using the master key - $ciphertext = openssl_encrypt($credential_password_cleartext, 'aes-128-cbc', $site_encryption_master_key, 0, $iv); - - return $iv . $ciphertext; -} - -function apiDecryptCredentialEntry($credential_ciphertext, $api_key_decrypt_hash, #[\SensitiveParameter]$api_key_decrypt_password) { - // Split the Credential entry (username/password) into IV and Ciphertext - $credential_iv = substr($credential_ciphertext, 0, 16); - $credential_ciphertext = $salt = substr($credential_ciphertext, 16); - - // Decrypt the api hash to get the master key - $site_encryption_master_key = decryptUserSpecificKey($api_key_decrypt_hash, $api_key_decrypt_password); - - // Decrypt the credential password using the master key - return openssl_decrypt($credential_ciphertext, 'aes-128-cbc', $site_encryption_master_key, 0, $credential_iv); -} - -function apiEncryptCredentialEntry(#[\SensitiveParameter]$credential_cleartext, $api_key_decrypt_hash, #[\SensitiveParameter]$api_key_decrypt_password) { - $iv = randomString(); - - // Decrypt the api hash to get the master key - $site_encryption_master_key = decryptUserSpecificKey($api_key_decrypt_hash, $api_key_decrypt_password); - - // Encrypt the credential using the master key - $ciphertext = openssl_encrypt($credential_cleartext, 'aes-128-cbc', $site_encryption_master_key, 0, $iv); - - return $iv . $ciphertext; -} - - -function strtoAZaz09($string) { - // Gets rid of non-alphanumerics - return preg_replace('/[^A-Za-z0-9_-]/', '', $string); -} - -// Cross-Site Request Forgery check for sensitive functions -// Validates the CSRF token provided matches the one in the users session -function validateCSRFToken($token) { - if (hash_equals($token, $_SESSION['csrf_token'])) { - return true; - } else { - $_SESSION['alert_type'] = "warning"; - $_SESSION['alert_message'] = "CSRF token verification failed. Try again, or log out to refresh your token."; - header("Location: index.php"); - exit(); - } -} - -/* - * LEGACY Role validation - * Admin - 3 - * Tech - 2 - * Accountant - 1 - */ - -function validateAdminRole() { - global $session_user_role; - if (!isset($session_user_role) || $session_user_role != 3) { - $_SESSION['alert_type'] = "danger"; - $_SESSION['alert_message'] = WORDING_ROLECHECK_FAILED; - header("Location: " . $_SERVER["HTTP_REFERER"]); - exit(); - } -} - -// LEGACY -// Validates a user is a tech (or admin). Stops page load and attempts to direct away from the page if not (i.e. user is an accountant) -function validateTechRole() { - global $session_user_role; - if (!isset($session_user_role) || $session_user_role == 1) { - $_SESSION['alert_type'] = "danger"; - $_SESSION['alert_message'] = WORDING_ROLECHECK_FAILED; - header("Location: " . $_SERVER["HTTP_REFERER"]); - exit(); - } -} - -// LEGACY -// Validates a user is an accountant (or admin). Stops page load and attempts to direct away from the page if not (i.e. user is a tech) -function validateAccountantRole() { - global $session_user_role; - if (!isset($session_user_role) || $session_user_role == 2) { - $_SESSION['alert_type'] = "danger"; - $_SESSION['alert_message'] = WORDING_ROLECHECK_FAILED; - header("Location: " . $_SERVER["HTTP_REFERER"]); - exit(); - } -} - -function roundUpToNearestMultiple($n, $increment = 1000) { - return (int) ($increment * ceil($n / $increment)); -} - -function getAssetIcon($asset_type) { - if ($asset_type == 'Laptop') { - $device_icon = "laptop"; - } elseif ($asset_type == 'Desktop') { - $device_icon = "desktop"; - } elseif ($asset_type == 'Server') { - $device_icon = "server"; - } elseif ($asset_type == 'Printer') { - $device_icon = "print"; - } elseif ($asset_type == 'Camera') { - $device_icon = "video"; - } elseif ($asset_type == 'Switch') { - $device_icon = "network-wired"; - } elseif ($asset_type == 'Firewall/Router') { - $device_icon = "fire-alt"; - } elseif ($asset_type == 'Access Point') { - $device_icon = "wifi"; - } elseif ($asset_type == 'Phone') { - $device_icon = "phone"; - } elseif ($asset_type == 'Mobile Phone') { - $device_icon = "mobile-alt"; - } elseif ($asset_type == 'Tablet') { - $device_icon = "tablet-alt"; - } elseif ($asset_type == 'Display') { - $device_icon = "tv"; - } elseif ($asset_type == 'Virtual Machine') { - $device_icon = "cloud"; - } else { - $device_icon = "tag"; - } - - return $device_icon; -} - -function getInvoiceBadgeColor($invoice_status) { - if ($invoice_status == "Sent") { - $invoice_badge_color = "warning text-white"; - } elseif ($invoice_status == "Viewed") { - $invoice_badge_color = "info"; - } elseif ($invoice_status == "Partial") { - $invoice_badge_color = "primary"; - } elseif ($invoice_status == "Paid") { - $invoice_badge_color = "success"; - } elseif ($invoice_status == "Cancelled") { - $invoice_badge_color = "danger"; - } else { - $invoice_badge_color = "secondary"; - } - - return $invoice_badge_color; -} - -// Pass $_FILE['file'] to check an uploaded file before saving it -function checkFileUpload($file, $allowed_extensions) { - // Variables - $name = $file['name']; - $tmp = $file['tmp_name']; - $size = $file['size']; - - $extarr = explode('.', $name); - $extension = strtolower(end($extarr)); - - // Check a file is actually attached/uploaded - if ($tmp === '') { - // No file uploaded - return false; - } - - // Check the extension is allowed - if (!in_array($extension, $allowed_extensions)) { - // Extension not allowed - return false; - } - - // Check the size is under 500 MB - $maxSizeBytes = 500 * 1024 * 1024; // 500 MB - if ($size > $maxSizeBytes) { - return "File size exceeds the limit."; - } - - // Read the file content - $fileContent = file_get_contents($tmp); - - // Hash the file content using SHA-256 - $hashedContent = hash('md5', $fileContent); - - // Generate a secure filename using the hashed content - $secureFilename = $hashedContent . randomString(2) . '.' . $extension; - - return $secureFilename; -} - -function sanitizeInput($input) { - global $mysqli; - - if (!empty($input)) { - // Only convert encoding if it's NOT valid UTF-8 - if (!mb_check_encoding($input, 'UTF-8')) { - // Try converting from Windows-1252 as a safe default fallback - $input = mb_convert_encoding($input, 'UTF-8', 'Windows-1252'); - } - } - - // Remove HTML and PHP tags - $input = strip_tags((string) $input); - - // Trim white space - $input = trim($input); - - // Escape for SQL - $input = mysqli_real_escape_string($mysqli, $input); - - return $input; -} - -function cleanInput($input) { - // Only process non-empty input - if (!empty($input)) { - // Normalize encoding to UTF-8 if it’s not valid - if (!mb_check_encoding($input, 'UTF-8')) { - // Convert from Windows-1252 as a safe fallback - $input = mb_convert_encoding($input, 'UTF-8', 'Windows-1252'); - } - } - - // Remove HTML and PHP tags - $input = strip_tags((string) $input); - - // Trim whitespace - $input = trim($input); - - return $input; -} - - -function sanitizeForEmail($data) { - $sanitized = htmlspecialchars($data); - $sanitized = strip_tags($sanitized); - $sanitized = trim($sanitized); - return $sanitized; -} - -function timeAgo($datetime) { - if (is_null($datetime)) { - return "-"; - } - - $time = strtotime($datetime); - $difference = $time - time(); // Changed to handle future dates - - if ($difference == 0) { - return 'right now'; - } - - $isFuture = $difference > 0; // Check if the date is in the future - $difference = abs($difference); // Absolute value for calculation - - $timeRules = array( - 31536000 => 'year', - 2592000 => 'month', - 604800 => 'week', - 86400 => 'day', - 3600 => 'hour', - 60 => 'minute', - 1 => 'second' - ); - - foreach ($timeRules as $secs => $str) { - $div = $difference / $secs; - if ($div >= 1) { - $t = round($div); - $timeStr = $t . ' ' . $str . ($t > 1 ? 's' : ''); - return $isFuture ? 'in ' . $timeStr : $timeStr . ' ago'; - } - } -} - -// Function to remove Emojis in messages, this seems to break the mail queue -function removeEmoji($text) -{ - return preg_replace('/\x{1F3F4}\x{E0067}\x{E0062}(?:\x{E0077}\x{E006C}\x{E0073}|\x{E0073}\x{E0063}\x{E0074}|\x{E0065}\x{E006E}\x{E0067})\x{E007F}|(?:\x{1F9D1}\x{1F3FF}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FF}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FF}\x{200D}\x{1FAF2})[\x{1F3FB}-\x{1F3FE}]|(?:\x{1F9D1}\x{1F3FE}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FE}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FE}\x{200D}\x{1FAF2})[\x{1F3FB}-\x{1F3FD}\x{1F3FF}]|(?:\x{1F9D1}\x{1F3FD}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FD}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FD}\x{200D}\x{1FAF2})[\x{1F3FB}\x{1F3FC}\x{1F3FE}\x{1F3FF}]|(?:\x{1F9D1}\x{1F3FC}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FC}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FC}\x{200D}\x{1FAF2})[\x{1F3FB}\x{1F3FD}-\x{1F3FF}]|(?:\x{1F9D1}\x{1F3FB}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FB}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FB}\x{200D}\x{1FAF2})[\x{1F3FC}-\x{1F3FF}]|\x{1F468}(?:\x{1F3FB}(?:\x{200D}(?:\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FF}]|\x{1F468}[\x{1F3FB}-\x{1F3FF}])|\x{200D}(?:\x{1F48B}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FF}]|\x{1F468}[\x{1F3FB}-\x{1F3FF}]))|\x{1F91D}\x{200D}\x{1F468}[\x{1F3FC}-\x{1F3FF}]|[\x{2695}\x{2696}\x{2708}]\x{FE0F}|[\x{2695}\x{2696}\x{2708}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]))?|[\x{1F3FC}-\x{1F3FF}]\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FF}]|\x{1F468}[\x{1F3FB}-\x{1F3FF}])|\x{200D}(?:\x{1F48B}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FF}]|\x{1F468}[\x{1F3FB}-\x{1F3FF}]))|\x{200D}(?:\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F468}|[\x{1F468}\x{1F469}]\x{200D}(?:\x{1F466}\x{200D}\x{1F466}|\x{1F467}\x{200D}[\x{1F466}\x{1F467}])|\x{1F466}\x{200D}\x{1F466}|\x{1F467}\x{200D}[\x{1F466}\x{1F467}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FF}\x{200D}(?:\x{1F91D}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FE}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FE}\x{200D}(?:\x{1F91D}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FD}\x{1F3FF}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FD}\x{200D}(?:\x{1F91D}\x{200D}\x{1F468}[\x{1F3FB}\x{1F3FC}\x{1F3FE}\x{1F3FF}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FC}\x{200D}(?:\x{1F91D}\x{200D}\x{1F468}[\x{1F3FB}\x{1F3FD}-\x{1F3FF}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])\x{FE0F}|\x{200D}(?:[\x{1F468}\x{1F469}]\x{200D}[\x{1F466}\x{1F467}]|[\x{1F466}\x{1F467}])|\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FF}|\x{1F3FE}|\x{1F3FD}|\x{1F3FC}|\x{200D}[\x{2695}\x{2696}\x{2708}])?|(?:\x{1F469}(?:\x{1F3FB}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}])|\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}]))|[\x{1F3FC}-\x{1F3FF}]\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}])|\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}])))|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]\x{200D}\x{1F91D}\x{200D}\x{1F9D1})[\x{1F3FB}-\x{1F3FF}]|\x{1F469}\x{200D}\x{1F469}\x{200D}(?:\x{1F466}\x{200D}\x{1F466}|\x{1F467}\x{200D}[\x{1F466}\x{1F467}])|\x{1F469}(?:\x{200D}(?:\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}])|\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}]))|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FF}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FE}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FD}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FC}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FB}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F9D1}(?:\x{200D}(?:\x{1F91D}\x{200D}\x{1F9D1}|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FF}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FE}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FD}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FC}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FB}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}[\x{1F466}\x{1F467}]|\x{1F469}\x{200D}\x{1F467}\x{200D}[\x{1F466}\x{1F467}]|(?:\x{1F441}\x{FE0F}?\x{200D}\x{1F5E8}|\x{1F9D1}(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FB}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])|\x{1F469}(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FB}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])|\x{1F636}\x{200D}\x{1F32B}|\x{1F3F3}\x{FE0F}?\x{200D}\x{26A7}|\x{1F43B}\x{200D}\x{2744}|(?:[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93D}\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}][\x{1F3FB}-\x{1F3FF}]|[\x{1F46F}\x{1F9DE}\x{1F9DF}])\x{200D}[\x{2640}\x{2642}]|[\x{26F9}\x{1F3CB}\x{1F3CC}\x{1F575}](?:[\x{FE0F}\x{1F3FB}-\x{1F3FF}]\x{200D}[\x{2640}\x{2642}]|\x{200D}[\x{2640}\x{2642}])|\x{1F3F4}\x{200D}\x{2620}|[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93C}-\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}]\x{200D}[\x{2640}\x{2642}]|[\xA9\xAE\x{203C}\x{2049}\x{2122}\x{2139}\x{2194}-\x{2199}\x{21A9}\x{21AA}\x{231A}\x{231B}\x{2328}\x{23CF}\x{23ED}-\x{23EF}\x{23F1}\x{23F2}\x{23F8}-\x{23FA}\x{24C2}\x{25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}\x{25FC}\x{25FE}\x{2600}-\x{2604}\x{260E}\x{2611}\x{2614}\x{2615}\x{2618}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E}\x{262F}\x{2638}-\x{263A}\x{2640}\x{2642}\x{2648}-\x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{2668}\x{267B}\x{267E}\x{267F}\x{2692}\x{2694}-\x{2697}\x{2699}\x{269B}\x{269C}\x{26A0}\x{26A7}\x{26AA}\x{26B0}\x{26B1}\x{26BD}\x{26BE}\x{26C4}\x{26C8}\x{26CF}\x{26D1}\x{26D3}\x{26E9}\x{26F0}-\x{26F5}\x{26F7}\x{26F8}\x{26FA}\x{2702}\x{2708}\x{2709}\x{270F}\x{2712}\x{2714}\x{2716}\x{271D}\x{2721}\x{2733}\x{2734}\x{2744}\x{2747}\x{2763}\x{27A1}\x{2934}\x{2935}\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x{2B55}\x{3030}\x{303D}\x{3297}\x{3299}\x{1F004}\x{1F170}\x{1F171}\x{1F17E}\x{1F17F}\x{1F202}\x{1F237}\x{1F321}\x{1F324}-\x{1F32C}\x{1F336}\x{1F37D}\x{1F396}\x{1F397}\x{1F399}-\x{1F39B}\x{1F39E}\x{1F39F}\x{1F3CD}\x{1F3CE}\x{1F3D4}-\x{1F3DF}\x{1F3F5}\x{1F3F7}\x{1F43F}\x{1F4FD}\x{1F549}\x{1F54A}\x{1F56F}\x{1F570}\x{1F573}\x{1F576}-\x{1F579}\x{1F587}\x{1F58A}-\x{1F58D}\x{1F5A5}\x{1F5A8}\x{1F5B1}\x{1F5B2}\x{1F5BC}\x{1F5C2}-\x{1F5C4}\x{1F5D1}-\x{1F5D3}\x{1F5DC}-\x{1F5DE}\x{1F5E1}\x{1F5E3}\x{1F5E8}\x{1F5EF}\x{1F5F3}\x{1F5FA}\x{1F6CB}\x{1F6CD}-\x{1F6CF}\x{1F6E0}-\x{1F6E5}\x{1F6E9}\x{1F6F0}\x{1F6F3}])\x{FE0F}|\x{1F441}\x{FE0F}?\x{200D}\x{1F5E8}|\x{1F9D1}(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FB}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])|\x{1F469}(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FB}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])|\x{1F3F3}\x{FE0F}?\x{200D}\x{1F308}|\x{1F469}\x{200D}\x{1F467}|\x{1F469}\x{200D}\x{1F466}|\x{1F636}\x{200D}\x{1F32B}|\x{1F3F3}\x{FE0F}?\x{200D}\x{26A7}|\x{1F635}\x{200D}\x{1F4AB}|\x{1F62E}\x{200D}\x{1F4A8}|\x{1F415}\x{200D}\x{1F9BA}|\x{1FAF1}(?:\x{1F3FF}|\x{1F3FE}|\x{1F3FD}|\x{1F3FC}|\x{1F3FB})?|\x{1F9D1}(?:\x{1F3FF}|\x{1F3FE}|\x{1F3FD}|\x{1F3FC}|\x{1F3FB})?|\x{1F469}(?:\x{1F3FF}|\x{1F3FE}|\x{1F3FD}|\x{1F3FC}|\x{1F3FB})?|\x{1F43B}\x{200D}\x{2744}|(?:[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93D}\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}][\x{1F3FB}-\x{1F3FF}]|[\x{1F46F}\x{1F9DE}\x{1F9DF}])\x{200D}[\x{2640}\x{2642}]|[\x{26F9}\x{1F3CB}\x{1F3CC}\x{1F575}](?:[\x{FE0F}\x{1F3FB}-\x{1F3FF}]\x{200D}[\x{2640}\x{2642}]|\x{200D}[\x{2640}\x{2642}])|\x{1F3F4}\x{200D}\x{2620}|\x{1F1FD}\x{1F1F0}|\x{1F1F6}\x{1F1E6}|\x{1F1F4}\x{1F1F2}|\x{1F408}\x{200D}\x{2B1B}|\x{2764}(?:\x{FE0F}\x{200D}[\x{1F525}\x{1FA79}]|\x{200D}[\x{1F525}\x{1FA79}])|\x{1F441}\x{FE0F}?|\x{1F3F3}\x{FE0F}?|[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93C}-\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}]\x{200D}[\x{2640}\x{2642}]|\x{1F1FF}[\x{1F1E6}\x{1F1F2}\x{1F1FC}]|\x{1F1FE}[\x{1F1EA}\x{1F1F9}]|\x{1F1FC}[\x{1F1EB}\x{1F1F8}]|\x{1F1FB}[\x{1F1E6}\x{1F1E8}\x{1F1EA}\x{1F1EC}\x{1F1EE}\x{1F1F3}\x{1F1FA}]|\x{1F1FA}[\x{1F1E6}\x{1F1EC}\x{1F1F2}\x{1F1F3}\x{1F1F8}\x{1F1FE}\x{1F1FF}]|\x{1F1F9}[\x{1F1E6}\x{1F1E8}\x{1F1E9}\x{1F1EB}-\x{1F1ED}\x{1F1EF}-\x{1F1F4}\x{1F1F7}\x{1F1F9}\x{1F1FB}\x{1F1FC}\x{1F1FF}]|\x{1F1F8}[\x{1F1E6}-\x{1F1EA}\x{1F1EC}-\x{1F1F4}\x{1F1F7}-\x{1F1F9}\x{1F1FB}\x{1F1FD}-\x{1F1FF}]|\x{1F1F7}[\x{1F1EA}\x{1F1F4}\x{1F1F8}\x{1F1FA}\x{1F1FC}]|\x{1F1F5}[\x{1F1E6}\x{1F1EA}-\x{1F1ED}\x{1F1F0}-\x{1F1F3}\x{1F1F7}-\x{1F1F9}\x{1F1FC}\x{1F1FE}]|\x{1F1F3}[\x{1F1E6}\x{1F1E8}\x{1F1EA}-\x{1F1EC}\x{1F1EE}\x{1F1F1}\x{1F1F4}\x{1F1F5}\x{1F1F7}\x{1F1FA}\x{1F1FF}]|\x{1F1F2}[\x{1F1E6}\x{1F1E8}-\x{1F1ED}\x{1F1F0}-\x{1F1FF}]|\x{1F1F1}[\x{1F1E6}-\x{1F1E8}\x{1F1EE}\x{1F1F0}\x{1F1F7}-\x{1F1FB}\x{1F1FE}]|\x{1F1F0}[\x{1F1EA}\x{1F1EC}-\x{1F1EE}\x{1F1F2}\x{1F1F3}\x{1F1F5}\x{1F1F7}\x{1F1FC}\x{1F1FE}\x{1F1FF}]|\x{1F1EF}[\x{1F1EA}\x{1F1F2}\x{1F1F4}\x{1F1F5}]|\x{1F1EE}[\x{1F1E8}-\x{1F1EA}\x{1F1F1}-\x{1F1F4}\x{1F1F6}-\x{1F1F9}]|\x{1F1ED}[\x{1F1F0}\x{1F1F2}\x{1F1F3}\x{1F1F7}\x{1F1F9}\x{1F1FA}]|\x{1F1EC}[\x{1F1E6}\x{1F1E7}\x{1F1E9}-\x{1F1EE}\x{1F1F1}-\x{1F1F3}\x{1F1F5}-\x{1F1FA}\x{1F1FC}\x{1F1FE}]|\x{1F1EB}[\x{1F1EE}-\x{1F1F0}\x{1F1F2}\x{1F1F4}\x{1F1F7}]|\x{1F1EA}[\x{1F1E6}\x{1F1E8}\x{1F1EA}\x{1F1EC}\x{1F1ED}\x{1F1F7}-\x{1F1FA}]|\x{1F1E9}[\x{1F1EA}\x{1F1EC}\x{1F1EF}\x{1F1F0}\x{1F1F2}\x{1F1F4}\x{1F1FF}]|\x{1F1E8}[\x{1F1E6}\x{1F1E8}\x{1F1E9}\x{1F1EB}-\x{1F1EE}\x{1F1F0}-\x{1F1F5}\x{1F1F7}\x{1F1FA}-\x{1F1FF}]|\x{1F1E7}[\x{1F1E6}\x{1F1E7}\x{1F1E9}-\x{1F1EF}\x{1F1F1}-\x{1F1F4}\x{1F1F6}-\x{1F1F9}\x{1F1FB}\x{1F1FC}\x{1F1FE}\x{1F1FF}]|\x{1F1E6}[\x{1F1E8}-\x{1F1EC}\x{1F1EE}\x{1F1F1}\x{1F1F2}\x{1F1F4}\x{1F1F6}-\x{1F1FA}\x{1F1FC}\x{1F1FD}\x{1F1FF}]|[#\*0-9]\x{FE0F}?\x{20E3}|\x{1F93C}[\x{1F3FB}-\x{1F3FF}]|\x{2764}\x{FE0F}?|[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93D}\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}][\x{1F3FB}-\x{1F3FF}]|[\x{26F9}\x{1F3CB}\x{1F3CC}\x{1F575}][\x{FE0F}\x{1F3FB}-\x{1F3FF}]?|\x{1F3F4}|[\x{270A}\x{270B}\x{1F385}\x{1F3C2}\x{1F3C7}\x{1F442}\x{1F443}\x{1F446}-\x{1F450}\x{1F466}\x{1F467}\x{1F46B}-\x{1F46D}\x{1F472}\x{1F474}-\x{1F476}\x{1F478}\x{1F47C}\x{1F483}\x{1F485}\x{1F48F}\x{1F491}\x{1F4AA}\x{1F57A}\x{1F595}\x{1F596}\x{1F64C}\x{1F64F}\x{1F6C0}\x{1F6CC}\x{1F90C}\x{1F90F}\x{1F918}-\x{1F91F}\x{1F930}-\x{1F934}\x{1F936}\x{1F977}\x{1F9B5}\x{1F9B6}\x{1F9BB}\x{1F9D2}\x{1F9D3}\x{1F9D5}\x{1FAC3}-\x{1FAC5}\x{1FAF0}\x{1FAF2}-\x{1FAF6}][\x{1F3FB}-\x{1F3FF}]|[\x{261D}\x{270C}\x{270D}\x{1F574}\x{1F590}][\x{FE0F}\x{1F3FB}-\x{1F3FF}]|[\x{261D}\x{270A}-\x{270D}\x{1F385}\x{1F3C2}\x{1F3C7}\x{1F408}\x{1F415}\x{1F43B}\x{1F442}\x{1F443}\x{1F446}-\x{1F450}\x{1F466}\x{1F467}\x{1F46B}-\x{1F46D}\x{1F472}\x{1F474}-\x{1F476}\x{1F478}\x{1F47C}\x{1F483}\x{1F485}\x{1F48F}\x{1F491}\x{1F4AA}\x{1F574}\x{1F57A}\x{1F590}\x{1F595}\x{1F596}\x{1F62E}\x{1F635}\x{1F636}\x{1F64C}\x{1F64F}\x{1F6C0}\x{1F6CC}\x{1F90C}\x{1F90F}\x{1F918}-\x{1F91F}\x{1F930}-\x{1F934}\x{1F936}\x{1F93C}\x{1F977}\x{1F9B5}\x{1F9B6}\x{1F9BB}\x{1F9D2}\x{1F9D3}\x{1F9D5}\x{1FAC3}-\x{1FAC5}\x{1FAF0}\x{1FAF2}-\x{1FAF6}]|[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93D}\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}]|[\x{1F46F}\x{1F9DE}\x{1F9DF}]|[\xA9\xAE\x{203C}\x{2049}\x{2122}\x{2139}\x{2194}-\x{2199}\x{21A9}\x{21AA}\x{231A}\x{231B}\x{2328}\x{23CF}\x{23ED}-\x{23EF}\x{23F1}\x{23F2}\x{23F8}-\x{23FA}\x{24C2}\x{25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}\x{25FC}\x{25FE}\x{2600}-\x{2604}\x{260E}\x{2611}\x{2614}\x{2615}\x{2618}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E}\x{262F}\x{2638}-\x{263A}\x{2640}\x{2642}\x{2648}-\x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{2668}\x{267B}\x{267E}\x{267F}\x{2692}\x{2694}-\x{2697}\x{2699}\x{269B}\x{269C}\x{26A0}\x{26A7}\x{26AA}\x{26B0}\x{26B1}\x{26BD}\x{26BE}\x{26C4}\x{26C8}\x{26CF}\x{26D1}\x{26D3}\x{26E9}\x{26F0}-\x{26F5}\x{26F7}\x{26F8}\x{26FA}\x{2702}\x{2708}\x{2709}\x{270F}\x{2712}\x{2714}\x{2716}\x{271D}\x{2721}\x{2733}\x{2734}\x{2744}\x{2747}\x{2763}\x{27A1}\x{2934}\x{2935}\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x{2B55}\x{3030}\x{303D}\x{3297}\x{3299}\x{1F004}\x{1F170}\x{1F171}\x{1F17E}\x{1F17F}\x{1F202}\x{1F237}\x{1F321}\x{1F324}-\x{1F32C}\x{1F336}\x{1F37D}\x{1F396}\x{1F397}\x{1F399}-\x{1F39B}\x{1F39E}\x{1F39F}\x{1F3CD}\x{1F3CE}\x{1F3D4}-\x{1F3DF}\x{1F3F5}\x{1F3F7}\x{1F43F}\x{1F4FD}\x{1F549}\x{1F54A}\x{1F56F}\x{1F570}\x{1F573}\x{1F576}-\x{1F579}\x{1F587}\x{1F58A}-\x{1F58D}\x{1F5A5}\x{1F5A8}\x{1F5B1}\x{1F5B2}\x{1F5BC}\x{1F5C2}-\x{1F5C4}\x{1F5D1}-\x{1F5D3}\x{1F5DC}-\x{1F5DE}\x{1F5E1}\x{1F5E3}\x{1F5E8}\x{1F5EF}\x{1F5F3}\x{1F5FA}\x{1F6CB}\x{1F6CD}-\x{1F6CF}\x{1F6E0}-\x{1F6E5}\x{1F6E9}\x{1F6F0}\x{1F6F3}]|[\x{23E9}-\x{23EC}\x{23F0}\x{23F3}\x{25FD}\x{2693}\x{26A1}\x{26AB}\x{26C5}\x{26CE}\x{26D4}\x{26EA}\x{26FD}\x{2705}\x{2728}\x{274C}\x{274E}\x{2753}-\x{2755}\x{2757}\x{2795}-\x{2797}\x{27B0}\x{27BF}\x{2B50}\x{1F0CF}\x{1F18E}\x{1F191}-\x{1F19A}\x{1F201}\x{1F21A}\x{1F22F}\x{1F232}-\x{1F236}\x{1F238}-\x{1F23A}\x{1F250}\x{1F251}\x{1F300}-\x{1F320}\x{1F32D}-\x{1F335}\x{1F337}-\x{1F37C}\x{1F37E}-\x{1F384}\x{1F386}-\x{1F393}\x{1F3A0}-\x{1F3C1}\x{1F3C5}\x{1F3C6}\x{1F3C8}\x{1F3C9}\x{1F3CF}-\x{1F3D3}\x{1F3E0}-\x{1F3F0}\x{1F3F8}-\x{1F407}\x{1F409}-\x{1F414}\x{1F416}-\x{1F43A}\x{1F43C}-\x{1F43E}\x{1F440}\x{1F444}\x{1F445}\x{1F451}-\x{1F465}\x{1F46A}\x{1F479}-\x{1F47B}\x{1F47D}-\x{1F480}\x{1F484}\x{1F488}-\x{1F48E}\x{1F490}\x{1F492}-\x{1F4A9}\x{1F4AB}-\x{1F4FC}\x{1F4FF}-\x{1F53D}\x{1F54B}-\x{1F54E}\x{1F550}-\x{1F567}\x{1F5A4}\x{1F5FB}-\x{1F62D}\x{1F62F}-\x{1F634}\x{1F637}-\x{1F644}\x{1F648}-\x{1F64A}\x{1F680}-\x{1F6A2}\x{1F6A4}-\x{1F6B3}\x{1F6B7}-\x{1F6BF}\x{1F6C1}-\x{1F6C5}\x{1F6D0}-\x{1F6D2}\x{1F6D5}-\x{1F6D7}\x{1F6DD}-\x{1F6DF}\x{1F6EB}\x{1F6EC}\x{1F6F4}-\x{1F6FC}\x{1F7E0}-\x{1F7EB}\x{1F7F0}\x{1F90D}\x{1F90E}\x{1F910}-\x{1F917}\x{1F920}-\x{1F925}\x{1F927}-\x{1F92F}\x{1F93A}\x{1F93F}-\x{1F945}\x{1F947}-\x{1F976}\x{1F978}-\x{1F9B4}\x{1F9B7}\x{1F9BA}\x{1F9BC}-\x{1F9CC}\x{1F9D0}\x{1F9E0}-\x{1F9FF}\x{1FA70}-\x{1FA74}\x{1FA78}-\x{1FA7C}\x{1FA80}-\x{1FA86}\x{1FA90}-\x{1FAAC}\x{1FAB0}-\x{1FABA}\x{1FAC0}-\x{1FAC2}\x{1FAD0}-\x{1FAD9}\x{1FAE0}-\x{1FAE7}]/u', '', $text); -} - -function shortenClient($client) { - // Pre-process by removing any non-alphanumeric characters except for certain punctuations. - $client = html_entity_decode($client); // Decode any HTML entities - $client = str_replace("'", "", $client); // Removing all occurrences of ' - $cleaned = preg_replace('/[^a-zA-Z0-9&]+/', ' ', $client); - - // Break into words. - $words = explode(' ', trim($cleaned)); - - $shortened = ''; - - // If there's only one word. - if (count($words) == 1) { - $word = $words[0]; - - if (strlen($word) <= 3) { - return strtoupper($word); - } - - // Prefer starting and ending characters. - $shortened = $word[0] . substr($word, -2); - } else { - // Less weightage to common words. - $commonWords = ['the', 'of', 'and']; - - foreach ($words as $word) { - if (!in_array(strtolower($word), $commonWords) || strlen($shortened) < 2) { - $shortened .= $word[0]; - } - } - - // If there are still not enough characters, take from the last word. - while (strlen($shortened) < 3 && !empty($word)) { - $shortened .= substr($word, 1, 1); - $word = substr($word, 1); - } - } - - return strtoupper(substr($shortened, 0, 3)); -} - -function roundToNearest15($time) { - // Validate the input time format - if (!preg_match('/^(\d{2}):(\d{2}):(\d{2})$/', $time, $matches)) { - return false; // or throw an exception - } - - // Extract hours, minutes, and seconds from the matched time string - list(, $hours, $minutes, $seconds) = $matches; - - // Convert everything to seconds for easier calculation - $totalSeconds = ($hours * 3600) + ($minutes * 60) + $seconds; - - // Calculate the remainder when divided by 900 seconds (15 minutes) - $remainder = $totalSeconds % 900; - - if ($remainder > 450) { // If remainder is more than 7.5 minutes (450 seconds), round up - $totalSeconds += (900 - $remainder); - } else { // Else round down - $totalSeconds -= $remainder; - } - - // Convert total seconds to decimal hours - $decimalHours = $totalSeconds / 3600; - - // Return the decimal hours - return number_format($decimalHours, 2); -} - -function getMonthlyTax($tax_name, $month, $year, $mysqli) -{ - // SQL to calculate monthly tax - $sql = "SELECT SUM(item_tax) AS monthly_tax FROM invoice_items - LEFT JOIN invoices ON invoice_items.item_invoice_id = invoices.invoice_id - LEFT JOIN payments ON invoices.invoice_id = payments.payment_invoice_id - WHERE YEAR(payments.payment_date) = $year AND MONTH(payments.payment_date) = $month - AND invoice_items.item_tax_id = (SELECT tax_id FROM taxes WHERE tax_name = '$tax_name')"; - $result = mysqli_query($mysqli, $sql); - $row = mysqli_fetch_assoc($result); - return $row['monthly_tax'] ?? 0; -} - -function getQuarterlyTax($tax_name, $quarter, $year, $mysqli) -{ - // Calculate start and end months for the quarter - $start_month = ($quarter - 1) * 3 + 1; - $end_month = $start_month + 2; - - // SQL to calculate quarterly tax - $sql = "SELECT SUM(item_tax) AS quarterly_tax FROM invoice_items - LEFT JOIN invoices ON invoice_items.item_invoice_id = invoices.invoice_id - LEFT JOIN payments ON invoices.invoice_id = payments.payment_invoice_id - WHERE YEAR(payments.payment_date) = $year AND MONTH(payments.payment_date) BETWEEN $start_month AND $end_month - AND invoice_items.item_tax_id = (SELECT tax_id FROM taxes WHERE tax_name = '$tax_name')"; - $result = mysqli_query($mysqli, $sql); - $row = mysqli_fetch_assoc($result); - return $row['quarterly_tax'] ?? 0; -} - -function getTotalTax($tax_name, $year, $mysqli) -{ - // SQL to calculate total tax - $sql = "SELECT SUM(item_tax) AS total_tax FROM invoice_items - LEFT JOIN invoices ON invoice_items.item_invoice_id = invoices.invoice_id - LEFT JOIN payments ON invoices.invoice_id = payments.payment_invoice_id - WHERE YEAR(payments.payment_date) = $year - AND invoice_items.item_tax_id = (SELECT tax_id FROM taxes WHERE tax_name = '$tax_name')"; - $result = mysqli_query($mysqli, $sql); - $row = mysqli_fetch_assoc($result); - return $row['total_tax'] ?? 0; -} - -function generateReadablePassword($security_level) -{ - // Cap security level at 5 - $security_level = intval($security_level); - $security_level = min($security_level, 5); - - // Arrays of words - $articles = ['The', 'A']; - $adjectives = ['Smart', 'Swift', 'Secure', 'Stable', 'Digital', 'Virtual', 'Active', 'Dynamic', 'Innovative', 'Efficient', 'Portable', 'Wireless', 'Rapid', 'Intuitive', 'Automated', 'Robust', 'Reliable', 'Sleek', 'Modern', 'Happy', 'Funny', 'Quick', 'Bright', 'Clever', 'Gentle', 'Brave', 'Calm', 'Eager', 'Fierce', 'Kind', 'Lucky', 'Proud', 'Silly', 'Witty', 'Bold', 'Curious', 'Elated', 'Gracious', 'Honest', 'Jolly', 'Merry', 'Noble', 'Optimistic', 'Playful', 'Quirky', 'Rustic', 'Steady', 'Tranquil', 'Upbeat']; - $nouns = ['Computer', 'Laptop', 'Tablet', 'Server', 'Router', 'Software', 'Hardware', 'Pixel', 'Byte', 'App', 'Network', 'Cloud', 'Firewall', 'Email', 'Database', 'Folder', 'Document', 'Interface', 'Program', 'Gadget', 'Dinosaur', 'Tiger', 'Elephant', 'Kangaroo', 'Monkey', 'Unicorn', 'Dragon', 'Puppy', 'Kitten', 'Parrot', 'Lion', 'Bear', 'Fox', 'Wolf', 'Rabbit', 'Deer', 'Owl', 'Hedgehog', 'Turtle', 'Frog', 'Butterfly', 'Panda', 'Giraffe', 'Zebra', 'Peacock', 'Koala', 'Raccoon', 'Squirrel', 'Hippo', 'Rhino', 'Book', "Monitor"]; - $verbs = ['Connects', 'Runs', 'Processes', 'Secures', 'Encrypts', 'Saves', 'Updates', 'Boots', 'Scans', 'Compiles', 'Executes', 'Restores', 'Installs', 'Configures', 'Downloads', 'Streams', 'BacksUp', 'Syncs', 'Browses', 'Navigates', 'Runs', 'Jumps', 'Flies', 'Swims', 'Dances', 'Sings', 'Hops', 'Skips', 'Races', 'Climbs', 'Crawls', 'Glides', 'Twirls', 'Swings', 'Sprints', 'Gallops', 'Trots', 'Wanders', 'Strolls', 'Marches']; - $adverbs = ['Quickly', 'Slowly', 'Gracefully', 'Wildly', 'Loudly', 'Silently', 'Cheerfully', 'Eagerly', 'Gently', 'Happily', 'Jovially', 'Kindly', 'Lazily', 'Merrily', 'Neatly', 'Politely', 'Quietly', 'Rapidly', 'Smoothly', 'Tightly', 'Swiftly', 'Securely', 'Efficiently', 'Rapidly', 'Smoothly', 'Reliably', 'Safely', 'Wirelessly', 'Instantly', 'Silently', 'Automatically', 'Seamlessly', 'Digitally', 'Virtually', 'Continuously', 'Regularly', 'Intelligently', 'Logically']; - - // Randomly select words from arrays - $adj = $adjectives[array_rand($adjectives)]; - $noun = $nouns[array_rand($nouns)]; - $verb = $verbs[array_rand($verbs)]; - $adv = $adverbs[array_rand($adverbs)]; - - // Combine to create a base password - $password = $adj . $noun . $verb . $adv; - - // Select an article randomly - $article = $articles[array_rand($articles)]; - - // Determine if we should use 'An' instead of 'A' - if ($article == 'A' && preg_match('/^[aeiouAEIOU]/', $adj)) { - $article = 'An'; - } - - // Add the article to the password - $password = $article . $password; - - // Mapping of letters to special characters and numbers - $mappings = [ - 'A' => '@', 'a' => '@', - 'E' => '3', 'e' => '3', - 'I' => '!', 'i' => '!', - 'O' => '0', 'o' => '0', - 'S' => '$', 's' => '$', - 'T' => '+', 't' => '+', - 'B' => '8', 'b' => '8' - ]; - - // Generate an array of indices based on the password length - $indices = range(0, strlen($password) - 1); - // Randomly shuffle the indices - shuffle($indices); - - // Iterate through the shuffled indices and replace characters based on the security level - for ($i = 0; $i < min($security_level, strlen($password)); $i++) { - $index = $indices[$i]; // Get a random index - $currentChar = $password[$index]; // Get the character at this index - // Check if the current character has a mapping and replace it - if (array_key_exists($currentChar, $mappings)) { - $password[$index] = $mappings[$currentChar]; - } - } - - // Add as many random numbers as the security level - $password .= rand(pow(10, $security_level - 1), pow(10, $security_level) - 1); - - return $password; -} - -function addToMailQueue($data) { - - global $mysqli; - - foreach ($data as $email) { - $from = strval($email['from']); - $from_name = strval($email['from_name']); - $recipient = strval($email['recipient']); - $recipient_name = strval($email['recipient_name']); - $subject = strval($email['subject']); - $body = strval($email['body']); - - $cal_str = ''; - if (isset($email['cal_str'])) { - $cal_str = mysqli_escape_string($mysqli, $email['cal_str']); - } - - // Check if 'email_queued_at' is set and not empty - if (isset($email['queued_at']) && !empty($email['queued_at'])) { - $queued_at = "'" . sanitizeInput($email['queued_at']) . "'"; - } else { - // Use the current date and time if 'email_queued_at' is not set or empty - $queued_at = 'CURRENT_TIMESTAMP()'; - } - - mysqli_query($mysqli, "INSERT INTO email_queue SET email_recipient = '$recipient', email_recipient_name = '$recipient_name', email_from = '$from', email_from_name = '$from_name', email_subject = '$subject', email_content = '$body', email_queued_at = $queued_at, email_cal_str = '$cal_str'"); - } - - return true; -} - -function createiCalStr($datetime, $title, $description, $location) -{ - require_once "libs/zapcal/zapcallib.php"; - - // Create the iCal object - $cal_event = new ZCiCal(); - $event = new ZCiCalNode("VEVENT", $cal_event->curnode); - - - // Set the method to REQUEST to indicate an invite - $event->addNode(new ZCiCalDataNode("METHOD:REQUEST")); - $event->addNode(new ZCiCalDataNode("SUMMARY:" . $title)); - $event->addNode(new ZCiCalDataNode("DTSTART:" . ZCiCal::fromSqlDateTime($datetime))); - // Assuming the end time is the same as start time. - // Todo: adjust this for actual duration - $event->addNode(new ZCiCalDataNode("DTEND:" . ZCiCal::fromSqlDateTime($datetime))); - $event->addNode(new ZCiCalDataNode("DTSTAMP:" . ZCiCal::fromSqlDateTime())); - $uid = date('Y-m-d-H-i-s') . "@" . $_SERVER['SERVER_NAME']; - $event->addNode(new ZCiCalDataNode("UID:" . $uid)); - $event->addNode(new ZCiCalDataNode("LOCATION:" . $location)); - $event->addNode(new ZCiCalDataNode("DESCRIPTION:" . $description)); - // Todo: add organizer details - // $event->addNode(new ZCiCalDataNode("ORGANIZER;CN=Organizer Name:MAILTO:organizer@example.com")); - - // Return the iCal string - return $cal_event->export(); -} - -function isMobile() -{ - // Check if the user agent is a mobile device - return preg_match('/(android|avantgo|blackberry|bolt|boost|cricket|docomo|fone|hiptop|mini|opera mini|palm|phone|pie|tablet|up.browser|up.link|webos|wos)/i', $_SERVER['HTTP_USER_AGENT']); -} - -function createiCalStrCancel($originaliCalStr) { - require_once "libs/zapcal/zapcallib.php"; - - // Import the original iCal string - $cal_event = new ZCiCal($originaliCalStr); - - // Iterate through the iCalendar object to find VEVENT nodes - foreach($cal_event->tree->child as $node) { - if($node->getName() == "VEVENT") { - // Check if STATUS node exists, update it, or add a new one - $statusFound = false; - foreach($node->data as $key => $value) { - if($key == "STATUS") { - $value->setValue("CANCELLED"); - $statusFound = true; - break; // Exit the loop once the STATUS is updated - } - } - // If STATUS node is not found, add a new STATUS node - if (!$statusFound) { - $node->addNode(new ZCiCalDataNode("STATUS:CANCELLED")); - } - } - } - - // Return the modified iCal string - return $cal_event->export(); -} - -function getTicketStatusName($ticket_status) { - - global $mysqli; - - $status_id = intval($ticket_status); - $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM ticket_statuses WHERE ticket_status_id = $status_id LIMIT 1")); - - if ($row) { - return nullable_htmlentities($row['ticket_status_name']); - } - - // Default return - return "Unknown"; - -} - -function fetchUpdates() { - - global $repo_branch; - - // Fetch the latest code changes but don't apply them - exec("git fetch", $output, $result); - $latest_version = exec("git rev-parse origin/$repo_branch"); - $current_version = exec("git rev-parse HEAD"); - - if ($current_version == $latest_version) { - $update_message = "No Updates available"; - } else { - $update_message = "New Updates are Available [$latest_version]"; - } - - - $updates = new stdClass(); - $updates->output = $output; - $updates->result = $result; - $updates->current_version = $current_version; - $updates->latest_version = $latest_version; - $updates->update_message = $update_message; - - - return $updates; - -} - - -// Plain HTTPS GET via curl (used for RDAP) -function rdapHttpGet($url, &$http_code = null) { - $ch = curl_init($url); - curl_setopt_array($ch, array( - CURLOPT_RETURNTRANSFER => true, - CURLOPT_CONNECTTIMEOUT => 5, - CURLOPT_TIMEOUT => 10, - CURLOPT_USERAGENT => 'ITFlow-Domain-Check', - CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, - CURLOPT_FOLLOWLOCATION => true, // RDAP bootstrap/redirectors use 30x - CURLOPT_MAXREDIRS => 5, - CURLOPT_HTTPHEADER => array('Accept: application/rdap+json'), - )); - - $response = curl_exec($ch); - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - return ($response === false) ? '' : $response; -} - -// Find the RDAP base URL for a TLD using IANA's bootstrap registry, -// cached locally for a week (the mapping changes very rarely) -function getRdapBaseUrl($tld) { - static $bootstrap = null; - - if ($bootstrap === null) { - $cache_file = sys_get_temp_dir() . '/itflow_rdap_bootstrap.json'; - - if (is_readable($cache_file) && (time() - filemtime($cache_file)) < 604800) { - $bootstrap = json_decode(file_get_contents($cache_file), true); - } - - if (empty($bootstrap['services'])) { - $raw = rdapHttpGet('https://data.iana.org/rdap/dns.json', $code); - if ($code === 200 && !empty($raw)) { - $decoded = json_decode($raw, true); - if (!empty($decoded['services'])) { - $bootstrap = $decoded; - @file_put_contents($cache_file, $raw); - } - } - } - - if (empty($bootstrap['services'])) { - $bootstrap = array('services' => array()); // don't retry every call this run - } - } - - foreach ($bootstrap['services'] as $service) { - // [0] = list of TLDs, [1] = list of base URLs - if (in_array($tld, $service[0])) { - return rtrim($service[1][0], '/') . '/'; - } - } - - return ''; -} - -// Fetch and decode the RDAP record for a domain (cached per-run so -// getDomainRecords + getDomainExpirationDate on the same domain = one request) -function getDomainRdap($domain) { - static $cache = array(); - - if (array_key_exists($domain, $cache)) { - return $cache[$domain]; - } - $cache[$domain] = null; - - $tld = substr(strrchr($domain, '.'), 1); - if (empty($tld)) { - return null; - } - - // Primary: IANA bootstrap -> registry RDAP server directly - $base = getRdapBaseUrl($tld); - if (!empty($base)) { - $raw = rdapHttpGet($base . 'domain/' . rawurlencode($domain), $code); - if ($code === 200 && !empty($raw)) { - $decoded = json_decode($raw, true); - if (is_array($decoded)) { - $cache[$domain] = $decoded; - return $decoded; - } - } - if ($code === 404) { - return null; // Domain genuinely not found - don't bother the fallback - } - } - - // Fallback: rdap.org redirector (covers gaps and bootstrap fetch failures) - $raw = rdapHttpGet('https://rdap.org/domain/' . rawurlencode($domain), $code); - if ($code === 200 && !empty($raw)) { - $decoded = json_decode($raw, true); - if (is_array($decoded)) { - $cache[$domain] = $decoded; - } - } - - return $cache[$domain]; -} - -// Pull a named event date (expiration, registration, last changed) from RDAP -function getRdapEventDate($rdap, $action) { - if (empty($rdap['events']) || !is_array($rdap['events'])) { - return ''; - } - foreach ($rdap['events'] as $event) { - if (isset($event['eventAction'], $event['eventDate']) && $event['eventAction'] === $action) { - return $event['eventDate']; - } - } - return ''; -} - -// Registrar name from RDAP entities (vCard "fn" field) -function getRdapRegistrar($rdap) { - if (empty($rdap['entities']) || !is_array($rdap['entities'])) { - return ''; - } - foreach ($rdap['entities'] as $entity) { - if (empty($entity['roles']) || !in_array('registrar', $entity['roles'])) { - continue; - } - if (!empty($entity['vcardArray'][1]) && is_array($entity['vcardArray'][1])) { - foreach ($entity['vcardArray'][1] as $field) { - if (isset($field[0], $field[3]) && $field[0] === 'fn' && is_string($field[3])) { - return $field[3]; - } - } - } - } - return ''; -} - -// Build a human-readable summary to fill the old "whois" display field -function getRdapSummary($rdap) { - $lines = array(); - - $registrar = getRdapRegistrar($rdap); - if (!empty($registrar)) { - $lines[] = "Registrar: $registrar"; - } - - $registered = getRdapEventDate($rdap, 'registration'); - if (!empty($registered)) { - $lines[] = 'Registered: ' . substr($registered, 0, 10); - } - - $expires = getRdapEventDate($rdap, 'expiration'); - if (!empty($expires)) { - $lines[] = 'Expires: ' . substr($expires, 0, 10); - } - - if (!empty($rdap['status']) && is_array($rdap['status'])) { - $lines[] = 'Status: ' . implode(', ', array_slice($rdap['status'], 0, 3)); - } - - if (!empty($rdap['nameservers']) && is_array($rdap['nameservers'])) { - $ns = array(); - foreach ($rdap['nameservers'] as $nameserver) { - if (!empty($nameserver['ldhName'])) { - $ns[] = strtolower($nameserver['ldhName']); - } - } - if (!empty($ns)) { - sort($ns); - $lines[] = 'Nameservers: ' . implode(', ', $ns); - } - } - - return implode("\n", $lines); -} - -// Raw whois query against a server on port 43 (fallback for TLDs without RDAP) -function whoisSocketQuery($server, $query) { - $response = ''; - - $fp = @fsockopen($server, 43, $errno, $errstr, 5); - if (!$fp) { - return ''; - } - - stream_set_timeout($fp, 5); - fwrite($fp, $query . "\r\n"); - - while (!feof($fp)) { - $line = fgets($fp, 1024); - if ($line === false) { - break; - } - $response .= $line; - - // Sanity cap - expiry/registrar fields always appear well before this - if (strlen($response) > 32768) { - break; - } - } - fclose($fp); - - return $response; -} - -// Look up the responsible whois server via IANA, query it, follow one registrar referral -// Returns the full raw response - callers trim as needed -function getDomainWhois($domain) { - $tld = substr(strrchr($domain, '.'), 1); - if (empty($tld)) { - return ''; - } - - // Ask IANA which whois server handles this TLD - $server = ''; - $iana_response = whoisSocketQuery('whois.iana.org', $tld); - if (preg_match('/^whois:\s*(\S+)/mi', $iana_response, $matches)) { - $server = $matches[1]; - } - if (empty($server)) { - return ''; - } - - // Verisign registries match nameservers too unless you use exact-match syntax - $query = in_array($tld, array('com', 'net')) ? "=$domain" : $domain; - - $result = whoisSocketQuery($server, $query); - - // Thin registries (.com/.net) refer to the registrar's whois - follow it once - if (preg_match('/Registrar WHOIS Server:\s*(\S+)/i', $result, $matches)) { - $referral = rtrim(trim($matches[1]), '/'); - $referral = preg_replace('#^r?whois://#i', '', $referral); - if (!empty($referral) && strcasecmp($referral, $server) !== 0) { - $referred_result = whoisSocketQuery($referral, $domain); - if (trim($referred_result) !== '') { - $result = $referred_result; - } - } - } - - return $result; -} - -// Get domain general info (whois + NS/A/MX records) - no shell_exec -function getDomainRecords($name) { - $records = array( - 'a' => '', - 'ns' => '', - 'mx' => '', - 'txt' => '', - 'whois' => '', - 'expire' => '' - ); - - // Only run if we think the domain is valid - if (!filter_var($name, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) || !checkdnsrr($name, 'SOA')) { - return $records; - } - - // Anchored so we don't mangle domains that merely start with "www" - $domain = preg_replace('/^www\./i', '', strtolower(trim($name))); - - // A records - $a = @dns_get_record($domain, DNS_A); - if (is_array($a) && !empty($a)) { - $a_records = array_column($a, 'ip'); - sort($a_records); - $records['a'] = implode("\n", $a_records); - } - - // NS records - $ns = @dns_get_record($domain, DNS_NS); - if (is_array($ns) && !empty($ns)) { - $ns_records = array_column($ns, 'target'); - sort($ns_records); - $records['ns'] = implode("\n", $ns_records); - } - - // MX records - mimic dig +short output format ("10 mail.example.com") - $mx = @dns_get_record($domain, DNS_MX); - if (is_array($mx) && !empty($mx)) { - $mx_records = array(); - foreach ($mx as $record) { - $mx_records[] = $record['pri'] . ' ' . $record['target']; - } - sort($mx_records, SORT_NATURAL); - $records['mx'] = implode("\n", $mx_records); - } - - // TXT records - $txt = @dns_get_record($domain, DNS_TXT); - if (is_array($txt) && !empty($txt)) { - $txt_records = array_column($txt, 'txt'); - sort($txt_records); - $records['txt'] = implode("\n", $txt_records); - } - - // Registration data - RDAP first, legacy whois only if the TLD has no RDAP - $rdap = getDomainRdap($domain); - if ($rdap !== null) { - $records['whois'] = substr(getRdapSummary($rdap), 0, 254); - - $expires = getRdapEventDate($rdap, 'expiration'); - if (!empty($expires)) { - $parsed = date_create($expires); - if ($parsed) { - $records['expire'] = $parsed->format('Y-m-d'); - } - } - } else { - $whois_raw = getDomainWhois($domain); - if (!empty($whois_raw) && stripos($whois_raw, 'rate limit') === false) { - // Approximate the old `head -30 | sed 's/ //g'` - $lines = array_slice(explode("\n", $whois_raw), 0, 30); - $lines = array_map(function ($line) { - return preg_replace('/ +/', ' ', rtrim($line)); - }, $lines); - $records['whois'] = substr(trim(strip_tags(implode("\n", $lines))), 0, 254); - } - } - - return $records; -} - -// Used to automatically attempt to get SSL certificates as part of adding domains -// The logic for the fetch (sync) button on the client_certificates page is in ajax.php, and allows ports other than 443 -function getSSL($full_name) { - - // Parse host and port - $name = parse_url("//$full_name", PHP_URL_HOST); - $port = parse_url("//$full_name", PHP_URL_PORT); - - // Default port - if (!$port) { - $port = "443"; - } - - $certificate = array(); - $certificate['success'] = false; - - // Only run if we think the domain is valid - if (!filter_var($name, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { - $certificate['expire'] = ''; - $certificate['issued_by'] = ''; - $certificate['public_key'] = ''; - return $certificate; - } - - // Get SSL/TSL certificate (using verify peer false to allow for self-signed certs) for domain on default port - $socket = "ssl://$name:$port"; - $get = stream_context_create(array("ssl" => array("capture_peer_cert" => true, "verify_peer" => false,))); - $read = stream_socket_client($socket, $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $get); - - // If the socket connected - if ($read) { - $cert = stream_context_get_params($read); - $cert_public_key_obj = openssl_x509_parse($cert['options']['ssl']['peer_certificate']); - openssl_x509_export($cert['options']['ssl']['peer_certificate'], $export); - - if ($cert_public_key_obj) { - $certificate['success'] = true; - $certificate['expire'] = date('Y-m-d', $cert_public_key_obj['validTo_time_t']); - $certificate['issued_by'] = strip_tags($cert_public_key_obj['issuer']['O']); - $certificate['public_key'] = $export; - } - } - - return $certificate; -} - -// Get domain expiration date - RDAP first, whois parsing as last resort -function getDomainExpirationDate($domain) { - if (!filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) || !checkdnsrr($domain, 'SOA')) { - return null; - } - - $domain = preg_replace('/^www\./i', '', strtolower(trim($domain))); - - // RDAP: expiration is a structured field - no regex, no date-format guessing - $rdap = getDomainRdap($domain); - if ($rdap !== null) { - $expires = getRdapEventDate($rdap, 'expiration'); - if (!empty($expires)) { - $parsed = date_create($expires); - if ($parsed) { - return $parsed->format('Y-m-d'); - } - } - return null; // RDAP answered but had no expiry (rare) - trust it, don't re-query - } - - // Fallback for TLDs without RDAP: legacy whois parsing - $result = getDomainWhois($domain); - if (empty($result) || stripos($result, 'rate limit') !== false) { - return null; - } - - // Every expiry label seen in the wild, longest/most-specific first - $labels = array( - 'Registrar Registration Expiration Date', - 'Registry Expiry Date', - 'Expiration Date', - 'Expiration Time', - '\[Expires on\]', - 'Expires On', - 'Expiry Date', - 'Expire Date', - 'expire-date', - 'renewal date', - 'Valid Until', - 'paid-till', - 'validity', - 'renewal', - 'Expires', - 'expiry', - 'expire', - ); - - if (!preg_match('/(?:' . implode('|', $labels) . ')\s*:?\s+(.+)/i', $result, $matches)) { - return null; - } - $expireDate = trim($matches[1]); - - // Known date formats (roundtrip-checked to avoid d-m-Y vs Y-m-d ambiguity) - $knownFormats = array( - 'Y-m-d', - 'Y.m.d', - 'Y/m/d', - 'Ymd', - 'Y. m. d.', - 'Y-M-d.', - 'd-M-Y', - 'd-F-Y', - 'd-m-Y', - 'd.m.Y', - 'd/m/Y', - 'Y/m/d H:i:s', - 'Ymd H:i:s', - 'Y.m.d H:i:s', - 'Y-m-d H:i:s', - 'd-M-Y H:i:s', - 'd.m.Y H:i:s', - 'd/m/Y H:i:s', - 'd M Y H:i:s', - 'd/m/Y H:i:s T', - 'D M d H:i:s T Y', - 'D M d Y', - ); - - foreach ($knownFormats as $format) { - $parsedDate = DateTime::createFromFormat($format, $expireDate); - if ($parsedDate && $parsedDate->format($format) === $expireDate) { - return $parsedDate->format('Y-m-d'); - } - } - - // Fallback - handles ISO 8601 (2026-07-05T04:00:00Z) and most everything else - $parsedDate = date_create($expireDate); - if ($parsedDate) { - $year = (int) $parsedDate->format('Y'); - // Reject obviously bogus parses - if ($year >= 1995 && $year <= ((int) date('Y') + 100)) { - return $parsedDate->format('Y-m-d'); - } - } - - return null; -} - - -function validateWhitelabelKey($key) { - $public_key = "-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0k+4ZJudkdGMCFLx5b9 -H/sOozvWphFJsjVIF0vPVx9J0bTdml65UdS+32JagIHfPtEUTohaMnI3IAxxCDzl -655qmtjL7RHHdx9UMIKCmtAZOtd2u6rEyZH7vB7cKA49ysKGIaQSGwTQc8DCgsrK -uxRuX04xq9T7T+zuzROw3Y9WjFy9RwrONqLuG8LqO0j7bk5LKYeLAV7u3E/QiqNx -lEljN2UVJ3FZ/LkXeg8ORkV+IHs/toRIfPs/4VQnjEwk5BU6DX2STOvbeZnTqwP3 -zgjRYR/zGN5l+az6RB3+0mJRdZdv/y2aRkBlwTxx2gOrPbQAco4a/IOmkE3EbHe7 -6wIDAQAP ------END PUBLIC KEY-----"; - - if (openssl_public_decrypt(base64_decode($key), $decrypted, $public_key)) { - $key_info = json_decode($decrypted, true); - if ($key_info['expires'] > date('Y-m-d H:i:s', strtotime('-7 day'))) { - return $key_info; - } - } - - return false; -} - -// When provided a module name (e.g. module_support), returns the associated permission level (false=none, 1=read, 2=write, 3=full) -function lookupUserPermission($module) { - global $mysqli, $session_is_admin, $session_user_role; - - if (isset($session_is_admin) && $session_is_admin === true) { - return 3; - } - - $module = sanitizeInput($module); - - $sql = mysqli_query( - $mysqli, - "SELECT - user_role_permissions.user_role_permission_level - FROM - modules - JOIN - user_role_permissions - ON - modules.module_id = user_role_permissions.module_id - WHERE - module_name = '$module' AND user_role_permissions.user_role_id = $session_user_role" - ); - - $row = mysqli_fetch_assoc($sql); - - if (isset($row['user_role_permission_level'])) { - return intval($row['user_role_permission_level']); - } - - // Default return for no module permission - return false; -} - -// Ensures a user has access to a module (e.g. module_support) with at least the required permission level provided (defaults to read) -function enforceUserPermission($module, $check_access_level = 1) { - $permitted_access_level = lookupUserPermission($module); - - if (!$permitted_access_level || $permitted_access_level < $check_access_level) { - $_SESSION['alert_type'] = "danger"; - $_SESSION['alert_message'] = WORDING_ROLECHECK_FAILED; - $map = [ - "1" => "read", - "2" => "write", - "3" => "full" - ]; - exit(WORDING_ROLECHECK_FAILED . "
Tell your admin: $map[$check_access_level] access to $module is not permitted for your role."); - } -} - -function enforceClientAccess($client_id = null) { - global $mysqli, $session_user_id, $session_is_admin, $session_name; - - // Use global $client_id if none passed - if ($client_id === null) { - global $client_id; - } - - if ($session_is_admin) { - return true; - } - - $client_id = (int) $client_id; - $session_user_id = (int) $session_user_id; - - if (empty($client_id) || empty($session_user_id)) { - flash_alert('Access Denied.', 'error'); - redirect('clients.php'); - } - - // Check if this user has any client permissions set - $permissions_sql = "SELECT client_id - FROM user_client_permissions - WHERE user_id = $session_user_id - LIMIT 1"; - - $permissions_result = mysqli_query($mysqli, $permissions_sql); - - // If no permission rows exist for this user, allow access by default - if ($permissions_result && mysqli_num_rows($permissions_result) == 0) { - return true; - } - - // If permission rows exist, require this client - $access_sql = "SELECT client_id - FROM user_client_permissions - WHERE user_id = $session_user_id - AND client_id = $client_id - LIMIT 1"; - - $access_result = mysqli_query($mysqli, $access_sql); - - if ($access_result && mysqli_num_rows($access_result) > 0) { - return true; - } - - logAction( - 'Client', - 'Access', - "$session_name was denied permission from accessing client", - $client_id, - $client_id - ); - - flash_alert('Access Denied - You do not have permission to access that client!', 'error'); - redirect('clients.php'); -} - -// TODO: Probably remove this -function enforceAdminPermission() { - global $session_is_admin; - if (!isset($session_is_admin) || !$session_is_admin) { - exit(WORDING_ROLECHECK_FAILED . "
Tell your admin: Your role does not have admin access."); - } - return true; -} - -function customAction($trigger, $entity) { - $original_dir = getcwd(); // Save - - chdir(dirname(__FILE__)); - if (file_exists(__DIR__ . "/custom/custom_action_handler.php")) { - include_once __DIR__ . "/custom/custom_action_handler.php"; - } - - chdir($original_dir); // Restore original working directory -} - -function appNotify($type, $details, $action = null, $client_id = 0, $entity_id = 0) { - global $mysqli; - - if (is_null($action)) { - $action = "NULL"; // Without quotes for SQL NULL - } - - $type = substr($type, 0, 200); - $details = substr($details, 0, 1000); - $action = substr($action, 0, 250); - - $sql = mysqli_query($mysqli, "SELECT user_id FROM users - WHERE user_type = 1 AND user_status = 1 AND user_archived_at IS NULL - "); - - while ($row = mysqli_fetch_assoc($sql)) { - $user_id = intval($row['user_id']); - - mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = '$type', notification = '$details', notification_action = '$action', notification_client_id = $client_id, notification_entity_id = $entity_id, notification_user_id = $user_id"); - } -} - -function logAction($type, $action, $description, $client_id = 0, $entity_id = 0) { - global $mysqli, $session_user_agent, $session_ip, $session_user_id; - - $client_id = intval($client_id); - $entity_id = intval($entity_id); - $session_user_id = intval($session_user_id); - - if (empty($session_user_id)) { - $session_user_id = 0; - } - - $type = substr($type, 0, 200); - $action = substr($action, 0, 255); - $description = substr($description, 0, 1000); - - mysqli_query($mysqli, "INSERT INTO logs SET log_type = '$type', log_action = '$action', log_description = '$description', log_ip = '$session_ip', log_user_agent = '$session_user_agent', log_client_id = $client_id, log_user_id = $session_user_id, log_entity_id = $entity_id"); -} - -function logApp($category, $type, $details) { - global $mysqli; - - $category = mysqli_real_escape_string($mysqli, substr($category, 0, 200)); - $details = mysqli_real_escape_string($mysqli, substr($details, 0, 1000)); - - mysqli_query($mysqli, "INSERT INTO app_logs SET app_log_category = '$category', app_log_type = '$type', app_log_details = '$details'"); -} - -function logAuth($status, $details) { - global $mysqli, $session_user_agent, $session_ip, $session_user_id; - - if (empty($session_user_id)) { - $session_user_id = 0; - } - - mysqli_query($mysqli, "INSERT INTO auth_logs SET auth_log_status = $status, auth_log_details = '$details', auth_log_ip = '$session_ip', auth_log_user_agent = '$session_user_agent', auth_log_user_id = $session_user_id"); -} - -// Helper function for missing data fallback -function getFallback($data) { - return !empty($data) ? $data : '-'; -} - -/** - * Retrieves a specified field's value from a table based on the record's id. - * It validates the table and field names, automatically determines the primary key (or uses the first column as fallback), - * and returns the field value with an appropriate escaping method. - * - * @param string $table The name of the table. - * @param int $id The record's id. - * @param string $field The field (column) to retrieve. - * @param string $escape_method The escape method: 'sql' (default, auto-detects int), 'html', 'json', or 'int'. - * - * @return mixed The escaped field value, or null if not found or invalid input. - */ -function getFieldById($table, $id, $field, $escape_method = 'sql') { - global $mysqli; // Use the global MySQLi connection - - // Validate table and field names to allow only letters, numbers, and underscores - if (!preg_match('/^[a-zA-Z0-9_]+$/', $table) || !preg_match('/^[a-zA-Z0-9_]+$/', $field)) { - return null; // Invalid table or field name - } - - // Sanitize id as an integer - $id = (int)$id; - - // Get the list of columns and their details from the table - $columns_result = mysqli_query($mysqli, "SHOW COLUMNS FROM `$table`"); - if (!$columns_result || mysqli_num_rows($columns_result) == 0) { - return null; // Table not found or has no columns - } - - // Build an associative array with column details - $columns = []; - while ($row = mysqli_fetch_assoc($columns_result)) { - $columns[$row['Field']] = [ - 'type' => $row['Type'], - 'key' => $row['Key'] - ]; - } - - // Find the primary key field if available - $id_field = null; - foreach ($columns as $col => $details) { - if ($details['key'] === 'PRI') { - $id_field = $col; - break; - } - } - // Fallback: if no primary key is found, use the first column - if (!$id_field) { - reset($columns); - $id_field = key($columns); - } - - // Ensure the requested field exists; if not, default to the id field - if (!array_key_exists($field, $columns)) { - $field = $id_field; - } - - // Build and execute the query to fetch the specified field value - $query = "SELECT `$field` FROM `$table` WHERE `$id_field` = $id"; - $sql = mysqli_query($mysqli, $query); - - if ($sql && mysqli_num_rows($sql) > 0) { - $row = mysqli_fetch_assoc($sql); - $value = $row[$field]; - - // Apply the desired escaping method or auto-detect integer type if using SQL escaping - switch ($escape_method) { - case 'raw': - return $value; // Return as-is from the database - case 'html': - return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8'); // Escape for HTML - case 'json': - return json_encode($value); // Escape for JSON - case 'int': - return (int)$value; // Explicitly cast value to integer - case 'sql': - default: - // Auto-detect if the field type is integer - if (stripos($columns[$field]['type'], 'int') !== false) { - return (int)$value; - } else { - return sanitizeInput($value); // Escape for SQL using a custom function - } - } - } - - return null; // Return null if no record was found -} - -// Recursive function to display folder options - Used in folders files and documents -function display_folder_options($parent_folder_id, $client_id, $indent = 0) { - global $mysqli; - - $sql_folders = mysqli_query($mysqli, "SELECT * FROM folders WHERE parent_folder = $parent_folder_id AND folder_client_id = $client_id ORDER BY folder_name ASC"); - while ($row = mysqli_fetch_assoc($sql_folders)) { - $folder_id = intval($row['folder_id']); - $folder_name = nullable_htmlentities($row['folder_name']); - - // Indentation for subfolders - $indentation = str_repeat(' ', $indent * 4); - - // Check if this folder is selected - $selected = ''; - if ((isset($_GET['folder_id']) && intval($_GET['folder_id']) === $folder_id) || - (isset($_POST['folder']) && intval($_POST['folder']) === $folder_id)) { - $selected = 'selected'; - } - - echo ""; - - // Recursively display subfolders - display_folder_options($folder_id, $client_id, $indent + 1); - } -} - -function sanitize_url($url) { - $allowed = ['http', 'https', 'file', 'ftp', 'ftps', 'sftp', 'dav', 'webdav', 'caldav', 'carddav', 'ssh', 'telnet', 'smb', 'rdp', 'vnc', 'rustdesk', 'anydesk', 'connectwise', 'splashtop', 'sip', 'sips', 'ldap', 'ldaps']; - $parts = parse_url($url ?? ''); - if (isset($parts['scheme']) && !in_array(strtolower($parts['scheme']), $allowed)) { - // Remove the scheme and colon - $pos = strpos($url, ':'); - $without_scheme = $url; - if ($pos !== false) { - $without_scheme = substr($url, $pos + 1); // This keeps slashes (e.g. //pizza.com) - } - // Prepend 'unsupported://' (strip any leading slashes from $without_scheme to avoid triple slashes) - $unsupported = 'unsupported://' . ltrim($without_scheme, '/'); - return htmlspecialchars($unsupported, ENT_QUOTES, 'UTF-8'); - } - - // Safe schemes: return escaped original URL - return htmlspecialchars($url ?? '', ENT_QUOTES, 'UTF-8'); -} - -// Redirect Function -function redirect($url = null, $permanent = false) { - // Use referer if no URL is provided - if (!$url) { - $url = $_SERVER['HTTP_REFERER'] ?? 'index.php'; - } - - if (!headers_sent()) { - header('Location: ' . $url, true, $permanent ? 301 : 302); - exit; - } else { - // Fallback for headers already sent - echo ""; - echo ''; - exit; - } -} - -//Flash Alert Function -function flash_alert(string $message, string $type = 'success'): void { - $_SESSION['alert_type'] = $type; - $_SESSION['alert_message'] = $message; -} - -// Sanitize File Names -function sanitize_filename($filename, $strict = false) { - // Remove path information and dots around the filename - $filename = basename($filename); - - // Replace spaces and underscores with dashes - $filename = str_replace([' ', '_'], '-', $filename); - - // Remove anything which isn't a word, number, dot, or dash - $filename = preg_replace('/[^A-Za-z0-9\.\-]/', '', $filename); - - // Optionally make filename strict alphanumeric (keep dot and dash) - if ($strict) { - $filename = preg_replace('/[^A-Za-z0-9\.\-]/', '', $filename); - } - - // Avoid multiple consecutive dashes - $filename = preg_replace('/-+/', '-', $filename); - - // Remove leading/trailing dots and dashes - $filename = trim($filename, '.-'); - - // Ensure it’s not empty - if (empty($filename)) { - $filename = 'file'; - } - - return $filename; -} - -function saveBase64Images(string $html, string $baseFsPath, string $baseWebPath, int $ownerId): string { - // Normalize paths - $baseFsPath = rtrim($baseFsPath, '/\\') . '/'; - $baseWebPath = rtrim($baseWebPath, '/\\') . '/'; - - $targetDir = $baseFsPath . $ownerId . "/"; - - $folderCreated = false; // <-- NEW FLAG - $savedAny = false; // <-- Track if ANY images processed - - libxml_use_internal_errors(true); - $dom = new DOMDocument(); - $dom->loadHTML('' . $html); - libxml_clear_errors(); - - $imgs = $dom->getElementsByTagName('img'); - - foreach ($imgs as $img) { - $src = $img->getAttribute('src'); - - // Match base64 images - if (preg_match('/^data:image\/([a-zA-Z0-9+]+);base64,(.*)$/s', $src, $matches)) { - - $savedAny = true; // <-- We are actually saving at least 1 image - - // Create folder ONLY when needed - if (!$folderCreated) { - if (!is_dir($targetDir)) { - mkdir($targetDir, 0775, true); - } - $folderCreated = true; - } - - $mimeType = strtolower($matches[1]); - $base64 = $matches[2]; - - $binary = base64_decode($base64); - if ($binary === false) { - continue; - } - - // Extension mapping - switch ($mimeType) { - case 'jpeg': - case 'jpg': $ext = 'jpg'; break; - case 'png': $ext = 'png'; break; - case 'gif': $ext = 'gif'; break; - case 'webp': $ext = 'webp'; break; - default: $ext = 'png'; - } - - // Secure random filename - $uid = bin2hex(random_bytes(16)); - $filename = "img_{$uid}.{$ext}"; - - $filePath = $targetDir . $filename; - - if (file_put_contents($filePath, $binary) !== false) { - $webPath = "/" . $baseWebPath . $ownerId . "/" . $filename; - $img->setAttribute('src', $webPath); - } - } - } - - // If no images were processed, return original HTML immediately - if (!$savedAny) { - return $html; - } - - // Extract body content only - $body = $dom->getElementsByTagName('body')->item(0); - - if ($body) { - $innerHTML = ''; - foreach ($body->childNodes as $child) { - $innerHTML .= $dom->saveHTML($child); - } - return $innerHTML; - } - - return $html; -} - -function cleanupUnusedImages(string $html, string $folderFsPath, string $folderWebPath) { - - $folderFsPath = rtrim($folderFsPath, '/\\') . '/'; - $folderWebPath = rtrim($folderWebPath, '/\\') . '/'; - - if (!is_dir($folderFsPath)) { - return; // no folder = nothing to delete - } - - // 1. Get all files currently on disk - $filesOnDisk = glob($folderFsPath . "*"); - - // 2. Find all - preg_match_all('/]+src=["\']([^"\']+)["\']/i', $html, $matches); - $htmlImagePaths = $matches[1] ?? []; - - // Normalize paths: keep only filenames belonging to this template folder - $referencedFiles = []; - - foreach ($htmlImagePaths as $src) { - if (strpos($src, $folderWebPath) !== false) { - $filename = basename($src); - $referencedFiles[] = $filename; - } - } - - // 3. Delete any physical file not referenced in the HTML - foreach ($filesOnDisk as $filePath) { - $filename = basename($filePath); - - if (!in_array($filename, $referencedFiles)) { - unlink($filePath); - } - } -} - -/** - * Simple mysqli helper functions - * - Prepared statements under the hood - * - "Old style" INSERT/UPDATE SET feeling - */ - -/** - * Core executor: prepares, binds, executes. - * - * @throws Exception on error - */ -function dbExecute(mysqli $mysqli, string $sql, array $params = []): mysqli_stmt -{ - $stmt = $mysqli->prepare($sql); - if (!$stmt) { - throw new Exception('MySQLi prepare error: ' . $mysqli->error . ' | SQL: ' . $sql); - } - - if (!empty($params)) { - $types = ''; - $values = []; - - foreach ($params as $param) { - if (is_int($param)) { - $types .= 'i'; - } elseif (is_float($param)) { - $types .= 'd'; - } elseif (is_bool($param)) { - $types .= 'i'; - $param = $param ? 1 : 0; - } elseif (is_null($param)) { - $types .= 's'; - $param = null; - } else { - $types .= 's'; - } - $values[] = $param; - } - - if (!$stmt->bind_param($types, ...$values)) { - throw new Exception('MySQLi bind_param error: ' . $stmt->error . ' | SQL: ' . $sql); - } - } - - if (!$stmt->execute()) { - throw new Exception('MySQLi execute error: ' . $stmt->error . ' | SQL: ' . $sql); - } - - return $stmt; -} - -/** - * Fetch all rows as associative arrays. - */ -function dbFetchAll(mysqli $mysqli, string $sql, array $params = []): array -{ - $stmt = dbExecute($mysqli, $sql, $params); - $result = $stmt->get_result(); - if ($result === false) { - return []; - } - return $result->fetch_all(MYSQLI_ASSOC); -} - -/** - * Fetch a single row (assoc) or null if none. - */ -function dbFetchOne(mysqli $mysqli, string $sql, array $params = []): ?array -{ - $stmt = dbExecute($mysqli, $sql, $params); - $result = $stmt->get_result(); - if ($result === false) { - return null; - } - $row = $result->fetch_assoc(); - return $row !== null ? $row : null; -} - -/** - * Fetch a single scalar value (first column of first row) or null. - */ -function dbFetchValue(mysqli $mysqli, string $sql, array $params = []) -{ - $row = dbFetchOne($mysqli, $sql, $params); - if ($row === null) { - return null; - } - return reset($row); -} - -/** - * INSERT using "SET" style. - * Example: - * $id = dbInsert($mysqli, 'clients', [ - * 'client_name' => $name, - * 'client_type' => $type, - * ]); - * - * @return int insert_id - * - * @throws InvalidArgumentException - * @throws Exception - */ -function dbInsert(mysqli $mysqli, string $table, array $data): int -{ - if (empty($data)) { - throw new InvalidArgumentException('dbInsert called with empty $data'); - } - - $setParts = []; - foreach ($data as $column => $_) { - $setParts[] = "$column = ?"; - } - - $sql = "INSERT INTO $table SET " . implode(', ', $setParts); - $params = array_values($data); - - dbExecute($mysqli, $sql, $params); - - return $mysqli->insert_id; -} - -function dbUpdate( - mysqli $mysqli, - string $table, - array $data, - $where, - array $whereParams = [] -): int { - if (empty($data)) { - throw new InvalidArgumentException('dbUpdate called with empty $data'); - } - if (empty($where)) { - throw new InvalidArgumentException('dbUpdate requires a WHERE clause'); - } - - $setParts = []; - foreach ($data as $column => $_) { - $setParts[] = "$column = ?"; - } - - if (is_array($where)) { - $whereParts = []; - $whereParams = []; - foreach ($where as $column => $value) { - $whereParts[] = "$column = ?"; - $whereParams[] = $value; - } - $whereSql = implode(' AND ', $whereParts); - } else { - $whereSql = $where; - } - - $sql = "UPDATE $table SET " . implode(', ', $setParts) . " WHERE $whereSql"; - $params = array_merge(array_values($data), $whereParams); - - $stmt = dbExecute($mysqli, $sql, $params); - return $stmt->affected_rows; -} - -/** - * DELETE helper. - * - * WHERE can be: - * - array: ['client_id' => $id] (auto "client_id = ?") - * - string: 'client_id = ?' (use with $whereParams) - * - * @return int affected_rows - * - * @throws InvalidArgumentException - * @throws Exception - */ -function dbDelete( - mysqli $mysqli, - string $table, - $where, - array $whereParams = [] -): int { - if (empty($where)) { - throw new InvalidArgumentException('dbDelete requires a WHERE clause'); - } - - if (is_array($where)) { - $whereParts = []; - $whereParams = []; - foreach ($where as $column => $value) { - $whereParts[] = "$column = ?"; - $whereParams[] = $value; - } - $whereSql = implode(' AND ', $whereParts); - } else { - $whereSql = $where; - } - - $sql = "DELETE FROM $table WHERE $whereSql"; - $stmt = dbExecute($mysqli, $sql, $whereParams); - return $stmt->affected_rows; -} - -/** - * Transaction helpers (optional sugar). - */ -function dbBegin(mysqli $mysqli): void -{ - $mysqli->begin_transaction(); -} - -function dbCommit(mysqli $mysqli): void -{ - $mysqli->commit(); -} - -function dbRollback(mysqli $mysqli): void -{ - $mysqli->rollback(); -} - -function formatDuration($time) { - // expects "HH:MM:SS" - [$h, $m, $s] = array_map('intval', explode(':', $time)); - - $parts = []; - - if ($h > 0) $parts[] = $h . 'h'; - if ($m > 0) $parts[] = $m . 'm'; - - // show seconds only if under 1 minute total OR if nothing else exists - if ($h == 0 && $m == 0) { - $parts[] = $s . 's'; - } - - return implode(' ', $parts); -} - -function validateDate($date) { - if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { - return $date; - } - return date('Y-m-d'); // Fallback -} +// functions.php is now a loader. Helper functions live in topical files +// under functions/ - see each file's header comment for scope. + +require_once __DIR__ . '/functions/security.php'; +require_once __DIR__ . '/functions/sanitize.php'; +require_once __DIR__ . '/functions/format.php'; +require_once __DIR__ . '/functions/request.php'; +require_once __DIR__ . '/functions/files.php'; +require_once __DIR__ . '/functions/domain.php'; +require_once __DIR__ . '/functions/auth.php'; +require_once __DIR__ . '/functions/logging.php'; +require_once __DIR__ . '/functions/app.php'; +require_once __DIR__ . '/functions/db.php'; diff --git a/functions/app.php b/functions/app.php new file mode 100644 index 00000000..13968e7c --- /dev/null +++ b/functions/app.php @@ -0,0 +1,341 @@ + $row['Type'], + 'key' => $row['Key'] + ]; + } + + // Find the primary key field if available + $id_field = null; + foreach ($columns as $col => $details) { + if ($details['key'] === 'PRI') { + $id_field = $col; + break; + } + } + // Fallback: if no primary key is found, use the first column + if (!$id_field) { + reset($columns); + $id_field = key($columns); + } + + // Ensure the requested field exists; if not, default to the id field + if (!array_key_exists($field, $columns)) { + $field = $id_field; + } + + // Build and execute the query to fetch the specified field value + $query = "SELECT `$field` FROM `$table` WHERE `$id_field` = $id"; + $sql = mysqli_query($mysqli, $query); + + if ($sql && mysqli_num_rows($sql) > 0) { + $row = mysqli_fetch_assoc($sql); + $value = $row[$field]; + + // Apply the desired escaping method or auto-detect integer type if using SQL escaping + switch ($escape_method) { + case 'raw': + return $value; // Return as-is from the database + case 'html': + return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8'); // Escape for HTML + case 'json': + return json_encode($value); // Escape for JSON + case 'int': + return (int)$value; // Explicitly cast value to integer + case 'sql': + default: + // Auto-detect if the field type is integer + if (stripos($columns[$field]['type'], 'int') !== false) { + return (int)$value; + } else { + return sanitizeInput($value); // Escape for SQL using a custom function + } + } + } + + return null; // Return null if no record was found +} + +// Recursive function to display folder options - Used in folders files and documents +function display_folder_options($parent_folder_id, $client_id, $indent = 0) { + global $mysqli; + + $sql_folders = mysqli_query($mysqli, "SELECT * FROM folders WHERE parent_folder = $parent_folder_id AND folder_client_id = $client_id ORDER BY folder_name ASC"); + while ($row = mysqli_fetch_assoc($sql_folders)) { + $folder_id = intval($row['folder_id']); + $folder_name = nullable_htmlentities($row['folder_name']); + + // Indentation for subfolders + $indentation = str_repeat(' ', $indent * 4); + + // Check if this folder is selected + $selected = ''; + if ((isset($_GET['folder_id']) && intval($_GET['folder_id']) === $folder_id) || + (isset($_POST['folder']) && intval($_POST['folder']) === $folder_id)) { + $selected = 'selected'; + } + + echo ""; + + // Recursively display subfolders + display_folder_options($folder_id, $client_id, $indent + 1); + } +} + +function fetchUpdates() { + + global $repo_branch; + + // Fetch the latest code changes but don't apply them + exec("git fetch", $output, $result); + $latest_version = exec("git rev-parse origin/$repo_branch"); + $current_version = exec("git rev-parse HEAD"); + + if ($current_version == $latest_version) { + $update_message = "No Updates available"; + } else { + $update_message = "New Updates are Available [$latest_version]"; + } + + + $updates = new stdClass(); + $updates->output = $output; + $updates->result = $result; + $updates->current_version = $current_version; + $updates->latest_version = $latest_version; + $updates->update_message = $update_message; + + + return $updates; + +} + +function getMonthlyTax($tax_name, $month, $year, $mysqli) +{ + // SQL to calculate monthly tax + $sql = "SELECT SUM(item_tax) AS monthly_tax FROM invoice_items + LEFT JOIN invoices ON invoice_items.item_invoice_id = invoices.invoice_id + LEFT JOIN payments ON invoices.invoice_id = payments.payment_invoice_id + WHERE YEAR(payments.payment_date) = $year AND MONTH(payments.payment_date) = $month + AND invoice_items.item_tax_id = (SELECT tax_id FROM taxes WHERE tax_name = '$tax_name')"; + $result = mysqli_query($mysqli, $sql); + $row = mysqli_fetch_assoc($result); + return $row['monthly_tax'] ?? 0; +} + +function getQuarterlyTax($tax_name, $quarter, $year, $mysqli) +{ + // Calculate start and end months for the quarter + $start_month = ($quarter - 1) * 3 + 1; + $end_month = $start_month + 2; + + // SQL to calculate quarterly tax + $sql = "SELECT SUM(item_tax) AS quarterly_tax FROM invoice_items + LEFT JOIN invoices ON invoice_items.item_invoice_id = invoices.invoice_id + LEFT JOIN payments ON invoices.invoice_id = payments.payment_invoice_id + WHERE YEAR(payments.payment_date) = $year AND MONTH(payments.payment_date) BETWEEN $start_month AND $end_month + AND invoice_items.item_tax_id = (SELECT tax_id FROM taxes WHERE tax_name = '$tax_name')"; + $result = mysqli_query($mysqli, $sql); + $row = mysqli_fetch_assoc($result); + return $row['quarterly_tax'] ?? 0; +} + +function addToMailQueue($data) { + + global $mysqli; + + foreach ($data as $email) { + $from = strval($email['from']); + $from_name = strval($email['from_name']); + $recipient = strval($email['recipient']); + $recipient_name = strval($email['recipient_name']); + $subject = strval($email['subject']); + $body = strval($email['body']); + + $cal_str = ''; + if (isset($email['cal_str'])) { + $cal_str = mysqli_escape_string($mysqli, $email['cal_str']); + } + + // Check if 'email_queued_at' is set and not empty + if (isset($email['queued_at']) && !empty($email['queued_at'])) { + $queued_at = "'" . sanitizeInput($email['queued_at']) . "'"; + } else { + // Use the current date and time if 'email_queued_at' is not set or empty + $queued_at = 'CURRENT_TIMESTAMP()'; + } + + mysqli_query($mysqli, "INSERT INTO email_queue SET email_recipient = '$recipient', email_recipient_name = '$recipient_name', email_from = '$from', email_from_name = '$from_name', email_subject = '$subject', email_content = '$body', email_queued_at = $queued_at, email_cal_str = '$cal_str'"); + } + + return true; +} + +function createiCalStr($datetime, $title, $description, $location) +{ + require_once "plugins/zapcal/zapcallib.php"; + + // Create the iCal object + $cal_event = new ZCiCal(); + $event = new ZCiCalNode("VEVENT", $cal_event->curnode); + + + // Set the method to REQUEST to indicate an invite + $event->addNode(new ZCiCalDataNode("METHOD:REQUEST")); + $event->addNode(new ZCiCalDataNode("SUMMARY:" . $title)); + $event->addNode(new ZCiCalDataNode("DTSTART:" . ZCiCal::fromSqlDateTime($datetime))); + // Assuming the end time is the same as start time. + // Todo: adjust this for actual duration + $event->addNode(new ZCiCalDataNode("DTEND:" . ZCiCal::fromSqlDateTime($datetime))); + $event->addNode(new ZCiCalDataNode("DTSTAMP:" . ZCiCal::fromSqlDateTime())); + $uid = date('Y-m-d-H-i-s') . "@" . $_SERVER['SERVER_NAME']; + $event->addNode(new ZCiCalDataNode("UID:" . $uid)); + $event->addNode(new ZCiCalDataNode("LOCATION:" . $location)); + $event->addNode(new ZCiCalDataNode("DESCRIPTION:" . $description)); + // Todo: add organizer details + // $event->addNode(new ZCiCalDataNode("ORGANIZER;CN=Organizer Name:MAILTO:organizer@example.com")); + + // Return the iCal string + return $cal_event->export(); +} + +function createiCalStrCancel($originaliCalStr) { + require_once "plugins/zapcal/zapcallib.php"; + + // Import the original iCal string + $cal_event = new ZCiCal($originaliCalStr); + + // Iterate through the iCalendar object to find VEVENT nodes + foreach($cal_event->tree->child as $node) { + if($node->getName() == "VEVENT") { + // Check if STATUS node exists, update it, or add a new one + $statusFound = false; + foreach($node->data as $key => $value) { + if($key == "STATUS") { + $value->setValue("CANCELLED"); + $statusFound = true; + break; // Exit the loop once the STATUS is updated + } + } + // If STATUS node is not found, add a new STATUS node + if (!$statusFound) { + $node->addNode(new ZCiCalDataNode("STATUS:CANCELLED")); + } + } + } + + // Return the modified iCal string + return $cal_event->export(); +} diff --git a/functions/auth.php b/functions/auth.php new file mode 100644 index 00000000..068966ae --- /dev/null +++ b/functions/auth.php @@ -0,0 +1,142 @@ + "read", + "2" => "write", + "3" => "full" + ]; + exit(WORDING_ROLECHECK_FAILED . "
Tell your admin: $map[$check_access_level] access to $module is not permitted for your role."); + } +} + +function enforceClientAccess($client_id = null) { + global $mysqli, $session_user_id, $session_is_admin, $session_name; + + // Use global $client_id if none passed + if ($client_id === null) { + global $client_id; + } + + if ($session_is_admin) { + return true; + } + + $client_id = (int) $client_id; + $session_user_id = (int) $session_user_id; + + if (empty($client_id) || empty($session_user_id)) { + flash_alert('Access Denied.', 'error'); + redirect('clients.php'); + } + + // Check if this user has any client permissions set + $permissions_sql = "SELECT client_id + FROM user_client_permissions + WHERE user_id = $session_user_id + LIMIT 1"; + + $permissions_result = mysqli_query($mysqli, $permissions_sql); + + // If no permission rows exist for this user, allow access by default + if ($permissions_result && mysqli_num_rows($permissions_result) == 0) { + return true; + } + + // If permission rows exist, require this client + $access_sql = "SELECT client_id + FROM user_client_permissions + WHERE user_id = $session_user_id + AND client_id = $client_id + LIMIT 1"; + + $access_result = mysqli_query($mysqli, $access_sql); + + if ($access_result && mysqli_num_rows($access_result) > 0) { + return true; + } + + logAction( + 'Client', + 'Access', + "$session_name was denied permission from accessing client", + $client_id, + $client_id + ); + + flash_alert('Access Denied - You do not have permission to access that client!', 'error'); + redirect('clients.php'); +} diff --git a/functions/db.php b/functions/db.php new file mode 100644 index 00000000..78ae2d02 --- /dev/null +++ b/functions/db.php @@ -0,0 +1,224 @@ +prepare($sql); + if (!$stmt) { + throw new Exception('MySQLi prepare error: ' . $mysqli->error . ' | SQL: ' . $sql); + } + + if (!empty($params)) { + $types = ''; + $values = []; + + foreach ($params as $param) { + if (is_int($param)) { + $types .= 'i'; + } elseif (is_float($param)) { + $types .= 'd'; + } elseif (is_bool($param)) { + $types .= 'i'; + $param = $param ? 1 : 0; + } elseif (is_null($param)) { + $types .= 's'; + $param = null; + } else { + $types .= 's'; + } + $values[] = $param; + } + + if (!$stmt->bind_param($types, ...$values)) { + throw new Exception('MySQLi bind_param error: ' . $stmt->error . ' | SQL: ' . $sql); + } + } + + if (!$stmt->execute()) { + throw new Exception('MySQLi execute error: ' . $stmt->error . ' | SQL: ' . $sql); + } + + return $stmt; +} + +/** + * Fetch all rows as associative arrays. + */ +function dbFetchAll(mysqli $mysqli, string $sql, array $params = []): array +{ + $stmt = dbExecute($mysqli, $sql, $params); + $result = $stmt->get_result(); + if ($result === false) { + return []; + } + return $result->fetch_all(MYSQLI_ASSOC); +} + +/** + * Fetch a single row (assoc) or null if none. + */ +function dbFetchOne(mysqli $mysqli, string $sql, array $params = []): ?array +{ + $stmt = dbExecute($mysqli, $sql, $params); + $result = $stmt->get_result(); + if ($result === false) { + return null; + } + $row = $result->fetch_assoc(); + return $row !== null ? $row : null; +} + +/** + * Fetch a single scalar value (first column of first row) or null. + */ +function dbFetchValue(mysqli $mysqli, string $sql, array $params = []) +{ + $row = dbFetchOne($mysqli, $sql, $params); + if ($row === null) { + return null; + } + return reset($row); +} + +/** + * INSERT using "SET" style. + * Example: + * $id = dbInsert($mysqli, 'clients', [ + * 'client_name' => $name, + * 'client_type' => $type, + * ]); + * + * @return int insert_id + * + * @throws InvalidArgumentException + * @throws Exception + */ +function dbInsert(mysqli $mysqli, string $table, array $data): int +{ + if (empty($data)) { + throw new InvalidArgumentException('dbInsert called with empty $data'); + } + + $setParts = []; + foreach ($data as $column => $_) { + $setParts[] = "$column = ?"; + } + + $sql = "INSERT INTO $table SET " . implode(', ', $setParts); + $params = array_values($data); + + dbExecute($mysqli, $sql, $params); + + return $mysqli->insert_id; +} + +function dbUpdate( + mysqli $mysqli, + string $table, + array $data, + $where, + array $whereParams = [] +): int { + if (empty($data)) { + throw new InvalidArgumentException('dbUpdate called with empty $data'); + } + if (empty($where)) { + throw new InvalidArgumentException('dbUpdate requires a WHERE clause'); + } + + $setParts = []; + foreach ($data as $column => $_) { + $setParts[] = "$column = ?"; + } + + if (is_array($where)) { + $whereParts = []; + $whereParams = []; + foreach ($where as $column => $value) { + $whereParts[] = "$column = ?"; + $whereParams[] = $value; + } + $whereSql = implode(' AND ', $whereParts); + } else { + $whereSql = $where; + } + + $sql = "UPDATE $table SET " . implode(', ', $setParts) . " WHERE $whereSql"; + $params = array_merge(array_values($data), $whereParams); + + $stmt = dbExecute($mysqli, $sql, $params); + return $stmt->affected_rows; +} + +/** + * DELETE helper. + * + * WHERE can be: + * - array: ['client_id' => $id] (auto "client_id = ?") + * - string: 'client_id = ?' (use with $whereParams) + * + * @return int affected_rows + * + * @throws InvalidArgumentException + * @throws Exception + */ +function dbDelete( + mysqli $mysqli, + string $table, + $where, + array $whereParams = [] +): int { + if (empty($where)) { + throw new InvalidArgumentException('dbDelete requires a WHERE clause'); + } + + if (is_array($where)) { + $whereParts = []; + $whereParams = []; + foreach ($where as $column => $value) { + $whereParts[] = "$column = ?"; + $whereParams[] = $value; + } + $whereSql = implode(' AND ', $whereParts); + } else { + $whereSql = $where; + } + + $sql = "DELETE FROM $table WHERE $whereSql"; + $stmt = dbExecute($mysqli, $sql, $whereParams); + return $stmt->affected_rows; +} + +/** + * Transaction helpers (optional sugar). + */ +function dbBegin(mysqli $mysqli): void +{ + $mysqli->begin_transaction(); +} + +function dbCommit(mysqli $mysqli): void +{ + $mysqli->commit(); +} + +function dbRollback(mysqli $mysqli): void +{ + $mysqli->rollback(); +} diff --git a/functions/domain.php b/functions/domain.php new file mode 100644 index 00000000..2db7fbd5 --- /dev/null +++ b/functions/domain.php @@ -0,0 +1,228 @@ + array("capture_peer_cert" => true, "verify_peer" => false,))); + $read = stream_socket_client($socket, $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $get); + + // If the socket connected + if ($read) { + $cert = stream_context_get_params($read); + $cert_public_key_obj = openssl_x509_parse($cert['options']['ssl']['peer_certificate']); + openssl_x509_export($cert['options']['ssl']['peer_certificate'], $export); + + if ($cert_public_key_obj) { + $certificate['success'] = true; + $certificate['expire'] = date('Y-m-d', $cert_public_key_obj['validTo_time_t']); + $certificate['issued_by'] = strip_tags($cert_public_key_obj['issuer']['O']); + $certificate['public_key'] = $export; + } + } + + return $certificate; +} + +function getDomainExpirationDate($domain) { + // Execute the whois command + $result = shell_exec("whois " . escapeshellarg($domain)); + if (!$result || !checkdnsrr($domain, 'SOA')) { + return null; // Return null if WHOIS query fails + } + + $expireDate = ''; + + // Regular expressions to match different date formats + $patterns = [ + '/Expiration Date: (.+)/', + '/Registry Expiry Date: (.+)/', + '/expires: (.+)/', + '/Expiry Date: (.+)/', + '/renewal date: (.+)/', + '/Expires On: (.+)/', + '/paid-till: (.+)/', + '/Expiration Time: (.+)/', + '/\[Expires on\]\s+(.+)/', + '/expire: (.+)/', + '/validity: (.+)/', + '/Expires on.*: (.+)/i', + '/Expiry on.*: (.+)/i', + '/renewal: (.+)/i', + '/Expir\w+ Date: (.+)/i', + '/Valid Until: (.+)/i', + '/Valid until: (.+)/i', + '/expire-date: (.+)/i', + '/Expiration Date: (.+)/i', + '/Registry Expiry Date: (.+)/i', + '/Expire Date: (.+)/i', + '/expiry: (.+)/i', + '/expires: (.+)/i', + '/Registry Expiry Date: (.+)/i', + '/Expiration Time: (.+)/i', + '/validity: (.+)/i', + '/expires: (.+)/i', + '/paid-till: (.+)/i', + '/Expire Date: (.+)/i', + '/Expiration Date: (.+)/i', + '/expire: (.+)/i', + '/expiry: (.+)/i', + '/renewal date: (.+)/i', + '/Expiration Date: (.+)/i', + '/Expiration Time: (.+)/i', + '/Expires: (.+)/i', + ]; + + // Known date formats + $knownFormats = [ + "d-M-Y", + "d-F-Y", + "d-m-Y", + "Y-m-d", + "d.m.Y", + "Y.m.d", + "Y/m/d", + "Y/m/d H:i:s", + "Ymd", + "Ymd H:i:s", + "d/m/Y", + "Y. m. d.", + "Y.m.d H:i:s", + "d-M-Y H:i:s", + "D M d H:i:s T Y", + "D M d Y", + "Y-m-d\TH:i:s", + "Y-m-d\TH:i:s\Z", + "Y-m-d H:i:s\Z", + "Y-m-d H:i:s", + "d M Y H:i:s", + "d/m/Y H:i:s", + "d/m/Y H:i:s T", + "B d Y", + "d.m.Y H:i:s", + "before M-Y", + "before Y-m-d", + "before Ymd", + "Y-m-d H:i:s (\T\Z\Z)", + "Y-M-d.", + ]; + + // Check each pattern to find a match + foreach ($patterns as $pattern) { + if (preg_match($pattern, $result, $matches)) { + $expireDate = trim($matches[1]); + break; + } + } + + if ($expireDate) { + // Try parsing with known formats + foreach ($knownFormats as $format) { + $parsedDate = DateTime::createFromFormat($format, $expireDate); + if ($parsedDate && $parsedDate->format($format) === $expireDate) { + return $parsedDate->format('Y-m-d'); + } + } + + // If none of the formats matched, try to parse it directly + $parsedDate = date_create($expireDate); + if ($parsedDate) { + return $parsedDate->format('Y-m-d'); + } + } + + return null; // Return null if expiration date is not found +} diff --git a/functions/files.php b/functions/files.php new file mode 100644 index 00000000..202a493e --- /dev/null +++ b/functions/files.php @@ -0,0 +1,168 @@ +loadHTML('' . $html); + libxml_clear_errors(); + + $imgs = $dom->getElementsByTagName('img'); + + foreach ($imgs as $img) { + $src = $img->getAttribute('src'); + + // Match base64 images + if (preg_match('/^data:image\/([a-zA-Z0-9+]+);base64,(.*)$/s', $src, $matches)) { + + $savedAny = true; // <-- We are actually saving at least 1 image + + // Create folder ONLY when needed + if (!$folderCreated) { + if (!is_dir($targetDir)) { + mkdir($targetDir, 0775, true); + } + $folderCreated = true; + } + + $mimeType = strtolower($matches[1]); + $base64 = $matches[2]; + + $binary = base64_decode($base64); + if ($binary === false) { + continue; + } + + // Extension mapping + switch ($mimeType) { + case 'jpeg': + case 'jpg': $ext = 'jpg'; break; + case 'png': $ext = 'png'; break; + case 'gif': $ext = 'gif'; break; + case 'webp': $ext = 'webp'; break; + default: $ext = 'png'; + } + + // Secure random filename + $uid = bin2hex(random_bytes(16)); + $filename = "img_{$uid}.{$ext}"; + + $filePath = $targetDir . $filename; + + if (file_put_contents($filePath, $binary) !== false) { + $webPath = "/" . $baseWebPath . $ownerId . "/" . $filename; + $img->setAttribute('src', $webPath); + } + } + } + + // If no images were processed, return original HTML immediately + if (!$savedAny) { + return $html; + } + + // Extract body content only + $body = $dom->getElementsByTagName('body')->item(0); + + if ($body) { + $innerHTML = ''; + foreach ($body->childNodes as $child) { + $innerHTML .= $dom->saveHTML($child); + } + return $innerHTML; + } + + return $html; +} + +function cleanupUnusedImages(string $html, string $folderFsPath, string $folderWebPath) { + + $folderFsPath = rtrim($folderFsPath, '/\\') . '/'; + $folderWebPath = rtrim($folderWebPath, '/\\') . '/'; + + if (!is_dir($folderFsPath)) { + return; // no folder = nothing to delete + } + + // 1. Get all files currently on disk + $filesOnDisk = glob($folderFsPath . "*"); + + // 2. Find all + preg_match_all('/]+src=["\']([^"\']+)["\']/i', $html, $matches); + $htmlImagePaths = $matches[1] ?? []; + + // Normalize paths: keep only filenames belonging to this template folder + $referencedFiles = []; + + foreach ($htmlImagePaths as $src) { + if (strpos($src, $folderWebPath) !== false) { + $filename = basename($src); + $referencedFiles[] = $filename; + } + } + + // 3. Delete any physical file not referenced in the HTML + foreach ($filesOnDisk as $filePath) { + $filename = basename($filePath); + + if (!in_array($filename, $referencedFiles)) { + unlink($filePath); + } + } +} diff --git a/functions/format.php b/functions/format.php new file mode 100644 index 00000000..c2c7aad8 --- /dev/null +++ b/functions/format.php @@ -0,0 +1,332 @@ += 9 && strlen($digits) <= 10) { + $formatted = '0' . substr($digits, 0, 2) . '-' . substr($digits, 2, 4) . '-' . substr($digits, 6); + } + break; + + case '49': // Germany + if ($startsWith($digits, '0')) { + $digits = substr($digits, 1); + } + if (strlen($digits) >= 10) { + $formatted = '0' . substr($digits, 0, 3) . ' ' . substr($digits, 3); + } + break; + + case '33': // France + if ($startsWith($digits, '0')) { + $digits = substr($digits, 1); + } + if (strlen($digits) === 9) { + $formatted = '0' . implode(' ', str_split($digits, 2)); + } + break; + + case '34': // Spain + if (strlen($digits) === 9) { + $formatted = substr($digits, 0, 3) . ' ' . substr($digits, 3, 3) . ' ' . substr($digits, 6); + } + break; + + case '39': // Italy + if ($startsWith($digits, '0')) { + $digits = substr($digits, 1); + } + $formatted = '0' . implode(' ', str_split($digits, 3)); + break; + + case '55': // Brazil + if (strlen($digits) === 11) { + $formatted = '(' . substr($digits, 0, 2) . ') ' . substr($digits, 2, 5) . '-' . substr($digits, 7); + } + break; + + case '7': // Russia + if ($startsWith($digits, '8')) { + $digits = substr($digits, 1); + } + if (strlen($digits) === 10) { + $formatted = '8 (' . substr($digits, 0, 3) . ') ' . substr($digits, 3, 3) . '-' . substr($digits, 6, 2) . '-' . substr($digits, 8); + } + break; + + case '86': // China + if (strlen($digits) === 11) { + $formatted = substr($digits, 0, 3) . ' ' . substr($digits, 3, 4) . ' ' . substr($digits, 7); + } + break; + + case '82': // South Korea + if (strlen($digits) === 11) { + $formatted = substr($digits, 0, 3) . '-' . substr($digits, 3, 4) . '-' . substr($digits, 7); + } + break; + + case '62': // Indonesia + if (!$startsWith($digits, '0')) { + $digits = '0' . $digits; + } + if (strlen($digits) === 12) { + $formatted = substr($digits, 0, 4) . ' ' . substr($digits, 4, 4) . ' ' . substr($digits, 8); + } + break; + + case '63': // Philippines + if (strlen($digits) === 11) { + $formatted = substr($digits, 0, 4) . ' ' . substr($digits, 4, 3) . ' ' . substr($digits, 7); + } + break; + + case '234': // Nigeria + if (!$startsWith($digits, '0')) { + $digits = '0' . $digits; + } + if (strlen($digits) === 11) { + $formatted = substr($digits, 0, 4) . ' ' . substr($digits, 4, 3) . ' ' . substr($digits, 7); + } + break; + + case '27': // South Africa + if (strlen($digits) >= 9 && strlen($digits) <= 10) { + $formatted = substr($digits, 0, 3) . ' ' . substr($digits, 3, 3) . ' ' . substr($digits, 6); + } + break; + + case '971': // UAE + if (strlen($digits) === 9) { + $formatted = substr($digits, 0, 3) . ' ' . substr($digits, 3, 3) . ' ' . substr($digits, 6); + } + break; + + default: + // fallback — do nothing, use raw digits later + break; + } + + if (!$formatted) { + $formatted = $digits ?: $phoneNumber; + } + + return $show_country_code && $country_code ? "+$country_code $formatted" : $formatted; +} + +function timeAgo($datetime) +{ + if (is_null($datetime)) { + return "-"; + } + + $time = strtotime($datetime); + $difference = $time - time(); // Changed to handle future dates + + if ($difference == 0) { + return 'right now'; + } + + $isFuture = $difference > 0; // Check if the date is in the future + $difference = abs($difference); // Absolute value for calculation + + $timeRules = array( + 31536000 => 'year', + 2592000 => 'month', + 604800 => 'week', + 86400 => 'day', + 3600 => 'hour', + 60 => 'minute', + 1 => 'second' + ); + + foreach ($timeRules as $secs => $str) { + $div = $difference / $secs; + if ($div >= 1) { + $t = round($div); + $timeStr = $t . ' ' . $str . ($t > 1 ? 's' : ''); + return $isFuture ? 'in ' . $timeStr : $timeStr . ' ago'; + } + } +} + +// Function to remove Emojis in messages, this seems to break the mail queue +function removeEmoji($text) +{ + return preg_replace('/\x{1F3F4}\x{E0067}\x{E0062}(?:\x{E0077}\x{E006C}\x{E0073}|\x{E0073}\x{E0063}\x{E0074}|\x{E0065}\x{E006E}\x{E0067})\x{E007F}|(?:\x{1F9D1}\x{1F3FF}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FF}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FF}\x{200D}\x{1FAF2})[\x{1F3FB}-\x{1F3FE}]|(?:\x{1F9D1}\x{1F3FE}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FE}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FE}\x{200D}\x{1FAF2})[\x{1F3FB}-\x{1F3FD}\x{1F3FF}]|(?:\x{1F9D1}\x{1F3FD}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FD}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FD}\x{200D}\x{1FAF2})[\x{1F3FB}\x{1F3FC}\x{1F3FE}\x{1F3FF}]|(?:\x{1F9D1}\x{1F3FC}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FC}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FC}\x{200D}\x{1FAF2})[\x{1F3FB}\x{1F3FD}-\x{1F3FF}]|(?:\x{1F9D1}\x{1F3FB}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F9D1}|\x{1F469}\x{1F3FB}\x{200D}\x{1F91D}\x{200D}[\x{1F468}\x{1F469}]|\x{1FAF1}\x{1F3FB}\x{200D}\x{1FAF2})[\x{1F3FC}-\x{1F3FF}]|\x{1F468}(?:\x{1F3FB}(?:\x{200D}(?:\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FF}]|\x{1F468}[\x{1F3FB}-\x{1F3FF}])|\x{200D}(?:\x{1F48B}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FF}]|\x{1F468}[\x{1F3FB}-\x{1F3FF}]))|\x{1F91D}\x{200D}\x{1F468}[\x{1F3FC}-\x{1F3FF}]|[\x{2695}\x{2696}\x{2708}]\x{FE0F}|[\x{2695}\x{2696}\x{2708}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]))?|[\x{1F3FC}-\x{1F3FF}]\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FF}]|\x{1F468}[\x{1F3FB}-\x{1F3FF}])|\x{200D}(?:\x{1F48B}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FF}]|\x{1F468}[\x{1F3FB}-\x{1F3FF}]))|\x{200D}(?:\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D})?|\x{200D}(?:\x{1F48B}\x{200D})?)\x{1F468}|[\x{1F468}\x{1F469}]\x{200D}(?:\x{1F466}\x{200D}\x{1F466}|\x{1F467}\x{200D}[\x{1F466}\x{1F467}])|\x{1F466}\x{200D}\x{1F466}|\x{1F467}\x{200D}[\x{1F466}\x{1F467}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FF}\x{200D}(?:\x{1F91D}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FE}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FE}\x{200D}(?:\x{1F91D}\x{200D}\x{1F468}[\x{1F3FB}-\x{1F3FD}\x{1F3FF}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FD}\x{200D}(?:\x{1F91D}\x{200D}\x{1F468}[\x{1F3FB}\x{1F3FC}\x{1F3FE}\x{1F3FF}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FC}\x{200D}(?:\x{1F91D}\x{200D}\x{1F468}[\x{1F3FB}\x{1F3FD}-\x{1F3FF}]|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])\x{FE0F}|\x{200D}(?:[\x{1F468}\x{1F469}]\x{200D}[\x{1F466}\x{1F467}]|[\x{1F466}\x{1F467}])|\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FF}|\x{1F3FE}|\x{1F3FD}|\x{1F3FC}|\x{200D}[\x{2695}\x{2696}\x{2708}])?|(?:\x{1F469}(?:\x{1F3FB}\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}])|\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}]))|[\x{1F3FC}-\x{1F3FF}]\x{200D}\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}])|\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}])))|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]\x{200D}\x{1F91D}\x{200D}\x{1F9D1})[\x{1F3FB}-\x{1F3FF}]|\x{1F469}\x{200D}\x{1F469}\x{200D}(?:\x{1F466}\x{200D}\x{1F466}|\x{1F467}\x{200D}[\x{1F466}\x{1F467}])|\x{1F469}(?:\x{200D}(?:\x{2764}(?:\x{FE0F}\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}])|\x{200D}(?:\x{1F48B}\x{200D}[\x{1F468}\x{1F469}]|[\x{1F468}\x{1F469}]))|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FF}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FE}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FD}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FC}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FB}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F9D1}(?:\x{200D}(?:\x{1F91D}\x{200D}\x{1F9D1}|[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F3FF}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FE}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FD}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FC}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}]|\x{1F3FB}\x{200D}[\x{1F33E}\x{1F373}\x{1F37C}\x{1F384}\x{1F393}\x{1F3A4}\x{1F3A8}\x{1F3EB}\x{1F3ED}\x{1F4BB}\x{1F4BC}\x{1F527}\x{1F52C}\x{1F680}\x{1F692}\x{1F9AF}-\x{1F9B3}\x{1F9BC}\x{1F9BD}])|\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}[\x{1F466}\x{1F467}]|\x{1F469}\x{200D}\x{1F467}\x{200D}[\x{1F466}\x{1F467}]|(?:\x{1F441}\x{FE0F}?\x{200D}\x{1F5E8}|\x{1F9D1}(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FB}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])|\x{1F469}(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FB}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])|\x{1F636}\x{200D}\x{1F32B}|\x{1F3F3}\x{FE0F}?\x{200D}\x{26A7}|\x{1F43B}\x{200D}\x{2744}|(?:[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93D}\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}][\x{1F3FB}-\x{1F3FF}]|[\x{1F46F}\x{1F9DE}\x{1F9DF}])\x{200D}[\x{2640}\x{2642}]|[\x{26F9}\x{1F3CB}\x{1F3CC}\x{1F575}](?:[\x{FE0F}\x{1F3FB}-\x{1F3FF}]\x{200D}[\x{2640}\x{2642}]|\x{200D}[\x{2640}\x{2642}])|\x{1F3F4}\x{200D}\x{2620}|[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93C}-\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}]\x{200D}[\x{2640}\x{2642}]|[\xA9\xAE\x{203C}\x{2049}\x{2122}\x{2139}\x{2194}-\x{2199}\x{21A9}\x{21AA}\x{231A}\x{231B}\x{2328}\x{23CF}\x{23ED}-\x{23EF}\x{23F1}\x{23F2}\x{23F8}-\x{23FA}\x{24C2}\x{25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}\x{25FC}\x{25FE}\x{2600}-\x{2604}\x{260E}\x{2611}\x{2614}\x{2615}\x{2618}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E}\x{262F}\x{2638}-\x{263A}\x{2640}\x{2642}\x{2648}-\x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{2668}\x{267B}\x{267E}\x{267F}\x{2692}\x{2694}-\x{2697}\x{2699}\x{269B}\x{269C}\x{26A0}\x{26A7}\x{26AA}\x{26B0}\x{26B1}\x{26BD}\x{26BE}\x{26C4}\x{26C8}\x{26CF}\x{26D1}\x{26D3}\x{26E9}\x{26F0}-\x{26F5}\x{26F7}\x{26F8}\x{26FA}\x{2702}\x{2708}\x{2709}\x{270F}\x{2712}\x{2714}\x{2716}\x{271D}\x{2721}\x{2733}\x{2734}\x{2744}\x{2747}\x{2763}\x{27A1}\x{2934}\x{2935}\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x{2B55}\x{3030}\x{303D}\x{3297}\x{3299}\x{1F004}\x{1F170}\x{1F171}\x{1F17E}\x{1F17F}\x{1F202}\x{1F237}\x{1F321}\x{1F324}-\x{1F32C}\x{1F336}\x{1F37D}\x{1F396}\x{1F397}\x{1F399}-\x{1F39B}\x{1F39E}\x{1F39F}\x{1F3CD}\x{1F3CE}\x{1F3D4}-\x{1F3DF}\x{1F3F5}\x{1F3F7}\x{1F43F}\x{1F4FD}\x{1F549}\x{1F54A}\x{1F56F}\x{1F570}\x{1F573}\x{1F576}-\x{1F579}\x{1F587}\x{1F58A}-\x{1F58D}\x{1F5A5}\x{1F5A8}\x{1F5B1}\x{1F5B2}\x{1F5BC}\x{1F5C2}-\x{1F5C4}\x{1F5D1}-\x{1F5D3}\x{1F5DC}-\x{1F5DE}\x{1F5E1}\x{1F5E3}\x{1F5E8}\x{1F5EF}\x{1F5F3}\x{1F5FA}\x{1F6CB}\x{1F6CD}-\x{1F6CF}\x{1F6E0}-\x{1F6E5}\x{1F6E9}\x{1F6F0}\x{1F6F3}])\x{FE0F}|\x{1F441}\x{FE0F}?\x{200D}\x{1F5E8}|\x{1F9D1}(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FB}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])|\x{1F469}(?:\x{1F3FF}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FE}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FD}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FC}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{1F3FB}\x{200D}[\x{2695}\x{2696}\x{2708}]|\x{200D}[\x{2695}\x{2696}\x{2708}])|\x{1F3F3}\x{FE0F}?\x{200D}\x{1F308}|\x{1F469}\x{200D}\x{1F467}|\x{1F469}\x{200D}\x{1F466}|\x{1F636}\x{200D}\x{1F32B}|\x{1F3F3}\x{FE0F}?\x{200D}\x{26A7}|\x{1F635}\x{200D}\x{1F4AB}|\x{1F62E}\x{200D}\x{1F4A8}|\x{1F415}\x{200D}\x{1F9BA}|\x{1FAF1}(?:\x{1F3FF}|\x{1F3FE}|\x{1F3FD}|\x{1F3FC}|\x{1F3FB})?|\x{1F9D1}(?:\x{1F3FF}|\x{1F3FE}|\x{1F3FD}|\x{1F3FC}|\x{1F3FB})?|\x{1F469}(?:\x{1F3FF}|\x{1F3FE}|\x{1F3FD}|\x{1F3FC}|\x{1F3FB})?|\x{1F43B}\x{200D}\x{2744}|(?:[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93D}\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}][\x{1F3FB}-\x{1F3FF}]|[\x{1F46F}\x{1F9DE}\x{1F9DF}])\x{200D}[\x{2640}\x{2642}]|[\x{26F9}\x{1F3CB}\x{1F3CC}\x{1F575}](?:[\x{FE0F}\x{1F3FB}-\x{1F3FF}]\x{200D}[\x{2640}\x{2642}]|\x{200D}[\x{2640}\x{2642}])|\x{1F3F4}\x{200D}\x{2620}|\x{1F1FD}\x{1F1F0}|\x{1F1F6}\x{1F1E6}|\x{1F1F4}\x{1F1F2}|\x{1F408}\x{200D}\x{2B1B}|\x{2764}(?:\x{FE0F}\x{200D}[\x{1F525}\x{1FA79}]|\x{200D}[\x{1F525}\x{1FA79}])|\x{1F441}\x{FE0F}?|\x{1F3F3}\x{FE0F}?|[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93C}-\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}]\x{200D}[\x{2640}\x{2642}]|\x{1F1FF}[\x{1F1E6}\x{1F1F2}\x{1F1FC}]|\x{1F1FE}[\x{1F1EA}\x{1F1F9}]|\x{1F1FC}[\x{1F1EB}\x{1F1F8}]|\x{1F1FB}[\x{1F1E6}\x{1F1E8}\x{1F1EA}\x{1F1EC}\x{1F1EE}\x{1F1F3}\x{1F1FA}]|\x{1F1FA}[\x{1F1E6}\x{1F1EC}\x{1F1F2}\x{1F1F3}\x{1F1F8}\x{1F1FE}\x{1F1FF}]|\x{1F1F9}[\x{1F1E6}\x{1F1E8}\x{1F1E9}\x{1F1EB}-\x{1F1ED}\x{1F1EF}-\x{1F1F4}\x{1F1F7}\x{1F1F9}\x{1F1FB}\x{1F1FC}\x{1F1FF}]|\x{1F1F8}[\x{1F1E6}-\x{1F1EA}\x{1F1EC}-\x{1F1F4}\x{1F1F7}-\x{1F1F9}\x{1F1FB}\x{1F1FD}-\x{1F1FF}]|\x{1F1F7}[\x{1F1EA}\x{1F1F4}\x{1F1F8}\x{1F1FA}\x{1F1FC}]|\x{1F1F5}[\x{1F1E6}\x{1F1EA}-\x{1F1ED}\x{1F1F0}-\x{1F1F3}\x{1F1F7}-\x{1F1F9}\x{1F1FC}\x{1F1FE}]|\x{1F1F3}[\x{1F1E6}\x{1F1E8}\x{1F1EA}-\x{1F1EC}\x{1F1EE}\x{1F1F1}\x{1F1F4}\x{1F1F5}\x{1F1F7}\x{1F1FA}\x{1F1FF}]|\x{1F1F2}[\x{1F1E6}\x{1F1E8}-\x{1F1ED}\x{1F1F0}-\x{1F1FF}]|\x{1F1F1}[\x{1F1E6}-\x{1F1E8}\x{1F1EE}\x{1F1F0}\x{1F1F7}-\x{1F1FB}\x{1F1FE}]|\x{1F1F0}[\x{1F1EA}\x{1F1EC}-\x{1F1EE}\x{1F1F2}\x{1F1F3}\x{1F1F5}\x{1F1F7}\x{1F1FC}\x{1F1FE}\x{1F1FF}]|\x{1F1EF}[\x{1F1EA}\x{1F1F2}\x{1F1F4}\x{1F1F5}]|\x{1F1EE}[\x{1F1E8}-\x{1F1EA}\x{1F1F1}-\x{1F1F4}\x{1F1F6}-\x{1F1F9}]|\x{1F1ED}[\x{1F1F0}\x{1F1F2}\x{1F1F3}\x{1F1F7}\x{1F1F9}\x{1F1FA}]|\x{1F1EC}[\x{1F1E6}\x{1F1E7}\x{1F1E9}-\x{1F1EE}\x{1F1F1}-\x{1F1F3}\x{1F1F5}-\x{1F1FA}\x{1F1FC}\x{1F1FE}]|\x{1F1EB}[\x{1F1EE}-\x{1F1F0}\x{1F1F2}\x{1F1F4}\x{1F1F7}]|\x{1F1EA}[\x{1F1E6}\x{1F1E8}\x{1F1EA}\x{1F1EC}\x{1F1ED}\x{1F1F7}-\x{1F1FA}]|\x{1F1E9}[\x{1F1EA}\x{1F1EC}\x{1F1EF}\x{1F1F0}\x{1F1F2}\x{1F1F4}\x{1F1FF}]|\x{1F1E8}[\x{1F1E6}\x{1F1E8}\x{1F1E9}\x{1F1EB}-\x{1F1EE}\x{1F1F0}-\x{1F1F5}\x{1F1F7}\x{1F1FA}-\x{1F1FF}]|\x{1F1E7}[\x{1F1E6}\x{1F1E7}\x{1F1E9}-\x{1F1EF}\x{1F1F1}-\x{1F1F4}\x{1F1F6}-\x{1F1F9}\x{1F1FB}\x{1F1FC}\x{1F1FE}\x{1F1FF}]|\x{1F1E6}[\x{1F1E8}-\x{1F1EC}\x{1F1EE}\x{1F1F1}\x{1F1F2}\x{1F1F4}\x{1F1F6}-\x{1F1FA}\x{1F1FC}\x{1F1FD}\x{1F1FF}]|[#\*0-9]\x{FE0F}?\x{20E3}|\x{1F93C}[\x{1F3FB}-\x{1F3FF}]|\x{2764}\x{FE0F}?|[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93D}\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}][\x{1F3FB}-\x{1F3FF}]|[\x{26F9}\x{1F3CB}\x{1F3CC}\x{1F575}][\x{FE0F}\x{1F3FB}-\x{1F3FF}]?|\x{1F3F4}|[\x{270A}\x{270B}\x{1F385}\x{1F3C2}\x{1F3C7}\x{1F442}\x{1F443}\x{1F446}-\x{1F450}\x{1F466}\x{1F467}\x{1F46B}-\x{1F46D}\x{1F472}\x{1F474}-\x{1F476}\x{1F478}\x{1F47C}\x{1F483}\x{1F485}\x{1F48F}\x{1F491}\x{1F4AA}\x{1F57A}\x{1F595}\x{1F596}\x{1F64C}\x{1F64F}\x{1F6C0}\x{1F6CC}\x{1F90C}\x{1F90F}\x{1F918}-\x{1F91F}\x{1F930}-\x{1F934}\x{1F936}\x{1F977}\x{1F9B5}\x{1F9B6}\x{1F9BB}\x{1F9D2}\x{1F9D3}\x{1F9D5}\x{1FAC3}-\x{1FAC5}\x{1FAF0}\x{1FAF2}-\x{1FAF6}][\x{1F3FB}-\x{1F3FF}]|[\x{261D}\x{270C}\x{270D}\x{1F574}\x{1F590}][\x{FE0F}\x{1F3FB}-\x{1F3FF}]|[\x{261D}\x{270A}-\x{270D}\x{1F385}\x{1F3C2}\x{1F3C7}\x{1F408}\x{1F415}\x{1F43B}\x{1F442}\x{1F443}\x{1F446}-\x{1F450}\x{1F466}\x{1F467}\x{1F46B}-\x{1F46D}\x{1F472}\x{1F474}-\x{1F476}\x{1F478}\x{1F47C}\x{1F483}\x{1F485}\x{1F48F}\x{1F491}\x{1F4AA}\x{1F574}\x{1F57A}\x{1F590}\x{1F595}\x{1F596}\x{1F62E}\x{1F635}\x{1F636}\x{1F64C}\x{1F64F}\x{1F6C0}\x{1F6CC}\x{1F90C}\x{1F90F}\x{1F918}-\x{1F91F}\x{1F930}-\x{1F934}\x{1F936}\x{1F93C}\x{1F977}\x{1F9B5}\x{1F9B6}\x{1F9BB}\x{1F9D2}\x{1F9D3}\x{1F9D5}\x{1FAC3}-\x{1FAC5}\x{1FAF0}\x{1FAF2}-\x{1FAF6}]|[\x{1F3C3}\x{1F3C4}\x{1F3CA}\x{1F46E}\x{1F470}\x{1F471}\x{1F473}\x{1F477}\x{1F481}\x{1F482}\x{1F486}\x{1F487}\x{1F645}-\x{1F647}\x{1F64B}\x{1F64D}\x{1F64E}\x{1F6A3}\x{1F6B4}-\x{1F6B6}\x{1F926}\x{1F935}\x{1F937}-\x{1F939}\x{1F93D}\x{1F93E}\x{1F9B8}\x{1F9B9}\x{1F9CD}-\x{1F9CF}\x{1F9D4}\x{1F9D6}-\x{1F9DD}]|[\x{1F46F}\x{1F9DE}\x{1F9DF}]|[\xA9\xAE\x{203C}\x{2049}\x{2122}\x{2139}\x{2194}-\x{2199}\x{21A9}\x{21AA}\x{231A}\x{231B}\x{2328}\x{23CF}\x{23ED}-\x{23EF}\x{23F1}\x{23F2}\x{23F8}-\x{23FA}\x{24C2}\x{25AA}\x{25AB}\x{25B6}\x{25C0}\x{25FB}\x{25FC}\x{25FE}\x{2600}-\x{2604}\x{260E}\x{2611}\x{2614}\x{2615}\x{2618}\x{2620}\x{2622}\x{2623}\x{2626}\x{262A}\x{262E}\x{262F}\x{2638}-\x{263A}\x{2640}\x{2642}\x{2648}-\x{2653}\x{265F}\x{2660}\x{2663}\x{2665}\x{2666}\x{2668}\x{267B}\x{267E}\x{267F}\x{2692}\x{2694}-\x{2697}\x{2699}\x{269B}\x{269C}\x{26A0}\x{26A7}\x{26AA}\x{26B0}\x{26B1}\x{26BD}\x{26BE}\x{26C4}\x{26C8}\x{26CF}\x{26D1}\x{26D3}\x{26E9}\x{26F0}-\x{26F5}\x{26F7}\x{26F8}\x{26FA}\x{2702}\x{2708}\x{2709}\x{270F}\x{2712}\x{2714}\x{2716}\x{271D}\x{2721}\x{2733}\x{2734}\x{2744}\x{2747}\x{2763}\x{27A1}\x{2934}\x{2935}\x{2B05}-\x{2B07}\x{2B1B}\x{2B1C}\x{2B55}\x{3030}\x{303D}\x{3297}\x{3299}\x{1F004}\x{1F170}\x{1F171}\x{1F17E}\x{1F17F}\x{1F202}\x{1F237}\x{1F321}\x{1F324}-\x{1F32C}\x{1F336}\x{1F37D}\x{1F396}\x{1F397}\x{1F399}-\x{1F39B}\x{1F39E}\x{1F39F}\x{1F3CD}\x{1F3CE}\x{1F3D4}-\x{1F3DF}\x{1F3F5}\x{1F3F7}\x{1F43F}\x{1F4FD}\x{1F549}\x{1F54A}\x{1F56F}\x{1F570}\x{1F573}\x{1F576}-\x{1F579}\x{1F587}\x{1F58A}-\x{1F58D}\x{1F5A5}\x{1F5A8}\x{1F5B1}\x{1F5B2}\x{1F5BC}\x{1F5C2}-\x{1F5C4}\x{1F5D1}-\x{1F5D3}\x{1F5DC}-\x{1F5DE}\x{1F5E1}\x{1F5E3}\x{1F5E8}\x{1F5EF}\x{1F5F3}\x{1F5FA}\x{1F6CB}\x{1F6CD}-\x{1F6CF}\x{1F6E0}-\x{1F6E5}\x{1F6E9}\x{1F6F0}\x{1F6F3}]|[\x{23E9}-\x{23EC}\x{23F0}\x{23F3}\x{25FD}\x{2693}\x{26A1}\x{26AB}\x{26C5}\x{26CE}\x{26D4}\x{26EA}\x{26FD}\x{2705}\x{2728}\x{274C}\x{274E}\x{2753}-\x{2755}\x{2757}\x{2795}-\x{2797}\x{27B0}\x{27BF}\x{2B50}\x{1F0CF}\x{1F18E}\x{1F191}-\x{1F19A}\x{1F201}\x{1F21A}\x{1F22F}\x{1F232}-\x{1F236}\x{1F238}-\x{1F23A}\x{1F250}\x{1F251}\x{1F300}-\x{1F320}\x{1F32D}-\x{1F335}\x{1F337}-\x{1F37C}\x{1F37E}-\x{1F384}\x{1F386}-\x{1F393}\x{1F3A0}-\x{1F3C1}\x{1F3C5}\x{1F3C6}\x{1F3C8}\x{1F3C9}\x{1F3CF}-\x{1F3D3}\x{1F3E0}-\x{1F3F0}\x{1F3F8}-\x{1F407}\x{1F409}-\x{1F414}\x{1F416}-\x{1F43A}\x{1F43C}-\x{1F43E}\x{1F440}\x{1F444}\x{1F445}\x{1F451}-\x{1F465}\x{1F46A}\x{1F479}-\x{1F47B}\x{1F47D}-\x{1F480}\x{1F484}\x{1F488}-\x{1F48E}\x{1F490}\x{1F492}-\x{1F4A9}\x{1F4AB}-\x{1F4FC}\x{1F4FF}-\x{1F53D}\x{1F54B}-\x{1F54E}\x{1F550}-\x{1F567}\x{1F5A4}\x{1F5FB}-\x{1F62D}\x{1F62F}-\x{1F634}\x{1F637}-\x{1F644}\x{1F648}-\x{1F64A}\x{1F680}-\x{1F6A2}\x{1F6A4}-\x{1F6B3}\x{1F6B7}-\x{1F6BF}\x{1F6C1}-\x{1F6C5}\x{1F6D0}-\x{1F6D2}\x{1F6D5}-\x{1F6D7}\x{1F6DD}-\x{1F6DF}\x{1F6EB}\x{1F6EC}\x{1F6F4}-\x{1F6FC}\x{1F7E0}-\x{1F7EB}\x{1F7F0}\x{1F90D}\x{1F90E}\x{1F910}-\x{1F917}\x{1F920}-\x{1F925}\x{1F927}-\x{1F92F}\x{1F93A}\x{1F93F}-\x{1F945}\x{1F947}-\x{1F976}\x{1F978}-\x{1F9B4}\x{1F9B7}\x{1F9BA}\x{1F9BC}-\x{1F9CC}\x{1F9D0}\x{1F9E0}-\x{1F9FF}\x{1FA70}-\x{1FA74}\x{1FA78}-\x{1FA7C}\x{1FA80}-\x{1FA86}\x{1FA90}-\x{1FAAC}\x{1FAB0}-\x{1FABA}\x{1FAC0}-\x{1FAC2}\x{1FAD0}-\x{1FAD9}\x{1FAE0}-\x{1FAE7}]/u', '', $text); +} + +function shortenClient($client) +{ + // Pre-process by removing any non-alphanumeric characters except for certain punctuations. + $client = html_entity_decode($client); // Decode any HTML entities + $client = str_replace("'", "", $client); // Removing all occurrences of ' + $cleaned = preg_replace('/[^a-zA-Z0-9&]+/', ' ', $client); + + // Break into words. + $words = explode(' ', trim($cleaned)); + + $shortened = ''; + + // If there's only one word. + if (count($words) == 1) { + $word = $words[0]; + + if (strlen($word) <= 3) { + return strtoupper($word); + } + + // Prefer starting and ending characters. + $shortened = $word[0] . substr($word, -2); + } else { + // Less weightage to common words. + $commonWords = ['the', 'of', 'and']; + + foreach ($words as $word) { + if (!in_array(strtolower($word), $commonWords) || strlen($shortened) < 2) { + $shortened .= $word[0]; + } + } + + // If there are still not enough characters, take from the last word. + while (strlen($shortened) < 3 && !empty($word)) { + $shortened .= substr($word, 1, 1); + $word = substr($word, 1); + } + } + + return strtoupper(substr($shortened, 0, 3)); +} + +function roundUpToNearestMultiple($n, $increment = 1000) +{ + return (int) ($increment * ceil($n / $increment)); +} + +function roundToNearest15($time) +{ + // Validate the input time format + if (!preg_match('/^(\d{2}):(\d{2}):(\d{2})$/', $time, $matches)) { + return false; // or throw an exception + } + + // Extract hours, minutes, and seconds from the matched time string + list(, $hours, $minutes, $seconds) = $matches; + + // Convert everything to seconds for easier calculation + $totalSeconds = ($hours * 3600) + ($minutes * 60) + $seconds; + + // Calculate the remainder when divided by 900 seconds (15 minutes) + $remainder = $totalSeconds % 900; + + if ($remainder > 450) { // If remainder is more than 7.5 minutes (450 seconds), round up + $totalSeconds += (900 - $remainder); + } else { // Else round down + $totalSeconds -= $remainder; + } + + // Convert total seconds to decimal hours + $decimalHours = $totalSeconds / 3600; + + // Return the decimal hours + return number_format($decimalHours, 2); +} + +function formatDuration($time) { + // expects "HH:MM:SS" + [$h, $m, $s] = array_map('intval', explode(':', $time)); + + $parts = []; + + if ($h > 0) $parts[] = $h . 'h'; + if ($m > 0) $parts[] = $m . 'm'; + + // show seconds only if under 1 minute total OR if nothing else exists + if ($h == 0 && $m == 0) { + $parts[] = $s . 's'; + } + + return implode(' ', $parts); +} + +function validateDate($date) { + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { + return $date; + } + return date('Y-m-d'); // Fallback +} diff --git a/functions/logging.php b/functions/logging.php new file mode 100644 index 00000000..591c9cbd --- /dev/null +++ b/functions/logging.php @@ -0,0 +1,65 @@ + " Internet Explorer", + '/firefox/i' => " Firefox", + '/safari/i' => " Safari", + '/chrome/i' => " Chrome", + '/edg/i' => " Edge", + '/opr/i' => " Opera", + '/ddg/i' => " DuckDuckGo" + ); + foreach ($browser_array as $regex => $value) { + if (preg_match($regex, $user_browser)) { + $browser = $value; + } + } + return $browser; +} + +function getOS($user_os) { + $os_platform = "-"; + $os_array = array( + '/windows/i' => " Windows", + '/macintosh|mac os x/i' => " MacOS", + '/linux/i' => " Linux", + '/ubuntu/i' => " Ubuntu", + '/fedora/i' => " Fedora", + '/iphone/i' => " iPhone", + '/ipad/i' => " iPad", + '/android/i' => " Android" + ); + foreach ($os_array as $regex => $value) { + if (preg_match($regex, $user_os)) { + $os_platform = $value; + } + } + return $os_platform; +} + +function isMobile() +{ + // Check if the user agent is a mobile device + return preg_match('/(android|avantgo|blackberry|bolt|boost|cricket|docomo|fone|hiptop|mini|opera mini|palm|phone|pie|tablet|up.browser|up.link|webos|wos)/i', $_SERVER['HTTP_USER_AGENT']); +} + +// Redirect Function +function redirect($url = null, $permanent = false) { + // Use referer if no URL is provided + if (!$url) { + $url = $_SERVER['HTTP_REFERER'] ?? 'index.php'; + } + + if (!headers_sent()) { + header('Location: ' . $url, true, $permanent ? 301 : 302); + exit; + } else { + // Fallback for headers already sent + echo ""; + echo ''; + exit; + } +} + +//Flash Alert Function +function flash_alert(string $message, string $type = 'success'): void { + $_SESSION['alert_type'] = $type; + $_SESSION['alert_message'] = $message; +} diff --git a/functions/sanitize.php b/functions/sanitize.php new file mode 100644 index 00000000..d79fe443 --- /dev/null +++ b/functions/sanitize.php @@ -0,0 +1,148 @@ + $maxSizeBytes) { + return "File size exceeds the limit."; + } + + // Read the file content + $fileContent = file_get_contents($tmp); + + // Hash the file content using SHA-256 + $hashedContent = hash('md5', $fileContent); + + // Generate a secure filename using the hashed content + $secureFilename = $hashedContent . randomString(2) . '.' . $extension; + + return $secureFilename; +} diff --git a/functions/security.php b/functions/security.php new file mode 100644 index 00000000..16d8eea6 --- /dev/null +++ b/functions/security.php @@ -0,0 +1,217 @@ + '/', 'secure' => true, 'httponly' => true, 'samesite' => 'None']); + } else { + setcookie("user_encryption_session_key", $user_encryption_session_key, 0, "/"); + $_SESSION['alert_message'] = "Unencrypted connection flag set: Using non-secure cookies."; + } +} + +// Decrypts an encrypted password (website/asset credentials), returns it as a string +function decryptCredentialEntry($credential_password_ciphertext) +{ + + // Split the credential into IV and Ciphertext + $credential_iv = substr($credential_password_ciphertext, 0, 16); + $credential_ciphertext = $salt = substr($credential_password_ciphertext, 16); + + // Get the user session info. + $user_encryption_session_ciphertext = $_SESSION['user_encryption_session_ciphertext']; + $user_encryption_session_iv = $_SESSION['user_encryption_session_iv']; + $user_encryption_session_key = $_COOKIE['user_encryption_session_key']; + + // Decrypt the session key to get the master key + $site_encryption_master_key = openssl_decrypt($user_encryption_session_ciphertext, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv); + + // Decrypt the credential password using the master key + return openssl_decrypt($credential_ciphertext, 'aes-128-cbc', $site_encryption_master_key, 0, $credential_iv); +} + +// Encrypts a website/asset credential password +function encryptCredentialEntry($credential_password_cleartext) +{ + $iv = randomString(); + + // Get the user session info. + $user_encryption_session_ciphertext = $_SESSION['user_encryption_session_ciphertext']; + $user_encryption_session_iv = $_SESSION['user_encryption_session_iv']; + $user_encryption_session_key = $_COOKIE['user_encryption_session_key']; + + //Decrypt the session key to get the master key + $site_encryption_master_key = openssl_decrypt($user_encryption_session_ciphertext, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv); + + //Encrypt the website/asset credential using the master key + $ciphertext = openssl_encrypt($credential_password_cleartext, 'aes-128-cbc', $site_encryption_master_key, 0, $iv); + + return $iv . $ciphertext; +} + +function apiDecryptCredentialEntry($credential_ciphertext, $api_key_decrypt_hash, #[\SensitiveParameter]$api_key_decrypt_password) +{ + // Split the Credential entry (username/password) into IV and Ciphertext + $credential_iv = substr($credential_ciphertext, 0, 16); + $credential_ciphertext = $salt = substr($credential_ciphertext, 16); + + // Decrypt the api hash to get the master key + $site_encryption_master_key = decryptUserSpecificKey($api_key_decrypt_hash, $api_key_decrypt_password); + + // Decrypt the credential password using the master key + return openssl_decrypt($credential_ciphertext, 'aes-128-cbc', $site_encryption_master_key, 0, $credential_iv); +} + +function apiEncryptCredentialEntry(#[\SensitiveParameter]$credential_cleartext, $api_key_decrypt_hash, #[\SensitiveParameter]$api_key_decrypt_password) +{ + $iv = randomString(); + + // Decrypt the api hash to get the master key + $site_encryption_master_key = decryptUserSpecificKey($api_key_decrypt_hash, $api_key_decrypt_password); + + // Encrypt the credential using the master key + $ciphertext = openssl_encrypt($credential_cleartext, 'aes-128-cbc', $site_encryption_master_key, 0, $iv); + + return $iv . $ciphertext; +} + +// Cross-Site Request Forgery check for sensitive functions +// Validates the CSRF token provided matches the one in the users session +function validateCSRFToken($token) +{ + if (hash_equals($token, $_SESSION['csrf_token'])) { + return true; + } else { + $_SESSION['alert_type'] = "warning"; + $_SESSION['alert_message'] = "CSRF token verification failed. Try again, or log out to refresh your token."; + header("Location: index.php"); + exit(); + } +} + +function validateWhitelabelKey($key) +{ + $public_key = "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0k+4ZJudkdGMCFLx5b9 +H/sOozvWphFJsjVIF0vPVx9J0bTdml65UdS+32JagIHfPtEUTohaMnI3IAxxCDzl +655qmtjL7RHHdx9UMIKCmtAZOtd2u6rEyZH7vB7cKA49ysKGIaQSGwTQc8DCgsrK +uxRuX04xq9T7T+zuzROw3Y9WjFy9RwrONqLuG8LqO0j7bk5LKYeLAV7u3E/QiqNx +lEljN2UVJ3FZ/LkXeg8ORkV+IHs/toRIfPs/4VQnjEwk5BU6DX2STOvbeZnTqwP3 +zgjRYR/zGN5l+az6RB3+0mJRdZdv/y2aRkBlwTxx2gOrPbQAco4a/IOmkE3EbHe7 +6wIDAQAP +-----END PUBLIC KEY-----"; + + if (openssl_public_decrypt(base64_decode($key), $decrypted, $public_key)) { + $key_info = json_decode($decrypted, true); + if ($key_info['expires'] > date('Y-m-d H:i:s', strtotime('-7 day'))) { + return $key_info; + } + } + + return false; +}