Conversation
- contest.php: 比赛私有/不存在的 403/404 页面没有 h3 和比赛信息,跳过处理 - problem.php: 错误页面不再显示题目切换器,题目列表为空时不再报错, 没有提交按钮或题目内容时跳过相关处理 - reinfo.php: 多subtask时遍历所有分组并跳过结构不同的元素;无权查看时不再报错 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135Ggqqp6xktQZytLRJQ9nc
改为统一处理:在每个对 querySelector 结果取属性/调用方法的地方先判断是否为 null, 替代上一个提交中针对单个页面的修复。 - 语句:前面加 if (xxx.querySelector(...) != null) - 条件:在条件中加 xxx.querySelector(...) != null && - 变量声明/赋值:为 null 时使用空值([] / "" / 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0135Ggqqp6xktQZytLRJQ9nc
Reviewer's GuideThe PR broadly prevents TypeErrors caused by XMOJ error, unauthorized, empty, and structurally different pages by adding defensive DOM checks and fallbacks throughout the userscript, with targeted fixes for problem, contest, and multi-subtask test-point pages. Flow diagram for defensive page handlingflowchart TD
A["Page loaded"] --> B{"Expected DOM exists?"}
B -->|Yes| C["Apply page enhancement"]
B -->|No| D["Use empty fallback or skip enhancement"]
C --> E["Continue without TypeError"]
D --> E
Flow diagram for problem and contest error pagesflowchart TD
A["Problem or contest page"] --> B{"Error or unauthorized page?"}
B -->|Yes| C["Skip switcher and page-specific processing"]
B -->|No| D{"Problem or contest content exists?"}
D -->|Yes| E["Apply submit, translation, status, and contest enhancements"]
D -->|No| C
C --> F["Page remains usable without TypeError"]
E --> F
Flow diagram for multi-subtask test-point processingflowchart TD
A["reinfo.php loads"] --> B{"Test-point groups exist?"}
B -->|No| C["Show no test-point information"]
B -->|Yes| D["Iterate through available groups"]
D --> E{"Group has expected structure?"}
E -->|Yes| F["Format time and size"]
E -->|No| G["Skip structurally different group"]
F --> H["Finish without TypeError"]
G --> H
C --> H
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
请向 |
2 similar comments
|
请向 |
|
请向 |
There was a problem hiding this comment.
Hey - I've found 7 issues
Fixed security issues:
- Cross-site scripting (XSS) via untrusted HTML/JS injection in web rendering sinks (link) · Dashboard
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="XMOJ.user.js" line_range="719" />
<code_context>
+ let Temp = (ParsedDocument.querySelector("#statics > tbody") != null) ? ParsedDocument.querySelector("#statics > tbody").children : [];
</code_context>
<issue_to_address>
**issue (bug_risk):** When the fetched user page has no `#statics > tbody`, `Temp` is set to an empty array and the next line evaluates `Temp[Temp.length - 1].children[1]`, throwing because `Temp[-1]` is undefined.
**Triggers:** When `userinfo.php` returns an error or an otherwise incomplete user page.
**Suggested fix:** Guard the email extraction with a non-empty `Temp` check and use an empty email when no suitable row exists.
</issue_to_address>
### Comment 2
<location path="XMOJ.user.js" line_range="3242" />
<code_context>
- document.querySelector('.mt-3 > center:nth-child(1)').innerHTML = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target);
- document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary').onclick = function () {
+ if (document.querySelector('.mt-3 > center:nth-child(1)') != null) document.querySelector('.mt-3 > center:nth-child(1)').innerHTML = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target);
+ if (document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary') != null) document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary').onclick = function () {
window.location.href = SubmitLink.href;
console.log(SubmitLink.href);
</code_context>
<issue_to_address>
**issue (bug_risk):** When the problem page contains no link whose text is `提交`, `SubmitLink` is undefined, but the code still reads `SubmitLink.href` and later calls `SubmitLink.parentNode.replaceChild`, so the error-page path throws before the new null checks can help.
**Triggers:** When a problem has no submission button or the page is an error/permission page.
**Suggested fix:** Return from the submission-button transformation unless `SubmitLink` is non-null, and only create the replacement button in that case.
</issue_to_address>
### Comment 3
<location path="XMOJ.user.js" line_range="3252" />
<code_context>
}
if (UtilityEnabled("RemoveUseless")) {
- document.querySelector("h2.lang_en").remove();
+ if (document.querySelector("h2.lang_en") != null) document.querySelector("h2.lang_en").remove();
document.getElementsByTagName("center")[1].remove();
}
</code_context>
<issue_to_address>
**issue (bug_risk):** When a problem page has no second `<center>` element, the `RemoveUseless` branch still executes `document.getElementsByTagName("center")[1].remove()`, throwing even though the preceding `h2.lang_en` removal is guarded.
**Triggers:** When the page has missing or reduced problem-content markup.
**Suggested fix:** Check that `document.getElementsByTagName("center")[1]` exists before removing it.
```suggestion
if (document.getElementsByTagName("center")[1] != null) document.getElementsByTagName("center")[1].remove();
```
</issue_to_address>
### Comment 4
<location path="XMOJ.user.js" line_range="5541-5544" />
<code_context>
+ if (document.querySelector("#results") != null) document.querySelector("#results").parentElement.innerHTML = "没有测试点信息";
} else {
- for (let i = 0; i < document.querySelector("#results > div").children.length; i++) {
+ for (let i = 0; document.querySelector("#results > div") != null && i < document.querySelector("#results > div").children.length; i++) {
let CurrentElement = document.querySelector("#results > div").children[i].children[0].children[0].children[0];
let Temp = CurrentElement.innerText.substring(0, CurrentElement.innerText.length - 2).split("/");
</code_context>
<issue_to_address>
**issue (bug_risk):** When a child of `#results > div` has a different structure, indexing `children[i].children[0].children[0].children[0]` produces an undefined intermediate value and `CurrentElement.innerText` throws. The loop only checks that the parent exists; it does not skip structurally different children.
**Triggers:** When `reinfo.php` contains multiple subtask groups or non-result elements with a different DOM shape.
**Suggested fix:** Validate each intermediate child before accessing `innerText`, and continue to the next group when the expected structure is absent.
```suggestion
for (let i = 0; document.querySelector("#results > div") != null && i < document.querySelector("#results > div").children.length; i++) {
let CurrentElement = document.querySelector("#results > div").children[i];
if (CurrentElement.children[0] == undefined) continue;
CurrentElement = CurrentElement.children[0];
if (CurrentElement.children[0] == undefined) continue;
CurrentElement = CurrentElement.children[0];
if (CurrentElement.children[0] == undefined) continue;
CurrentElement = CurrentElement.children[0];
let Temp = CurrentElement.innerText.substring(0, CurrentElement.innerText.length - 2).split("/");
CurrentElement.innerText = TimeToStringTime(Temp[0]) + "/" + SizeToStringSize(Temp[1]);
}
```
</issue_to_address>
### Comment 5
<location path="XMOJ.user.js" line_range="3716" />
<code_context>
document.getElementsByTagName("h3")[0].innerHTML = "比赛" + document.getElementsByTagName("h3")[0].innerHTML.substring(7);
if (document.querySelector("#time_left") != null) {
- let EndTime = document.querySelector("body > div > div.mt-3 > center").childNodes[3].data;
+ let EndTime = (document.querySelector("body > div > div.mt-3 > center") != null) ? document.querySelector("body > div > div.mt-3 > center").childNodes[3].data : "";
EndTime = EndTime.substring(EndTime.indexOf("结束时间是:") + 6, EndTime.lastIndexOf("。"));
EndTime = new Date(EndTime).getTime();
</code_context>
<issue_to_address>
**issue (broader_impact):** On a contest 403/404 page without an `h3`, the preceding unconditional `document.getElementsByTagName("h3")[0].innerHTML` access still throws before this newly guarded `EndTime` extraction is reached.
**Triggers:** When `contest.php` serves a private, nonexistent, or unauthorized contest page without contest markup.
**Suggested fix:** Guard the `h3` rewrite and skip the remainder of the contest-rendering branch when no contest header exists.
</issue_to_address>
### Comment 6
<location path="XMOJ.user.js" line_range="5425" />
<code_context>
ErrorText.style.marginBottom = "5px";
- document.querySelector("#login").appendChild(ErrorText);
+ if (document.querySelector("#login") != null) document.querySelector("#login").appendChild(ErrorText);
let LoginButton = document.getElementsByName("submit")[0];
LoginButton.addEventListener("click", async () => {
let Username = document.getElementsByName("user_id")[0].value;
</code_context>
<issue_to_address>
**issue (bug_risk):** Guarding the `#login` container does not guard `LoginButton`: if the login form is absent, `document.getElementsByName("submit")[0]` is undefined and `LoginButton.addEventListener` throws.
**Triggers:** When `loginpage.php` returns an error or incomplete page without a submit control.
**Suggested fix:** Check that `LoginButton` exists before registering the click handler, or return when the login form is absent.
```suggestion
let LoginButton = document.getElementsByName("submit")[0];
if (LoginButton == null) return;
```
</issue_to_address>
### Comment 7
<location path="XMOJ.user.js" line_range="6373" />
<code_context>
});
}
- document.querySelector("body > div > div.mt-3").innerHTML = `<textarea>${Code}</textarea>`;
+ if (document.querySelector("body > div > div.mt-3") != null) document.querySelector("body > div > div.mt-3").innerHTML = `<textarea>${Code}</textarea>`;
CodeMirror.fromTextArea(document.querySelector("body > div > div.mt-3 > textarea"), {
lineNumbers: true,
</code_context>
<issue_to_address>
**issue (bug_risk):** If the `body > div > div.mt-3` container is absent, the guarded assignment does nothing but `CodeMirror.fromTextArea(document.querySelector("body > div > div.mt-3 > textarea"), ...)` still receives `null` and throws.
**Triggers:** When the source/message page has no expected content container.
**Suggested fix:** Only initialize CodeMirror after confirming that the container and its textarea both exist.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 7 findings to address first, and the guards around login, password/profile updates, and CAPTCHA handling can turn a missing form element into an empty value or skip a client-side check, potentially sending an unintended account change or submission. Reverting prevents future occurrences, but requests already sent and resulting account or submission changes would remain.
Blocking findings: XMOJ.user.js:719, XMOJ.user.js:3242, XMOJ.user.js:3252, XMOJ.user.js:5544, XMOJ.user.js:3716, and 2 more
| let Rating = (parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText.trim()) / parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText.trim())).toFixed(3) * 1000; | ||
| let Temp = ParsedDocument.querySelector("#statics > tbody").children; | ||
| let Rating = (ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)") != null && ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)") != null) ? (parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText.trim()) / parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText.trim())).toFixed(3) * 1000 : 0; | ||
| let Temp = (ParsedDocument.querySelector("#statics > tbody") != null) ? ParsedDocument.querySelector("#statics > tbody").children : []; |
There was a problem hiding this comment.
issue (bug_risk): When the fetched user page has no #statics > tbody, Temp is set to an empty array and the next line evaluates Temp[Temp.length - 1].children[1], throwing because Temp[-1] is undefined.
Triggers: When userinfo.php returns an error or an otherwise incomplete user page.
Suggested fix: Guard the email extraction with a non-empty Temp check and use an empty email when no suitable row exists.
| document.querySelector('.mt-3 > center:nth-child(1)').innerHTML = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); | ||
| document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary').onclick = function () { | ||
| if (document.querySelector('.mt-3 > center:nth-child(1)') != null) document.querySelector('.mt-3 > center:nth-child(1)').innerHTML = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); | ||
| if (document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary') != null) document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary').onclick = function () { |
There was a problem hiding this comment.
issue (bug_risk): When the problem page contains no link whose text is 提交, SubmitLink is undefined, but the code still reads SubmitLink.href and later calls SubmitLink.parentNode.replaceChild, so the error-page path throws before the new null checks can help.
Triggers: When a problem has no submission button or the page is an error/permission page.
Suggested fix: Return from the submission-button transformation unless SubmitLink is non-null, and only create the replacement button in that case.
| @@ -3248,7 +3248,7 @@ async function main() { | |||
| Temp[i].parentElement.className = "card"; | |||
| } | |||
| if (UtilityEnabled("RemoveUseless")) { | |||
| document.querySelector("h2.lang_en").remove(); | |||
| if (document.querySelector("h2.lang_en") != null) document.querySelector("h2.lang_en").remove(); | |||
| document.getElementsByTagName("center")[1].remove(); | |||
There was a problem hiding this comment.
issue (bug_risk): When a problem page has no second <center> element, the RemoveUseless branch still executes document.getElementsByTagName("center")[1].remove(), throwing even though the preceding h2.lang_en removal is guarded.
Triggers: When the page has missing or reduced problem-content markup.
Suggested fix: Check that document.getElementsByTagName("center")[1] exists before removing it.
| document.getElementsByTagName("center")[1].remove(); | |
| if (document.getElementsByTagName("center")[1] != null) document.getElementsByTagName("center")[1].remove(); |
| for (let i = 0; document.querySelector("#results > div") != null && i < document.querySelector("#results > div").children.length; i++) { | ||
| let CurrentElement = document.querySelector("#results > div").children[i].children[0].children[0].children[0]; | ||
| let Temp = CurrentElement.innerText.substring(0, CurrentElement.innerText.length - 2).split("/"); | ||
| CurrentElement.innerText = TimeToStringTime(Temp[0]) + "/" + SizeToStringSize(Temp[1]); |
There was a problem hiding this comment.
issue (bug_risk): When a child of #results > div has a different structure, indexing children[i].children[0].children[0].children[0] produces an undefined intermediate value and CurrentElement.innerText throws. The loop only checks that the parent exists; it does not skip structurally different children.
Triggers: When reinfo.php contains multiple subtask groups or non-result elements with a different DOM shape.
Suggested fix: Validate each intermediate child before accessing innerText, and continue to the next group when the expected structure is absent.
| for (let i = 0; document.querySelector("#results > div") != null && i < document.querySelector("#results > div").children.length; i++) { | |
| let CurrentElement = document.querySelector("#results > div").children[i].children[0].children[0].children[0]; | |
| let Temp = CurrentElement.innerText.substring(0, CurrentElement.innerText.length - 2).split("/"); | |
| CurrentElement.innerText = TimeToStringTime(Temp[0]) + "/" + SizeToStringSize(Temp[1]); | |
| for (let i = 0; document.querySelector("#results > div") != null && i < document.querySelector("#results > div").children.length; i++) { | |
| let CurrentElement = document.querySelector("#results > div").children[i]; | |
| if (CurrentElement.children[0] == undefined) continue; | |
| CurrentElement = CurrentElement.children[0]; | |
| if (CurrentElement.children[0] == undefined) continue; | |
| CurrentElement = CurrentElement.children[0]; | |
| if (CurrentElement.children[0] == undefined) continue; | |
| CurrentElement = CurrentElement.children[0]; | |
| let Temp = CurrentElement.innerText.substring(0, CurrentElement.innerText.length - 2).split("/"); | |
| CurrentElement.innerText = TimeToStringTime(Temp[0]) + "/" + SizeToStringSize(Temp[1]); | |
| } |
| @@ -3713,24 +3713,24 @@ async function main() { | |||
| } else { | |||
| document.getElementsByTagName("h3")[0].innerHTML = "比赛" + document.getElementsByTagName("h3")[0].innerHTML.substring(7); | |||
| if (document.querySelector("#time_left") != null) { | |||
| let EndTime = document.querySelector("body > div > div.mt-3 > center").childNodes[3].data; | |||
| let EndTime = (document.querySelector("body > div > div.mt-3 > center") != null) ? document.querySelector("body > div > div.mt-3 > center").childNodes[3].data : ""; | |||
There was a problem hiding this comment.
issue (broader_impact): On a contest 403/404 page without an h3, the preceding unconditional document.getElementsByTagName("h3")[0].innerHTML access still throws before this newly guarded EndTime extraction is reached.
Triggers: When contest.php serves a private, nonexistent, or unauthorized contest page without contest markup.
Suggested fix: Guard the h3 rewrite and skip the remainder of the contest-rendering branch when no contest header exists.
| @@ -5421,7 +5421,7 @@ async function main() { | |||
| let ErrorText = document.createElement("div"); | |||
| ErrorText.style.color = "red"; | |||
| ErrorText.style.marginBottom = "5px"; | |||
| document.querySelector("#login").appendChild(ErrorText); | |||
| if (document.querySelector("#login") != null) document.querySelector("#login").appendChild(ErrorText); | |||
| let LoginButton = document.getElementsByName("submit")[0]; | |||
There was a problem hiding this comment.
issue (bug_risk): Guarding the #login container does not guard LoginButton: if the login form is absent, document.getElementsByName("submit")[0] is undefined and LoginButton.addEventListener throws.
Triggers: When loginpage.php returns an error or incomplete page without a submit control.
Suggested fix: Check that LoginButton exists before registering the click handler, or return when the login form is absent.
| let LoginButton = document.getElementsByName("submit")[0]; | |
| let LoginButton = document.getElementsByName("submit")[0]; | |
| if (LoginButton == null) return; |
| @@ -6370,7 +6370,7 @@ cerr<<b93(gz(rd()))<<endl;abort();} | |||
| }); | |||
| }); | |||
| } | |||
| document.querySelector("body > div > div.mt-3").innerHTML = `<textarea>${Code}</textarea>`; | |||
| if (document.querySelector("body > div > div.mt-3") != null) document.querySelector("body > div > div.mt-3").innerHTML = `<textarea>${Code}</textarea>`; | |||
There was a problem hiding this comment.
issue (bug_risk): If the body > div > div.mt-3 container is absent, the guarded assignment does nothing but CodeMirror.fromTextArea(document.querySelector("body > div > div.mt-3 > textarea"), ...) still receives null and throws.
Triggers: When the source/message page has no expected content container.
Suggested fix: Only initialize CodeMirror after confirming that the container and its textarea both exist.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c3be851c60
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| setTimeout(() => { | ||
| location.href = "https://www.xmoj.tech/problemset.php"; | ||
| }, 1000); | ||
| } else { | ||
| let PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); | ||
| if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { | ||
| if (document.querySelector("body > div > div.mt-3 > center") != null && document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { |
There was a problem hiding this comment.
Stop processing problem error pages
When /problem.php returns an error page that has neither the expected <center> nor the <h2> handled above, this guard merely skips the margin adjustment and execution continues. SubmitLink is then undefined, so SubmitLink.href at line 3230 still throws the TypeError this change is intended to prevent; the entire normal-page processing branch should be skipped when the problem container is absent.
Useful? React with 👍 / 👎.
| @@ -3713,24 +3713,24 @@ async function main() { | |||
| } else { | |||
| document.getElementsByTagName("h3")[0].innerHTML = "比赛" + document.getElementsByTagName("h3")[0].innerHTML.substring(7); | |||
| if (document.querySelector("#time_left") != null) { | |||
| let EndTime = document.querySelector("body > div > div.mt-3 > center").childNodes[3].data; | |||
| let EndTime = (document.querySelector("body > div > div.mt-3 > center") != null) ? document.querySelector("body > div > div.mt-3 > center").childNodes[3].data : ""; | |||
There was a problem hiding this comment.
Guard the contest branch before reading its heading
On private or nonexistent contests, the ?cid= response can lack both the contest heading and detail container. This fallback is reached only after line 3714 has already dereferenced document.getElementsByTagName("h3")[0], so those 403/404 pages still throw before any of the new null checks run; gate the entire contest-detail branch on the required heading/container.
Useful? React with 👍 / 👎.
| } else { | ||
| for (let i = 0; i < document.querySelector("#results > div").children.length; i++) { | ||
| for (let i = 0; document.querySelector("#results > div") != null && i < document.querySelector("#results > div").children.length; i++) { |
There was a problem hiding this comment.
Validate each result row before descending
For a multi-subtask /reinfo.php response, #results contains multiple groups and group-title elements do not necessarily have the four-level child structure assumed on the next line. Checking only that the first #results > div exists therefore still lets .children[0] be read from undefined, aborting formatting at the first differently shaped element; iterate all matching groups and skip rows whose descendant chain is absent.
Useful? React with 👍 / 👎.
没有提交按钮或题目内容时跳过相关处理
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_0135Ggqqp6xktQZytLRJQ9nc
Summary by cubic
修复了
XMOJ.user.js中错误页面和多 subtask 测试点页面的 TypeError,避免页面在缺少预期 DOM 结构时崩溃。querySelector结果前增加 null 检查,替代原先针对 contest.php、problem.php、reinfo.php 的零散修复。Written for commit c3be851. Summary will update on new commits.
Summary by Sourcery
Harden the userscript against missing or unexpected page elements so error pages and multi-subtask views no longer cause runtime failures.
Bug Fixes:
Enhancements: