From 545d6e35844e37bed2a9c5182c901921e7238b3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 21:47:02 +0000 Subject: [PATCH 01/22] Initial plan From 63a07d3fff2f10e89c83d06c333a400c8d7ae21b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 21:48:54 +0000 Subject: [PATCH 02/22] feat: add CSV import for tempos in admin panel Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/c15f61c7-4d55-40de-b428-433cbce7baa9 Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/tempos.php | 70 +++++++++++++++++++++++++++++++++++++ assets/csvsample_tempos.csv | 5 +++ 2 files changed, 75 insertions(+) create mode 100644 assets/csvsample_tempos.csv diff --git a/admin/tempos.php b/admin/tempos.php index bacfa81..4703d95 100644 --- a/admin/tempos.php +++ b/admin/tempos.php @@ -1,6 +1,17 @@

Gestão de Tempos

+
+
Importar Tempos via CSV
+ Download do modelo CSV +

Deve de consultar o manual do administrador para mais informações.

+
+
+ +
+ +
+
Adicionar um tempo Erro ao fazer upload do ficheiro.
"; + break; + } + + $db->set_charset("utf8mb4"); + $fileContent = file_get_contents($_FILES['csvfile']['tmp_name']); + $encoding = mb_detect_encoding($fileContent, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true); + if ($encoding && $encoding !== 'UTF-8') { + $fileContent = mb_convert_encoding($fileContent, 'UTF-8', $encoding); + } + + $tempFile = tmpfile(); + fwrite($tempFile, $fileContent); + rewind($tempFile); + + $successCount = 0; + $errorCount = 0; + $errors = []; + $lineNumber = 0; + + while (($data = fgetcsv($tempFile, 0, ';')) !== FALSE) { + $lineNumber++; + + if (!isset($data[0]) || trim($data[0]) === '') { + continue; + } + + if ($lineNumber == 1 && in_array(strtolower(trim($data[0])), ['horario', 'horashumanos', 'tempo', 'time'])) { + continue; + } + + $horashumanos = trim($data[0]); + + $randomuuid = uuid4(); + $stmt = $db->prepare("INSERT INTO tempos (id, horashumanos) VALUES (?, ?)"); + $stmt->bind_param("ss", $randomuuid, $horashumanos); + if ($stmt->execute()) { + $successCount++; + } else { + $errorCount++; + $errors[] = "Erro ao inserir tempo '" . htmlspecialchars($horashumanos, ENT_QUOTES, 'UTF-8') . "': " . htmlspecialchars($stmt->error, ENT_QUOTES, 'UTF-8'); + } + $stmt->close(); + } + + fclose($tempFile); + + if ($successCount > 0) { + echo ""; + acaoexecutada("Importação de Tempos via CSV"); + } + if ($errorCount > 0) { + echo ""; + } + break; + // caso seja preenchido o formulário de criação: case "criar": if (!isset($_POST['horahumana'])) { diff --git a/assets/csvsample_tempos.csv b/assets/csvsample_tempos.csv new file mode 100644 index 0000000..e1ee964 --- /dev/null +++ b/assets/csvsample_tempos.csv @@ -0,0 +1,5 @@ +Horario +08:00-08:50 +08:55-09:45 +09:50-10:40 +10:55-11:45 From f6b35ffa648d08fe422b48065863f1052eb27749 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 21:50:08 +0000 Subject: [PATCH 03/22] fix: add csrf validation to tempos CSV import form Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/c15f61c7-4d55-40de-b428-433cbce7baa9 Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/tempos.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/admin/tempos.php b/admin/tempos.php index 4703d95..2e704bd 100644 --- a/admin/tempos.php +++ b/admin/tempos.php @@ -1,11 +1,12 @@ - +

Gestão de Tempos

Importar Tempos via CSV
Download do modelo CSV -

Deve de consultar o manual do administrador para mais informações.

+

Deve consultar o manual do administrador para mais informações.

+
@@ -23,6 +24,11 @@ switch (isset($_GET['action']) ? $_GET['action'] : null){ // Import CSV case "import": + if (!isset($_POST['csrf_token']) || !verify_csrf_token($_POST['csrf_token'])) { + echo ""; + break; + } + if (!isset($_FILES['csvfile']) || $_FILES['csvfile']['error'] !== UPLOAD_ERR_OK) { echo ""; break; From 99cf8273c93c26594f7e67ec036b56d0290453c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:00:02 +0000 Subject: [PATCH 04/22] revert: remove tempos CSV import changes Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/3ff397cc-c4ca-405e-846d-9e183bfa8375 Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/tempos.php | 78 +------------------------------------ assets/csvsample_tempos.csv | 5 --- 2 files changed, 1 insertion(+), 82 deletions(-) delete mode 100644 assets/csvsample_tempos.csv diff --git a/admin/tempos.php b/admin/tempos.php index 2e704bd..bacfa81 100644 --- a/admin/tempos.php +++ b/admin/tempos.php @@ -1,18 +1,6 @@ - +

Gestão de Tempos

-
-
Importar Tempos via CSV
- Download do modelo CSV -

Deve consultar o manual do administrador para mais informações.

- - -
- -
- - -
Adicionar um tempo Token CSRF inválido.
"; - break; - } - - if (!isset($_FILES['csvfile']) || $_FILES['csvfile']['error'] !== UPLOAD_ERR_OK) { - echo ""; - break; - } - - $db->set_charset("utf8mb4"); - $fileContent = file_get_contents($_FILES['csvfile']['tmp_name']); - $encoding = mb_detect_encoding($fileContent, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true); - if ($encoding && $encoding !== 'UTF-8') { - $fileContent = mb_convert_encoding($fileContent, 'UTF-8', $encoding); - } - - $tempFile = tmpfile(); - fwrite($tempFile, $fileContent); - rewind($tempFile); - - $successCount = 0; - $errorCount = 0; - $errors = []; - $lineNumber = 0; - - while (($data = fgetcsv($tempFile, 0, ';')) !== FALSE) { - $lineNumber++; - - if (!isset($data[0]) || trim($data[0]) === '') { - continue; - } - - if ($lineNumber == 1 && in_array(strtolower(trim($data[0])), ['horario', 'horashumanos', 'tempo', 'time'])) { - continue; - } - - $horashumanos = trim($data[0]); - - $randomuuid = uuid4(); - $stmt = $db->prepare("INSERT INTO tempos (id, horashumanos) VALUES (?, ?)"); - $stmt->bind_param("ss", $randomuuid, $horashumanos); - if ($stmt->execute()) { - $successCount++; - } else { - $errorCount++; - $errors[] = "Erro ao inserir tempo '" . htmlspecialchars($horashumanos, ENT_QUOTES, 'UTF-8') . "': " . htmlspecialchars($stmt->error, ENT_QUOTES, 'UTF-8'); - } - $stmt->close(); - } - - fclose($tempFile); - - if ($successCount > 0) { - echo ""; - acaoexecutada("Importação de Tempos via CSV"); - } - if ($errorCount > 0) { - echo ""; - } - break; - // caso seja preenchido o formulário de criação: case "criar": if (!isset($_POST['horahumana'])) { diff --git a/assets/csvsample_tempos.csv b/assets/csvsample_tempos.csv deleted file mode 100644 index e1ee964..0000000 --- a/assets/csvsample_tempos.csv +++ /dev/null @@ -1,5 +0,0 @@ -Horario -08:00-08:50 -08:55-09:45 -09:50-10:40 -10:55-11:45 From f93b8fc7cf72cd7be4ba3fcce5d127e8b363b1f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:03:58 +0000 Subject: [PATCH 05/22] feat: add CSV import for reservas em massa Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/4b49d234-7d64-4371-9ed8-1404a6bdde04 Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/reservaemmassa.php | 154 +++++++++++++++++++++++++++++++++- assets/csvsample_reservas.csv | 2 + 2 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 assets/csvsample_reservas.csv diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index f7f9ff4..e7587fb 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -1,6 +1,7 @@

Reserva em Massa

@@ -114,14 +115,163 @@ function validateForm(event) { } document.addEventListener('DOMContentLoaded', function() { - const form = document.querySelector('form'); + const form = document.getElementById('massReservationForm'); if (form) { form.addEventListener('submit', validateForm); } }); -
+Erro: Token CSRF inválido.
"; + } elseif (!isset($_FILES['csvfile']) || $_FILES['csvfile']['error'] !== UPLOAD_ERR_OK) { + echo ""; + } else { + $fileContent = file_get_contents($_FILES['csvfile']['tmp_name']); + $encoding = mb_detect_encoding($fileContent, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true); + if ($encoding && $encoding !== 'UTF-8') { + $fileContent = mb_convert_encoding($fileContent, 'UTF-8', $encoding); + } + + $tempFile = tmpfile(); + fwrite($tempFile, $fileContent); + rewind($tempFile); + + $salasValidas = []; + $resultSalas = $db->query("SELECT id FROM salas"); + while ($row = $resultSalas->fetch_assoc()) { + $salasValidas[$row['id']] = true; + } + + $requisitoresValidos = []; + $resultRequisitores = $db->query("SELECT id FROM cache"); + while ($row = $resultRequisitores->fetch_assoc()) { + $requisitoresValidos[$row['id']] = true; + } + + $temposValidos = []; + $resultTempos = $db->query("SELECT id FROM tempos"); + while ($row = $resultTempos->fetch_assoc()) { + $temposValidos[$row['id']] = true; + } + + $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, ?, ?)"); + + $lineNumber = 0; + $successCount = 0; + $errorCount = 0; + $duplicateCount = 0; + $errors = []; + + while (($data = fgetcsv($tempFile, 0, ';')) !== false) { + $lineNumber++; + + if (!isset($data[0]) || trim($data[0]) === '') { + continue; + } + + $firstColumn = preg_replace('/^\xEF\xBB\xBF/', '', trim($data[0])); + if ($lineNumber === 1 && (strtolower($firstColumn) === 'salaid' || strtolower($firstColumn) === 'sala')) { + continue; + } + + if (count($data) < 5) { + $errorCount++; + $errors[] = "Linha {$lineNumber} inválida (mínimo 5 colunas)."; + continue; + } + + $salaId = $firstColumn; + $requisitorId = trim($data[1]); + $tempoId = trim($data[2]); + $dataReserva = trim($data[3]); + $motivo = trim($data[4]); + $extra = isset($data[5]) ? trim($data[5]) : ''; + + $dateObj = DateTime::createFromFormat('Y-m-d', $dataReserva); + $isValidDate = $dateObj && $dateObj->format('Y-m-d') === $dataReserva; + if (!$isValidDate) { + $errorCount++; + $errors[] = "Linha {$lineNumber}: Data inválida '{$dataReserva}' (formato esperado: YYYY-MM-DD)."; + continue; + } + + if (!isset($salasValidas[$salaId])) { + $errorCount++; + $errors[] = "Linha {$lineNumber}: Sala inválida '{$salaId}'."; + continue; + } + + if (!isset($requisitoresValidos[$requisitorId])) { + $errorCount++; + $errors[] = "Linha {$lineNumber}: Requisitor inválido '{$requisitorId}'."; + continue; + } + + if (!isset($temposValidos[$tempoId])) { + $errorCount++; + $errors[] = "Linha {$lineNumber}: Tempo inválido '{$tempoId}'."; + continue; + } + + if ($motivo === '') { + $motivo = 'Importada via CSV (reserva em massa)'; + } + + $stmtCheck->bind_param("sss", $salaId, $tempoId, $dataReserva); + $stmtCheck->execute(); + $exists = $stmtCheck->get_result()->fetch_assoc(); + if ($exists) { + $duplicateCount++; + continue; + } + + $stmtInsert->bind_param("ssssss", $salaId, $tempoId, $dataReserva, $requisitorId, $motivo, $extra); + if ($stmtInsert->execute()) { + $successCount++; + } else { + $errorCount++; + $errors[] = "Linha {$lineNumber}: Erro ao inserir reserva."; + } + } + + $stmtCheck->close(); + $stmtInsert->close(); + fclose($tempFile); + + if ($successCount > 0) { + echo ""; + acaoexecutada("Importação de Reservas em Massa via CSV"); + } + if ($duplicateCount > 0) { + echo ""; + } + if ($errorCount > 0) { + echo ""; + } + } +} +?> + +
+
Importar Reservas via CSV
+ Download do modelo CSV +

Formato: SalaID;RequisitorID;TempoID;Data(YYYY-MM-DD);Motivo;Extra(opcional)

+ + +
+ +
+ + +
+ +
diff --git a/assets/csvsample_reservas.csv b/assets/csvsample_reservas.csv new file mode 100644 index 0000000..cf7f3c8 --- /dev/null +++ b/assets/csvsample_reservas.csv @@ -0,0 +1,2 @@ +SalaID;RequisitorID;TempoID;Data;Motivo;Extra +00000000-0000-0000-0000-000000000001;00000000-0000-0000-0000-000000000002;00000000-0000-0000-0000-000000000003;2026-09-15;Aula de reposição;Turma 10.ºA From 9b07a1563613a454527ed5cb184930b99af53599 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:05:01 +0000 Subject: [PATCH 06/22] fix: harden reserva CSV import validation flow Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/4b49d234-7d64-4371-9ed8-1404a6bdde04 Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/reservaemmassa.php | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index e7587fb..39a84f9 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -2,6 +2,7 @@ require __DIR__ . '/index.php'; require_once(__DIR__ . '/../func/email_helper.php'); require_once(__DIR__ . '/../func/csrf.php'); +require_once(__DIR__ . '/../func/validation.php'); ?>

Reserva em Massa

@@ -124,38 +125,47 @@ function validateForm(event) { Erro: Token CSRF inválido.
"; } elseif (!isset($_FILES['csvfile']) || $_FILES['csvfile']['error'] !== UPLOAD_ERR_OK) { echo ""; } else { - $fileContent = file_get_contents($_FILES['csvfile']['tmp_name']); - $encoding = mb_detect_encoding($fileContent, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true); - if ($encoding && $encoding !== 'UTF-8') { - $fileContent = mb_convert_encoding($fileContent, 'UTF-8', $encoding); + $tempFile = fopen($_FILES['csvfile']['tmp_name'], 'r'); + if ($tempFile === false) { + echo ""; + $tempFile = null; } + } - $tempFile = tmpfile(); - fwrite($tempFile, $fileContent); - rewind($tempFile); + if ($tempFile) { $salasValidas = []; - $resultSalas = $db->query("SELECT id FROM salas"); + $stmtSalas = $db->prepare("SELECT id FROM salas"); + $stmtSalas->execute(); + $resultSalas = $stmtSalas->get_result(); while ($row = $resultSalas->fetch_assoc()) { $salasValidas[$row['id']] = true; } + $stmtSalas->close(); $requisitoresValidos = []; - $resultRequisitores = $db->query("SELECT id FROM cache"); + $stmtRequisitores = $db->prepare("SELECT id FROM cache"); + $stmtRequisitores->execute(); + $resultRequisitores = $stmtRequisitores->get_result(); while ($row = $resultRequisitores->fetch_assoc()) { $requisitoresValidos[$row['id']] = true; } + $stmtRequisitores->close(); $temposValidos = []; - $resultTempos = $db->query("SELECT id FROM tempos"); + $stmtTempos = $db->prepare("SELECT id FROM tempos"); + $stmtTempos->execute(); + $resultTempos = $stmtTempos->get_result(); while ($row = $resultTempos->fetch_assoc()) { $temposValidos[$row['id']] = true; } + $stmtTempos->close(); $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, ?, ?)"); @@ -191,9 +201,7 @@ function validateForm(event) { $motivo = trim($data[4]); $extra = isset($data[5]) ? trim($data[5]) : ''; - $dateObj = DateTime::createFromFormat('Y-m-d', $dataReserva); - $isValidDate = $dateObj && $dateObj->format('Y-m-d') === $dataReserva; - if (!$isValidDate) { + if (!validate_date($dataReserva)) { $errorCount++; $errors[] = "Linha {$lineNumber}: Data inválida '{$dataReserva}' (formato esperado: YYYY-MM-DD)."; continue; @@ -260,7 +268,7 @@ function validateForm(event) {
Importar Reservas via CSV
- Download do modelo CSV + Download do modelo CSV

Formato: SalaID;RequisitorID;TempoID;Data(YYYY-MM-DD);Motivo;Extra(opcional)

From a332071147f1d3e1ba56848908ef2b9c66b1caed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:05:47 +0000 Subject: [PATCH 07/22] fix: improve CSV import header and error feedback Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/4b49d234-7d64-4371-9ed8-1404a6bdde04 Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/reservaemmassa.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index 39a84f9..922a54b 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -184,7 +184,7 @@ function validateForm(event) { } $firstColumn = preg_replace('/^\xEF\xBB\xBF/', '', trim($data[0])); - if ($lineNumber === 1 && (strtolower($firstColumn) === 'salaid' || strtolower($firstColumn) === 'sala')) { + if ($lineNumber === 1 && strtolower($firstColumn) === 'salaid') { continue; } @@ -258,9 +258,12 @@ function validateForm(event) { echo ""; } if ($errorCount > 0) { - echo ""; + }, $errors), 0, $displayLimit); + $truncatedNote = $errorCount > $displayLimit ? "
A mostrar os primeiros {$displayLimit} de {$errorCount} erro(s)." : ""; + echo ""; } } } From d711f8f4355bd5ce206c9e262cf74cc64e9586ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:20:22 +0000 Subject: [PATCH 08/22] feat: add tabbed modal ID lookup for CSV imports Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/16198339-3d5e-4b7b-b357-4e2ab6d7ecad Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/api/requisitor_lookup.php | 44 +++++++++++ admin/api/sala_lookup.php | 43 ++++++++++ admin/api/tempo_lookup.php | 43 ++++++++++ admin/reservaemmassa.php | 135 ++++++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+) create mode 100644 admin/api/requisitor_lookup.php create mode 100644 admin/api/sala_lookup.php create mode 100644 admin/api/tempo_lookup.php diff --git a/admin/api/requisitor_lookup.php b/admin/api/requisitor_lookup.php new file mode 100644 index 0000000..46bffeb --- /dev/null +++ b/admin/api/requisitor_lookup.php @@ -0,0 +1,44 @@ + 'Acesso negado']); + exit; +} + +$query = isset($_GET['q']) ? sanitize_input($_GET['q'], 100) : ''; +$query = trim($query); + +if (mb_strlen($query) < 2) { + echo json_encode(['items' => [], 'limit' => 10, 'requiresFilter' => true]); + $db->close(); + exit; +} + +$escaped = str_replace(['%', '_'], ['\\%', '\\_'], $query); +$searchParam = '%' . $escaped . '%'; +$limit = 10; + +$stmt = $db->prepare("SELECT id, nome, email FROM cache WHERE id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' OR email LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); +$stmt->bind_param("sssi", $searchParam, $searchParam, $searchParam, $limit); +$stmt->execute(); +$result = $stmt->get_result(); + +$items = []; +while ($row = $result->fetch_assoc()) { + $items[] = [ + 'id' => $row['id'], + 'title' => $row['nome'], + 'subtitle' => $row['email'] + ]; +} + +$stmt->close(); +$db->close(); + +echo json_encode(['items' => $items, 'limit' => $limit, 'requiresFilter' => false]); +?> diff --git a/admin/api/sala_lookup.php b/admin/api/sala_lookup.php new file mode 100644 index 0000000..b2bd5b9 --- /dev/null +++ b/admin/api/sala_lookup.php @@ -0,0 +1,43 @@ + 'Acesso negado']); + exit; +} + +$query = isset($_GET['q']) ? sanitize_input($_GET['q'], 100) : ''; +$query = trim($query); + +if (mb_strlen($query) < 2) { + echo json_encode(['items' => [], 'limit' => 10, 'requiresFilter' => true]); + $db->close(); + exit; +} + +$escaped = str_replace(['%', '_'], ['\\%', '\\_'], $query); +$searchParam = '%' . $escaped . '%'; +$limit = 10; + +$stmt = $db->prepare("SELECT id, nome FROM salas WHERE id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); +$stmt->bind_param("ssi", $searchParam, $searchParam, $limit); +$stmt->execute(); +$result = $stmt->get_result(); + +$items = []; +while ($row = $result->fetch_assoc()) { + $items[] = [ + 'id' => $row['id'], + 'title' => $row['nome'] + ]; +} + +$stmt->close(); +$db->close(); + +echo json_encode(['items' => $items, 'limit' => $limit, 'requiresFilter' => false]); +?> diff --git a/admin/api/tempo_lookup.php b/admin/api/tempo_lookup.php new file mode 100644 index 0000000..ad7560c --- /dev/null +++ b/admin/api/tempo_lookup.php @@ -0,0 +1,43 @@ + 'Acesso negado']); + exit; +} + +$query = isset($_GET['q']) ? sanitize_input($_GET['q'], 100) : ''; +$query = trim($query); + +if (mb_strlen($query) < 2) { + echo json_encode(['items' => [], 'limit' => 10, 'requiresFilter' => true]); + $db->close(); + exit; +} + +$escaped = str_replace(['%', '_'], ['\\%', '\\_'], $query); +$searchParam = '%' . $escaped . '%'; +$limit = 10; + +$stmt = $db->prepare("SELECT id, horashumanos FROM tempos WHERE id LIKE ? ESCAPE '\\\\' OR horashumanos LIKE ? ESCAPE '\\\\' ORDER BY horashumanos ASC LIMIT ?"); +$stmt->bind_param("ssi", $searchParam, $searchParam, $limit); +$stmt->execute(); +$result = $stmt->get_result(); + +$items = []; +while ($row = $result->fetch_assoc()) { + $items[] = [ + 'id' => $row['id'], + 'title' => $row['horashumanos'] + ]; +} + +$stmt->close(); +$db->close(); + +echo json_encode(['items' => $items, 'limit' => $limit, 'requiresFilter' => false]); +?> diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index 922a54b..b018408 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -103,6 +103,94 @@ function clearUserSelection() { document.getElementById('selectedUserDisplay').value = ''; document.getElementById('selectedUserDisplay').placeholder = 'Selecione um utilizador...'; } + + const lookupConfig = { + requisitor: { + endpoint: '/admin/api/requisitor_lookup.php', + inputId: 'lookupRequisitorInput', + resultsId: 'lookupRequisitorResults', + emptyMessage: 'Sem resultados de requisitorID.' + }, + tempo: { + endpoint: '/admin/api/tempo_lookup.php', + inputId: 'lookupTempoInput', + resultsId: 'lookupTempoResults', + emptyMessage: 'Sem resultados de tempoID.' + }, + sala: { + endpoint: '/admin/api/sala_lookup.php', + inputId: 'lookupSalaInput', + resultsId: 'lookupSalaResults', + emptyMessage: 'Sem resultados de salaID.' + } + }; + + function lookupEscapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + function showLookupSkeleton(targetId) { + const target = document.getElementById(targetId); + target.innerHTML = ` +
+
+
+ `; + } + + function searchLookup(type) { + const config = lookupConfig[type]; + if (!config) return; + + const input = document.getElementById(config.inputId); + const target = document.getElementById(config.resultsId); + const query = input.value.trim(); + + if (query.length < 2) { + target.innerHTML = "
Digite pelo menos 2 caracteres para pesquisar (máx. 10 resultados).
"; + return; + } + + showLookupSkeleton(config.resultsId); + + fetch(config.endpoint + '?q=' + encodeURIComponent(query)) + .then(response => response.json()) + .then(data => { + if (!Array.isArray(data.items) || data.items.length === 0) { + target.innerHTML = "
" + config.emptyMessage + "
"; + return; + } + + const itemsHtml = data.items.slice(0, 10).map(item => { + const title = item.title ? `${lookupEscapeHtml(item.title)}
` : ''; + const subtitle = item.subtitle ? `${lookupEscapeHtml(item.subtitle)}
` : ''; + const itemId = lookupEscapeHtml(item.id || ''); + return `
+ ${title} + ${subtitle} + ${itemId} +
`; + }).join(''); + + target.innerHTML = `
A mostrar até 10 resultados.
${itemsHtml}
`; + }) + .catch(() => { + target.innerHTML = "
Erro ao pesquisar. Tente novamente.
"; + }); + } + + function initLookupTabs() { + const tabButtons = document.querySelectorAll('#csvLookupTabs button[data-bs-toggle="tab"]'); + tabButtons.forEach(button => { + button.addEventListener('shown.bs.tab', function (event) { + const targetType = event.target.getAttribute('data-lookup-type'); + if (!targetType) return; + searchLookup(targetType); + }); + }); + } // Form validation function validateForm(event) { @@ -120,6 +208,8 @@ function validateForm(event) { if (form) { form.addEventListener('submit', validateForm); } + initLookupTabs(); + searchLookup('requisitor'); }); @@ -273,6 +363,9 @@ function validateForm(event) {
Importar Reservas via CSV
Download do modelo CSV

Formato: SalaID;RequisitorID;TempoID;Data(YYYY-MM-DD);Motivo;Extra(opcional)

+
@@ -282,6 +375,48 @@ function validateForm(event) {
+ +
From 1c5228ab30bebb1d0cb4bc9952a15df9e95dd9df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:20:59 +0000 Subject: [PATCH 09/22] fix: build lookup API URLs with URLSearchParams Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/16198339-3d5e-4b7b-b357-4e2ab6d7ecad Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/reservaemmassa.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index b018408..9212ade 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -155,7 +155,10 @@ function searchLookup(type) { showLookupSkeleton(config.resultsId); - fetch(config.endpoint + '?q=' + encodeURIComponent(query)) + const url = new URL(config.endpoint, window.location.origin); + url.searchParams.set('q', query); + + fetch(url.toString()) .then(response => response.json()) .then(data => { if (!Array.isArray(data.items) || data.items.length === 0) { From 4cd4c380f30a161e6030d85ab66d26991ae8a9d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:21:38 +0000 Subject: [PATCH 10/22] fix: avoid origin-based URL construction in lookup fetch Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/16198339-3d5e-4b7b-b357-4e2ab6d7ecad Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/reservaemmassa.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index 9212ade..6b893fd 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -155,10 +155,9 @@ function searchLookup(type) { showLookupSkeleton(config.resultsId); - const url = new URL(config.endpoint, window.location.origin); - url.searchParams.set('q', query); - - fetch(url.toString()) + const params = new URLSearchParams(); + params.set('q', query); + fetch(config.endpoint + '?' + params.toString()) .then(response => response.json()) .then(data => { if (!Array.isArray(data.items) || data.items.length === 0) { From 3dfce7d0d6c0f391d496a2c38bfb7727a58f5b47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:22:40 +0000 Subject: [PATCH 11/22] fix: optimize lookup API matching and error logging Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/16198339-3d5e-4b7b-b357-4e2ab6d7ecad Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/api/requisitor_lookup.php | 5 +++-- admin/api/sala_lookup.php | 5 +++-- admin/api/tempo_lookup.php | 5 +++-- admin/reservaemmassa.php | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/admin/api/requisitor_lookup.php b/admin/api/requisitor_lookup.php index 46bffeb..bc74357 100644 --- a/admin/api/requisitor_lookup.php +++ b/admin/api/requisitor_lookup.php @@ -20,11 +20,12 @@ } $escaped = str_replace(['%', '_'], ['\\%', '\\_'], $query); +$idPrefixParam = $escaped . '%'; $searchParam = '%' . $escaped . '%'; $limit = 10; -$stmt = $db->prepare("SELECT id, nome, email FROM cache WHERE id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' OR email LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); -$stmt->bind_param("sssi", $searchParam, $searchParam, $searchParam, $limit); +$stmt = $db->prepare("SELECT id, nome, email FROM cache WHERE id = ? OR id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' OR email LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); +$stmt->bind_param("ssssi", $query, $idPrefixParam, $searchParam, $searchParam, $limit); $stmt->execute(); $result = $stmt->get_result(); diff --git a/admin/api/sala_lookup.php b/admin/api/sala_lookup.php index b2bd5b9..a10a605 100644 --- a/admin/api/sala_lookup.php +++ b/admin/api/sala_lookup.php @@ -20,11 +20,12 @@ } $escaped = str_replace(['%', '_'], ['\\%', '\\_'], $query); +$idPrefixParam = $escaped . '%'; $searchParam = '%' . $escaped . '%'; $limit = 10; -$stmt = $db->prepare("SELECT id, nome FROM salas WHERE id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); -$stmt->bind_param("ssi", $searchParam, $searchParam, $limit); +$stmt = $db->prepare("SELECT id, nome FROM salas WHERE id = ? OR id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); +$stmt->bind_param("sssi", $query, $idPrefixParam, $searchParam, $limit); $stmt->execute(); $result = $stmt->get_result(); diff --git a/admin/api/tempo_lookup.php b/admin/api/tempo_lookup.php index ad7560c..90c3518 100644 --- a/admin/api/tempo_lookup.php +++ b/admin/api/tempo_lookup.php @@ -20,11 +20,12 @@ } $escaped = str_replace(['%', '_'], ['\\%', '\\_'], $query); +$idPrefixParam = $escaped . '%'; $searchParam = '%' . $escaped . '%'; $limit = 10; -$stmt = $db->prepare("SELECT id, horashumanos FROM tempos WHERE id LIKE ? ESCAPE '\\\\' OR horashumanos LIKE ? ESCAPE '\\\\' ORDER BY horashumanos ASC LIMIT ?"); -$stmt->bind_param("ssi", $searchParam, $searchParam, $limit); +$stmt = $db->prepare("SELECT id, horashumanos FROM tempos WHERE id = ? OR id LIKE ? ESCAPE '\\\\' OR horashumanos LIKE ? ESCAPE '\\\\' ORDER BY horashumanos ASC LIMIT ?"); +$stmt->bind_param("sssi", $query, $idPrefixParam, $searchParam, $limit); $stmt->execute(); $result = $stmt->get_result(); diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index 6b893fd..677e6aa 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -178,7 +178,8 @@ function searchLookup(type) { target.innerHTML = `
A mostrar até 10 resultados.
${itemsHtml}
`; }) - .catch(() => { + .catch((err) => { + console.error(err); target.innerHTML = "
Erro ao pesquisar. Tente novamente.
"; }); } From d741a96d5d90171bbf9122a48180af7600f48f67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:23:19 +0000 Subject: [PATCH 12/22] refactor: rename lookup html escape helper Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/16198339-3d5e-4b7b-b357-4e2ab6d7ecad Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/reservaemmassa.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index 677e6aa..ca9c4c5 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -125,7 +125,7 @@ function clearUserSelection() { } }; - function lookupEscapeHtml(text) { + function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; @@ -166,9 +166,9 @@ function searchLookup(type) { } const itemsHtml = data.items.slice(0, 10).map(item => { - const title = item.title ? `${lookupEscapeHtml(item.title)}
` : ''; - const subtitle = item.subtitle ? `${lookupEscapeHtml(item.subtitle)}
` : ''; - const itemId = lookupEscapeHtml(item.id || ''); + const title = item.title ? `${escapeHtml(item.title)}
` : ''; + const subtitle = item.subtitle ? `${escapeHtml(item.subtitle)}
` : ''; + const itemId = escapeHtml(item.id || ''); return `
${title} ${subtitle} From 2c8886e0d693d08093ca71152624cb71933398dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:24:40 +0000 Subject: [PATCH 13/22] fix: harden lookup fetch handling and simplify API filters Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/16198339-3d5e-4b7b-b357-4e2ab6d7ecad Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/api/requisitor_lookup.php | 4 ++-- admin/api/sala_lookup.php | 4 ++-- admin/api/tempo_lookup.php | 4 ++-- admin/reservaemmassa.php | 16 ++++++++++++---- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/admin/api/requisitor_lookup.php b/admin/api/requisitor_lookup.php index bc74357..c529a25 100644 --- a/admin/api/requisitor_lookup.php +++ b/admin/api/requisitor_lookup.php @@ -24,8 +24,8 @@ $searchParam = '%' . $escaped . '%'; $limit = 10; -$stmt = $db->prepare("SELECT id, nome, email FROM cache WHERE id = ? OR id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' OR email LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); -$stmt->bind_param("ssssi", $query, $idPrefixParam, $searchParam, $searchParam, $limit); +$stmt = $db->prepare("SELECT id, nome, email FROM cache WHERE id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' OR email LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); +$stmt->bind_param("sssi", $idPrefixParam, $searchParam, $searchParam, $limit); $stmt->execute(); $result = $stmt->get_result(); diff --git a/admin/api/sala_lookup.php b/admin/api/sala_lookup.php index a10a605..c848ea5 100644 --- a/admin/api/sala_lookup.php +++ b/admin/api/sala_lookup.php @@ -24,8 +24,8 @@ $searchParam = '%' . $escaped . '%'; $limit = 10; -$stmt = $db->prepare("SELECT id, nome FROM salas WHERE id = ? OR id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); -$stmt->bind_param("sssi", $query, $idPrefixParam, $searchParam, $limit); +$stmt = $db->prepare("SELECT id, nome FROM salas WHERE id LIKE ? ESCAPE '\\\\' OR nome LIKE ? ESCAPE '\\\\' ORDER BY nome ASC LIMIT ?"); +$stmt->bind_param("ssi", $idPrefixParam, $searchParam, $limit); $stmt->execute(); $result = $stmt->get_result(); diff --git a/admin/api/tempo_lookup.php b/admin/api/tempo_lookup.php index 90c3518..57063a5 100644 --- a/admin/api/tempo_lookup.php +++ b/admin/api/tempo_lookup.php @@ -24,8 +24,8 @@ $searchParam = '%' . $escaped . '%'; $limit = 10; -$stmt = $db->prepare("SELECT id, horashumanos FROM tempos WHERE id = ? OR id LIKE ? ESCAPE '\\\\' OR horashumanos LIKE ? ESCAPE '\\\\' ORDER BY horashumanos ASC LIMIT ?"); -$stmt->bind_param("sssi", $query, $idPrefixParam, $searchParam, $limit); +$stmt = $db->prepare("SELECT id, horashumanos FROM tempos WHERE id LIKE ? ESCAPE '\\\\' OR horashumanos LIKE ? ESCAPE '\\\\' ORDER BY horashumanos ASC LIMIT ?"); +$stmt->bind_param("ssi", $idPrefixParam, $searchParam, $limit); $stmt->execute(); $result = $stmt->get_result(); diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index ca9c4c5..ebf264f 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -126,9 +126,12 @@ function clearUserSelection() { }; function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); } function showLookupSkeleton(targetId) { @@ -158,7 +161,12 @@ function searchLookup(type) { const params = new URLSearchParams(); params.set('q', query); fetch(config.endpoint + '?' + params.toString()) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error('HTTP ' + response.status); + } + return response.json(); + }) .then(data => { if (!Array.isArray(data.items) || data.items.length === 0) { target.innerHTML = "
" + config.emptyMessage + "
"; From 5c61670a5cb7b036479f816417e057d9387d28fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:25:23 +0000 Subject: [PATCH 14/22] fix: use relative lookup API endpoints in admin modal Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/16198339-3d5e-4b7b-b357-4e2ab6d7ecad Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/reservaemmassa.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index ebf264f..ba196df 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -106,19 +106,19 @@ function clearUserSelection() { const lookupConfig = { requisitor: { - endpoint: '/admin/api/requisitor_lookup.php', + endpoint: 'api/requisitor_lookup.php', inputId: 'lookupRequisitorInput', resultsId: 'lookupRequisitorResults', emptyMessage: 'Sem resultados de requisitorID.' }, tempo: { - endpoint: '/admin/api/tempo_lookup.php', + endpoint: 'api/tempo_lookup.php', inputId: 'lookupTempoInput', resultsId: 'lookupTempoResults', emptyMessage: 'Sem resultados de tempoID.' }, sala: { - endpoint: '/admin/api/sala_lookup.php', + endpoint: 'api/sala_lookup.php', inputId: 'lookupSalaInput', resultsId: 'lookupSalaResults', emptyMessage: 'Sem resultados de salaID.' From 45815ec33fbbc4ebed3b747613fdc4b0ab6e8816 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:26:07 +0000 Subject: [PATCH 15/22] perf: defer modal lookup fetch until modal is opened Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/16198339-3d5e-4b7b-b357-4e2ab6d7ecad Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/reservaemmassa.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index ba196df..d15b628 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -173,7 +173,7 @@ function searchLookup(type) { return; } - const itemsHtml = data.items.slice(0, 10).map(item => { + const itemsHtml = data.items.map(item => { const title = item.title ? `${escapeHtml(item.title)}
` : ''; const subtitle = item.subtitle ? `${escapeHtml(item.subtitle)}
` : ''; const itemId = escapeHtml(item.id || ''); @@ -201,6 +201,17 @@ function initLookupTabs() { searchLookup(targetType); }); }); + + const lookupModal = document.getElementById('csvLookupModal'); + let lookupModalInitialized = false; + if (lookupModal) { + lookupModal.addEventListener('shown.bs.modal', function () { + if (!lookupModalInitialized) { + searchLookup('requisitor'); + lookupModalInitialized = true; + } + }); + } } // Form validation @@ -220,7 +231,6 @@ function validateForm(event) { form.addEventListener('submit', validateForm); } initLookupTabs(); - searchLookup('requisitor'); }); From efbe4c5a91433bce213d040de1f3e699c689e78f Mon Sep 17 00:00:00 2001 From: Marco Pisco Date: Thu, 21 May 2026 22:32:02 +0000 Subject: [PATCH 16/22] fix: remover texto de (limitado a 10 para reduzir carga, texto gerado por IA) --- admin/reservaemmassa.php | 1 - 1 file changed, 1 deletion(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index d15b628..e510ccb 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -431,7 +431,6 @@ function validateForm(event) {
From 9035d51b8c868e001f26ad3b701c0148da400b20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 22:45:56 +0000 Subject: [PATCH 17/22] fix: accept 4-column rows in reserva CSV import Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/7ee34890-325a-4694-af70-b9c8c4869a66 Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/reservaemmassa.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index e510ccb..9bdb84a 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -299,9 +299,9 @@ function validateForm(event) { continue; } - if (count($data) < 5) { + if (count($data) < 4) { $errorCount++; - $errors[] = "Linha {$lineNumber} inválida (mínimo 5 colunas)."; + $errors[] = "Linha {$lineNumber} inválida (mínimo 4 colunas)."; continue; } @@ -309,7 +309,7 @@ function validateForm(event) { $requisitorId = trim($data[1]); $tempoId = trim($data[2]); $dataReserva = trim($data[3]); - $motivo = trim($data[4]); + $motivo = isset($data[4]) ? trim($data[4]) : ''; $extra = isset($data[5]) ? trim($data[5]) : ''; if (!validate_date($dataReserva)) { From 7b54cbd6ba92b817cbc7bddb1c0f7e95c71b444a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 23:02:49 +0000 Subject: [PATCH 18/22] Changes before error encountered Agent-Logs-Url: https://github.com/marpisco/ClassLink/sessions/41bc349a-24b5-4d23-a36a-79d2398775f9 Co-authored-by: marpisco <162377105+marpisco@users.noreply.github.com> --- admin/materiais.php | 4 ++-- admin/reservaemmassa.php | 4 ++-- assets/csvsample.csv | 6 ------ assets/csvsample_materiais.csv | 6 ++++++ assets/csvsample_reservas.csv | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 assets/csvsample.csv create mode 100644 assets/csvsample_materiais.csv diff --git a/admin/materiais.php b/admin/materiais.php index 8a2bb9c..7ffe1b7 100644 --- a/admin/materiais.php +++ b/admin/materiais.php @@ -4,7 +4,7 @@
Importar Materiais via CSV
- Download do modelo CSV + Download do modelo CSV

Nota: Para obter o RoomID de uma sala, consulte a gestão de salas ou use a listagem abaixo.

Deve de consultar o manual do administrador para mais informações.

@@ -53,7 +53,7 @@ $errors = []; $lineNumber = 0; - while (($data = fgetcsv($tempFile, 0, ';')) !== FALSE) { // Changed: Added ';' as delimiter + while (($data = fgetcsv($tempFile, 0, ',')) !== FALSE) { $lineNumber++; // Skip empty lines diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index 9bdb84a..760a647 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -287,7 +287,7 @@ function validateForm(event) { $duplicateCount = 0; $errors = []; - while (($data = fgetcsv($tempFile, 0, ';')) !== false) { + while (($data = fgetcsv($tempFile, 0, ',')) !== false) { $lineNumber++; if (!isset($data[0]) || trim($data[0]) === '') { @@ -383,7 +383,7 @@ function validateForm(event) {
Importar Reservas via CSV
Download do modelo CSV -

Formato: SalaID;RequisitorID;TempoID;Data(YYYY-MM-DD);Motivo;Extra(opcional)

+

Formato: SalaID,RequisitorID,TempoID,Data(YYYY-MM-DD),Motivo,Extra(opcional)

diff --git a/assets/csvsample.csv b/assets/csvsample.csv deleted file mode 100644 index bfa4eb9..0000000 --- a/assets/csvsample.csv +++ /dev/null @@ -1,6 +0,0 @@ -MaterialName;MaterialDescription;RoomID -Projetor HD;Projetor Full HD 1080p com cabo HDMI;example-sala-uuid-123 -Computador Portátil;Dell Latitude 15" com Windows 11;example-sala-uuid-123 -Quadro Interativo;Smart Board 75" com canetas digitais;example-sala-uuid-456 -Colunas de Som;Sistema de som estéreo 100W;example-sala-uuid-123 -Webcam HD;Logitech C920 Full HD 1080p;example-sala-uuid-456 \ No newline at end of file diff --git a/assets/csvsample_materiais.csv b/assets/csvsample_materiais.csv new file mode 100644 index 0000000..935634f --- /dev/null +++ b/assets/csvsample_materiais.csv @@ -0,0 +1,6 @@ +MaterialName,MaterialDescription,RoomID +Projetor HD,Projetor Full HD 1080p com cabo HDMI,example-sala-uuid-123 +Computador Portátil,Dell Latitude 15" com Windows 11,example-sala-uuid-123 +Quadro Interativo,Smart Board 75" com canetas digitais,example-sala-uuid-456 +Colunas de Som,Sistema de som estéreo 100W,example-sala-uuid-123 +Webcam HD,Logitech C920 Full HD 1080p,example-sala-uuid-456 \ No newline at end of file diff --git a/assets/csvsample_reservas.csv b/assets/csvsample_reservas.csv index cf7f3c8..01e1779 100644 --- a/assets/csvsample_reservas.csv +++ b/assets/csvsample_reservas.csv @@ -1,2 +1,2 @@ -SalaID;RequisitorID;TempoID;Data;Motivo;Extra -00000000-0000-0000-0000-000000000001;00000000-0000-0000-0000-000000000002;00000000-0000-0000-0000-000000000003;2026-09-15;Aula de reposição;Turma 10.ºA +SalaID,RequisitorID,TempoID,Data,Motivo,Extra +00000000-0000-0000-0000-000000000001,00000000-0000-0000-0000-000000000002,00000000-0000-0000-0000-000000000003,2026-09-15,Aula de reposição,Turma 10.ºA From 74f95ccee4a75d11002ef33427d2cdc45c4a92a1 Mon Sep 17 00:00:00 2001 From: Marco Pisco Date: Fri, 22 May 2026 08:31:52 +0000 Subject: [PATCH 19/22] perf: replace full-table loads with point-query validation in CSV import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substitui a pré-carga integral das tabelas salas, cache e tempos por consultas SELECT 1 ... WHERE id = ? LIMIT 1 com cache por ID durante o parsing do CSV. Evita carregar tabelas completas para memória. --- admin/reservaemmassa.php | 62 +++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index 760a647..96076e0 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -251,32 +251,33 @@ function validateForm(event) { if ($tempFile) { - $salasValidas = []; - $stmtSalas = $db->prepare("SELECT id FROM salas"); - $stmtSalas->execute(); - $resultSalas = $stmtSalas->get_result(); - while ($row = $resultSalas->fetch_assoc()) { - $salasValidas[$row['id']] = true; - } - $stmtSalas->close(); - - $requisitoresValidos = []; - $stmtRequisitores = $db->prepare("SELECT id FROM cache"); - $stmtRequisitores->execute(); - $resultRequisitores = $stmtRequisitores->get_result(); - while ($row = $resultRequisitores->fetch_assoc()) { - $requisitoresValidos[$row['id']] = true; - } - $stmtRequisitores->close(); - - $temposValidos = []; - $stmtTempos = $db->prepare("SELECT id FROM tempos"); - $stmtTempos->execute(); - $resultTempos = $stmtTempos->get_result(); - while ($row = $resultTempos->fetch_assoc()) { - $temposValidos[$row['id']] = true; - } - $stmtTempos->close(); + $stmtSalaExists = $db->prepare("SELECT 1 FROM salas WHERE id = ? LIMIT 1"); + $stmtRequisitorExists = $db->prepare("SELECT 1 FROM cache WHERE id = ? LIMIT 1"); + $stmtTempoExists = $db->prepare("SELECT 1 FROM tempos WHERE id = ? LIMIT 1"); + + $salaValidationCache = []; + $requisitorValidationCache = []; + $tempoValidationCache = []; + + $validateExists = function (mysqli_stmt $stmt, string $id, array &$cache): bool { + if ($id === '') { + return false; + } + + if (array_key_exists($id, $cache)) { + return $cache[$id]; + } + + $lookupId = $id; + $stmt->bind_param("s", $lookupId); + $stmt->execute(); + $stmt->store_result(); + $exists = $stmt->num_rows > 0; + $stmt->free_result(); + + $cache[$id] = $exists; + return $exists; + }; $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, ?, ?)"); @@ -318,19 +319,19 @@ function validateForm(event) { continue; } - if (!isset($salasValidas[$salaId])) { + if (!$validateExists($stmtSalaExists, $salaId, $salaValidationCache)) { $errorCount++; $errors[] = "Linha {$lineNumber}: Sala inválida '{$salaId}'."; continue; } - if (!isset($requisitoresValidos[$requisitorId])) { + if (!$validateExists($stmtRequisitorExists, $requisitorId, $requisitorValidationCache)) { $errorCount++; $errors[] = "Linha {$lineNumber}: Requisitor inválido '{$requisitorId}'."; continue; } - if (!isset($temposValidos[$tempoId])) { + if (!$validateExists($stmtTempoExists, $tempoId, $tempoValidationCache)) { $errorCount++; $errors[] = "Linha {$lineNumber}: Tempo inválido '{$tempoId}'."; continue; @@ -357,6 +358,9 @@ function validateForm(event) { } } + $stmtSalaExists->close(); + $stmtRequisitorExists->close(); + $stmtTempoExists->close(); $stmtCheck->close(); $stmtInsert->close(); fclose($tempFile); From c10a15a603c8928879483b5b57af8e5d7563ace4 Mon Sep 17 00:00:00 2001 From: Marco Pisco Date: Fri, 22 May 2026 08:40:31 +0000 Subject: [PATCH 20/22] perf: cap error message collection to display limit in CSV import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evita acumular todas as mensagens de erro em memória quando o CSV contém muitas linhas com erros. Apenas guarda até $maxDisplayedErrors (10) mensagens, que é o limite efetivo de exibição. --- admin/reservaemmassa.php | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index 96076e0..d3befd6 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -287,6 +287,13 @@ function validateForm(event) { $errorCount = 0; $duplicateCount = 0; $errors = []; + $maxDisplayedErrors = 10; + + $recordError = function(string $message) use (&$errors, $maxDisplayedErrors): void { + if (count($errors) < $maxDisplayedErrors) { + $errors[] = $message; + } + }; while (($data = fgetcsv($tempFile, 0, ',')) !== false) { $lineNumber++; @@ -302,7 +309,7 @@ function validateForm(event) { if (count($data) < 4) { $errorCount++; - $errors[] = "Linha {$lineNumber} inválida (mínimo 4 colunas)."; + $recordError("Linha {$lineNumber} inválida (mínimo 4 colunas)."); continue; } @@ -315,25 +322,25 @@ function validateForm(event) { if (!validate_date($dataReserva)) { $errorCount++; - $errors[] = "Linha {$lineNumber}: Data inválida '{$dataReserva}' (formato esperado: YYYY-MM-DD)."; + $recordError("Linha {$lineNumber}: Data inválida '{$dataReserva}' (formato esperado: YYYY-MM-DD)."); continue; } if (!$validateExists($stmtSalaExists, $salaId, $salaValidationCache)) { $errorCount++; - $errors[] = "Linha {$lineNumber}: Sala inválida '{$salaId}'."; + $recordError("Linha {$lineNumber}: Sala inválida '{$salaId}'."); continue; } if (!$validateExists($stmtRequisitorExists, $requisitorId, $requisitorValidationCache)) { $errorCount++; - $errors[] = "Linha {$lineNumber}: Requisitor inválido '{$requisitorId}'."; + $recordError("Linha {$lineNumber}: Requisitor inválido '{$requisitorId}'."); continue; } if (!$validateExists($stmtTempoExists, $tempoId, $tempoValidationCache)) { $errorCount++; - $errors[] = "Linha {$lineNumber}: Tempo inválido '{$tempoId}'."; + $recordError("Linha {$lineNumber}: Tempo inválido '{$tempoId}'."); continue; } @@ -354,7 +361,7 @@ function validateForm(event) { $successCount++; } else { $errorCount++; - $errors[] = "Linha {$lineNumber}: Erro ao inserir reserva."; + $recordError("Linha {$lineNumber}: Erro ao inserir reserva."); } } @@ -373,11 +380,10 @@ function validateForm(event) { echo ""; } if ($errorCount > 0) { - $displayLimit = 10; - $displayedErrors = array_slice(array_map(function($error) { + $displayedErrors = array_map(function($error) { return htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); - }, $errors), 0, $displayLimit); - $truncatedNote = $errorCount > $displayLimit ? "
A mostrar os primeiros {$displayLimit} de {$errorCount} erro(s)." : ""; + }, $errors); + $truncatedNote = $errorCount > $maxDisplayedErrors ? "
A mostrar os primeiros {$maxDisplayedErrors} de {$errorCount} erro(s)." : ""; echo ""; } } From 21f116d3f10e1091d9a4e683eec4bd8665442644 Mon Sep 17 00:00:00 2001 From: Marco Pisco Date: Fri, 22 May 2026 09:06:40 +0000 Subject: [PATCH 21/22] fix: add file size limit and encoding detection to CSV import Adds 2MB max file size validation to prevent resource exhaustion from large CSV uploads. Adds mb_detect_encoding + mb_convert_encoding for UTF-8 conversion (consistent with materiais.php pattern) to handle non-UTF-8 CSVs (e.g. Windows-1252 from Excel). Uses tmpfile() instead of direct fopen() on the upload temp path to process converted content. --- admin/reservaemmassa.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/admin/reservaemmassa.php b/admin/reservaemmassa.php index d3befd6..71efdeb 100644 --- a/admin/reservaemmassa.php +++ b/admin/reservaemmassa.php @@ -237,12 +237,26 @@ function validateForm(event) { Erro: Token CSRF inválido.
"; } elseif (!isset($_FILES['csvfile']) || $_FILES['csvfile']['error'] !== UPLOAD_ERR_OK) { echo ""; + } elseif ($_FILES['csvfile']['size'] > $maxFileSize) { + echo ""; } else { - $tempFile = fopen($_FILES['csvfile']['tmp_name'], 'r'); + // Detect encoding and convert to UTF-8 if needed (consistent with materiais.php CSV import) + $fileContent = file_get_contents($_FILES['csvfile']['tmp_name']); + $encoding = mb_detect_encoding($fileContent, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true); + if ($encoding && $encoding !== 'UTF-8') { + $fileContent = mb_convert_encoding($fileContent, 'UTF-8', $encoding); + } + + $tempFile = tmpfile(); + fwrite($tempFile, $fileContent); + rewind($tempFile); + unset($fileContent); + if ($tempFile === false) { echo ""; $tempFile = null; From 25452cb1a313130f1ecf4bfb7a8715b5b2b41803 Mon Sep 17 00:00:00 2001 From: Marco Pisco Date: Fri, 22 May 2026 09:25:25 +0000 Subject: [PATCH 22/22] fix: add CSRF protection and file size limit to materiais CSV import The materiais.php CSV import lacked CSRF token verification (unlike the new reservaemmassa.php import) and had no file size limit. Both issues could allow abuse: CSRF bypass for automated submissions and resource exhaustion via large file uploads. Now consistent with reservaemmassa.php security pattern. --- admin/materiais.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/admin/materiais.php b/admin/materiais.php index 7ffe1b7..92ebca0 100644 --- a/admin/materiais.php +++ b/admin/materiais.php @@ -1,4 +1,6 @@ - +

Gestão de Materiais

@@ -8,6 +10,7 @@

Nota: Para obter o RoomID de uma sala, consulte a gestão de salas ou use a listagem abaixo.

Deve de consultar o manual do administrador para mais informações.

+
@@ -26,9 +29,16 @@ switch (isset($_GET['action']) ? $_GET['action'] : null){ // Import CSV case "import": - if (!isset($_FILES['csvfile']) || $_FILES['csvfile']['error'] !== UPLOAD_ERR_OK) { + $maxFileSize = 2 * 1024 * 1024; // 2 MB + if (!isset($_POST['csrf_token']) || !verify_csrf_token($_POST['csrf_token'])) { + echo ""; + break; + } elseif (!isset($_FILES['csvfile']) || $_FILES['csvfile']['error'] !== UPLOAD_ERR_OK) { echo ""; break; + } elseif ($_FILES['csvfile']['size'] > $maxFileSize) { + echo ""; + break; } // Set database charset to utf8mb4