Skip to content
Closed
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
42 changes: 26 additions & 16 deletions XMOJ.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,29 @@ let RequestAPI = (Action, Data, CallBack) => {
}
};

unsafeWindow.GetContestProblemList = async function(RefreshList) {
try {
const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + SearchParams.get("cid"));
const res = await contestReq.text();
if (contestReq.status === 200 && res.indexOf("比赛尚未开始或私有,不能查看题目。") === -1) {
const parser = new DOMParser();
const dom = parser.parseFromString(res, "text/html");
const rows = (dom.querySelector("#problemset > tbody")).rows;
let problemList = [];
for (let i = 0; i < rows.length; i++) {
problemList.push({
"title": rows[i].children[2].innerText,
"url": rows[i].children[2].children[0].href
});
}
localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList", JSON.stringify(problemList));
if (RefreshList) location.reload();
}
} catch (e) {
console.error(e);
}
}

// WebSocket Notification System
let NotificationSocket = null;
let NotificationSocketReconnectAttempts = 0;
Expand Down Expand Up @@ -2270,22 +2293,8 @@ async function main() {
}
let ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList");
if (ContestProblemList == null) {
const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + SearchParams.get("cid"));
const res = await contestReq.text();
if (contestReq.status === 200 && res.indexOf("比赛尚未开始或私有,不能查看题目。") === -1) {
const parser = new DOMParser();
const dom = parser.parseFromString(res, "text/html");
const rows = (dom.querySelector("#problemset > tbody")).rows;
let problemList = [];
for (let i = 0; i < rows.length; i++) {
problemList.push({
"title": rows[i].children[2].innerText,
"url": rows[i].children[2].children[0].href
});
}
localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList", JSON.stringify(problemList));
ContestProblemList = JSON.stringify(problemList);
}
unsafeWindow.GetContestProblemList(false);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The async GetContestProblemList call is not awaited before reading from localStorage, which can lead to a race condition.

Previously the fetch and localStorage.setItem completed inline before ContestProblemList was used. Now GetContestProblemList is async, but unsafeWindow.GetContestProblemList(false); is called without await and localStorage is read immediately afterward, so ContestProblemList may still be null, causing JSON.parse(ContestProblemList) to throw. Please either await unsafeWindow.GetContestProblemList(false); (marking the caller async if needed) before reading from localStorage, or refactor GetContestProblemList to return the list directly instead of going through localStorage.

Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Await the async cache refresh before reading ContestProblemList, otherwise first-load problem switcher rendering still races the fetch and can crash on problemList.length.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At XMOJ.user.js, line 2296:

<comment>Await the async cache refresh before reading `ContestProblemList`, otherwise first-load problem switcher rendering still races the fetch and can crash on `problemList.length`.</comment>

<file context>
@@ -2270,22 +2293,8 @@ async function main() {
-                                localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList", JSON.stringify(problemList));
-                                ContestProblemList = JSON.stringify(problemList);
-                            }
+                            unsafeWindow.GetContestProblemList(false);
+                            ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList");
                         }
</file context>
Suggested change
unsafeWindow.GetContestProblemList(false);
await unsafeWindow.GetContestProblemList(false);
Fix with Cubic

ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList");
}

let problemSwitcher = document.createElement("div");
Comment on lines 2294 to 2300
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): JSON.parse is called even when ContestProblemList may still be null, which can throw at runtime.

On failure of GetContestProblemList (network error, non-200, contest not visible), the localStorage key isn’t set and ContestProblemList stays null. Since JSON.parse(null) throws SyntaxError, this refactor makes that failure path more likely. Please guard before parsing (e.g., handle the null case explicitly or default to an empty array) instead of parsing null.

Expand All @@ -2308,6 +2317,7 @@ async function main() {
problemSwitcher.style.flexDirection = "column";

let problemList = JSON.parse(ContestProblemList);
problemSwitcher.innerHTML += `<a href="javascript:void(0)" onclick="GetContestProblemList(true)" title="刷新列表" class="mb-2" style="text-align: center;" active>刷新</a>`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.browser.security.insecure-document-method): User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities

Source: opengrep

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.browser.security.insecure-innerhtml): User controlled data in a problemSwitcher.innerHTML is an anti-pattern that can lead to XSS vulnerabilities

Source: opengrep

for (let i = 0; i < problemList.length; i++) {
let buttonText = "";
if (i < 26) {
Expand Down
Loading