Chat's updates for security, db and workflow
Security and workflow regressions / regression (push) Has been cancelled

This commit is contained in:
2026-10-20 06:45:30 -04:00
parent 02223083f6
commit 1e63076123
116 changed files with 3252 additions and 2932 deletions
+39 -41
View File
@@ -13,7 +13,7 @@ class Base {
// Constants
// =========
const APPLICATION_NAME = "Hunting Club Roster";
const APPLICATION_NAME = "My Hunting Club Roster";
const APPLICATION_MNEMONIC = "HUNTINGCLUBROSTER";
const VERSION = "1.00";
const TIMEZONE = "America/New_York";
@@ -136,11 +136,16 @@ class Base {
$dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $host, (int) ($settings->DatabasePort ?? 3306), $database);
return new PDO($dsn, $username, $password,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
$options=[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => 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());
@@ -216,9 +221,7 @@ class Base {
// =====================
public function displayHome() {
$content = $this->applyXSL("", $this->getXSL("home"));
$this->displayContent($content);
(new Roster())->home();
}
// =======================
@@ -250,6 +253,7 @@ class Base {
}
}
$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);
@@ -275,9 +279,9 @@ class Base {
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])) {
@@ -296,6 +300,7 @@ class Base {
(new Users())->changePassword();
return;
}
// Only named, reviewed entry points are routable. Helpers are never endpoints.
$routes = [
'' => [Base::class, 'displayHome', false],
@@ -306,24 +311,31 @@ class Base {
'validatedate' => [Base::class, 'validateDate', false],
'getage' => [Base::class, 'getAge', false],
'users.changepassword' => [Users::class, 'changePassword', false],
'print' => [Base::class, 'print', true],
'print_all_permitsbybuilder.print' => ['Print_All_PermitsByBuilder', 'print', true],
'export_allpermitsbybuildercsv.export' => ['Export_AllPermitsByBuilderCSV', 'export', true],
'export_allpermitsbybuilderexcel.export' => ['Export_AllPermitsByBuilderExcel', 'export', true],
'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', 'addUser', 'editUser', 'deleteUser', 'resetPassword', 'userExists', 'getUser_AJAX', 'updateActiveStatus', 'userActive'],
'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', 'addReport', 'editReport', 'deleteReport', 'selectReport', 'reportExists'],
'Reports' => ['setupReports', 'selectReport'],
'Statuses' => ['setupStatuses', 'addStatus', 'editStatus', 'deleteStatus', 'statusExists', 'statusActive'],
'Journal' => ['viewJournal', 'clearJournal'],
'Notes' => ['addNote', 'editNote', 'deleteNote', 'addNoteQuick'],
'Journal' => ['viewJournal'],
'Notes' => ['addNote', 'editNote', 'deleteNote'],
];
foreach ($adminRoutes as $class => $methods) {
foreach ($methods as $entry)
foreach ($methods as $entry) {
$routes[strtolower($class . '.' . $entry)] = [$class, $entry, true];
}
}
if (!isset($routes[$action])) {
http_response_code(404);
@@ -334,11 +346,15 @@ class Base {
http_response_code(403);
exit('You do not have permission to perform this action.');
}
if ($method !== 'POST' && !in_array($action, ['', './', 'displayhome', 'users.changepassword'], true)) {
if ($method !== 'POST' && !in_array($action, ['', './', 'displayhome', 'users.changepassword','roster.workspace','roster.help'], true)) {
http_response_code(405);
exit('Use POST for this action.');
}
(new $class())->$entry();
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 {
@@ -420,7 +436,7 @@ class Base {
public function getSettings() {
$path = getenv('HCR_SETTINGS_FILE') ?: (is_file(__DIR__ . '/../xml/settings.local.xml') ? __DIR__ . '/../xml/settings.local.xml' : __DIR__ . '/../xml/settings.xml');
$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.');
@@ -673,12 +689,8 @@ class Base {
// ============
public function logError($error_message = "") {
(new Journal())->journalEntry($error_message);
$this->displayHome();
exit();
error_log($error_message);
throw new AppException('The requested record is invalid or unavailable.',404);
}
// ==========================
@@ -725,20 +737,6 @@ class Base {
// Print a Report
// ==============
public function print() {
$_SESSION[self::REPORTS_GET] = $_GET;
$_SESSION[self::REPORTS_POST] = $_POST;
$report = filter_input(INPUT_POST, "report") ?: "";
if ($report !== 'Print_All_PermitsByBuilder') {
http_response_code(404);
exit('Report not found.');
}
(new $report())->print();
}
// =================
// Sanitize a string
// =================