requireAdministrator(); } private function requireAdministrator(): void { $statement = $this->connect()->prepare('SELECT user_name, user_role, user_status FROM users WHERE user_serial = :user'); $statement->execute([':user' => (int) ($_SESSION[self::USER_SERIAL] ?? 0)]); $user = $statement->fetch(PDO::FETCH_ASSOC); $allowed = $user && $user['user_status'] === 'active' && in_array($user['user_role'], ['Administrator', 'System Administrator'], true); if ($user && $user['user_status'] === 'active') { foreach ($this->getSettings()->xpath('//SystemAdministrator') as $name) { $allowed = $allowed || (string) $name === $user['user_name']; } } if (!$allowed) { http_response_code(403); exit('Administrator access is required.'); } } public function dashboard(): void { try { $statement = $this->connect()->prepare('SELECT a.* FROM applications a JOIN user_applications ua ON ua.application_serial = a.application_serial WHERE ua.user_serial = :user AND a.application_active = 1 ORDER BY a.application_name'); $statement->execute([':user' => (int) $_SESSION[self::USER_SERIAL]]); $xml = $this->getTableXML('applications', $statement); } catch (PDOException $e) { if ((int) ($e->errorInfo[1] ?? 0) !== 1146) { throw $e; } $xml = new SimpleXMLElement(''); $xml->addAttribute('setup_required', 'true'); } $this->displayContent($this->applyXSL($xml, $this->getXSL('home'))); } public function manageAccess(): void { $this->requireAdministrator(); $connection = $this->connect(); $userSerial = (int) (filter_input(INPUT_GET, 'user_serial', FILTER_VALIDATE_INT) ?: 0); $xml = new SimpleXMLElement(''); $xml->addAttribute('selected_user', (string) $userSerial); $xml->addAttribute('saved', filter_input(INPUT_GET, 'saved') === '1' ? 'true' : 'false'); $xml = $this->mergeXML($this->getTableXML('users', $connection->query('SELECT user_serial, user_name, user_status FROM users ORDER BY user_name')), $xml); try { $statement = $connection->prepare('SELECT a.*, CASE WHEN ua.user_serial IS NULL THEN 0 ELSE 1 END AS assigned FROM applications a LEFT JOIN user_applications ua ON ua.application_serial = a.application_serial AND ua.user_serial = :user ORDER BY a.application_name'); $statement->execute([':user' => $userSerial]); } catch (PDOException $e) { if ((int) ($e->errorInfo[1] ?? 0) !== 1146) { throw $e; } $this->displayContent($this->applyXSL('', $this->getXSL('applicationAccess'))); return; } $xml = $this->mergeXML($this->getTableXML('applications', $statement), $xml); try { $links = $connection->prepare('SELECT application_serial, local_user_serial FROM user_application_accounts WHERE user_serial = :user'); $links->execute([':user' => $userSerial]); $xml = $this->mergeXML($this->getTableXML('account_links', $links), $xml); } catch (PDOException $e) { if ((int) ($e->errorInfo[1] ?? 0) !== 1146) { throw $e; } } $this->displayContent($this->applyXSL($xml, $this->getXSL('applicationAccess'))); } public function saveAccess(): void { $this->requireAdministrator(); if (!$this->validAuthRequest()) { http_response_code(403); exit('Invalid request. Reload the application access page.'); } $userSerial = filter_input(INPUT_POST, 'user_serial', FILTER_VALIDATE_INT) ?: 0; $selected = filter_input(INPUT_POST, 'applications', FILTER_VALIDATE_INT, FILTER_REQUIRE_ARRAY); $selected = $selected === null ? [] : $selected; if (!is_array($selected) || in_array(false, $selected, true) || in_array(null, $selected, true)) { http_response_code(422); exit('Invalid application selection.'); } $selected = array_unique($selected); $links = filter_input(INPUT_POST, 'local_users', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); if ($links !== null && !is_array($links)) { http_response_code(422); exit('Invalid account links.'); } $linkedAccounts = []; foreach ($links ?? [] as $application => $localUser) { if (!ctype_digit((string) $application) || !is_string($localUser)) { http_response_code(422); exit('Invalid account link.'); } if (trim($localUser) === '') { continue; } $localUser = filter_var(trim($localUser), FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); if ($localUser === false) { http_response_code(422); exit('Account IDs must be positive whole numbers.'); } $linkedAccounts[(int) $application] = $localUser; } $connection = $this->connect(); $connection->beginTransaction(); try { $statement = $connection->prepare('SELECT user_serial FROM users WHERE user_serial = :user FOR UPDATE'); $statement->execute([':user' => $userSerial]); if (!$statement->fetchColumn()) { $connection->rollBack(); http_response_code(422); exit('Select an existing user.'); } $valid = array_map('intval', $connection->query('SELECT application_serial FROM applications')->fetchAll(PDO::FETCH_COLUMN)); if (array_diff($selected, $valid)) { $connection->rollBack(); http_response_code(422); exit('Invalid application selection.'); } if (array_diff(array_keys($links ?? []), $valid)) { $connection->rollBack(); http_response_code(422); exit('Invalid application account link.'); } if ($links !== null) { $delete = $connection->prepare('DELETE FROM user_application_accounts WHERE user_serial = :user'); $delete->execute([':user' => $userSerial]); $insert = $connection->prepare('INSERT INTO user_application_accounts (user_serial, application_serial, local_user_serial, linked_by) VALUES (:user, :application, :local, :actor)'); foreach ($linkedAccounts as $application => $localUser) { $insert->execute([':user' => $userSerial, ':application' => $application, ':local' => $localUser, ':actor' => $_SESSION[self::USER_NAME]]); } } $statement = $connection->prepare('DELETE FROM user_applications WHERE user_serial = :user'); $statement->execute([':user' => $userSerial]); $statement = $connection->prepare('INSERT INTO user_applications (user_serial, application_serial, assigned_by) VALUES (:user, :application, :actor)'); foreach ($selected as $application) { $statement->execute([':user' => $userSerial, ':application' => $application, ':actor' => $_SESSION[self::USER_NAME]]); } $journal = $connection->prepare('INSERT INTO journal (journal_origin, journal_ip, journal_entry) VALUES (:actor, :ip, :entry)'); $journal->execute([':actor' => $_SESSION[self::USER_NAME], ':ip' => $_SERVER['REMOTE_ADDR'] ?? '', ':entry' => 'Updated application access for user ' . $userSerial . ': ' . implode(', ', $selected) . ($links !== null ? '; account links: ' . json_encode($linkedAccounts) : '')]); $connection->commit(); } catch (Throwable $e) { if ($connection->inTransaction()) { $connection->rollBack(); } if ($e instanceof PDOException && (int) ($e->errorInfo[1] ?? 0) === 1062) { http_response_code(409); exit('That application account is already linked to another DEC user. No changes were saved.'); } throw $e; } header('Location: main.php?Xaction=Applications.manageAccess&user_serial=' . $userSerial . '&saved=1', true, 303); exit(); } public function setup(): void { $this->requireAdministrator(); if (!$this->validAuthRequest()) { http_response_code(403); exit('Invalid setup request. Reload the page.'); } $connection = $this->connect(); foreach (['001_application_access.sql', '002_application_account_links.sql'] as $migration) { $sql = file_get_contents(__DIR__ . '/../migrations/' . $migration); foreach (explode(';', $sql) as $statement) { if (trim($statement) !== '') { $connection->exec($statement); } } } header('Location: main.php?Xaction=Applications.manageAccess', true, 303); exit(); } public function launch(): void { $application = filter_input(INPUT_GET, 'application_serial', FILTER_VALIDATE_INT) ?: 0; $statement = $this->connect()->prepare('SELECT a.application_url FROM applications a JOIN user_applications ua ON ua.application_serial = a.application_serial WHERE a.application_serial = :application AND ua.user_serial = :user AND a.application_active = 1'); $statement->execute([':application' => $application, ':user' => (int) $_SESSION[self::USER_SERIAL]]); $url = $statement->fetchColumn(); if (!$url) { http_response_code(403); exit('This application is not assigned to your account.'); } if (!filter_var($url, FILTER_VALIDATE_URL) || !in_array(strtolower(parse_url($url, PHP_URL_SCHEME) ?: ''), ['http', 'https'], true) || preg_match('/[\r\n]/', $url)) { http_response_code(500); exit('The application URL is invalid. Contact your administrator.'); } header('Location: ' . $url, true, 303); exit(); } }