Codex's updates...

This commit is contained in:
2026-10-18 19:27:17 -04:00
parent a1643eb1b7
commit 02223083f6
338 changed files with 14757 additions and 7986 deletions
+216
View File
@@ -0,0 +1,216 @@
<?php
/** Shared account storage and security rules; no HTML or request dispatch here. */
class AuthService extends Base {
public const IDLE_TIMEOUT = 1800;
public function findUser(string $identifier): ?array {
$stmt = $this->connect()->prepare('SELECT * FROM view_users WHERE user_email = :email OR user_name = :name LIMIT 1');
$stmt->execute([':email' => trim($identifier), ':name' => trim($identifier)]);
return $stmt->fetch() ?: null;
}
public static function passwordError(string $password, string $confirmation): string {
if ($password !== $confirmation)
return 'Passwords must match.';
// PASSWORD_DEFAULT is bcrypt on supported PHP versions; avoid silent truncation.
if (strlen($password) < 12 || strlen($password) > 72 || str_contains($password, "\0")) {
return 'Use a password between 12 and 72 bytes long.';
}
return '';
}
/** Shared database counters survive session/cookie changes. Requires the auth migration. */
public function allowAttempt(string $scope, string $identifier, int $limit, int $seconds): bool {
$db = $this->connect();
$key = hash('sha256', $scope . ':' . strtolower(trim($identifier)));
$stmt = $db->prepare('INSERT INTO auth_attempts (attempt_key, attempts, window_started) VALUES (:key, 1, UTC_TIMESTAMP())
ON DUPLICATE KEY UPDATE attempts = IF(window_started < DATE_SUB(UTC_TIMESTAMP(), INTERVAL :seconds SECOND), 1, attempts + 1),
window_started = IF(window_started < DATE_SUB(UTC_TIMESTAMP(), INTERVAL :window SECOND), UTC_TIMESTAMP(), window_started)');
$stmt->execute([':key' => $key, ':seconds' => $seconds, ':window' => $seconds]);
$stmt = $db->prepare('SELECT attempts FROM auth_attempts WHERE attempt_key = :key');
$stmt->execute([':key' => $key]);
return (int) $stmt->fetchColumn() <= $limit;
}
public function login(string $identifier, string $password): bool {
$ipAllowed = $this->allowAttempt('login-ip', $_SERVER['REMOTE_ADDR'] ?? '', 50, 900);
$accountAllowed = $this->allowAttempt('login-account', $identifier, 10, 900);
if (!$ipAllowed || !$accountAllowed)
return false;
$user = $this->findUser($identifier);
// A real hash avoids an immediate return for unknown accounts.
$hash = $user['user_password_hash'] ?? '$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.';
$verified = password_verify($password, $hash);
if (!$user || !$verified || $user['user_status'] !== 'active')
return false;
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$stmt = $this->connect()->prepare('UPDATE users SET user_password_hash = :hash WHERE user_serial = :id AND user_password_hash = :old_hash');
$stmt->execute([':hash' => password_hash($password, PASSWORD_DEFAULT), ':id' => $user['user_serial'], ':old_hash' => $hash]);
}
session_regenerate_id(true);
$_SESSION[self::USER_AUTHENTICATED] = true;
$_SESSION[self::USER_SERIAL] = (int) $user['user_serial'];
$_SESSION[self::USER_NAME] = $user['user_name'];
$_SESSION[self::USER_FULL_NAME] = $user['user_full_name'];
$_SESSION[self::USER_EMAIL] = $user['user_email'];
$_SESSION[self::USER_ROLE] = $user['user_role'];
$_SESSION['auth_version'] = (int) $user['user_auth_version'];
$_SESSION['last_activity'] = time();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
$stmt = $this->connect()->prepare('UPDATE users SET user_last_login = :now WHERE user_serial = :id');
$stmt->execute([':now' => $this->getTimestamp(), ':id' => $user['user_serial']]);
return true;
}
public function currentUser(): ?array {
if (empty($_SESSION[self::USER_AUTHENTICATED]))
return null;
if (time() - ($_SESSION['last_activity'] ?? 0) > self::IDLE_TIMEOUT)
return null;
$stmt = $this->connect()->prepare('SELECT * FROM view_users WHERE user_serial = :id');
$stmt->execute([':id' => $_SESSION[self::USER_SERIAL] ?? 0]);
$user = $stmt->fetch();
if (!$user || $user['user_status'] !== 'active' || (int) $user['user_auth_version'] !== ($_SESSION['auth_version'] ?? -1))
return null;
$_SESSION['last_activity'] = time();
$_SESSION[self::USER_ROLE] = $user['user_role'];
$_SESSION[self::USER_NAME] = $user['user_name'];
$_SESSION[self::USER_FULL_NAME] = $user['user_full_name'];
$_SESSION[self::USER_EMAIL] = $user['user_email'];
return $user;
}
public function register(array $input): void {
$first = trim($input['user_first_name'] ?? '');
$last = trim($input['user_last_name'] ?? '');
$email = trim($input['user_email'] ?? '');
$name = trim($input['user_name'] ?? '');
if (!$first || !$last || strlen($first) > 128 || strlen($last) > 128)
throw new InvalidArgumentException('Enter your first and last name (up to 128 bytes each).');
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > 100)
throw new InvalidArgumentException('Enter a valid email address (up to 100 bytes).');
if (!preg_match('/\A[A-Za-z0-9_.-]{3,64}\z/', $name))
throw new InvalidArgumentException('Use 3–64 letters, numbers, dots, underscores or hyphens for your username.');
$error = self::passwordError($input['user_password'] ?? '', $input['user_password_confirm'] ?? '');
if ($error)
throw new InvalidArgumentException($error);
$db = $this->connect();
// Cross-field uniqueness keeps email-or-username login unambiguous.
$stmt = $db->prepare('SELECT user_serial FROM users WHERE user_name IN (:name, :email) OR user_email IN (:email2, :name2) LIMIT 1');
$stmt->execute([':name' => $name, ':email' => $email, ':email2' => $email, ':name2' => $name]);
if ($stmt->fetch())
throw new InvalidArgumentException('Unable to create an account with these details. Try signing in or resetting your password.');
$stmt = $db->prepare("INSERT INTO users (user_name, user_first_name, user_last_name, user_email, user_password_hash, user_role, user_status, user_creator) VALUES (:name, :first, :last, :email, :hash, 'User', 'active', 'Self registration')");
try {
$stmt->execute([':name' => $name, ':first' => $first, ':last' => $last, ':email' => $email, ':hash' => password_hash($input['user_password'], PASSWORD_DEFAULT)]);
} catch (PDOException $e) {
if ($e->getCode() === '23000')
throw new InvalidArgumentException('Unable to create an account with these details. Try signing in or resetting your password.');
throw $e;
}
}
/** Never modify the password when somebody merely requests an email. */
public function requestReset(string $identifier): void {
$ipAllowed = $this->allowAttempt('reset-ip', $_SERVER['REMOTE_ADDR'] ?? '', 10, 3600);
$accountAllowed = $this->allowAttempt('reset-account', $identifier, 3, 3600);
if (!$ipAllowed || !$accountAllowed)
return;
$user = $this->findUser($identifier);
if (!$user || $user['user_status'] !== 'active')
return;
$baseUrl = rtrim((string) $this->getSettings()->ApplicationUrl, '/');
if (!filter_var($baseUrl, FILTER_VALIDATE_URL) || parse_url($baseUrl, PHP_URL_SCHEME) !== 'https'
|| parse_url($baseUrl, PHP_URL_QUERY) !== null || parse_url($baseUrl, PHP_URL_FRAGMENT) !== null
|| parse_url($baseUrl, PHP_URL_USER) !== null || parse_url($baseUrl, PHP_URL_PASS) !== null) {
throw new RuntimeException('Configure an HTTPS ApplicationUrl before sending reset emails.');
}
$token = bin2hex(random_bytes(32));
$db = $this->connect();
$stmt = $db->prepare('INSERT INTO password_resets (token_hash, user_serial, expires_at) VALUES (:hash, :id, DATE_ADD(UTC_TIMESTAMP(), INTERVAL 30 MINUTE))');
$hash = hash('sha256', $token);
$stmt->execute([':hash' => $hash, ':id' => $user['user_serial']]);
$url = $baseUrl . '/main.php?Xaction=resetpassword&token=' . rawurlencode($token);
if (!$this->deliverResetEmail($user, $url)) {
$stmt = $db->prepare('DELETE FROM password_resets WHERE token_hash = :hash');
$stmt->execute([':hash' => $hash]);
throw new RuntimeException('Password reset email delivery failed.');
}
}
protected function deliverResetEmail(array $user, string $url): bool {
return (new Email())->sendPasswordReset($user, $url);
}
public function resetPassword(string $token, string $password, string $confirmation): bool {
$error = self::passwordError($password, $confirmation);
if ($error)
throw new InvalidArgumentException($error);
if (!preg_match('/\A[a-f0-9]{64}\z/', $token))
return false;
$db = $this->connect();
$db->beginTransaction();
try {
// Serialize resets and password changes on the user row.
$stmt = $db->prepare('SELECT user_serial FROM password_resets WHERE token_hash = :hash');
$stmt->execute([':hash' => hash('sha256', $token)]);
$id = $stmt->fetchColumn();
if (!$id) {
$db->rollBack();
return false;
}
$stmt = $db->prepare("SELECT user_serial FROM users WHERE user_serial = :id AND user_status = 'active' FOR UPDATE");
$stmt->execute([':id' => $id]);
if (!$stmt->fetch()) {
$db->rollBack();
return false;
}
$stmt = $db->prepare('SELECT token_hash FROM password_resets WHERE token_hash = :hash AND expires_at > UTC_TIMESTAMP() FOR UPDATE');
$stmt->execute([':hash' => hash('sha256', $token)]);
if (!$stmt->fetch()) {
$db->rollBack();
return false;
}
$this->storePassword($db, (int) $id, $password);
$db->commit();
return true;
} catch (Throwable $e) {
if ($db->inTransaction())
$db->rollBack();
throw $e;
}
}
public function changePassword(int $id, string $current, string $password, string $confirmation): bool {
$error = self::passwordError($password, $confirmation);
if ($error)
throw new InvalidArgumentException($error);
$db = $this->connect();
$db->beginTransaction();
try {
$stmt = $db->prepare("SELECT user_password_hash, user_auth_version FROM users WHERE user_serial = :id AND user_status = 'active' FOR UPDATE");
$stmt->execute([':id' => $id]);
$user = $stmt->fetch();
if (!$user || (int) $user['user_auth_version'] !== ($_SESSION['auth_version'] ?? -1) || !password_verify($current, $user['user_password_hash'])) {
$db->rollBack();
return false;
}
$this->storePassword($db, $id, $password);
$db->commit();
return true;
} catch (Throwable $e) {
if ($db->inTransaction())
$db->rollBack();
throw $e;
}
}
private function storePassword(PDO $db, int $id, string $password): void {
$stmt = $db->prepare('UPDATE users SET user_password_hash = :hash, user_change_password = 0, user_auth_version = user_auth_version + 1 WHERE user_serial = :id');
$stmt->execute([':hash' => password_hash($password, PASSWORD_DEFAULT), ':id' => $id]);
$stmt = $db->prepare('DELETE FROM password_resets WHERE user_serial = :id');
$stmt->execute([':id' => $id]);
}
}
+35 -380
View File
@@ -1,394 +1,49 @@
<?php
// =====================================================================
// Title: Authentication Class
// Purpose: This class stands outside the Base Class for password resets
// =====================================================================
require_once "vendor/autoload.php";
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
class Authentication {
// Variables
private $settings = "";
private $ghrconnection = "";
// =================
// Class Constructor
// =================
public function __construct() {
// Get Settings
$this->settings = new SimpleXMLElement((file_exists("xml/settings.xml") ? file_get_contents("xml/settings.xml") : "<settings/>"));
if ($this->settings->count() === 0) {
error_log("GHR Authentication Class : Unable to initialize settings");
exit();
}
// Open Database Connections
$this->ghrconnection = $this->connect();
}
// ======================================
// Return a GHR MySQL Database Connection
// ======================================
public function connect() {
if ($this->settings->Environment == 'Production') {
$host = 'localhost';
} else {
$host = '192.168.1.190';
$host = '100.67.215.67';
}
try {
$database = (string) ($this->settings->DatabaseName ?? "");
$user = (string) ($this->settings->DatabaseUser ?? "");
$password = (string) ($this->settings->DatabasePassword ?? "");
$connection = new PDO("mysql:host={$host};dbname={$database};charset=utf8", $user, $password);
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException | Exception $e) {
$this->debug($e->getMessage());
exit();
}
return $connection;
}
// ========================
// Generate Random Password
// ========================
private function generateRandomPassword($length = 12) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_+=';
$password = '';
for ($i = 0; $i < $length; $i++) {
$index = rand(0, strlen($characters) - 1);
$password .= $characters[$index];
}
return $password;
}
// ===============
// Debug an Object
// ===============
private function debug($object = "") {
echo "<pre>";
print_r($object);
exit();
}
// ==============
// Reset Password
// ==============
/** Reset page controller; account storage and email are shared with the other flows. */
class Authentication extends Base {
public function resetPassword() {
$username = filter_input(INPUT_POST, "user_name") ?: "";
if (empty($username)) {
return false;
}
$exists = $this->userExists($username);
if (!$exists) {
return false;
}
$user = $this->getUser($username);
$password = $this->resetUsersPassword($user->record->user_serial);
$this->emailUserPasswordResetLink($user, $password);
$this->journalEntry("{$username} reset their password.");
return true;
}
// ===========
// User Exists
// ===========
private function userExists($username) {
$SQL = 'select user_name from view_users where user_name = :username';
$statement = $this->ghrconnection->prepare($SQL);
$statement->bindValue(':username', $username, PDO::PARAM_STR);
$statement->execute();
$user = $statement->fetch(PDO::FETCH_ASSOC);
return ($user !== false);
}
// =================
// Get a User Object
// =================
private function getUser($username) {
$SQL = 'select user_serial, user_name, user_email from view_users where user_name = :username';
$statement = $this->ghrconnection->prepare($SQL);
$statement->bindValue(':username', $username, PDO::PARAM_STR);
$statement->execute();
$user = $statement->fetch(PDO::FETCH_ASSOC);
$xml = new SimpleXMLElement('<users/>');
$record = $xml->addChild('record');
$this->arrayToXml($user, $record);
return $xml;
}
private function arrayToXml(array $data, SimpleXMLElement $xml): void {
foreach ($data as $key => $value) {
$key = is_numeric($key) ? 'item' : $key;
if (is_array($value)) {
$child = $xml->addChild($key);
$this->arrayToXml($value, $child);
} else {
$xml->addChild($key, htmlspecialchars((string) $value));
}
}
}
// ========================
// Reset the Users Password
// ========================
private function resetUsersPassword($user_serial) {
$user_random_password = $this->generateRandomPassword();
$user_temp_password = password_hash($user_random_password, PASSWORD_DEFAULT);
$SQL = "update users
set user_password = :user_password,
user_change_password = :user_change_password
where user_serial = :user_serial";
$statement = $this->ghrconnection->prepare($SQL);
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_password", $user_temp_password);
$statement->bindValue(":user_change_password", 1);
$statement->execute();
return $user_random_password;
}
// =================================
// Email a users Password Reset Link
// =================================
public function emailUserPasswordResetLink($user, $password) {
if (!$user instanceof SimpleXMLElement) {
return;
}
// Prepare the email.
$XSLParms['USER-TEMP-PASSWORD'] = $password;
$to = $user->record->user_email;
$subject = "DEC International DMS - Password Reset Request";
$message = $this->applyXSL($user, $this->getXSL("emailPasswordResetLink"), $XSLParms);
// Send the Email
$this->sendEmail($to, $subject, $message);
}
// =======================================
// Send an email with optional attachments
// =======================================
public function sendEmail($to = "", $subject = "", $message = "", $attachments = "", $attachment_names = "unknown") {
if (empty($to) or empty($subject) or empty($message)) {
return false;
}
$SmtpHost = $this->settings->SmtpServer;
$SmtpUsername = $this->settings->SmtpUserName;
$SmtpPassword = $this->settings->SmtpPassword;
$SmtpPort = $this->settings->SmtpPort;
$email = new PHPMailer(true);
// Only for development
$message = "<span style='font-family: Calibri, Arial, sans-serif;'>" . $message . "</span>";
try {
//Server settings
$email->isSMTP();
$email->Host = $SmtpHost;
$email->SMTPAuth = true;
$email->Username = $SmtpUsername; // Your Gmail address
$email->Password = $SmtpPassword; // App password from Google
// $email->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // or 'tls'
$email->Port = $SmtpPort;
$email->isHTML(true);
$email->SetFrom("info@dec-international.com", "DEC-International");
if (is_array($to)) {
foreach ($to as $address) {
$address = trim(str_replace(" ", "", $address));
if (empty($address)) {
continue;
$token = $_POST['token'] ?? $_GET['token'] ?? '';
$params = ['TOKEN' => $token, 'MESSAGE' => '', 'ERROR' => '', 'COMPLETE' => 'FALSE'];
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
$this->requireCsrf();
if (($_POST['step'] ?? '') === 'reset') {
try {
$auth = new AuthService();
if (!$auth->allowAttempt('reset-submit-ip', $_SERVER['REMOTE_ADDR'] ?? '', 20, 900)) {
throw new InvalidArgumentException('Too many attempts. Please try again later.');
}
$email->addAddress($address);
}
} else {
$email->addAddress($to);
}
$email->Subject = $subject;
$email->Body = $message;
if (is_array($attachments)) {
foreach ($attachments as $index => $item) {
if (!empty($item)) {
$email->addStringAttachment($item, ($attachment_names[$index] ?? "unknown"));
if (!$auth->resetPassword($token, $_POST['user_password'] ?? '', $_POST['user_password_confirm'] ?? '')) {
throw new InvalidArgumentException('This reset link is invalid or expired. Request a new link.');
}
$this->logoutSession();
$_SESSION['password_reset_success'] = true;
header('Location: main.php?Xaction=resetpassword', true, 303);
exit;
} catch (InvalidArgumentException $e) {
$params['ERROR'] = $e->getMessage();
}
} else {
if (!empty($attachments)) {
$email->addStringAttachment($attachments, $attachment_names);
try {
(new AuthService())->requestReset(trim($_POST['user_email'] ?? ''));
} catch (Throwable $e) {
error_log('Password reset request failed: ' . $e->getMessage());
}
$_SESSION['password_reset_requested'] = true;
header('Location: main.php?Xaction=resetpassword', true, 303);
exit;
}
$success = $email->Send();
} catch (phpMailerException $e) {
$this->journalEntry($e->errorMessage());
$success = false;
} catch (Exception $e) {
$this->journalEntry($e->getMessage());
$success = false;
}
return $success;
}
// ======================
// Create a Journal Entry
// ======================
public function journalEntry($journal_entry = "") {
if (empty($journal_entry)) {
return;
if (!empty($_SESSION['password_reset_success'])) {
$params['MESSAGE'] = 'Your password has been changed. Please sign in with your new password.';
$params['COMPLETE'] = 'TRUE';
unset($_SESSION['password_reset_success']);
} elseif (!empty($_SESSION['password_reset_requested'])) {
$params['MESSAGE'] = 'If an active account matches, you will receive a password reset link. The link expires in 30 minutes.';
unset($_SESSION['password_reset_requested']);
}
$SQL = "insert into journal
( journal_origin,
journal_ip,
journal_entry )
VALUES ( :journal_origin,
:journal_ip,
:journal_entry )";
$statement = $this->ghrconnection->prepare($SQL);
$statement->bindValue(":journal_origin", "Authentication Class");
$statement->bindValue(":journal_ip", $_SERVER["REMOTE_ADDR"] ?? "");
$statement->bindValue(":journal_entry", $journal_entry);
$statement->execute();
}
// ==========================================
// 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["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);
}
if ($XSLString) {
$XSL->loadXML($XSLString);
}
$XSLT->importStylesheet($XSL);
if ($XSLParms) {
$XSLT->setParameter("", $XSLParms);
}
$document = $XSLT->transformToXml($XML);
return $document;
}
// ========================
// Return an XSL Stylesheet
// ========================
public function getXSL($document = "") {
if (trim($document) == "") {
return "";
}
$document = "xsl/" . trim($document) . ".xsl";
$XSL = (file_exists($document) ? file_get_contents($document) : "");
return $XSL;
echo $this->applyXSL('<XML/>', $this->getXSL('resetPassword'), $params);
exit;
}
}
+13 -11
View File
@@ -13,35 +13,37 @@ class Base_Autoloader {
if (function_exists("__autoload")) {
spl_autoload_register("__autoload");
}
return spl_autoload_register(array("Base_Autoloader", "Load"), true, true);
}
public static function Load($pClassName) {
if (class_exists($pClassName, false)) {
return false;
}
if (!preg_match('/\A[A-Za-z_][A-Za-z0-9_]*\z/', $pClassName)) return false;
// Try for a "classes" Class first, then a "reports" Class.
$pClassFilePath = "classes/{$pClassName}.php";
$pClassFilePath = __DIR__ . "/{$pClassName}.php";
if ((file_exists($pClassFilePath) === false) || (is_readable($pClassFilePath) === false)) {
$pClassFilePath = "reports/{$pClassName}.php";
$pClassFilePath = __DIR__ . "/../reports/{$pClassName}.php";
if ((file_exists($pClassFilePath) === false) || (is_readable($pClassFilePath) === false)) {
return false;
}
}
require($pClassFilePath);
}
}
?>
?>
+237 -186
View File
@@ -4,8 +4,8 @@
// Base Class
// ==========
require_once "vendor/autoload.php";
require_once "classes/Autoloader.php";
require_once __DIR__ . "/../vendor/autoload.php";
require_once __DIR__ . "/Autoloader.php";
class Base {
@@ -13,31 +13,30 @@ class Base {
// Constants
// =========
const APPLICATION_NAME = "Hunting Club Roster";
const APPLICATION_NAME = "Hunting Club Roster";
const APPLICATION_MNEMONIC = "HUNTINGCLUBROSTER";
const VERSION = "1.00";
const TIMEZONE = "America/New_York";
const VERSION = "1.00";
const TIMEZONE = "America/New_York";
// =================
// Session Variables
// =================
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";
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";
// =================
// Class Constructor
@@ -56,7 +55,7 @@ class Base {
public function appendSXE(SimpleXMLElement $from, SimpleXMLElement $to) {
$toDOM = dom_import_simplexml($to);
$toDOM = dom_import_simplexml($to);
$fromDOM = dom_import_simplexml($from);
$toDOM->appendChild($toDOM->ownerDocument->importNode($fromDOM, true));
@@ -80,30 +79,31 @@ class Base {
$XSLParms = array();
}
$XSLParms["APPLICATION_NAME"] = self::APPLICATION_NAME;
$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"] = $_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");
$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();
$XML = new DOMDocument();
$XSL = new DOMDocument();
$XSLT = new XSLTProcessor();
if ($XMLString) {
$XML->loadXML($XMLString);
$XML->loadXML($XMLString, LIBXML_NONET);
}
if ($XSLString) {
$XSL->loadXML($XSLString);
$XSL->loadXML($XSLString, LIBXML_NONET);
}
$XSLT->importStylesheet($XSL);
@@ -112,6 +112,8 @@ class Base {
$XSLT->setParameter("", $XSLParms);
}
if (!$XML->documentElement)
$XML->loadXML('<XML/>');
$document = $XSLT->transformToXml($XML);
return $document;
@@ -122,27 +124,26 @@ class Base {
// ============================
public function connect(): PDO {
$settings = $this->getSettings();
$host = ((string) $settings->Environment === 'Production') ? 'localhost' : '192.168.1.190';
$database = (string) $settings->DatabaseName;
$username = (string) $settings->DatabaseUser;
$password = (string) $settings->DatabasePassword;
$host = (string) $settings->DatabaseServer;
$database = (string) $settings->DatabaseName;
$username = (string) $settings->DatabaseUser;
$password = (string) $settings->DatabasePassword;
try {
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $host, $database);
$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,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => false,]);
return new PDO($dsn, $username, $password,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => false,]);
} catch (PDOException $e) {
$this->logError("Database Connection Failed: " . $e->getMessage());
error_log("Database Connection Failed: " . $e->getMessage());
throw new Exception("Unable to connect to the database.", 0, $e);
}
@@ -182,32 +183,17 @@ class Base {
// Custom error handler to display informative error page
// ======================================================
public function displayError($error_number = "", $error_message = "", $error_file = "", $error_line = "") {
if (error_reporting() == 0) {
return;
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}");
$error_file = explode("/", $error_file);
$error_file = end($error_file);
$error_message = str_replace("'", '"', $error_message);
(new Journal())->journalEntry("{$error_file} | {$error_line} | {$error_message}");
if (($_SESSION[self::ENVIRONMENT] ?? "Production") == "Production") {
(new Email())->sendErrorEmail($error_message, $error_file, $error_line);
if (in_array($error_number, [E_DEPRECATED, E_USER_DEPRECATED], true)) {
return true;
}
$XSLParms["FILE_NAME"] = $error_file;
$XSLParms["LINE_NUMBER"] = $error_line;
$XSLParms["ERROR_MESSAGE"] = $error_message;
$XSLParms["ERROR_NUMBER"] = $error_number;
$content = $this->applyXSL("", $this->getXSL("error"), $XSLParms);
$this->displayContent($content);
exit(); // Exit so that only one error is presented.
throw new ErrorException($error_message, 0, $error_number, $error_file, $error_line);
}
// =====================
@@ -241,71 +227,139 @@ class Base {
public function do() {
$_SESSION[self::TODAY] = date("Y-m-d");
// Get the requested Action
$action = filter_input(INPUT_POST, "action") ?: "";
$Xaction = filter_input(INPUT_GET, "Xaction") ?: "";
$action = (empty($Xaction) ? $action : $Xaction);
if ($Xaction == 'resetpassword') {
$this->resetpassword();
}
// Login
if (($_SESSION[self::USER_AUTHENTICATED] ?? false) === false) {
(new SignIn())->login();
}
// Determine if user needs a password change
if ($this->userChangePassword() === true) {
(new Users())->changePassword();
exit();
}
// Determine if this is an AJAX request.
$_SESSION[self::AJAX] = (isset($_SERVER["HTTP_X_REQUESTED_WITH"]) and strtolower($_SERVER["HTTP_X_REQUESTED_WITH"]) === "xmlhttprequest");
// Get the requested Action
$action = filter_input(INPUT_POST, "action") ?: "";
$Xaction = filter_input(INPUT_GET, "Xaction") ?: "";
$action = (empty($Xaction) ? $action : $Xaction);
// Sign Out if requested.
if (strtolower(trim($action)) === "signout") {
if (empty($_SESSION[self::ACTIVE_SESSION])) {
$this->initializeSession();
}
// Process the requested Action.
$_SESSION[self::AJAX] = strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'xmlhttprequest';
list($class, $method) = array_pad(explode('.', $action), 2, "");
$_SESSION[self::TODAY] = date('Y-m-d');
if (empty($method)) {
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$method = $class;
if (method_exists(__CLASS__, $method)) {
$this->$method();
return;
}
} else {
if (method_exists($class, $method)) {
(new $class())->$method();
return;
}
if (!in_array($method, ['GET', 'POST'], true)) {
http_response_code(405);
exit('Method not allowed.');
}
// Default to the Home Page.
$this->displayHome();
// 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 === '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],
'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],
];
$adminRoutes = [
'Users' => ['setupUsers', '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'],
'Statuses' => ['setupStatuses', 'addStatus', 'editStatus', 'deleteStatus', 'statusExists', 'statusActive'],
'Journal' => ['viewJournal', 'clearJournal'],
'Notes' => ['addNote', 'editNote', 'deleteNote', 'addNoteQuick'],
];
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'], true)) {
http_response_code(405);
exit('Use POST for this action.');
}
(new $class())->$entry();
}
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();
}
// ==========================
@@ -314,12 +368,12 @@ class Base {
protected function encrypt($string = "", $key = "") {
$key = (empty($key)) ? __CLASS__ : $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);
$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;
@@ -366,7 +420,15 @@ class Base {
public function getSettings() {
$settings = new SimpleXMLElement($this->getXML("settings"));
$path = getenv('HCR_SETTINGS_FILE') ?: (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;
}
@@ -378,11 +440,11 @@ class Base {
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;
$date = filter_input(INPUT_POST, "date", FILTER_UNSAFE_RAW) ?: $date;
$age = 0;
$age = 0;
$date = (empty($date)) ? new Datetime() : new DateTime($date);
$date = (empty($date)) ? new Datetime() : new DateTime($date);
$birthdate = (empty($birthdate)) ? new Datetime() : new DateTime($birthdate);
$age = $birthdate->diff($date)->format("%y");
@@ -529,8 +591,8 @@ class Base {
return "";
}
$document = "xml/" . trim($document) . ".xml";
$XML = (file_exists($document) ? file_get_contents($document) : "");
$document = __DIR__ . "/../xml/" . trim($document) . ".xml";
$XML = (file_exists($document) ? file_get_contents($document) : "");
return $XML;
}
@@ -545,8 +607,8 @@ class Base {
return "";
}
$document = "xsl/" . trim($document) . ".xsl";
$XSL = (file_exists($document) ? file_get_contents($document) : "");
$document = __DIR__ . "/../xsl/" . trim($document) . ".xsl";
$XSL = (file_exists($document) ? file_get_contents($document) : "");
return $XSL;
}
@@ -557,13 +619,13 @@ class Base {
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);
$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
$SQL = "select *
from zipcodes
where zipcode_code = {$zip_code}";
$zipcodes = $this->getTable("zipcodes", $SQL);
@@ -571,9 +633,9 @@ class Base {
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["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;
}
@@ -592,18 +654,18 @@ class Base {
$settings = $this->getSettings();
$_SESSION[self::ACTIVE_SESSION] = true;
$_SESSION[self::THEME] = "light";
$_SESSION[self::ENVIRONMENT] = (string) ($settings->Environment ?? "Production");
$_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;
$_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;
}
// ============
@@ -647,7 +709,7 @@ class Base {
$thatDOM = new DOMDocument();
$thatDOM->loadXML($thatXML);
$xpath = new DOMXpath($thisDOM);
$xpath = new DOMXpath($thisDOM);
$xpathQuery = $xpath->query("/*");
for ($i = 0; $i < $xpathQuery->length; $i++) {
@@ -665,11 +727,15 @@ class Base {
public function print() {
$_SESSION[self::REPORTS_GET] = $_GET;
$_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();
}
@@ -697,10 +763,10 @@ class Base {
public function validateDate($date = "", $format = "Y-m-d") {
$date = filter_input(INPUT_POST, "date") ?: $date;
$date = filter_input(INPUT_POST, "date") ?: $date;
$datetime = DateTime::createFromFormat($format, $date);
$valid = $datetime && $datetime->format($format) === $date;
$valid = $datetime && $datetime->format($format) === $date;
if ($_SESSION[self::AJAX]) {
echo json_encode($valid);
@@ -716,10 +782,10 @@ class Base {
public function generateRandomPassword($length = 12) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_+=';
$password = '';
$password = '';
for ($i = 0; $i < $length; $i++) {
$index = rand(0, strlen($characters) - 1);
$index = random_int(0, strlen($characters) - 1);
$password .= $characters[$index];
}
@@ -759,7 +825,7 @@ class Base {
public function testing() {
$to = "james.richie@onesourceits.com";
$to = "james.richie@onesourceits.com";
$subject = "Test Subject";
$message = "Test Message";
@@ -773,19 +839,4 @@ class Base {
// =====================
// Forgot/Reset Password
// =====================
public function resetPassword() {
require_once 'classes/Authentication.php';
$XSLParms = [
"BTN_SUBMIT" => "main.php?action=Users.signIn&step=authenticate",
"RESET_PASSWORD_SUCCESSFUL" => (new Authentication())->resetPassword() ? "TRUE" : "FALSE"
];
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
exit;
}
}
?>
+23 -48
View File
@@ -9,6 +9,13 @@ use PHPMailer\PHPMailer\Exception;
class Email extends Base {
public function sendPasswordReset(array $user, string $url): bool {
$xml = new SimpleXMLElement('<users><record/></users>');
$xml->record->user_name = $user['user_first_name'] ?: $user['user_name'];
$body = $this->applyXSL($xml, $this->getXSL('emailPasswordResetLink'), ['RESET_URL' => $url]);
return $this->sendEmail((string) $user['user_email'], self::APPLICATION_NAME . ' - Password reset', $body);
}
// Variables.
private $current_user_name = "";
@@ -32,45 +39,14 @@ class Email extends Base {
// Email a new users credentials
// =============================
public function emailNewUserCredentials($user, $password) {
if (!$user instanceof SimpleXMLElement) {
return;
}
// Prepare the email
$to = (string) $user->record->user_email;
$XSLParms['USER-TEMP-PASSWORD'] = $password;
$subject = "New User Credentials";
$message = $this->applyXSL($user, $this->getXSL("emailNewUserCredentials"), $XSLParms);
// Send the Email
$this->sendEmail($to, $subject, $message);
public function emailNewUserCredentials($user, $password = null) {
$this->emailUserPasswordResetLink($user);
}
// =================================
// Email a users Password Reset Link
// =================================
public function emailUserPasswordResetLink($user, $password) {
if (!$user instanceof SimpleXMLElement) {
return;
public function emailUserPasswordResetLink($user, $password = null) {
if ($user instanceof SimpleXMLElement) {
(new AuthService())->requestReset((string) $user->record->user_email);
}
// Prepare the email.
$XSLParms['USER-TEMP-PASSWORD'] = $password;
$to = (string) $user->record->user_email;
$subject = "Password Reset Request";
$message = $this->applyXSL($user, $this->getXSL("emailPasswordResetLink"), $XSLParms);
// Send the Email
$this->sendEmail($to, $subject, $message);
}
// =======================================
@@ -101,15 +77,17 @@ class Email extends Base {
//Server settings
$email->isSMTP();
$email->Host = $SmtpHost;
$email->Host = (string) $SmtpHost;
$email->SMTPAuth = true;
$email->Username = $SmtpUsername; // Your Gmail address
$email->Password = $SmtpPassword; // App password from Google
// $email->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // or 'tls'
$email->Port = $SmtpPort;
$email->Username = (string) $SmtpUsername;
$email->Password = (string) $SmtpPassword;
$email->Port = (int) $SmtpPort;
$email->SMTPSecure = $email->Port === 465 ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
$email->Timeout = 15;
$email->CharSet = 'UTF-8';
$email->isHTML(true);
$email->SetFrom("info@onesourceits.com", "OneSource IT Solutions LLC");
$email->setFrom((string) ($settings->SmtpFromEmail ?? $settings->SmtpUserName), self::APPLICATION_NAME);
if (is_array($to)) {
foreach ($to as $address) {
@@ -125,6 +103,7 @@ class Email extends Base {
$email->Subject = $subject;
$email->Body = $message;
$email->AltBody = html_entity_decode(strip_tags(preg_replace('/<a\s+href="([^"]*)"[^>]*>(.*?)<\/a>/is', '$2 ($1)', $message)), ENT_QUOTES | ENT_HTML5, 'UTF-8');
if (is_array($attachments)) {
foreach ($attachments as $index => $item) {
@@ -139,13 +118,9 @@ class Email extends Base {
}
$success = $email->Send();
} catch (phpMailerException $e) {
} catch (\Throwable $e) {
(new Journal())->journalEntry($e->errorMessage());
$success = false;
} catch (Exception $e) {
(new Journal())->journalEntry($e->getMessage());
error_log('Email delivery failed: ' . $e->getMessage());
$success = false;
}
+10 -112
View File
@@ -1,118 +1,16 @@
<?php
// =============
// Sign In Class
// =============
class SignIn extends Base {
// Variables
private $current_user_serial = "";
private $current_user_name = "";
private $current_user_full_name = "";
private $current_user_email = "";
// ===============================
// __construct : Class Constructor
// ===============================
public function __construct() {
set_error_handler(array($this, "displayError"));
date_default_timezone_set(self::TIMEZONE);
$this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
$this->current_user_full_name = $_SESSION[self::USER_FULL_NAME] ?? "";
$this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
}
// ========================
// Login to the application
// ========================
public function login() {
$step = filter_input(INPUT_POST, "step") ?: "prompt";
switch ($step) {
case "prompt":
$XSLParms["BTN_SUBMIT"] = "main.php?action=SignIn.login&step=authenticate";
$XSLParms["INVALID_SIGNIN"] = "FALSE";
echo $this->applyXSL("", $this->getXSL("login"), $XSLParms);
exit();
break;
case "authenticate":
$user_name = trim(filter_input(INPUT_POST, "user_name") ?: "");
$user_password = filter_input(INPUT_POST, "user_password") ?: "";
$XSLParms["BTN_SUBMIT"] = "main.php?action=SignIn.login&step=authenticate";
$XSLParms["INVALID_SIGNIN"] = "TRUE";
$user = (new Users())->getUserByUserName($user_name);
if ($user->count() == 0) {
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
die();
}
if ((string) $user->record->user_status !== 'active') {
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
die();
}
$stored_hashed_password = $user->record->user_password_hash;
if (!password_verify($user_password, $stored_hashed_password)) {
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
die();
}
session_regenerate_id(true);
$_SESSION[self::USER_AUTHENTICATED] = true;
$_SESSION[self::USER_SERIAL] = (int) $user->record->user_serial;
$_SESSION[self::USER_NAME] = (string) $user->record->user_name;
$_SESSION[self::USER_FULL_NAME] = (string) $user->record->user_full_name;
$_SESSION[self::USER_EMAIL] = (string) $user->record->user_email;
$_SESSION[self::USER_ROLE] = (string) $user->record->user_role;
$this->current_user_serial = $_SESSION[self::USER_SERIAL] ?? 0;
$this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
$this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
$system_admins = $this->getSettings()->xpath("//SystemAdministrator");
foreach ($system_admins as $admin) {
if ((string) $admin === $user_name) {
$_SESSION[self::USER_ROLE] = "System Administrator";
break;
}
}
(new Journal())->journalEntry("{$_SESSION[self::USER_NAME]} logged in ({$_SESSION[self::USER_ROLE]}).");
(new Users())->updateLogin();
header("Location: ./");
exit();
break;
default:
// User NOT Validated.
header("Location: ./");
exit();
$params = ['BTN_SUBMIT' => 'main.php', 'INVALID_SIGNIN' => 'FALSE'];
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
$this->requireCsrf();
if ((new AuthService())->login(trim($_POST['user_email'] ?? $_POST['user_name'] ?? ''), $_POST['user_password'] ?? '')) {
header('Location: main.php', true, 303);
exit;
}
$params['INVALID_SIGNIN'] = 'TRUE';
}
echo $this->applyXSL('<XML/>', $this->getXSL('login'), $params);
exit;
}
}
?>
+23 -94
View File
@@ -1,103 +1,32 @@
<?php
// =============
// Sign In Class
// =============
class SignUp {
// ===============================
// __construct : Class Constructor
// ===============================
public function __construct() {
}
// ==============
// Create Account
// ==============
class SignUp extends Base {
public function createAccount() {
$step = filter_input(INPUT_POST, "step") ?: "prompt";
switch ($step) {
case "prompt":
$XSLParms["BTN_SUBMIT"] = "main.php?action=SignUp.createAccount&step=create";
echo $this->applyXSL("", $this->getXSL("createAccount"), $XSLParms);
exit();
break;
case "create":
$user_name = trim(filter_input(INPUT_POST, "user_name") ?: "");
$user_password = filter_input(INPUT_POST, "user_password") ?: "";
$XSLParms["BTN_SUBMIT"] = "main.php?action=SignIn.login&step=authenticate";
$XSLParms["INVALID_SIGNIN"] = "TRUE";
$user = (new Users())->getUserByUserName($user_name);
if ($user->count() == 0) {
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
die();
$params = ['BTN_SUBMIT' => 'main.php', 'REGISTER_ACTION' => 'SignUp.createAccount'];
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
$this->requireCsrf();
try {
$auth = new AuthService();
if (!$auth->allowAttempt('signup-ip', $_SERVER['REMOTE_ADDR'] ?? '', 5, 3600)) {
throw new InvalidArgumentException('Too many account requests. Please try again later.');
}
if ((string) $user->record->user_status !== 'active') {
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
die();
$auth->register($_POST);
$_SESSION['signup_success'] = true;
header('Location: main.php?Xaction=signup', true, 303);
exit;
} catch (InvalidArgumentException $e) {
$params['SIGNUP_ERROR'] = $e->getMessage();
foreach (['user_first_name', 'user_last_name', 'user_email', 'user_name'] as $field) {
$params[strtoupper($field)] = $_POST[$field] ?? '';
}
$stored_hashed_password = $user->record->user_password_hash;
if (!password_verify($user_password, $stored_hashed_password)) {
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
die();
}
session_regenerate_id(true);
$_SESSION[self::USER_AUTHENTICATED] = true;
$_SESSION[self::USER_SERIAL] = (int) $user->record->user_serial;
$_SESSION[self::USER_NAME] = (string) $user->record->user_name;
$_SESSION[self::USER_FULL_NAME] = (string) $user->record->user_full_name;
$_SESSION[self::USER_EMAIL] = (string) $user->record->user_email;
$_SESSION[self::USER_ROLE] = (string) $user->record->user_role;
$this->current_user_serial = $_SESSION[self::USER_SERIAL] ?? 0;
$this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
$this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
$system_admins = $this->getSettings()->xpath("//SystemAdministrator");
foreach ($system_admins as $admin) {
if ((string) $admin === $user_name) {
$_SESSION[self::USER_ROLE] = "System Administrator";
break;
}
}
(new Journal())->journalEntry("{$_SESSION[self::USER_NAME]} logged in ({$_SESSION[self::USER_ROLE]}).");
(new Users())->updateLogin();
header("Location: ./");
exit();
break;
default:
// User NOT Validated.
header("Location: ./");
exit();
}
}
if (!empty($_SESSION['signup_success'])) {
$params['SIGNUP_SUCCESSFUL'] = 'TRUE';
unset($_SESSION['signup_success']);
}
echo $this->applyXSL('<XML/>', $this->getXSL('createAccount'), $params);
exit;
}
}
?>
+64 -133
View File
@@ -25,6 +25,7 @@ class Users extends Base {
date_default_timezone_set(self::TIMEZONE);
$this->current_user_serial = (int) ($_SESSION[self::USER_SERIAL] ?? 0);
$this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
$this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
$this->pagination_page = $_SESSION[self::PAGINATION_PAGE] ?? 1;
@@ -100,9 +101,13 @@ class Users extends Base {
case "add":
$user_name = filter_input(INPUT_POST, "user_name") ?: "";
$user_client_name = filter_input(INPUT_POST, "user_client_name") ?: "";
$user_first_name = filter_input(INPUT_POST, "user_first_name") ?: "";
$user_email = filter_input(INPUT_POST, "user_email") ?: "";
$user_role = filter_input(INPUT_POST, "user_role") ?: "";
if (!in_array($user_role, ['User', 'Guest', 'Administrator'], true) || !filter_var($user_email, FILTER_VALIDATE_EMAIL) || strlen($user_email) > 100 || !preg_match('/\A[A-Za-z0-9_.-]{3,64}\z/', $user_name)) {
http_response_code(422); exit('Enter a valid username, email address and role.');
}
$user_name = strtoupper(preg_replace("/\s+/", " ", $user_name));
@@ -110,31 +115,31 @@ class Users extends Base {
$user_temp_password = password_hash($user_random_password, PASSWORD_DEFAULT);
$user_change_password = (int) true;
$user_active = (int) true;
$user_active = 'active';
// Add the User
$connection = $this->connect();
$SQL = "insert into users
( user_name,
user_email,
user_client_name,
user_password,
user_first_name,
user_password_hash,
user_role,
user_change_password,
user_active,
user_status,
user_creator,
user_created )
values ( :user_name,
:user_email,
:user_client_name,
:user_password,
:user_first_name,
:user_password_hash,
:user_role,
:user_change_password,
:user_active,
:user_status,
:user_creator,
:user_created )";
@@ -142,11 +147,11 @@ class Users extends Base {
$statement->bindValue(":user_name", $user_name);
$statement->bindValue(":user_email", $user_email);
$statement->bindValue(":user_client_name", $user_client_name);
$statement->bindValue(":user_password", $user_temp_password);
$statement->bindValue(":user_first_name", $user_first_name);
$statement->bindValue(":user_password_hash", $user_temp_password);
$statement->bindValue(":user_role", $user_role);
$statement->bindValue(":user_change_password", $user_change_password, PDO::PARAM_BOOL);
$statement->bindValue(":user_active", $user_active, PDO::PARAM_BOOL);
$statement->bindValue(":user_status", $user_active);
$statement->bindValue(":user_creator", $this->current_user_name);
$statement->bindValue(":user_created", $this->getTimestamp());
@@ -158,7 +163,7 @@ class Users extends Base {
// Send User Credentials
(new Email())->emailNewUserCredentials($user, $user_random_password);
(new AuthService())->requestReset($user_email);
break;
@@ -226,9 +231,13 @@ class Users extends Base {
$user_serial = filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
$user_name = filter_input(INPUT_POST, "user_name") ?: "";
$user_email = filter_input(INPUT_POST, "user_email") ?: "";
$user_client_name = filter_input(INPUT_POST, "user_client_name") ?: "";
$user_first_name = filter_input(INPUT_POST, "user_first_name") ?: "";
$user_role = filter_input(INPUT_POST, "user_role") ?: "";
$user_active = filter_input(INPUT_POST, "user_active", FILTER_VALIDATE_BOOLEAN) ?: false;
if (!in_array($user_role, ['User', 'Guest', 'Administrator'], true) || !filter_var($user_email, FILTER_VALIDATE_EMAIL) || strlen($user_email) > 100 || !preg_match('/\A[A-Za-z0-9_.-]{3,64}\z/', $user_name)) {
http_response_code(422); exit('Enter a valid username, email address and role.');
}
$user_active = filter_input(INPUT_POST, 'user_active', FILTER_VALIDATE_BOOLEAN) ? 'active' : 'inactive';
$users = $this->getUser($user_serial);
@@ -238,12 +247,13 @@ class Users extends Base {
// Update the User
$SQL = "update users
$SQL = "update users
set user_name = :user_name,
user_email = :user_email,
user_client_name = :user_client_name,
user_first_name = :user_first_name,
user_role = :user_role,
user_active = :user_active,
user_auth_version = user_auth_version + 1,
user_status = :user_status,
user_changer = :user_changer,
user_changed = :user_changed
where user_serial = :user_serial";
@@ -253,9 +263,9 @@ class Users extends Base {
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_name", $user_name);
$statement->bindValue(":user_email", $user_email);
$statement->bindValue(":user_client_name", $user_client_name);
$statement->bindValue(":user_first_name", $user_first_name);
$statement->bindValue(":user_role", $user_role);
$statement->bindValue(":user_active", $user_active, PDO::PARAM_BOOL);
$statement->bindValue(":user_status", $user_active);
$statement->bindValue(":user_changer", $this->current_user_name);
$statement->bindValue(":user_changed", $this->getTimestamp());
@@ -274,6 +284,7 @@ class Users extends Base {
// ====================
public function getUser($user_serial = 0) {
$user_serial = (int) $user_serial;
$SQL = "select view_users.*
from view_users
@@ -287,12 +298,9 @@ class Users extends Base {
// ====================
public function getUserByUserName($user_name) {
$SQL = "select view_users.*
from view_users
where user_name = '{$user_name}'";
return $this->getTable("user", $SQL);
$stmt = $this->connect()->prepare('SELECT * FROM view_users WHERE user_name = :name');
$stmt->execute([':name' => $user_name]);
return $this->getTableXML('users', $stmt);
}
// =====================
@@ -337,14 +345,14 @@ class Users extends Base {
$SQL = "select count(*) as count
from view_users
where user_name regexp :user_name
or user_client_name regexp :user_client_name
or user_first_name regexp :user_first_name
or user_email regexp :user_email
or user_role regexp :user_role";
$statement = $connection->prepare($SQL);
$statement->bindValue(":user_name", $find_words);
$statement->bindValue(":user_client_name", $find_words);
$statement->bindValue(":user_first_name", $find_words);
$statement->bindValue(":user_email", $find_words);
$statement->bindValue(":user_role", $find_words);
@@ -379,7 +387,7 @@ class Users extends Base {
$SQL = "select view_users.*
from view_users
where user_name regexp :user_name
or user_client_name regexp :user_client_name
or user_first_name regexp :user_first_name
or user_email regexp :user_email
or user_role regexp :user_role
limit {$this->pagination_size}
@@ -388,7 +396,7 @@ class Users extends Base {
$statement = $connection->prepare($SQL);
$statement->bindValue(":user_name", $find_words);
$statement->bindValue(":user_client_name", $find_words);
$statement->bindValue(":user_first_name", $find_words);
$statement->bindValue(":user_email", $find_words);
$statement->bindValue(":user_role", $find_words);
@@ -485,111 +493,33 @@ class Users extends Base {
// ===============
public function changePassword() {
$step = filter_input(INPUT_POST, "step") ?: "prompt";
switch ($step) {
case "prompt":
$popovers = (new Popovers())->getPopovers();
$XML = $this->mergeXML($popovers, "<XML/>");
$content = $this->applyXSL($XML, $this->getXSL("changePassword"));
$this->displayContent($content);
break;
case "change":
$user_serial = $_SESSION[self::USER_SERIAL];
$user_new_password = filter_input(INPUT_POST, "user_new_password") ?: "";
$user_verify_password = filter_input(INPUT_POST, "user_verify_password") ?: "";
if (!$user_new_password == $user_verify_password) {
$this->initializeSession();
$params = ['PASSWORD_ERROR' => ''];
if (($_POST['step'] ?? '') === 'change') {
$this->requireCsrf();
try {
$auth = new AuthService();
if (!$auth->allowAttempt('change-password', (string) $_SESSION[self::USER_SERIAL], 10, 900)) {
throw new InvalidArgumentException('Too many attempts. Please try again later.');
}
$user_new_hashed_password = password_hash($user_new_password, PASSWORD_DEFAULT);
// Update User Password
$connection = $this->connect();
$SQL = "update users
set user_password = :user_password,
user_change_password = :user_change_password,
user_changer = :user_changer,
user_changed = :user_changed
where user_serial = :user_serial";
$statement = $connection->prepare($SQL);
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_password", $user_new_hashed_password);
$statement->bindValue(":user_change_password", 0);
$statement->bindValue(":user_changer", $this->current_user_name);
$statement->bindValue(":user_changed", $this->getTimestamp());
if (!$statement->execute()) {
$this->debug($statement->errorInfo()[2]);
if (!$auth->changePassword((int) $_SESSION[self::USER_SERIAL], $_POST['user_current_password'] ?? '', $_POST['user_new_password'] ?? '', $_POST['user_verify_password'] ?? '')) {
throw new InvalidArgumentException('Your current password is incorrect or your session has expired.');
}
(new Journal())->journalEntry("{$_SESSION[self::USER_NAME]} changed their password.");
break;
default:
$this->displayHome();
$this->logoutSession();
header('Location: main.php', true, 303);
exit;
} catch (InvalidArgumentException $e) { $params['PASSWORD_ERROR'] = $e->getMessage(); }
}
$this->displayContent($this->applyXSL('<XML/>', $this->getXSL('changePassword'), $params));
}
// ==============
// Reset Password
// ==============
public function resetPassword() {
$reset = false;
$user_serial = filter_input(INPUT_POST, "user_serial") ?: "";
$user = $this->getUser($user_serial);
$user_name = $user->record->user_name;
$user_random_password = $this->generateRandomPassword();
$user_temp_password = password_hash($user_random_password, PASSWORD_DEFAULT);
// Update User Password
$connection = $this->connect();
$SQL = "update users
set user_password = :user_password,
user_change_password = :user_change_password
where user_serial = :user_serial";
$statement = $connection->prepare($SQL);
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_password", $user_temp_password);
$statement->bindValue(":user_change_password", 1);
if (!$statement->execute()) {
$this->debug($statement->errorInfo()[2]);
} else {
$reset = true;
}
// Send User Credentials
(new Email())->emailUserPasswordResetLink($user, $user_random_password);
(new Journal())->journalEntry("{$_SESSION[self::USER_NAME]} reset {$user_name}'s password.");
echo json_encode($reset);
$this->requireCsrf();
$stmt = $this->connect()->prepare('SELECT user_email FROM users WHERE user_serial = :id');
$stmt->execute([':id' => (int) ($_POST['user_serial'] ?? 0)]);
$email = $stmt->fetchColumn();
if ($email) (new AuthService())->requestReset($email);
header('Content-Type: application/json');
echo json_encode(true);
}
// ===============================================
@@ -599,7 +529,7 @@ class Users extends Base {
public function updateLogin() {
$SQL = "update users
set user_login = :user_login
set user_last_login = :user_login
where user_serial = :user_serial";
$statement = $this->connect()->prepare($SQL);
@@ -617,10 +547,11 @@ class Users extends Base {
public function updateActiveStatus() {
$user_serial = filter_input(INPUT_POST, "user_serial", FILTER_SANITIZE_NUMBER_INT) ?: 0;
$user_active = filter_input(INPUT_POST, "user_active", FILTER_SANITIZE_NUMBER_INT) ?: 0;
$user_active = filter_input(INPUT_POST, 'user_active', FILTER_VALIDATE_BOOLEAN) ? 'active' : 'inactive';
$SQL = "update users
set user_active = :user_active,
set user_status = :user_status,
user_auth_version = user_auth_version + 1,
user_changed = :user_changed,
user_changer = :user_changer
where user_serial = :user_serial";
@@ -628,7 +559,7 @@ class Users extends Base {
$statement = $this->connect()->prepare($SQL);
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_active", $user_active);
$statement->bindValue(":user_status", $user_active);
$statement->bindValue(":user_changed", $this->getTimestamp());
$statement->bindValue(":user_changer", $this->current_user_name);