`
+ });
+
+ resolve();
+ }
+ else if (subjectExams.length === 0) {
+ // If there are no exams for that subject, show the error screen (why face emoji)
+ changeScreensDisplay("flex", "none", "none", "none");
+ }
+
+ }, 15);
+ /*
+ We added a 5 millisecond delay because of a behaviour in JavaScript
+ Seems like "tasks" in JavaScript will be blocking, until a certain task is done JavaScript
+ will move onto the next task. Thus, adding a 5 millisecond delay to this will allow the loading
+ wheel to show
+ */
+ }
+ ).then(
+ () => {
+ // Once exams are found show the search results
+ changeScreensDisplay("none", "flex", "none", "none");
+ }
+ );
}
-document.querySelector("#search-text").addEventListener("keydown", (event) => {
- if (event.keyCode === 9 && autocomplete.textContent !== "Enter standard number or subject name") {
+const tabKeyCode = 9;
+const enterKeyCode = 13;
+
+//Stupid tab button, it has to be done on the keydown event because when keyup, the focus will have been shifted
+document.querySelector('#search-text').addEventListener('keydown', (event) => {
+
+ /*
+ Why we use autocomplete.textContent instead of innerHTML:
+ This is because Earth & Space Science has an ampersand, which is displayed as & in HTML.
+ If we use autocomplete.innerHTML, then we are comparing searchText.value (which is plain) text,
+ with HTML markup.
+ This leads to errors such as when autocompleting for Earth & Space Science, the searchText will
+ be displayed as "Earth & Space Science".
+ To avoid this we have to use the raw plain text of autocomplete. Hence, we use textContent.
+ */
+
+ // Keycode 9 is tab key
+ if (event.keyCode == tabKeyCode && autocomplete.textContent != "Enter standard number or subject name") {
+ // Prevents pressing the tab key to select elements
event.preventDefault();
- searchText.value = autocomplete.textContent;
+
+ if (searchText.value == autocomplete.textContent) {
+ showSearchResults();
+ }
+ else {
+ // If the current input text is not equal to autoComplete's text, will auto complete
+ searchText.value = autocomplete.textContent;
+ }
}
});
-document.querySelector("#search-text").addEventListener("keyup", (event) => {
- if (event.keyCode === 13) showSearchResults();
+// Enter key autocomplete and stuff. Done on keyup because enter key is not special like tab
+document.querySelector('#search-text').addEventListener('keyup', (event) => {
+
+ // Key code 13 is enter key
+ if (event.keyCode == enterKeyCode && autocomplete.textContent != "Enter standard number or subject name") {
+
+ if (searchText.value == autocomplete.textContent) {
+ showSearchResults();
+ }
+ else {
+ // If the current input text is not equal to autoComplete's text, will auto complete
+ searchText.value = autocomplete.textContent;
+ }
+ }
+
setAutoCompleteText();
});
// #endregion
+// #region Contributors Screen
+// Contributors button
+const contributorsButton = document.querySelector(".contributors-button");
+
+contributorsButton.addEventListener("click", (e) => {
+ if (contributorsScreen.style.display === "flex") {
+ // Display search results
+ changeScreensDisplay("none", "flex", "none", "none");
+ } else {
+ /*
+ Will stop the click event fired by the user clicking
+ from going up to the body. If this was not included, then
+ the body click event will be fired and code will be executed
+ (the code to be executed is below this callback function)
+ */
+ e.stopPropagation();
+
+ changeScreensDisplay("none", "none", "none", "flex");
+ }
+});
+
+/*
+If the user clicks anywhere on the screen, and the contributors screen is showing,
+then will hide contributor screen and show the exam paper search results
+*/
+document.body.addEventListener("click", () => {
+ if (contributorsScreen.style.display === "flex") {
+ // Display search results
+ changeScreensDisplay("none", "flex", "none", "none");
+ }
+});
+// #endregion
+
// #region Subjects Screen
let showingSubjects = false;
-document.querySelector("#subject-button").addEventListener("click", () => {
- searchResults.innerHTML = showingSubjects ? "" : subjectList.map((subject) =>
- ``
- ).join("\n");
+document.querySelector("#subject-button").addEventListener("click", ()=>{
+ if (!showingSubjects) {
- showingSubjects = !showingSubjects;
-});
+ // Remove search results
+ searchResults.innerHTML = "";
-searchResults.addEventListener("click", (event) => {
- if (event.target.classList.contains("subject-card")) {
- searchText.value = event.target.textContent;
- showSearchResults();
+ changeScreensDisplay("none", "flex", "none", "none");
+
+ // Show subject list buttons
+ for (let index = 0; index < subjectList.length; index++) {
+ const subject = subjectList[index];
+ searchResults.innerHTML += `\n';
+ }
+ showingSubjects = true;
}
-});
+ else {
+ // Hides the subject list buttons
+ searchResults.innerHTML = "";
+ showingSubjects = false;
+ }
+})
+
+// Searching for subject exams from clicking the subject buttons
+function search(term){
+
+ autocomplete.textContent = term;
+ searchText.value = term;
+
+ showSearchResults();
+}
// #endregion
From 7e7149f4a56b3dffbed4674c86b15aaa498bcfa6 Mon Sep 17 00:00:00 2001
From: moneybagsmahoney <79066541+WheeledCord@users.noreply.github.com>
Date: Tue, 25 Feb 2025 19:49:10 +1300
Subject: [PATCH 2/2] fix xss
---
script.js | 86 ++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 59 insertions(+), 27 deletions(-)
diff --git a/script.js b/script.js
index 8616266..e1a5511 100644
--- a/script.js
+++ b/script.js
@@ -6,6 +6,19 @@ window.addEventListener("load", () => {
});
// #endregion
+// --- Added sanitization helper function ---
+function escapeHTML(str) {
+ if (typeof str !== "string") {
+ str = String(str);
+ }
+ return str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
// #region Scrollbar
// Helper function to convert a hex color to RGB
function hexToRgb(hex) {
@@ -73,24 +86,28 @@ let fullData;
let subjectList;
-fetch("subjects.json").then((res) => {return res.json()}).then((data)=>{
- subjectList = data;
-});
+fetch("subjects.json")
+ .then((res) => res.json())
+ .then((data)=>{
+ subjectList = data;
+ });
-fetch("searchIndex.json").then((res) => { return res.json()}).then((data) => {
- idx = lunr(function () {
- this.ref('id');
- this.field('title');
- this.field('subject');
- this.field('number');
- this.field('credits');
+fetch("searchIndex.json")
+ .then((res) => res.json())
+ .then((data) => {
+ idx = lunr(function () {
+ this.ref('id');
+ this.field('title');
+ this.field('subject');
+ this.field('number');
+ this.field('credits');
+
+ data.forEach(function (doc) {
+ this.add(doc)
+ }, this)
+ })
- data.forEach(function (doc) {
- this.add(doc)
- }, this)
- })
-
- fullData = data;
+ fullData = data;
});
// #endregion
@@ -105,11 +122,13 @@ const githubContributeScreen = document.querySelector(
// Texts in input field
const searchText = document.querySelector("#search-text");
+// Use textContent when simply displaying text to avoid HTML injection.
const autocomplete = document.querySelector("#autocomplete");
const minCredits = 0
function setAutoCompleteText() {
- autocomplete.innerHTML = searchText.value;
+ // Use textContent instead of innerHTML for user-supplied text.
+ autocomplete.textContent = searchText.value;
if (searchText.value.length != 0) {
for (let index = 0; index < subjectList.length; index++) {
@@ -119,6 +138,7 @@ function setAutoCompleteText() {
subject.toLowerCase().substr(0, searchText.value.length) ==
searchText.value.toLowerCase()
) {
+ // Set the autocomplete text safely
autocomplete.textContent = subject;
let autoCompleteContent = subject
@@ -130,7 +150,8 @@ function setAutoCompleteText() {
}
}
} else {
- autocomplete.innerHTML = "Enter standard number or subject name";
+ // Set a safe default text message
+ autocomplete.textContent = "Enter standard number or subject name";
}
}
@@ -156,7 +177,7 @@ function addFilter(filter) {
}
searchText.value += filter;
- autocomplete.innerHTML = "";
+ autocomplete.textContent = "";
searchText.focus();
}
@@ -210,19 +231,29 @@ function showSearchResults() {
if (subjectExams.length > 0) {
// Add the exam card buttons for each exam there are for that subject
subjectExams.forEach((result) => {
+ // Sanitize each dynamic field
+ const examNumber = escapeHTML(fullData[result.ref]["number"]);
+ const startYear = escapeHTML(fullData[result.ref]["start-year"]);
+ const endYear = escapeHTML(fullData[result.ref]["end-year"]);
+ const examTitle = escapeHTML(fullData[result.ref]["title"]);
+ const creditsText = escapeHTML(fullData[result.ref]["credits"]);
+ const examLevel = escapeHTML(fullData[result.ref]["level"]);
+ // For URL parts, use encodeURIComponent
+ const examNumberURL = encodeURIComponent(fullData[result.ref]["number"]);
+
searchResults.innerHTML +=
`