Improve security
This commit is contained in:
parent
0fde080ab8
commit
853f88c66b
3 changed files with 275 additions and 228 deletions
|
|
@ -6,56 +6,69 @@ const getApiBase = () => {
|
|||
const hostname = window.location.hostname;
|
||||
|
||||
// If running on localhost, assume backend is on port 3001
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
||||
return `${protocol}//${hostname}:3001`;
|
||||
}
|
||||
|
||||
// For production, assume backend is on same origin
|
||||
return "https://fizika-backend.schmelczer.dev"
|
||||
return "https://fizika-backend.schmelczer.dev";
|
||||
};
|
||||
|
||||
const API_BASE = getApiBase();
|
||||
|
||||
// Fetch the question bank from the backend, which is the single source of
|
||||
// truth. Cached after the first successful load.
|
||||
const ensureQuestionsLoaded = async () => {
|
||||
if (questions !== null) return questions;
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/fizika`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
questions = await response.json();
|
||||
return questions;
|
||||
};
|
||||
|
||||
const escapeHtml = (value) =>
|
||||
String(value).replace(
|
||||
/[&<>"']/g,
|
||||
(ch) =>
|
||||
({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
})[ch],
|
||||
);
|
||||
|
||||
// Question text is trusted but contains light markup (<sub>, <sup>, <br>) and
|
||||
// pre-encoded entities. Escape everything first, then restore only that
|
||||
// allowlist, so any other injected markup (e.g. <script>) stays inert.
|
||||
const sanitizeQuestionHtml = (value) =>
|
||||
escapeHtml(value)
|
||||
.replace(/<(\/?)(sub|sup)>/g, "<$1$2>")
|
||||
.replace(/<br\s*\/?>/g, "<br>")
|
||||
.replace(/&(lt|gt|amp|quot|#\d+|#x[0-9a-fA-F]+);/g, "&$1;");
|
||||
|
||||
const loadQuestions = async (
|
||||
isSearch,
|
||||
categories,
|
||||
sourceScheme,
|
||||
questionCount
|
||||
questionCount,
|
||||
) => {
|
||||
if (questions === null) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/fizika`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
questions = await response.json();
|
||||
console.log('Questions loaded from backend API');
|
||||
} catch (error) {
|
||||
console.warn('Failed to load questions from API, falling back to local file:', error);
|
||||
try {
|
||||
const fallbackResponse = await fetch("fizika.json");
|
||||
if (!fallbackResponse.ok) {
|
||||
throw new Error(`Local file not available: ${fallbackResponse.status}`);
|
||||
}
|
||||
questions = await fallbackResponse.json();
|
||||
console.log('Questions loaded from local fallback file');
|
||||
} catch (fallbackError) {
|
||||
console.error('Both API and local file failed:', fallbackError);
|
||||
throw new Error('Unable to load quiz data from either backend API or local file');
|
||||
}
|
||||
}
|
||||
}
|
||||
await ensureQuestionsLoaded();
|
||||
|
||||
let currentQuestions = questions.slice();
|
||||
|
||||
if (isSearch) {
|
||||
currentQuestions = currentQuestions.filter((q) =>
|
||||
q.source.match(sourceScheme)
|
||||
q.source.match(sourceScheme),
|
||||
);
|
||||
} else {
|
||||
shuffleArray(currentQuestions);
|
||||
currentQuestions = currentQuestions.filter((q) =>
|
||||
categories.includes(q.type)
|
||||
categories.includes(q.type),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -64,45 +77,49 @@ const loadQuestions = async (
|
|||
currentQuestions = currentQuestions.slice(0, questionCount);
|
||||
currentQuestions.forEach(
|
||||
({ id, source, description, a, b, c, d, correct, image }, i) => {
|
||||
const qid = Number(id);
|
||||
const correctAnswer = Number(correct);
|
||||
const safeImage = image ? encodeURIComponent(image) : "";
|
||||
resultHtml += `
|
||||
<div class="feladat card" id="feladat${id}">
|
||||
<h2 style="float: left;">${i + 1}.</h2><h2>${source}</h2>
|
||||
<pre>${description}</pre>
|
||||
${image ? `<img src="${API_BASE}/api/pics/${image}" crossorigin="anonymous" onerror="this.src='pics/${image}'"><br>` : ""}
|
||||
<form id="form${id}"">
|
||||
<div class="feladat card" id="feladat${qid}">
|
||||
<h2 style="float: left;">${i + 1}.</h2><h2>${sanitizeQuestionHtml(source)}</h2>
|
||||
<pre>${sanitizeQuestionHtml(description)}</pre>
|
||||
${image ? `<img src="${API_BASE}/api/pics/${safeImage}" crossorigin="anonymous" onerror="this.src='pics/${safeImage}'"><br>` : ""}
|
||||
<form id="form${qid}">
|
||||
<input type="radio" id="rad1" name="group">
|
||||
<label id="label${id}" class="rad1">${a}</label>
|
||||
<label id="label${qid}" class="rad1">${sanitizeQuestionHtml(a)}</label>
|
||||
<br>
|
||||
<input type="radio" id="rad2" name="group">
|
||||
<label id="label${id}" class="rad2">${b}</label>
|
||||
<label id="label${qid}" class="rad2">${sanitizeQuestionHtml(b)}</label>
|
||||
<br>
|
||||
<input type="radio" id="rad3" name="group">
|
||||
<label id="label${id}" class="rad3">${c}</label>
|
||||
<label id="label${qid}" class="rad3">${sanitizeQuestionHtml(c)}</label>
|
||||
<br>
|
||||
${d
|
||||
? `
|
||||
${
|
||||
d
|
||||
? `
|
||||
<input type="radio" id="rad4" name="group">
|
||||
<label id="label${id}" class="rad4">${d}</label>
|
||||
<label id="label${qid}" class="rad4">${sanitizeQuestionHtml(d)}</label>
|
||||
<br>`
|
||||
: ""
|
||||
}
|
||||
: ""
|
||||
}
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
totalPoints++;
|
||||
$("#ans").click(function(event){
|
||||
event.preventDefault();
|
||||
teszt(${id}, ${correct});
|
||||
teszt(${qid}, ${correctAnswer});
|
||||
});
|
||||
$("#cAns").click(function(event){
|
||||
event.preventDefault();
|
||||
showCorrect(${id}, ${correct});
|
||||
showCorrect(${qid}, ${correctAnswer});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
resultHtml +=
|
||||
|
|
@ -126,34 +143,15 @@ function shuffleArray(array) {
|
|||
// Initialize year dropdown with dynamic years from question data
|
||||
const initializeYearDropdown = async () => {
|
||||
try {
|
||||
// Load questions if not already loaded
|
||||
if (questions === null) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/fizika`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
questions = await response.json();
|
||||
console.log('Questions loaded for year dropdown initialization');
|
||||
} catch (error) {
|
||||
console.warn('Failed to load questions from API, falling back to local file:', error);
|
||||
try {
|
||||
const fallbackResponse = await fetch("fizika.json");
|
||||
if (!fallbackResponse.ok) {
|
||||
throw new Error(`Local file not available: ${fallbackResponse.status}`);
|
||||
}
|
||||
questions = await fallbackResponse.json();
|
||||
console.log('Questions loaded from local fallback file for year dropdown');
|
||||
} catch (fallbackError) {
|
||||
console.error('Both API and local file failed:', fallbackError);
|
||||
return; // Don't update dropdown if data unavailable
|
||||
}
|
||||
}
|
||||
}
|
||||
await ensureQuestionsLoaded();
|
||||
|
||||
// Reflect the live question count on the info page.
|
||||
const countEl = document.getElementById("questionCount");
|
||||
if (countEl) countEl.textContent = questions.length;
|
||||
|
||||
// Extract unique years from question sources
|
||||
const yearSet = new Set();
|
||||
questions.forEach(q => {
|
||||
questions.forEach((q) => {
|
||||
const yearMatch = q.source.match(/^(\d{4})\//);
|
||||
if (yearMatch) {
|
||||
yearSet.add(parseInt(yearMatch[1]));
|
||||
|
|
@ -164,21 +162,18 @@ const initializeYearDropdown = async () => {
|
|||
const uniqueYears = Array.from(yearSet).sort((a, b) => b - a);
|
||||
|
||||
// Get existing dropdown
|
||||
const yearDropdown = document.getElementById('evszam');
|
||||
const yearDropdown = document.getElementById("evszam");
|
||||
if (!yearDropdown) return;
|
||||
|
||||
// Preserve the "Összes év" option and add dynamic years
|
||||
const allYearsOption = '<option value="all/">Összes év</option>';
|
||||
const yearOptions = uniqueYears.map(year =>
|
||||
`<option value="${year}/">${year}</option>`
|
||||
).join('');
|
||||
const yearOptions = uniqueYears
|
||||
.map((year) => `<option value="${year}/">${year}</option>`)
|
||||
.join("");
|
||||
|
||||
yearDropdown.innerHTML = allYearsOption + yearOptions;
|
||||
|
||||
console.log('Year dropdown initialized with years:', uniqueYears);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize year dropdown:', error);
|
||||
console.error("Failed to initialize year dropdown:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -186,16 +181,16 @@ const initializeYearDropdown = async () => {
|
|||
const initializeMonthDropdown = (selectedYear) => {
|
||||
if (!questions) return;
|
||||
|
||||
const monthDropdown = document.getElementById('honap');
|
||||
const monthDropdown = document.getElementById("honap");
|
||||
if (!monthDropdown) return;
|
||||
|
||||
// Always include "Összes feladatsora" option
|
||||
let monthOptions = '<option value="all">Összes feladatsora</option>';
|
||||
|
||||
if (selectedYear === 'all/') {
|
||||
if (selectedYear === "all/") {
|
||||
// Show all possible month patterns
|
||||
const monthSet = new Set();
|
||||
questions.forEach(q => {
|
||||
questions.forEach((q) => {
|
||||
const sourceMatch = q.source.match(/^(\d{4})\/(.+?)\//);
|
||||
if (sourceMatch) {
|
||||
monthSet.add(sourceMatch[2]);
|
||||
|
|
@ -204,17 +199,16 @@ const initializeMonthDropdown = (selectedYear) => {
|
|||
|
||||
// Convert to sorted array and create options
|
||||
const uniqueMonths = Array.from(monthSet).sort();
|
||||
uniqueMonths.forEach(month => {
|
||||
uniqueMonths.forEach((month) => {
|
||||
monthOptions += `<option value="${month}" class="fdynamic">${getMonthLabel(month)}</option>`;
|
||||
});
|
||||
|
||||
} else {
|
||||
// Extract year from selected value (e.g., "2024/" -> "2024")
|
||||
const year = selectedYear.replace('/', '');
|
||||
const year = selectedYear.replace("/", "");
|
||||
|
||||
// Get unique months for this specific year
|
||||
const monthSet = new Set();
|
||||
questions.forEach(q => {
|
||||
questions.forEach((q) => {
|
||||
const sourceMatch = q.source.match(`^${year}\/(.+?)\/`);
|
||||
if (sourceMatch) {
|
||||
monthSet.add(sourceMatch[1]);
|
||||
|
|
@ -224,50 +218,52 @@ const initializeMonthDropdown = (selectedYear) => {
|
|||
const uniqueMonths = Array.from(monthSet).sort();
|
||||
|
||||
// Special handling for known year patterns
|
||||
if (year === '2006') {
|
||||
if (year === "2006") {
|
||||
// Preserve existing 2006 logic but add any new months found
|
||||
monthOptions += '<option value="1" class="f2006">Február-Március</option>';
|
||||
monthOptions +=
|
||||
'<option value="1" class="f2006">Február-Március</option>';
|
||||
monthOptions += '<option value="2" class="f2006">Május-Június</option>';
|
||||
monthOptions += '<option value="3" class="f2006">Október-November</option>';
|
||||
monthOptions +=
|
||||
'<option value="3" class="f2006">Október-November</option>';
|
||||
|
||||
// Add any dynamic months not covered by the standard ones
|
||||
uniqueMonths.forEach(month => {
|
||||
if (!['1', '2', '3'].includes(month)) {
|
||||
uniqueMonths.forEach((month) => {
|
||||
if (!["1", "2", "3"].includes(month)) {
|
||||
monthOptions += `<option value="${month}" class="f2006">${getMonthLabel(month)}</option>`;
|
||||
}
|
||||
});
|
||||
|
||||
} else if (year === '2016') {
|
||||
// Preserve existing 2016 logic but add any new months found
|
||||
} else if (year === "2016") {
|
||||
// Preserve existing 2016 logic but add any new months found
|
||||
monthOptions += '<option value="1" class="f">Május-Június</option>';
|
||||
monthOptions += '<option value="2" class="f">Október-November</option>';
|
||||
monthOptions += '<option value="m1" class="f2016">1. Mintafeladatsor</option>';
|
||||
monthOptions += '<option value="m2" class="f2016">2. Mintafeladatsor</option>';
|
||||
monthOptions += '<option value="m3" class="f2016">3. Mintafeladatsor</option>';
|
||||
monthOptions +=
|
||||
'<option value="m1" class="f2016">1. Mintafeladatsor</option>';
|
||||
monthOptions +=
|
||||
'<option value="m2" class="f2016">2. Mintafeladatsor</option>';
|
||||
monthOptions +=
|
||||
'<option value="m3" class="f2016">3. Mintafeladatsor</option>';
|
||||
|
||||
// Add any dynamic months not covered
|
||||
uniqueMonths.forEach(month => {
|
||||
if (!['1', '2', 'm1', 'm2', 'm3'].includes(month)) {
|
||||
uniqueMonths.forEach((month) => {
|
||||
if (!["1", "2", "m1", "m2", "m3"].includes(month)) {
|
||||
monthOptions += `<option value="${month}" class="f">${getMonthLabel(month)}</option>`;
|
||||
}
|
||||
});
|
||||
|
||||
} else if (year === '2017') {
|
||||
} else if (year === "2017") {
|
||||
// Preserve existing 2017 logic but add any new months found
|
||||
monthOptions += '<option value="1" class="f">Május-Június</option>';
|
||||
monthOptions += '<option value="2" class="f">Október-November</option>';
|
||||
|
||||
// Add any dynamic months
|
||||
uniqueMonths.forEach(month => {
|
||||
if (!['1', '2'].includes(month)) {
|
||||
uniqueMonths.forEach((month) => {
|
||||
if (!["1", "2"].includes(month)) {
|
||||
monthOptions += `<option value="${month}" class="f">${getMonthLabel(month)}</option>`;
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
// For other years, use standard logic plus dynamic months
|
||||
const hasStandard1 = uniqueMonths.includes('1');
|
||||
const hasStandard2 = uniqueMonths.includes('2');
|
||||
const hasStandard1 = uniqueMonths.includes("1");
|
||||
const hasStandard2 = uniqueMonths.includes("2");
|
||||
|
||||
if (hasStandard1) {
|
||||
monthOptions += '<option value="1" class="f">Május-Június</option>';
|
||||
|
|
@ -277,8 +273,8 @@ const initializeMonthDropdown = (selectedYear) => {
|
|||
}
|
||||
|
||||
// Add any non-standard months
|
||||
uniqueMonths.forEach(month => {
|
||||
if (!['1', '2'].includes(month)) {
|
||||
uniqueMonths.forEach((month) => {
|
||||
if (!["1", "2"].includes(month)) {
|
||||
monthOptions += `<option value="${month}" class="f">${getMonthLabel(month)}</option>`;
|
||||
}
|
||||
});
|
||||
|
|
@ -286,19 +282,18 @@ const initializeMonthDropdown = (selectedYear) => {
|
|||
}
|
||||
|
||||
monthDropdown.innerHTML = monthOptions;
|
||||
console.log(`Month dropdown initialized for year: ${selectedYear}`);
|
||||
};
|
||||
|
||||
// Helper function to get readable month labels
|
||||
const getMonthLabel = (monthValue) => {
|
||||
// Handle known patterns
|
||||
const knownLabels = {
|
||||
'1': 'Május-Június',
|
||||
'2': 'Október-November',
|
||||
'3': 'Harmadik időszak',
|
||||
'm1': '1. Mintafeladatsor',
|
||||
'm2': '2. Mintafeladatsor',
|
||||
'm3': '3. Mintafeladatsor'
|
||||
1: "Május-Június",
|
||||
2: "Október-November",
|
||||
3: "Harmadik időszak",
|
||||
m1: "1. Mintafeladatsor",
|
||||
m2: "2. Mintafeladatsor",
|
||||
m3: "3. Mintafeladatsor",
|
||||
};
|
||||
|
||||
return knownLabels[monthValue] || monthValue;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue