2026-10-18 17:01:36 +00:00
<? php
// ==========
// Base Class
// ==========
2026-10-18 19:27:17 -04:00
require_once __DIR__ . "/../vendor/autoload.php" ;
require_once __DIR__ . "/Autoloader.php" ;
2026-10-18 17:01:36 +00:00
class Base {
// =========
// Constants
// =========
2026-10-20 06:45:30 -04:00
const APPLICATION_NAME = "My Hunting Club Roster" ;
2026-10-18 13:09:40 -04:00
const APPLICATION_MNEMONIC = "HUNTINGCLUBROSTER" ;
2026-10-21 06:53:45 -04:00
const VERSION = "1.04" ;
2026-10-18 19:27:17 -04:00
const TIMEZONE = "America/New_York" ;
2026-10-18 17:01:36 +00:00
// =================
// Session Variables
// =================
2026-10-18 19:27:17 -04:00
const ENVIRONMENT = "HUNTINGCLUBROSTER_ENVIRONMENT" ;
const ACTIVE_SESSION = "HUNTINGCLUBROSTER_ACTIVE_SESSION" ;
const USER_SERIAL = "HUNTINGCLUBROSTER_USER_SERIAL" ;
const USER_NAME = "HUNTINGCLUBROSTER_USER_NAME" ;
const USER_FULL_NAME = "HUNTINGCLUBROSTER_USER_FULL_NAME" ;
const USER_EMAIL = "HUNTINGCLUBROSTER_USER_EMAIL" ;
const USER_ROLE = "HUNTINGCLUBROSTER_USER_ROLE" ;
const USER_AUTHENTICATED = "HUNTINGCLUBROSTER_USER_AUTHENTICATED" ;
const REPORTS_GET = "HUNTINGCLUBROSTER_REPORTS_GET" ;
const REPORTS_POST = "HUNTINGCLUBROSTER_REPORTS_POST" ;
const THEME = "HUNTINGCLUBROSTER_THEME" ;
const AJAX = "HUNTINGCLUBROSTER_AJAX" ;
const TODAY = "HUNTINGCLUBROSTER_TODAY" ;
const COPYRIGHT = "HUNTINGCLUBROSTER_COPYRIGHT" ;
const PAGE = "HUNTINGCLUBROSTER_PAGE" ;
const PAGINATION_PAGE = "HUNTINGCLUBROSTER_PAGINATION_PAGE" ;
2026-10-18 17:01:36 +00:00
// =================
// Class Constructor
// =================
public function __construct () {
set_error_handler ( array ( $this , "displayError" ));
date_default_timezone_set ( self :: TIMEZONE );
}
// ===============================================
// Append a SimpleXMLElement to a SimpleXMLElememt
// ===============================================
public function appendSXE ( SimpleXMLElement $from , SimpleXMLElement $to ) {
2026-10-18 19:27:17 -04:00
$toDOM = dom_import_simplexml ( $to );
2026-10-18 17:01:36 +00:00
$fromDOM = dom_import_simplexml ( $from );
$toDOM -> 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 ();
}
2026-10-18 19:27:17 -04:00
$XSLParms [ "CSRF_TOKEN" ] = $this -> csrfToken ();
$XSLParms [ "APPLICATION_NAME" ] = self :: APPLICATION_NAME ;
2026-10-18 17:01:36 +00:00
$XSLParms [ "APPLICATION_MNEMONIC" ] = self :: APPLICATION_MNEMONIC ;
2026-10-18 19:27:17 -04:00
$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 ();
2026-10-18 17:01:36 +00:00
$XSLT = new XSLTProcessor ();
if ( $XMLString ) {
2026-10-18 19:27:17 -04:00
$XML -> loadXML ( $XMLString , LIBXML_NONET );
2026-10-18 17:01:36 +00:00
}
if ( $XSLString ) {
2026-10-18 19:27:17 -04:00
$XSL -> loadXML ( $XSLString , LIBXML_NONET );
2026-10-18 17:01:36 +00:00
}
$XSLT -> importStylesheet ( $XSL );
if ( $XSLParms ) {
$XSLT -> setParameter ( "" , $XSLParms );
}
2026-10-18 19:27:17 -04:00
if ( ! $XML -> documentElement )
$XML -> loadXML ( '<XML/>' );
2026-10-18 17:01:36 +00:00
$document = $XSLT -> transformToXml ( $XML );
return $document ;
}
// ============================
// Return a Database Connection
// ============================
public function connect () : PDO {
2026-10-18 19:27:17 -04:00
2026-10-18 17:01:36 +00:00
$settings = $this -> getSettings ();
2026-10-18 19:27:17 -04:00
$host = ( string ) $settings -> DatabaseServer ;
$database = ( string ) $settings -> DatabaseName ;
$username = ( string ) $settings -> DatabaseUser ;
$password = ( string ) $settings -> DatabasePassword ;
2026-10-18 17:01:36 +00:00
try {
2026-10-18 19:27:17 -04:00
$dsn = sprintf ( 'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4' , $host , ( int ) ( $settings -> DatabasePort ?? 3306 ), $database );
2026-10-18 17:01:36 +00:00
2026-10-20 06:45:30 -04:00
$options = [ PDO :: ATTR_ERRMODE => PDO :: ERRMODE_EXCEPTION ,
2026-10-18 19:27:17 -04:00
PDO :: ATTR_DEFAULT_FETCH_MODE => PDO :: FETCH_ASSOC ,
PDO :: ATTR_EMULATE_PREPARES => false ,
2026-10-20 06:45:30 -04:00
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 ;
2026-10-18 17:01:36 +00:00
} catch ( PDOException $e ) {
2026-10-18 19:27:17 -04:00
error_log ( "Database Connection Failed: " . $e -> getMessage ());
2026-10-18 17:01:36 +00:00
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 "<pre>" ;
print_r ( $object );
}
exit ();
}
// ======================
// Display a Content Page
// ======================
public function displayContent ( $content = "" ) {
2026-10-21 06:53:45 -04:00
$html = $this -> applyXSL ( "" , $this -> getXSL ( "content" ), [ "IS_SUPER" => ( new TenantAccess ()) -> isSuper () ? "1" : "0" , "CAN_MANAGE_CLUBS" => ( new TenantAccess ()) -> managesAny () ? "1" : "0" ]);
2026-10-18 17:01:36 +00:00
$html = str_replace ( "$$$-CONTENT-$$$" , $content , $html );
echo $html ;
exit ();
}
// ======================================================
// Custom error handler to display informative error page
// ======================================================
2026-10-18 19:27:17 -04:00
public function displayError ( $error_number = 0 , $error_message = '' , $error_file = '' , $error_line = 0 ) {
if ( ! ( error_reporting () & $error_number )) {
return false ;
2026-10-18 17:01:36 +00:00
}
2026-10-18 19:27:17 -04:00
// Never log application failures through the failing database or SMTP service.
error_log ( "PHP { $error_number } : { $error_message } in { $error_file } : { $error_line } " );
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
if ( in_array ( $error_number , [ E_DEPRECATED , E_USER_DEPRECATED ], true )) {
return true ;
2026-10-18 17:01:36 +00:00
}
2026-10-18 19:27:17 -04:00
throw new ErrorException ( $error_message , 0 , $error_number , $error_file , $error_line );
2026-10-18 17:01:36 +00:00
}
// =====================
// 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 () {
2026-10-20 06:45:30 -04:00
( new Roster ()) -> home ();
2026-10-18 17:01:36 +00:00
}
// =======================
// Do the requested Action
// =======================
public function do () {
2026-10-18 19:27:17 -04:00
if ( empty ( $_SESSION [ self :: ACTIVE_SESSION ])) {
$this -> initializeSession ();
}
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
$_SESSION [ self :: AJAX ] = strtolower ( $_SERVER [ 'HTTP_X_REQUESTED_WITH' ] ?? '' ) === 'xmlhttprequest' ;
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
$_SESSION [ self :: TODAY ] = date ( 'Y-m-d' );
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
$method = $_SERVER [ 'REQUEST_METHOD' ] ?? 'GET' ;
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
if ( ! in_array ( $method , [ 'GET' , 'POST' ], true )) {
http_response_code ( 405 );
exit ( 'Method not allowed.' );
2026-10-18 17:01:36 +00:00
}
2026-10-21 06:53:45 -04:00
// Ticket bodies need room for 8,000 four-byte Unicode characters. Keep
// a hard transport ceiling; field validators enforce the character limit.
$candidate = $_POST [ 'action' ] ?? $_GET [ 'Xaction' ] ?? $_GET [ 'action' ] ?? '' ;
$ticketSubmission = is_string ( $candidate ) && strtolower ( trim ( $candidate )) === 'roster.tickets'
&& in_array ( $_POST [ 'step' ] ?? null ,[ 'openTicket' , 'reply' ], true );
foreach ([ $_GET , $_POST ] as $source => $values ) {
foreach ( $values as $field => $value ){
$byteLimit = $source === 1 && $ticketSubmission && $field === 'message' ? 65536 : 8192 ;
if ( ! is_string ( $value ) || strlen ( $value ) > $byteLimit ) {
http_response_code ( 400 );
exit ( 'Invalid request.' );
}
2026-10-18 19:27:17 -04:00
}
}
$action = strtolower ( trim ( $_POST [ 'action' ] ?? $_GET [ 'Xaction' ] ?? $_GET [ 'action' ] ?? '' ));
2026-10-20 06:45:30 -04:00
if ( $action === 'verifyemail' ){( new EmailVerification ()) -> page (); return ;}
2026-10-18 19:27:17 -04:00
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 )) {
2026-10-18 17:01:36 +00:00
( new SignIn ()) -> login ();
}
2026-10-20 06:45:30 -04:00
2026-10-18 19:27:17 -04:00
$user = ( new AuthService ()) -> currentUser ();
2026-10-20 06:45:30 -04:00
2026-10-18 19:27:17 -04:00
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 ();
}
2026-10-21 06:53:45 -04:00
$mfa = new MfaService ();
if ( $mfa -> gate ( $user , $action )) return ;
$sensitive = in_array ( $action ,[ 'users.changepassword' , 'users.adduser' , 'users.edituser' , 'users.deleteuser' , 'users.updateactivestatus' , 'users.useractive' , 'users.resetpassword' ], true )
|| ( $action === 'roster.membersummary' && ( $_POST [ 'step' ] ?? '' ) === 'member' )
|| ( $action === 'roster.workspace' && (( $_POST [ 'step' ] ?? '' ) === 'member' || (( $_POST [ 'step' ] ?? '' ) === 'invite' && ( $_POST [ 'membership_role' ] ?? '' ) === 'administrator' )));
if ( $method === 'POST' && $sensitive && ( new TenantAccess ()) -> managesAny ()) $mfa -> requireRecent ();
2026-10-18 19:27:17 -04:00
if (( bool ) $user [ 'user_change_password' ]) {
2026-10-18 17:01:36 +00:00
( new Users ()) -> changePassword ();
2026-10-18 19:27:17 -04:00
return ;
2026-10-18 17:01:36 +00:00
}
2026-10-20 06:45:30 -04:00
2026-10-18 19:27:17 -04:00
// 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 ],
2026-10-20 06:45:30 -04:00
'roster.workspace' => [ Roster :: class , 'workspace' , false ],
2026-10-21 06:53:45 -04:00
'roster.membersummary' => [ MemberSummary :: class , 'show' , false ],
'roster.createclub' => [ Roster :: class , 'createClub' , 'super' ],
2026-10-20 06:45:30 -04:00
'roster.help' => [ Roster :: class , 'help' , false ],
2026-10-21 06:53:45 -04:00
'roster.invitations' => [ Roster :: class , 'invitations' , false ],
'roster.tickets' => [ Roster :: class , 'tickets' , false ],
'rosterreports.catalog' => [ RosterReports :: class , 'catalog' , true ],
2026-10-20 06:45:30 -04:00
'rosterreports.ledger' => [ RosterReports :: class , 'ledger' , true ],
'rosterreports.csv' => [ RosterReports :: class , 'csv' , true ],
'rosterreports.excel' => [ RosterReports :: class , 'excel' , true ],
'rosterreports.pdf' => [ RosterReports :: class , 'pdf' , true ],
2026-10-18 19:27:17 -04:00
];
$adminRoutes = [
2026-10-20 06:45:30 -04:00
'Users' => [ 'setupUsers' , 'userSummary' , 'addUser' , 'editUser' , 'deleteUser' , 'resetPassword' , 'userExists' , 'getUser_AJAX' , 'updateActiveStatus' , 'userActive' ],
2026-10-18 19:27:17 -04:00
'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' ],
2026-10-20 06:45:30 -04:00
'Reports' => [ 'setupReports' , 'selectReport' ],
2026-10-18 19:27:17 -04:00
'Statuses' => [ 'setupStatuses' , 'addStatus' , 'editStatus' , 'deleteStatus' , 'statusExists' , 'statusActive' ],
2026-10-20 06:45:30 -04:00
'Journal' => [ 'viewJournal' ],
'Notes' => [ 'addNote' , 'editNote' , 'deleteNote' ],
2026-10-18 19:27:17 -04:00
];
foreach ( $adminRoutes as $class => $methods ) {
2026-10-20 06:45:30 -04:00
foreach ( $methods as $entry ) {
2026-10-21 06:53:45 -04:00
$routes [ strtolower ( $class . '.' . $entry )] = [ $class , $entry , $class === 'Reports' ? true : 'super' ];
2026-10-20 06:45:30 -04:00
}
2026-10-18 17:01:36 +00:00
}
2026-10-18 19:27:17 -04:00
if ( ! isset ( $routes [ $action ])) {
http_response_code ( 404 );
exit ( 'Page not found.' );
}
[ $class , $entry , $adminOnly ] = $routes [ $action ];
2026-10-21 06:53:45 -04:00
if (( $adminOnly === 'super' && ! ( new TenantAccess ()) -> isSuper ()) || ( $adminOnly === true && ! ( new TenantAccess ()) -> managesAny ())) {
2026-10-18 19:27:17 -04:00
http_response_code ( 403 );
exit ( 'You do not have permission to perform this action.' );
}
2026-10-21 06:53:45 -04:00
if ( $method !== 'POST' && ! in_array ( $action , [ '' , './' , 'displayhome' , 'users.changepassword' , 'roster.workspace' , 'roster.membersummary' , 'roster.createclub' , 'roster.help' , 'roster.invitations' , 'roster.tickets' , 'rosterreports.catalog' ], true )) {
2026-10-18 19:27:17 -04:00
http_response_code ( 405 );
exit ( 'Use POST for this action.' );
}
2026-10-20 06:45:30 -04:00
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 ;
2026-10-18 19:27:17 -04:00
}
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
public function csrfToken () : string {
if ( empty ( $_SESSION [ 'csrf_token' ]))
$_SESSION [ 'csrf_token' ] = bin2hex ( random_bytes ( 32 ));
return $_SESSION [ 'csrf_token' ];
}
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
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.' );
2026-10-18 17:01:36 +00:00
}
2026-10-18 19:27:17 -04:00
}
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
public function logoutSession () : void {
$_SESSION = [];
session_regenerate_id ( true );
$this -> initializeSession ();
$this -> csrfToken ();
2026-10-18 17:01:36 +00:00
}
// ==========================
// Return an encrypted string
// ==========================
protected function encrypt ( $string = "" , $key = "" ) {
2026-10-18 19:27:17 -04:00
$key = ( empty ( $key )) ? __CLASS__ : $key ;
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
$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 );
2026-10-18 17:01:36 +00:00
$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 () {
2026-10-20 06:45:30 -04:00
$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' );
2026-10-18 19:27:17 -04:00
$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 ;
}
2026-10-18 17:01:36 +00:00
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 ;
2026-10-18 19:27:17 -04:00
$date = filter_input ( INPUT_POST , "date" , FILTER_UNSAFE_RAW ) ?: $date ;
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
$age = 0 ;
2026-10-18 17:01:36 +00:00
2026-10-18 19:27:17 -04:00
$date = ( empty ( $date )) ? new Datetime () : new DateTime ( $date );
2026-10-18 17:01:36 +00:00
$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 ( "<unknown/>" );
}
$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 ( "<unknown/>" );
}
$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 "" ;
}
2026-10-18 19:27:17 -04:00
$document = __DIR__ . "/../xml/" . trim ( $document ) . ".xml" ;
$XML = ( file_exists ( $document ) ? file_get_contents ( $document ) : "" );
2026-10-18 17:01:36 +00:00
return $XML ;
}
// ========================
// Return an XSL Stylesheet
// ========================
public function getXSL ( $document = "" ) {
if ( trim ( $document ) == "" ) {
return "" ;
}
2026-10-18 19:27:17 -04:00
$document = __DIR__ . "/../xsl/" . trim ( $document ) . ".xsl" ;
$XSL = ( file_exists ( $document ) ? file_get_contents ( $document ) : "" );
2026-10-18 17:01:36 +00:00
return $XSL ;
}
// =======================================
// Return a Zip Code Detail Object (Array)
// =======================================
public function getZipcode ( $zip_code = 0 ) {
2026-10-18 19:27:17 -04:00
$zip_code = filter_input ( INPUT_POST , "zip_code" , FILTER_SANITIZE_NUMBER_INT ) ?: 0 ;
$zip_code = ( int ) substr (( string ) $zip_code , 0 , 5 );
2026-10-18 17:01:36 +00:00
$zipCodeDetail = null ;
2026-10-18 19:27:17 -04:00
$SQL = "select *
from zipcodes
2026-10-18 17:01:36 +00:00
where zipcode_code = { $zip_code } " ;
$zipcodes = $this -> getTable ( "zipcodes" , $SQL );
if ( $zipcodes -> count () > 0 ) {
$zipCodeDetail [ "ZipCode" ] = $zip_code ;
2026-10-18 19:27:17 -04:00
$zipCodeDetail [ "City" ] = ( string ) $zipcodes -> record -> zipcode_city ;
$zipCodeDetail [ "State" ] = ( string ) $zipcodes -> record -> zipcode_state ;
$zipCodeDetail [ "County" ] = ( string ) $zipcodes -> record -> zipcode_county ;
2026-10-18 17:01:36 +00:00
$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 ();
2026-10-18 19:27:17 -04:00
$_SESSION [ self :: ACTIVE_SESSION ] = true ;
$_SESSION [ self :: THEME ] = "light" ;
$_SESSION [ self :: ENVIRONMENT ] = ( string ) ( $settings -> Environment ?? "Production" );
2026-10-18 17:01:36 +00:00
$_SESSION [ self :: USER_AUTHENTICATED ] = false ;
2026-10-18 19:27:17 -04:00
$_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 ;
2026-10-18 17:01:36 +00:00
}
// ============
// Log an Error
// ============
public function logError ( $error_message = "" ) {
2026-10-20 06:45:30 -04:00
error_log ( $error_message );
throw new AppException ( 'The requested record is invalid or unavailable.' , 404 );
2026-10-18 17:01:36 +00:00
}
// ==========================
// 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 );
2026-10-18 19:27:17 -04:00
$xpath = new DOMXpath ( $thisDOM );
2026-10-18 17:01:36 +00:00
$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" ) {
2026-10-18 19:27:17 -04:00
$date = filter_input ( INPUT_POST , "date" ) ?: $date ;
2026-10-18 17:01:36 +00:00
$datetime = DateTime :: createFromFormat ( $format , $date );
2026-10-18 19:27:17 -04:00
$valid = $datetime && $datetime -> format ( $format ) === $date ;
2026-10-18 17:01:36 +00:00
if ( $_SESSION [ self :: AJAX ]) {
echo json_encode ( $valid );
} else {
return $valid ;
}
}
// ========================
// Generate Random Password
// ========================
public function generateRandomPassword ( $length = 12 ) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_+=' ;
2026-10-18 19:27:17 -04:00
$password = '' ;
2026-10-18 17:01:36 +00:00
for ( $i = 0 ; $i < $length ; $i ++ ) {
2026-10-18 19:27:17 -04:00
$index = random_int ( 0 , strlen ( $characters ) - 1 );
2026-10-18 17:01:36 +00:00
$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 () {
2026-10-18 19:27:17 -04:00
$to = "james.richie@onesourceits.com" ;
2026-10-18 17:01:36 +00:00
$subject = "Test Subject" ;
$message = "Test Message" ;
$email = ( new Email ()) -> sendEmail ( $to , $subject , $message );
$this -> debug ( $email );
exit ();
}
// =====================
// Forgot/Reset Password
// =====================
}