appendChild($toDOM->ownerDocument->importNode($fromDOM, true)); } // ========================================== // Apply an XSL Stylesheet to an XML Document // ========================================== public function applyXSL($XMLString = "", $XSLString = "", $XSLParms = "") { if ($XMLString instanceof SimpleXMLElement) { $XMLString = $XMLString->asXML(); } if (empty($XSLString)) { trigger_error("No XSL Stylesheet provided."); } if (!is_array($XSLParms)) { $XSLParms = array(); } $XSLParms["CSRF_TOKEN"] = $this->csrfToken(); $XSLParms["APPLICATION_NAME"] = self::APPLICATION_NAME; $XSLParms["APPLICATION_MNEMONIC"] = self::APPLICATION_MNEMONIC; $XSLParms["VERSION"] = self::VERSION; $XSLParms["THEME"] = $_SESSION[self::THEME] ?? self::THEME; $XSLParms["ENVIRONMENT"] = $_SESSION[self::ENVIRONMENT] ?? "Production"; $XSLParms["USER_NAME"] = $XSLParms["USER_NAME"] ?? $_SESSION[self::USER_NAME] ?? ""; $XSLParms["USER_FULL_NAME"] = $_SESSION[self::USER_FULL_NAME] ?? ""; $XSLParms["USER_ROLE"] = $_SESSION[self::USER_ROLE] ?? "Guest"; $XSLParms["RANDOM_NUMBER"] = rand(); $XSLParms["TODAY_YYYYMMDD"] = date("Y-m-d"); $XSLParms["TODAY_MMDDYYYY"] = date("m/d/Y"); $XSLParms["TODAY_HHMM"] = date("Hi"); $XSLParms["COPYRIGHT"] = date("Y"); $XML = new DOMDocument(); $XSL = new DOMDocument(); $XSLT = new XSLTProcessor(); if ($XMLString) { $XML->loadXML($XMLString, LIBXML_NONET); } if ($XSLString) { $XSL->loadXML($XSLString, LIBXML_NONET); } $XSLT->importStylesheet($XSL); if ($XSLParms) { $XSLT->setParameter("", $XSLParms); } if (!$XML->documentElement) $XML->loadXML(''); $document = $XSLT->transformToXml($XML); return $document; } // ============================ // Return a Database Connection // ============================ public function connect(): PDO { $settings = $this->getSettings(); $host = (string) $settings->DatabaseServer; $database = (string) $settings->DatabaseName; $username = (string) $settings->DatabaseUser; $password = (string) $settings->DatabasePassword; try { $dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $host, (int) ($settings->DatabasePort ?? 3306), $database); $options=[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_PERSISTENT => false,]; $ca=getenv('HCR_DB_TLS_CA')?:''; if((string)$settings->Environment==='Production'&&!in_array(strtolower($host),['localhost','127.0.0.1','::1'],true)&&!$ca)throw new RuntimeException('Remote production database connections require HCR_DB_TLS_CA and verified TLS.'); if($ca){if(!is_readable($ca))throw new RuntimeException('The database TLS CA file is not readable.');$options[PDO::MYSQL_ATTR_SSL_CA]=$ca;$options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT]=true;} $db=new PDO($dsn,$username,$password,$options); if($ca){$cipher=$db->query("SHOW SESSION STATUS LIKE 'Ssl_cipher'")->fetch();if(!$cipher||empty($cipher['Value']))throw new RuntimeException('The database connection did not negotiate TLS.');} return $db; } catch (PDOException $e) { error_log("Database Connection Failed: " . $e->getMessage()); throw new Exception("Unable to connect to the database.", 0, $e); } } // =============== // Debug an Object // =============== public function debug($object = "") { if ($_SESSION[self::AJAX]) { error_log(print_r($object, true)); } else { echo "
";
            print_r($object);
        }

        exit();
    }

    // ======================
    // Display a Content Page
    // ======================

    public function displayContent($content = "") {

        $html = $this->applyXSL("", $this->getXSL("content"));
        $html = str_replace("$$$-CONTENT-$$$", $content, $html);

        echo $html;

        exit();
    }

    // ======================================================
    // Custom error handler to display informative error page
    // ======================================================

    public function displayError($error_number = 0, $error_message = '', $error_file = '', $error_line = 0) {
        if (!(error_reporting() & $error_number)) {
            return false;
        }
        // Never log application failures through the failing database or SMTP service.
        error_log("PHP {$error_number}: {$error_message} in {$error_file}:{$error_line}");

        if (in_array($error_number, [E_DEPRECATED, E_USER_DEPRECATED], true)) {
            return true;
        }
        throw new ErrorException($error_message, 0, $error_number, $error_file, $error_line);
    }

    // =====================
    // Display a Notice Page
    // =====================

    public function displayNotice($messgae = "") {

        $XSLParms["MESSAGE"] = $messgae;

        $content = $this->applyXSL("", $this->getXSL("notice"), $XSLParms);

        $this->displayContent($content);

        exit();
    }

    // =====================
    // Display the Home Page
    // =====================

    public function displayHome() {
        (new Roster())->home();
    }

    // =======================
    // Do the requested Action
    // =======================

    public function do() {

        if (empty($_SESSION[self::ACTIVE_SESSION])) {
            $this->initializeSession();
        }

        $_SESSION[self::AJAX] = strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'xmlhttprequest';

        $_SESSION[self::TODAY] = date('Y-m-d');

        $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';

        if (!in_array($method, ['GET', 'POST'], true)) {
            http_response_code(405);
            exit('Method not allowed.');
        }

        // Reject arrays and oversized values before passing input to string operations.
        foreach (array_merge($_GET, $_POST) as $value) {
            if (!is_string($value) || strlen($value) > 8192) {
                http_response_code(400);
                exit('Invalid request.');
            }
        }
        $action = strtolower(trim($_POST['action'] ?? $_GET['Xaction'] ?? $_GET['action'] ?? ''));
        if($action==='verifyemail'){(new EmailVerification())->page();return;}
        if ($action === 'signout') {
            if ($method !== 'POST') {
                http_response_code(405);
                exit('Use POST to sign out.');
            }
            $this->requireCsrf();
            $ajax = $_SESSION[self::AJAX];
            $this->logoutSession();
            if ($ajax) {
                header('Content-Type: application/json');
                echo json_encode(true);
                exit;
            }
            header('Location: main.php', true, 303);
            exit;
        }
        if (in_array($action, ['resetpassword', 'authentication.resetpassword'], true)) {
            (new Authentication())->resetPassword();
        }
        if (in_array($action, ['signup', 'signup.createaccount'], true)) {
            (new SignUp())->createAccount();
        }
        if (in_array($action, ['signin', 'signin.login', 'users.login'], true)) {
            (new SignIn())->login();
        }

        $user = (new AuthService())->currentUser();

        if (!$user) {
            $ajax = $_SESSION[self::AJAX];
            if (!empty($_SESSION[self::USER_AUTHENTICATED])) {
                $this->logoutSession();
            }
            if ($ajax) {
                http_response_code(401);
                exit('Please sign in again.');
            }
            (new SignIn())->login();
        }
        if ($method === 'POST') {
            $this->requireCsrf();
        }
        if ((bool) $user['user_change_password']) {
            (new Users())->changePassword();
            return;
        }
        
        // Only named, reviewed entry points are routable. Helpers are never endpoints.
        $routes = [
            '' => [Base::class, 'displayHome', false],
            './' => [Base::class, 'displayHome', false],
            'displayhome' => [Base::class, 'displayHome', false],
            'toggletheme' => [Base::class, 'toggleTheme', false],
            'getzipcode' => [Base::class, 'getZipcode', false],
            'validatedate' => [Base::class, 'validateDate', false],
            'getage' => [Base::class, 'getAge', false],
            'users.changepassword' => [Users::class, 'changePassword', false],
            'roster.workspace' => [Roster::class,'workspace',false],
            'roster.help' => [Roster::class,'help',false],
            'rosterreports.ledger' => [RosterReports::class,'ledger',true],
            'rosterreports.csv' => [RosterReports::class,'csv',true],
            'rosterreports.excel' => [RosterReports::class,'excel',true],
            'rosterreports.pdf' => [RosterReports::class,'pdf',true],




        ];
        $adminRoutes = [
            'Users' => ['setupUsers', 'userSummary', 'addUser', 'editUser', 'deleteUser', 'resetPassword', 'userExists', 'getUser_AJAX', 'updateActiveStatus', 'userActive'],
            'Alerts' => ['setupAlerts', 'addAlert', 'editAlert', 'deleteAlert'],
            'Dropdowns' => ['setupDropdowns', 'setupDropdowntypes', 'addDropdown', 'editDropdown', 'deleteDropdown', 'addDropdowntype', 'editDropdowntype', 'deleteDropdowntype', 'dropdownExists', 'dropdowntypeExists', 'dropdowntypetitleExists', 'dropdownActive', 'dropdowntypeActive', 'updateDropdownActive', 'updateDropdownDefault'],
            'Popovers' => ['setupPopovers', 'addPopover', 'editPopover', 'deletePopover', 'popoverExists'],
            'Reports' => ['setupReports', 'selectReport'],
            'Statuses' => ['setupStatuses', 'addStatus', 'editStatus', 'deleteStatus', 'statusExists', 'statusActive'],
            'Journal' => ['viewJournal'],
            'Notes' => ['addNote', 'editNote', 'deleteNote'],
        ];
        foreach ($adminRoutes as $class => $methods) {
            foreach ($methods as $entry) {
                $routes[strtolower($class . '.' . $entry)] = [$class, $entry, true];
            }
        }
        if (!isset($routes[$action])) {
            http_response_code(404);
            exit('Page not found.');
        }
        [$class, $entry, $adminOnly] = $routes[$action];
        if ($adminOnly && !in_array($user['user_role'], ['Administrator', 'System Administrator'], true)) {
            http_response_code(403);
            exit('You do not have permission to perform this action.');
        }
        if ($method !== 'POST' && !in_array($action, ['', './', 'displayhome', 'users.changepassword','roster.workspace','roster.help'], true)) {
            http_response_code(405);
            exit('Use POST for this action.');
        }
        ob_start();
        try{(new $class())->$entry();$body=ob_get_clean();}
        catch(Throwable $e){ob_end_clean();throw $e;}
        if($_SESSION[self::AJAX]??false){header('Content-Type: application/json');echo trim($body)===''?json_encode(['ok'=>true]):$body;}
        else echo $body;
    }

    public function csrfToken(): string {
        if (empty($_SESSION['csrf_token']))
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        return $_SESSION['csrf_token'];
    }

    public function requireCsrf(): void {
        $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
        if (!is_string($token) || !hash_equals($this->csrfToken(), $token)) {
            http_response_code(403);
            exit('Your form expired. Reload the page and try again.');
        }
    }

    public function logoutSession(): void {
        $_SESSION = [];
        session_regenerate_id(true);
        $this->initializeSession();
        $this->csrfToken();
    }

    // ==========================
    // Return an encrypted string
    // ==========================

    protected function encrypt($string = "", $key = "") {

        $key = (empty($key)) ? __CLASS__ : $key;

        $ivlen = openssl_cipher_iv_length($cipher = "AES-128-CBC");
        $iv = openssl_random_pseudo_bytes($ivlen);
        $raw = openssl_encrypt($string, $cipher, $key, $options = OPENSSL_RAW_DATA, $iv);
        $hmac = hash_hmac("sha256", $raw, $key, $as_binary = true);
        $encrypted = base64_encode($iv . $hmac . $raw);

        return $encrypted;
    }

    // ========================
    // Execute an SQL Statement
    // ========================

    public function executeSQL($SQL = "") {

        if (trim($SQL) == "") {
            return false;
        }

        return $this->connect()->query($SQL);
    }

    // ======================
    // Format an XML document
    // ======================

    public function formatXML($XML = "") {

        if (!$XML) {
            return false;
        }

        if ($XML instanceof SimpleXMLElement) {
            $XML = $XML->asXML();
        }

        $dom = new DOMDocument("1.0");
        $dom->preserveWhiteSpace = false;
        $dom->formatOutput = true;
        $dom->loadXML($XML);

        return $dom->saveXML();
    }

    // =====================================
    // Return an Application Settings Object
    // =====================================

    public function getSettings() {

        $path = getenv('HCR_SETTINGS_FILE') ?: (getenv('HCR_ENVIRONMENT')!=='Production' && is_file(__DIR__ . '/../xml/settings.local.xml') ? __DIR__ . '/../xml/settings.local.xml' : __DIR__ . '/../xml/settings.xml');
        $settings = simplexml_load_file($path, SimpleXMLElement::class, LIBXML_NONET);
        if (!$settings)
            throw new RuntimeException('Unable to load application settings.');
        foreach (['Environment' => 'HCR_ENVIRONMENT', 'DatabaseServer' => 'HCR_DB_HOST', 'DatabasePort' => 'HCR_DB_PORT', 'DatabaseName' => 'HCR_DB_NAME', 'DatabaseUser' => 'HCR_DB_USER', 'DatabasePassword' => 'HCR_DB_PASSWORD', 'SmtpServer' => 'HCR_SMTP_HOST', 'SmtpUserName' => 'HCR_SMTP_USER', 'SmtpPassword' => 'HCR_SMTP_PASSWORD', 'SmtpPort' => 'HCR_SMTP_PORT', 'SmtpFromEmail' => 'HCR_SMTP_FROM', 'ApplicationUrl' => 'HCR_APPLICATION_URL'] as $name => $variable) {
            $value = getenv($variable);
            if ($value !== false)
                $settings->$name = $value;
        }

        return $settings;
    }

    // ==================================================================
    // Return Age in Years based on given Birthdate ("yyyy-mm-dd" format)
    // ==================================================================

    public function getAge($birthdate = "", $date = "") {

        $birthdate = filter_input(INPUT_POST, "birthdate", FILTER_UNSAFE_RAW) ?: $birthdate;
        $date = filter_input(INPUT_POST, "date", FILTER_UNSAFE_RAW) ?: $date;

        $age = 0;

        $date = (empty($date)) ? new Datetime() : new DateTime($date);
        $birthdate = (empty($birthdate)) ? new Datetime() : new DateTime($birthdate);

        $age = $birthdate->diff($date)->format("%y");

        if ($_SESSION[self::AJAX]) {
            echo json_encode($age);
        } else {
            return $age;
        }
    }

    // =========================
    // Return a Constants Object
    // =========================

    public function getConstants() {

        $Constants = new SimpleXMLElement($this->getXML("constants"));

        return $Constants;
    }

    // ==================
    // Return a Recordset
    // ==================

    public function getRecordset($SQL = "") {

        if (empty($SQL)) {
            return false;
        }

        $recordSet = $this->connect()->query($SQL);

        return $recordSet;
    }

    // =======================================
    // Return Table Data as a SimpleXMLElement
    // =======================================

    public function getTable($table = "", $SQL = "") {

        if (empty($table)) {
            return new SimpleXMLElement("");
        }

        $SXE = new SimpleXMLElement("<{$table}/>");

        $recordset = $this->getRecordset(empty($SQL) ? "select * from {$table}" : $SQL);

        if ($recordset) {

            while ($row = $recordset->fetch(PDO::FETCH_ASSOC)) {

                $record = $SXE->addChild("record");

                foreach ($row as $name => $value) {
                    $record->$name = $value;
                }
            }
        }

        return $SXE;
    }

    // =======================================
    // Return Table Data as a SimpleXMLElement
    // =======================================

    public function getTableXML($table = "", $statement = "") {

        if (empty($table)) {
            return new SimpleXMLElement("");
        }

        $SXE = new SimpleXMLElement("<{$table}/>");

        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {

            $record = $SXE->addChild("record");

            foreach ($row as $name => $value) {
                $record->$name = $value;
            }
        }

        return $SXE;
    }

    // =================================
    // Return the Time Elapsed since now
    // =================================

    public function getTimeElapsed($timestamp = "") {

        $now = new DateTime;
        $then = new DateTime($timestamp);
        $diff = (array) $now->diff($then);

        $diff["w"] = floor($diff["d"] / 7);
        $diff["d"] -= $diff["w"] * 7;

        $string = array(
            "y" => "year",
            "m" => "month",
            "w" => "week",
            "d" => "day",
            "h" => "hour",
            "i" => "minute",
            "s" => "second",
        );

        foreach ($string as $k => & $v) {

            if ($diff[$k]) {
                $v = $diff[$k] . " " . $v . ($diff[$k] > 1 ? "s" : "");
            } else {
                unset($string[$k]);
            }
        }

        $string = array_slice($string, 0, 1);

        return $string ? implode(", ", $string) . " ago" : "just now";
    }

    // ===================
    // Return a Time Stamp
    // ===================

    public function getTimestamp() {

        return date("Y-m-d H:i:s");
    }

    // ======================
    // Return an XML Document
    // ======================

    public function getXML($document = "") {

        if (trim($document) == "") {
            return "";
        }

        $document = __DIR__ . "/../xml/" . trim($document) . ".xml";
        $XML = (file_exists($document) ? file_get_contents($document) : "");

        return $XML;
    }

    // ========================
    // Return an XSL Stylesheet
    // ========================

    public function getXSL($document = "") {

        if (trim($document) == "") {
            return "";
        }

        $document = __DIR__ . "/../xsl/" . trim($document) . ".xsl";
        $XSL = (file_exists($document) ? file_get_contents($document) : "");

        return $XSL;
    }

    // =======================================
    // Return a Zip Code Detail Object (Array)
    // =======================================

    public function getZipcode($zip_code = 0) {

        $zip_code = filter_input(INPUT_POST, "zip_code", FILTER_SANITIZE_NUMBER_INT) ?: 0;
        $zip_code = (int) substr((string) $zip_code, 0, 5);

        $zipCodeDetail = null;

        $SQL = "select *
                  from zipcodes
                 where zipcode_code = {$zip_code}";

        $zipcodes = $this->getTable("zipcodes", $SQL);

        if ($zipcodes->count() > 0) {

            $zipCodeDetail["ZipCode"] = $zip_code;
            $zipCodeDetail["City"] = (string) $zipcodes->record->zipcode_city;
            $zipCodeDetail["State"] = (string) $zipcodes->record->zipcode_state;
            $zipCodeDetail["County"] = (string) $zipcodes->record->zipcode_county;
            $zipCodeDetail["Country"] = (string) $zipcodes->record->zipcode_country;
        }

        if ($_SESSION[self::AJAX]) {
            echo json_encode($zipCodeDetail);
        } else {
            return $zipCodeDetail;
        }
    }

    // ======================
    // Initialize the Session
    // ======================

    public function initializeSession() {

        $settings = $this->getSettings();

        $_SESSION[self::ACTIVE_SESSION] = true;
        $_SESSION[self::THEME] = "light";
        $_SESSION[self::ENVIRONMENT] = (string) ($settings->Environment ?? "Production");
        $_SESSION[self::USER_AUTHENTICATED] = false;
        $_SESSION[self::USER_SERIAL] = 0;
        $_SESSION[self::USER_NAME] = "";
        $_SESSION[self::USER_FULL_NAME] = "";
        $_SESSION[self::USER_EMAIL] = "";
        $_SESSION[self::USER_ROLE] = "Guest";
        $_SESSION[self::REPORTS_GET] = "";
        $_SESSION[self::REPORTS_POST] = "";
        $_SESSION[self::AJAX] = false;
    }

    // ============
    // Log an Error
    // ============

    public function logError($error_message = "") {
        error_log($error_message);
        throw new AppException('The requested record is invalid or unavailable.',404);
    }

    // ==========================
    // Merge thisXML with thatXML
    // ==========================

    public function mergeXML($thisXML = "", $thatXML = "") {

        if ($thisXML instanceof SimpleXMLElement) {
            $thisXML = $thisXML->asXML();
        }

        if ($thatXML instanceof SimpleXMLElement) {
            $thatXML = $thatXML->asXML();
        }

        if (trim($thisXML) == "") {
            return $thatXML;
        }

        if (trim($thatXML) == "") {
            return $thisXML;
        }

        $thisDOM = new DOMDocument();
        $thisDOM->loadXML($thisXML);

        $thatDOM = new DOMDocument();
        $thatDOM->loadXML($thatXML);

        $xpath = new DOMXpath($thisDOM);
        $xpathQuery = $xpath->query("/*");

        for ($i = 0; $i < $xpathQuery->length; $i++) {
            $thatDOM->documentElement->appendChild($thatDOM->importNode($xpathQuery->item($i), true));
        }

        $thatSXE = simplexml_import_dom($thatDOM);

        return $thatSXE->asXML();
    }

    // ==============
    // Print a Report
    // ==============

    // =================
    // Sanitize a string
    // =================

    public function sanitizeString($string = "") {

        return trim(preg_replace("/\s+/", " ", htmlspecialchars($string, ENT_NOQUOTES | ENT_HTML5)));
    }

    // ========================
    // Toggle the current Theme
    // ========================

    public function toggleTheme() {

        $_SESSION[self::THEME] = (($_SESSION[self::THEME] ?? "light") === "dark") ? "light" : "dark";
    }

    // ===============================
    // Validate a Date (local or AJAX)
    // ===============================

    public function validateDate($date = "", $format = "Y-m-d") {

        $date = filter_input(INPUT_POST, "date") ?: $date;

        $datetime = DateTime::createFromFormat($format, $date);
        $valid = $datetime && $datetime->format($format) === $date;

        if ($_SESSION[self::AJAX]) {
            echo json_encode($valid);
        } else {
            return $valid;
        }
    }

    // ========================
    // Generate Random Password
    // ========================

    public function generateRandomPassword($length = 12) {

        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_+=';
        $password = '';

        for ($i = 0; $i < $length; $i++) {
            $index = random_int(0, strlen($characters) - 1);
            $password .= $characters[$index];
        }

        return $password;
    }

    // ====================
    // User Change Password
    // ====================

    public function userChangePassword() {

        $user = (new Users())->getUserByUserName($_SESSION[self::USER_NAME]);

        if ($user->record->user_change_password == 1) {

            return true;
        }

        return false;
    }

    // ==============================
    // Alter Sting to Parahgraph Case
    // ==============================

    public function toParagraphCase($text) {

        $text = ucwords(strtolower($text));

        return $text;
    }

    // ==========================
    // Developer Testing Function
    // ==========================

    public function testing() {

        $to = "james.richie@onesourceits.com";
        $subject = "Test Subject";
        $message = "Test Message";

        $email = (new Email())->sendEmail($to, $subject, $message);

        $this->debug($email);

        exit();
    }

    // =====================
    // Forgot/Reset Password
    // =====================
}