Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions admin/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<?php
require_once(__DIR__ . '/../src/db.php');
require_once(__DIR__ . '/../func/genuuid.php');
require_once(__DIR__ . '/../func/csrf.php');
if (session_status() === PHP_SESSION_NONE) { session_start(); }

require_once(__DIR__ . '/../func/get_config.php');
Expand Down Expand Up @@ -42,6 +43,24 @@
$_SESSION['validity'] = time() + 1800;
}
}

$parsedRequestPath = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
if ($parsedRequestPath === false) {
$parsedRequestPath = '';
}
$requestPath = $parsedRequestPath;
$requestPath = rawurldecode($requestPath);
$requestPath = str_replace('\\', '/', $requestPath);
$requestPath = preg_replace('#/+#', '/', $requestPath);
$requestPath = '/' . ltrim($requestPath, '/');
$isAdminApiRequest = str_starts_with($requestPath, '/admin/api/');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$isAdminApiRequest) {
$csrfToken = $_POST['csrf_token'] ?? '';
if (!verify_csrf_token($csrfToken)) {
http_response_code(403);
die("<div class='alert alert-danger text-center'>Pedido inválido. Atualize a página e tente novamente.</div>");
}
}
?>
<?php
// Criação da Sidebar (reaproveito do módulo para as subpáginas)
Expand Down Expand Up @@ -181,6 +200,33 @@ function sidebarDropdownLink($url, $nome) {
// Fechar Navbar no HTML, e passar o conteúdo para baixo
echo "</ul></div></div></nav><div class='container-fluid mt-4 justify-content-center text-center'>";

$csrfTokenHtml = htmlspecialchars(generate_csrf_token(), ENT_QUOTES, 'UTF-8');
echo "<input type='hidden' id='global-csrf-token' value='{$csrfTokenHtml}'>";
echo "<script>
(function() {
function ensureCsrf(form) {
if (!form || String(form.method).toLowerCase() !== 'post') return;
if (!form.querySelector('input[name=\"csrf_token\"]')) {
const tokenSource = document.getElementById('global-csrf-token');
const csrfToken = tokenSource ? tokenSource.value : '';
if (!csrfToken) return;
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'csrf_token';
input.value = csrfToken;
form.appendChild(input);
}
}

document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('form[method=\"POST\"], form[method=\"post\"]').forEach(ensureCsrf);
});
document.addEventListener('submit', function(event) {
ensureCsrf(event.target);
}, true);
})();
</script>";

?>

<?php
Expand Down Expand Up @@ -303,6 +349,7 @@ function sidebarDropdownLink($url, $nome) {
function formulario($action, $inputs) {
$action_safe = htmlspecialchars($action, ENT_QUOTES, 'UTF-8');
echo "<form action='$action_safe' method='POST' class='d-flex align-items-center'>";
echo csrf_token_field();
foreach ($inputs as $input) {
$id_safe = htmlspecialchars($input['id'], ENT_QUOTES, 'UTF-8');
$value_safe = htmlspecialchars($input['value'], ENT_QUOTES, 'UTF-8');
Expand Down
12 changes: 9 additions & 3 deletions admin/materiais.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
echo "<div class='alert alert-info fade show' role='alert'>Completar informações do material</div>";
?>
<form action="materiais.php?action=criar_completo" method="POST" class="mb-3">
<?= csrf_token_field(); ?>
<div class="form-floating mb-2">
<input type="text" class="form-control" id="nomematerial" name="nomematerial" placeholder="Nome do Material" value="<?php echo htmlspecialchars($_POST['nomematerial'], ENT_QUOTES, 'UTF-8'); ?>" required>
<label for="nomematerial">Nome do Material</label>
Expand Down Expand Up @@ -175,13 +176,13 @@

// Delete material
case "apagar":
if (!isset($_GET['id'])) {
if (!isset($_POST['id'])) {
echo "<div class='alert alert-danger fade show' role='alert'>ID inválido.</div>";
break;
}

$stmt = $db->prepare("DELETE FROM materiais WHERE id = ?");
$stmt->bind_param("s", $_GET['id']);
$stmt->bind_param("s", $_POST['id']);
$stmt->execute();
$stmt->close();
acaoexecutada("Eliminação de Material");
Expand All @@ -208,6 +209,7 @@
echo "<div class='alert alert-warning fade show' role='alert'>A editar o Material <b>" . htmlspecialchars($d['nome'], ENT_QUOTES, 'UTF-8') . "</b>.</div>";
?>
<form action="materiais.php?action=update&id=<?php echo urlencode($d['id']); ?>" method="POST" class="mb-3">
<?= csrf_token_field(); ?>
<div class="form-floating mb-2">
<input type="text" class="form-control" id="nomematerial" name="nomematerial" placeholder="Nome do Material" value="<?php echo htmlspecialchars($d['nome'], ENT_QUOTES, 'UTF-8'); ?>" required>
<label for="nomematerial">Nome do Material</label>
Expand Down Expand Up @@ -298,7 +300,11 @@
echo "<td><span class='badge bg-info'>{$salaNome}</span></td>";
echo "<td>";
echo "<a href='/admin/materiais.php?action=edit&id={$idEnc}' class='btn btn-sm btn-outline-primary me-1'>Editar</a>";
echo "<a href='/admin/materiais.php?action=apagar&id={$idEnc}' class='btn btn-sm btn-outline-danger' onclick='return confirm(\"Tem a certeza que pretende apagar este material?\");'>Apagar</a>";
echo "<form action='/admin/materiais.php?action=apagar' method='POST' style='display:inline;' onsubmit='return confirm(\"Tem a certeza que pretende apagar este material?\");'>";
echo csrf_token_field();
echo "<input type='hidden' name='id' value='{$idEnc}'>";
echo "<button type='submit' class='btn btn-sm btn-outline-danger'>Apagar</button>";
echo "</form>";
echo "</td>";
echo "</tr>";
}
Expand Down
6 changes: 6 additions & 0 deletions admin/relatorios.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
<?php
require_once(__DIR__ . '/../func/logaction.php');
require_once(__DIR__ . '/../func/csrf.php');
require_once(__DIR__ . '/../src/db.php');
require_once(__DIR__ . '/../vendor/autoload.php');

// Handle PDF generation
if (isset($_POST['gerar_pdf'])) {
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
http_response_code(403);
die("<div class='alert alert-danger text-center'>Pedido inválido. Atualize a página e tente novamente.</div>");
}
// Guard: redirect pending TOTP/setup flows to completion
if (isset($_SESSION['pending_totp_user'])) { header('Location: /login?step=totp'); exit(); }
if (isset($_SESSION['pending_user_setup'])) { header('Location: /login?step=setup'); exit(); }
Expand Down Expand Up @@ -164,6 +169,7 @@ public function Footer() {
<p>Gere um relatório em PDF da utilização de salas para um dia específico.</p>

<form method="POST" style="max-width: 500px; margin: 20px auto;">
<?= csrf_token_field(); ?>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Selecione a Data</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" value="<?php echo date('Y-m-d'); ?>" required>
Expand Down
20 changes: 20 additions & 0 deletions admin/reservaemmassa.php
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,12 @@ function validateForm(event) {
$stmtCheck = $db->prepare("SELECT 1 FROM reservas WHERE sala = ? AND tempo = ? AND data = ? LIMIT 1");
$stmtInsert = $db->prepare("INSERT INTO reservas (sala, tempo, data, requisitor, aprovado, motivo, extra) VALUES (?, ?, ?, ?, 1, ?, ?)");

$db->begin_transaction();

$lineNumber = 0;
$successCount = 0;
$errorCount = 0;
$insertErrorCount = 0;
$duplicateCount = 0;
$errors = [];
$maxDisplayedErrors = 10;
Expand Down Expand Up @@ -375,10 +378,17 @@ function validateForm(event) {
$successCount++;
} else {
$errorCount++;
$insertErrorCount++;
$recordError("Linha {$lineNumber}: Erro ao inserir reserva.");
}
}

if ($insertErrorCount > 0) {
$db->rollback();
} else {
$db->commit();
}

$stmtSalaExists->close();
$stmtRequisitorExists->close();
$stmtTempoExists->close();
Expand Down Expand Up @@ -462,6 +472,7 @@ function validateForm(event) {
</div>

<form id="massReservationForm" action="reservaemmassa.php" method="POST" class="mt-4">
<?= csrf_token_field(); ?>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-floating">
Expand Down Expand Up @@ -690,6 +701,8 @@ function validateForm(event) {
$reservas_duplicadas = 0;
$erros = [];
$num_semanas = 0;
$db->begin_transaction();


// Create reservations for each week until we pass the end date
$current_date = $first_date;
Expand Down Expand Up @@ -740,6 +753,13 @@ function validateForm(event) {
}

// Send email notification to the user if any reservations were created
if (!empty($erros)) {
$db->rollback();
$reservas_criadas = 0;
} else {
$db->commit();
}

if ($reservas_criadas > 0) {
sendRecurringWeeklyReservationsEmail(
$db,
Expand Down
30 changes: 28 additions & 2 deletions login/index.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
<?php
if (session_status() === PHP_SESSION_NONE) { session_start(); }
require_once(__DIR__ . '/../func/csrf.php');

// Handle database selection
if (isset($_POST['action']) && $_POST['action'] === 'select_db') {
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
http_response_code(403);
die('Pedido inválido. Atualize a página e tente novamente.');
}
$selectedDb = $_POST['db_selection'] ?? null;
if ($selectedDb) {
$_SESSION['selected_db'] = $selectedDb;
Expand All @@ -28,6 +33,7 @@
$localAuthInfo = null;
$localAuthStage = 'email';
$emailValue = '';
$csrfTokenField = csrf_token_field();

// --- Helper Functions for OTP/TOTP ---

Expand Down Expand Up @@ -144,6 +150,11 @@ function start_authenticated_session($userId, $userName, $userEmail, $isAdmin) {
// --- POST Request Handling ---

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
http_response_code(403);
die('Pedido inválido. Atualize a página e tente novamente.');
}

$action = $_POST['action'] ?? null;

if ($action === 'send_code' && isset($_POST['email'])) {
Expand Down Expand Up @@ -339,7 +350,7 @@ function start_authenticated_session($userId, $userName, $userEmail, $isAdmin) {
exit();
}
$devModeBanner = is_development_mode() ? '<div style="background-color: #dc3545; color: white; padding: 4px; font-size: 12px; font-weight: bold; text-align: center; position: absolute; top: 0; width: 100%; z-index: 100;">⚠️ MODO DE DESENVOLVIMENTO - Dados de teste | Base de dados de desenvolvimento</div>' : '';
$content = $devModeBanner . '<div class="login-box"><h1>Verificação de Segurança</h1><p class="small">Introduza o código do seu autenticador para prosseguir.</p>' . (!empty($localAuthError) ? '<div class="error-msg">' . htmlspecialchars($localAuthError) . '</div>' : '') . '<form method="POST" action="/login/index.php"><input type="hidden" name="action" value="verify_totp"><div class="form-group"><input type="text" name="totp_code" placeholder="Código do autenticador" pattern="\d{6}" maxlength="6" autocomplete="one-time-code" required></div><button type="submit">Autenticar</button></form></div>';
$content = $devModeBanner . '<div class="login-box"><h1>Verificação de Segurança</h1><p class="small">Introduza o código do seu autenticador para prosseguir.</p>' . (!empty($localAuthError) ? '<div class="error-msg">' . htmlspecialchars($localAuthError) . '</div>' : '') . '<form method="POST" action="/login/index.php">' . $csrfTokenField . '<input type="hidden" name="action" value="verify_totp"><div class="form-group"><input type="text" name="totp_code" placeholder="Código do autenticador" pattern="\d{6}" maxlength="6" autocomplete="one-time-code" required></div><button type="submit">Autenticar</button></form></div>';
render_login_template('Verificação de Segurança', $content);
die();
}
Expand All @@ -357,6 +368,7 @@ function start_authenticated_session($userId, $userName, $userEmail, $isAdmin) {
$content .= '<p class="small">Por favor, introduza o seu nome completo.</p>';
if (!empty($localAuthError)) { $content .= '<div class="error-msg">' . htmlspecialchars($localAuthError) . '</div>'; }
$content .= '<form method="POST" action="/login/index.php">';
$content .= $csrfTokenField;
$content .= '<input type="hidden" name="action" value="setup_name">';
$content .= '<div class="form-group"><input type="text" name="nome" placeholder="Nome completo" required></div>';
$content .= '<button type="submit">Continuar</button>';
Expand Down Expand Up @@ -399,6 +411,7 @@ function start_authenticated_session($userId, $userName, $userEmail, $isAdmin) {
$content .= '<div class="qr-container"><img src="' . htmlspecialchars($qrCodeImage) . '" alt="QR Code"></div>';
$content .= '<div class="info-msg"><strong>Código manual:</strong><br><span class="manual-code">' . htmlspecialchars($secret) . '</span></div>';
$content .= '<form method="POST" action="/login/index.php">';
$content .= $csrfTokenField;
$content .= '<input type="hidden" name="action" value="verify_totp_setup">';
$content .= '<div class="form-group"><input type="text" name="totp_code" placeholder="Código do autenticador" pattern="\\d{6}" maxlength="6" autocomplete="one-time-code" required></div>';
$content .= '<button type="submit">Validar e Ativar</button>';
Expand Down Expand Up @@ -454,6 +467,7 @@ function start_authenticated_session($userId, $userName, $userEmail, $isAdmin) {
<h1>Iniciar Sessão no ClassLink</h1>
<?php if ($showDbPicker): ?>
<form method="POST" action="/login/index.php" style="margin-bottom: 1rem;">
<?= $csrfTokenField ?>
<input type="hidden" name="action" value="select_db">
<select name="db_selection" onchange="this.form.submit()" class="form-select" style="max-width: 200px; margin: 0 auto;">
<option value="">Selecionar Base de Dados...</option>
Expand All @@ -478,6 +492,7 @@ function start_authenticated_session($userId, $userName, $userEmail, $isAdmin) {

<?php if ($localAuthStage === 'email'): ?>
<form method="POST" action="/login/index.php">
<?= $csrfTokenField ?>
<input type="hidden" name="action" value="send_code">
<div class="form-group">
<input type="email" name="email" placeholder="Endereço Eletrónico" value="<?= htmlspecialchars($emailValue) ?>" required>
Expand All @@ -486,6 +501,7 @@ function start_authenticated_session($userId, $userName, $userEmail, $isAdmin) {
</form>
<?php else: ?>
<form method="POST" action="/login/index.php">
<?= $csrfTokenField ?>
<input type="hidden" name="action" value="verify_code">
<div class="form-group">
<input type="email" name="email" placeholder="Endereço Eletrónico" value="<?= htmlspecialchars($emailValue) ?>" readonly style="opacity: 0.7;">
Expand Down Expand Up @@ -539,7 +555,17 @@ function start_authenticated_session($userId, $userName, $userEmail, $isAdmin) {
} else if (isset($_GET['error'])) {
?>
<?php
}else if (isset($_GET['code'])){ $now = time(); try {
}else if (isset($_GET['code'])){ $now = time();
if (!isset($_GET['state']) || !isset($_SESSION['oauth2state']) || $_GET['state'] === '' || $_SESSION['oauth2state'] === '' || !hash_equals($_SESSION['oauth2state'], $_GET['state'])) {
$clientIp = get_client_ip();
$sessionHash = substr(hash('sha256', session_id()), 0, 8);
error_log("ClassLink OAuth state validation failed for callback. ip={$clientIp}; session_hash={$sessionHash}");
unset($_SESSION['oauth2state']);
header('Location: /login/');
exit();
}
unset($_SESSION['oauth2state']);
try {
$accessToken = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
Expand Down
7 changes: 7 additions & 0 deletions reservar/index.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php
require_once(__DIR__ . '/../src/db.php');
require_once(__DIR__ . '/../func/csrf.php');
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (isset($_SESSION['pending_totp_user'])) { header('Location: /login?step=totp'); exit(); }
if (isset($_SESSION['pending_user_setup'])) { header('Location: /login?step=setup'); exit(); }
Expand All @@ -8,6 +9,10 @@
header("Location: /login");
die("A reencaminhar para iniciar sessão...");
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !verify_csrf_token($_POST['csrf_token'] ?? '')) {
http_response_code(403);
die("Pedido inválido. Atualize a página e tente novamente.");
}
?>
<!DOCTYPE html>
<html lang="pt">
Expand Down Expand Up @@ -122,6 +127,7 @@ function clearBulkUserSelection() {
<div class="d-flex align-items-center justify-content-center flex-column">
<p class="h2 fw-light">Reservar uma Sala</p>
<form action="<?php echo $_SERVER['REQUEST_URI']; ?>" method="POST" class="d-flex align-items-center">
<?= csrf_token_field(); ?>
<div class="form-floating me-2">
<select class="form-select" id="sala" name="sala" required onchange="this.form.submit();">
<?php if ($_POST['sala'] == "0" | !$_POST['sala']) {
Expand Down Expand Up @@ -185,6 +191,7 @@ function clearBulkUserSelection() {

echo (
"<form id='bulkReservationForm' method='POST' action='/reservar/manage.php?subaction=bulk' data-prevent-double-submit>
" . csrf_token_field() . "
<div class='reservation-table-container'>
<table class='table table-bordered' style='table-layout: fixed; width: 100%; max-width: 70%; margin: 0 auto; font-size: 0.85rem;'><thead><tr><th scope='col' style='font-size: 0.75rem;'>Tempos</th>"
);
Expand Down
Loading
Loading