fix: 比赛结束后回退提交不再被静默丢弃 - #1021
Conversation
回退路径本身可以解析出真实题号,问题在 POST 之后:内层 fetch 只把响应
console.log 掉,从不判断成功与否,随后外层无条件覆盖成"提交失败"。日志里
那次回退实际收到的是 XMOJ 的提交冷却页(`请勿重复提交`,HUSTOJ 的
$OJ_SUBMIT_COOLDOWN_TIME 默认 5 秒,XMOJ 渲染成页面而非 302),响应被丢掉,
所以状态里没有任何提交记录。
- 把回退逻辑抽成 SubmitToEndedContestProblem,返回 {Success, Message}
- 只把 redirected 当成功信号,成功后 return,不再被外层覆盖
- 遇到 `请勿重复提交` 等冷却过去后重试(3 秒一次,最多 5 次)
- 其余失败从响应的 .jumbotron 取服务端原文,不再显示通用报错
- 遇到 `验证码错误` 立即刷新验证码并停止重试(答案已被消耗)
- 题号改用 /\d+/ 提取,原 substring(2, 6) 会把 5 位题号截成 4 位
- 补齐缺失的 GetCaptchaParameter(),并在解析失败时恢复提交按钮
Closes #1017
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewer's Guide本 PR 重构已结束比赛提交页的原题回退逻辑,正确识别提交成功、处理冷却和验证码、展示服务端错误,并修复题号解析及失败时按钮状态未恢复的问题;作者已完成 node 语法检查和 mock 场景测试,但仍需人工验证新旧 UI 及真实已结束比赛端到端流程。 Sequence diagram for ended-contest fallback submissionsequenceDiagram
participant User
participant SubmitPage
participant ContestPage
participant SubmitEndpoint
participant Browser
User->>SubmitPage: click PassCheck
SubmitPage->>SubmitEndpoint: POST cid+pid submission
SubmitEndpoint-->>SubmitPage: 没有这个比赛!
SubmitPage->>ContestPage: fetch contest.php?cid=ContestID
ContestPage-->>SubmitPage: contest HTML
SubmitPage->>SubmitPage: parse RealPID with /\d+/
loop Up to 5 attempts
SubmitPage->>SubmitEndpoint: POST id=RealPID + source + GetCaptchaParameter()
alt redirected
SubmitEndpoint-->>SubmitPage: redirect response
SubmitPage->>Browser: navigate to redirect URL
else 请勿重复提交
SubmitEndpoint-->>SubmitPage: cooldown page
SubmitPage->>SubmitPage: wait 3 seconds
else 验证码错误
SubmitEndpoint-->>SubmitPage: captcha error page
SubmitPage->>SubmitPage: RefreshCaptcha("")
else other failure
SubmitEndpoint-->>SubmitPage: error page
SubmitPage->>SubmitPage: extract .jumbotron message
end
end
Flow diagram for ended-contest fallback outcomesflowchart TD
A[提交比赛题目] --> B{响应包含没有这个比赛!}
B -->|否| C[显示服务端或默认错误]
B -->|是| D[读取 contest.php]
D --> E{题目列表和题号有效}
E -->|否| F[显示解析失败原因并恢复提交按钮]
E -->|是| G[向真实题号提交并携带验证码]
G --> H{提交响应}
H -->|redirected| I[跳转到提交结果页]
H -->|请勿重复提交| J{重试次数小于5}
J -->|是| K[提示进度并等待3秒]
K --> G
J -->|否| L[显示冷却失败并恢复提交按钮]
H -->|验证码错误| M[RefreshCaptcha并停止重试]
H -->|其他错误| N[显示.jumbotron服务端原文]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue
Fixed security issues:
- Cross-site scripting (XSS) via untrusted HTML/JS injection in web rendering sinks (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="XMOJ.user.js" line_range="4497-4498" />
<code_context>
+ async function SubmitToEndedContestProblem(Source, O2Switch, ReportStatus) {
+ const ContestID = new URL(location.href).searchParams.get("cid");
+ const ProblemNumber = new URL(location.href).searchParams.get("pid");
+ const ContestResponse = await fetch("https://www.xmoj.tech/contest.php?cid=" + ContestID);
+ const ContestPage = await ContestResponse.text();
+ if (ContestResponse.status !== 200 || ContestPage.indexOf("比赛尚未开始或私有,不能查看题目。") !== -1) {
+ console.error("Failed to get contest page!");
</code_context>
<issue_to_address>
**issue (bug_risk):** A network failure in the contest-page fetch or either fallback submission fetch rejects `SubmitToEndedContestProblem`; the rejection is not caught by the click handler, so the handler exits before restoring `Submit.disabled` and its value, leaving the submit button permanently disabled as “正在提交...”.
**Triggers:** When contest.php or submit.php is unreachable, the request is aborted, or the browser reports another fetch-level error.
**Suggested fix:** Wrap the fallback workflow in try/catch and return a failure result, then restore the submit button in a finally block or in the caller's rejection path.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 1 finding to address first, and when the normal contest submission is rejected, this change automatically sends the source code to the mapped standalone problem and creates a new submission record. Reverting prevents future fallback submissions but cannot undo records already created, especially if the contest-to-problem mapping is wrong.
Blocking findings: XMOJ.user.js:4498
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 586d916992
ℹ️ 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".
| }, | ||
| "referrer": location.href, | ||
| "method": "POST", | ||
| "body": "id=" + RealPID + "&language=1&" + "source=" + encodeURIComponent(Source) + O2Switch + GetCaptchaParameter() |
There was a problem hiding this comment.
Recheck the captcha before every retry
When the judge captcha is visible and a fallback attempt enters the 3-second cooldown, the input and captcha image remain interactive. If the user clears the field or refreshes the image during that wait, the next iteration builds another POST from the now-empty GetCaptchaParameter() without invoking CaptchaIsMissing(). As documented by the existing submission guard, that blank request causes the server to mark the captcha session as failed and switch challenges, so the retry aborts with a captcha error instead of waiting for the user; reapply the guard immediately before each retry POST.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
确认是真问题,已在 ab272ee 修掉。
冷却等待是 await 出来的窗口,页面仍然可交互:Submit 被禁用了,但 #vcode 和刷新验证码的图片没有。所以用户在这 3 秒里清空输入框,下一次重试就会用空的 GetCaptchaParameter() 发请求——按 #1019 里那段注释的说法,空答案会让服务端把本 session 的 4 位验证码换成 8 位,代价不小。原来单次 POST 的代码没有这个窗口,是我加重试循环引入的。
改法:在循环里每次 POST 之前调用 CaptchaIsMissing()(它自己会提示、聚焦输入框并恢复提交按钮),返回 {Success: false, Handled: true} 让外层直接 return,不覆盖它刚设置的提示。放在 ReportStatus 之前,否则会把进度提示冲掉。
顺带也覆盖了抓 contest.php 那段等待窗口,不只是重试之间。
回归脚本里加了一条:模拟第一次 POST 拿到冷却页、等待期间清空验证码,断言第二次 POST 根本没发出、且发出去的请求没有一条是空 vcode。反向用 mutation 去掉这个 guard 确认这条断言不是空跑——去掉后第二个 body 是 id=1865&language=1&source=...&enable_O2=on,确实没有 &vcode=。
冷却等待期间验证码输入框和刷新按钮仍可交互。如果用户在这 3 秒里清空了 输入框或刷新了图片,下一次重试会用空的 GetCaptchaParameter() 发出请求, 而空答案会让服务端把本 session 的 4 位验证码换成 8 位。 在循环每次 POST 之前调用 CaptchaIsMissing()(它自己会提示并恢复按钮), 并用 Handled 标志让外层直接返回,不覆盖它设置的提示。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="XMOJ.user.js">
<violation number="1" location="XMOJ.user.js:4518">
P2: For malformed `pid` values such as `length`, this indexes array properties instead of validating an integer position, so the fallback can submit the row count as a real problem ID. Validate `pid` as a numeric index before indexing so malformed URLs take the existing failure path.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| const ProblemNumberMatch = ProblemTable.rows[i].children[1].textContent.match(/\d+/); | ||
| ContestProblems.push(ProblemNumberMatch === null ? "" : ProblemNumberMatch[0]); | ||
| } | ||
| RealPID = ContestProblems[ProblemNumber]; |
There was a problem hiding this comment.
P2: For malformed pid values such as length, this indexes array properties instead of validating an integer position, so the fallback can submit the row count as a real problem ID. Validate pid as a numeric index before indexing so malformed URLs take the existing failure path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At XMOJ.user.js, line 4518:
<comment>For malformed `pid` values such as `length`, this indexes array properties instead of validating an integer position, so the fallback can submit the row count as a real problem ID. Validate `pid` as a numeric index before indexing so malformed URLs take the existing failure path.</comment>
<file context>
@@ -4473,6 +4473,102 @@ async function main() {
+ const ProblemNumberMatch = ProblemTable.rows[i].children[1].textContent.match(/\d+/);
+ ContestProblems.push(ProblemNumberMatch === null ? "" : ProblemNumberMatch[0]);
+ }
+ RealPID = ContestProblems[ProblemNumber];
+ if (UtilityEnabled("DebugMode")) {
+ console.log("Contest Problems:", ContestProblems);
</file context>
| RealPID = ContestProblems[ProblemNumber]; | |
| const ProblemIndex = /^\d+$/.test(ProblemNumber || "") ? Number(ProblemNumber) : -1; | |
| RealPID = ProblemIndex >= 0 ? ContestProblems[ProblemIndex] : undefined; |
There was a problem hiding this comment.
2 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="XMOJ.user.js">
<violation number="1" location="XMOJ.user.js:3">
P3: This PR manually bumps `@version` to 3.6.5, but repository automation (`Update/UpdateVersion.js` via the UpdateVersion workflow) is supposed to be the only writer of version numbers on PRs to `dev`, and it re-derives the patch from `package.json`. A manual bump can conflict with that automation (double-bump or enforced re-sync), and the version is enforced to stay consistent across `package.json`, `XMOJ.user.js`, and `Update.json`. Recommend leaving the version line untouched and letting the workflow bump it.</violation>
<violation number="2" location="XMOJ.user.js:4571">
P3: After the fifth cooldown response, this code displays a retry message and sleeps for three seconds, then returns without another submission. Guard the status update and delay with `Attempt < 4`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| ReportStatus("提交过于频繁, 3 秒后重新尝试向题目 " + RealPID + " 提交"); | ||
| await new Promise((Resolve) => setTimeout(Resolve, 3000)); |
There was a problem hiding this comment.
P3: After the fifth cooldown response, this code displays a retry message and sleeps for three seconds, then returns without another submission. Guard the status update and delay with Attempt < 4.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At XMOJ.user.js, line 4566:
<comment>After the fifth cooldown response, this code displays a retry message and sleeps for three seconds, then returns without another submission. Guard the status update and delay with `Attempt < 4`.</comment>
<file context>
@@ -4473,6 +4473,102 @@ async function main() {
+ }
+ return {Success: false, Message: "向题目 " + RealPID + " 提交失败!" + (ServerMessage === "" ? "请关闭脚本后重试!" : ServerMessage)};
+ }
+ ReportStatus("提交过于频繁, 3 秒后重新尝试向题目 " + RealPID + " 提交");
+ await new Promise((Resolve) => setTimeout(Resolve, 3000));
+ }
</file context>
| ReportStatus("提交过于频繁, 3 秒后重新尝试向题目 " + RealPID + " 提交"); | |
| await new Promise((Resolve) => setTimeout(Resolve, 3000)); | |
| if (Attempt < 4) { | |
| ReportStatus("提交过于频繁, 3 秒后重新尝试向题目 " + RealPID + " 提交"); | |
| await new Promise((Resolve) => setTimeout(Resolve, 3000)); | |
| } |
| // ==UserScript== | ||
| // @name XMOJ | ||
| // @version 3.6.4 | ||
| // @version 3.6.5 |
There was a problem hiding this comment.
P3: This PR manually bumps @version to 3.6.5, but repository automation (Update/UpdateVersion.js via the UpdateVersion workflow) is supposed to be the only writer of version numbers on PRs to dev, and it re-derives the patch from package.json. A manual bump can conflict with that automation (double-bump or enforced re-sync), and the version is enforced to stay consistent across package.json, XMOJ.user.js, and Update.json. Recommend leaving the version line untouched and letting the workflow bump it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At XMOJ.user.js, line 3:
<comment>This PR manually bumps `@version` to 3.6.5, but repository automation (`Update/UpdateVersion.js` via the UpdateVersion workflow) is supposed to be the only writer of version numbers on PRs to `dev`, and it re-derives the patch from `package.json`. A manual bump can conflict with that automation (double-bump or enforced re-sync), and the version is enforced to stay consistent across `package.json`, `XMOJ.user.js`, and `Update.json`. Recommend leaving the version line untouched and letting the workflow bump it.</comment>
<file context>
@@ -1,6 +1,6 @@
// ==UserScript==
// @name XMOJ
-// @version 3.6.4
+// @version 3.6.5
// @description XMOJ增强脚本
// @author @XMOJ-Script-dev, @langningchen and the community
</file context>
| // @version 3.6.5 | |
| // @version 3.6.4 |
contest.php 或 submit.php 的 fetch 被 reject(网络错误)时,异常会一路穿出 SubmitToEndedContestProblem、穿出 .then 回调、穿出 PassCheck 的 async 监听器 ——整条链上没有任何 catch。结果 ShowSubmitStatus 和恢复按钮的两行都不会执行, 提交按钮永远停在"正在提交...",而错误框在监听器开头已经被设成 display: none, 用户什么提示都看不到。这正是本 PR 声称要修掉的那个症状。 把两处网络请求都包进 try/catch,失败时返回错误信息交给外层显示。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| async function SubmitToEndedContestProblem(Source, O2Switch, ReportStatus) { | ||
| const ContestID = new URL(location.href).searchParams.get("cid"); | ||
| const ProblemNumber = new URL(location.href).searchParams.get("pid"); | ||
| // A rejected fetch here would unwind all the way out of the click handler, which |
There was a problem hiding this comment.
有效,已在 157e9b2 修掉。
确认了一遍控制流:整条链上唯一的 try/catch 是包 _xmoj_disposeErrorMessageEditors() 的那两处,fetch 链上没有 .catch()。所以 reject 会穿出 SubmitToEndedContestProblem、穿出那个 async .then 回调(回调是 async,抛出会让 .then 返回的 promise reject)、再穿出 PassCheck 的 await,最后变成事件监听器里的 unhandled rejection。ShowSubmitStatus 和恢复按钮的两行都不执行,而 ErrorElement.style.display = "none" 在监听器开头就已经设过了——按钮卡在"正在提交...",且没有任何提示。确实就是本 PR 声称修掉的那个症状。
除了你指的 contest.php,循环里 submit.php 的那次 fetch 是同一个问题,所以两处一起包了 try/catch。contest.php 失败复用原来的"无法读取比赛页面"提示(对用户来说是同一种情况);submit.php 失败返回"网络错误,请稍后重试"。.text() 也可能 reject,一并包在里面了。
回归脚本加了三条:contest.php fetch reject、contest.php .text() reject、回退 submit reject,都断言函数返回错误信息而不是抛出。反向 mutation(MUT=6,去掉 contest.php 的 guard)不是让某条断言失败,而是直接让测试进程带着 uncaught TypeError: Failed to fetch 崩掉——和它在浏览器里干掉点击监听器的方式一模一样。现在 15/15。
另外提一下,外层 PassCheck 里那次 submit.php 的 fetch(第 4586 行左右)有同样的暴露面,不过那是 dev 上既有的、和回退路径无关,本 PR 没动。要不要顺手在监听器上加个兜底的 try/catch,我听 maintainer 的。
What does this PR aim to accomplish?:
修复 #1017:从已结束比赛的提交页提交时,#287 / #671 实现的"自动向原题提交"回退功能失效,页面显示"提交失败!请关闭脚本后重试!",且状态里没有新的提交记录。
回退路径本身是好的——它能正确检测
没有这个比赛!、抓取 contest.php、解析出真实题号(issue 附的日志里Real PID: 1865就是对的)。坏的是 POST 之后的部分:fetch只console.log(await Response.text()),从不判断提交是否成功。return/else,控制流无条件继续执行外层的失败分支,把状态覆盖成"提交失败"——即使回退提交成功了也一样。至于"状态里没有提交记录":日志里那次回退提交收到的其实是 XMOJ 的提交冷却页。HUSTOJ
submit.php里有$OJ_SUBMIT_COOLDOWN_TIME(默认 5 秒)的冷却检查,上游命中时返回 302,但 XMOJ 改成了直接渲染页面,.jumbotron里写请勿重复提交。所以Response.redirected是 false,响应体被丢掉,提交也就从未发生。顺序上比赛校验在冷却检查之前,因此第一次 cid+pid 的失败提交本身不会写 solution 表。How does this PR accomplish the above?:
把回退逻辑抽成
SubmitToEndedContestProblem(Source, O2Switch, ReportStatus),返回{Success, Message}:redirected当成功信号,成功后return,外层不再覆盖状态。请勿重复提交时等冷却过去再重试,3 秒一次、最多 5 次,期间在 ErrorMessage 里提示进度。这是 [Bug] 已结束比赛的直接题目提交回退功能失效 #1017 里"没有提交记录"的直接原因。.jumbotron取,不再一律显示"提交失败!请关闭脚本后重试!"。验证码错误立即RefreshCaptcha("")并停止重试——答案已经被消耗,重试没有意义。GetCaptchaParameter():原回退提交没带 vcode,评测队列繁忙时会因缺验证码失败。/\d+/提取:原来的substring(2, 6)会把 5 位题号截成 4 位(18650→1865),静默提交到错误的题目。#problemset > tbody不存在、题号解析不出来时都会显示原因并恢复提交按钮。原代码在这些分支直接return,按钮会永远停在"正在提交..."。rPID,并把重复的错误显示代码收拢成ShowSubmitStatus。验证方式:
node --check通过;另外写了一个一次性回归脚本,从XMOJ.user.js里抽出SubmitToEndedContestProblem本体,用 mock 的 submit.php 响应跑 10 个场景(成功重定向、冷却后重试成功、冷却始终不放行、导航栏里的 status.php 不被误判成成功、带上 vcode、验证码错误、contest.php 不可达、缺 tbody、5 位题号、题号无数字),全部通过;并用 4 个 mutation 反向确认这些断言不是空跑。By submitting this pull request, I confirm the following:
🤖 Generated with Claude Code
Summary by Sourcery
确保已结束比赛的提交能够可靠回退到原题,并在冷却或失败时提供可操作的状态反馈。
Bug Fixes:
Enhancements:
Build:
Chores:
Summary by cubic
Fixes the fallback that resubmits to the original problem after a contest ends, so ended-contest submissions no longer fail silently with a generic "提交失败" message and the status page now records the new submission. Closes #1017.
SubmitToEndedContestProblem(), returns{Success, Message}, and only treats a redirect as success so the outer failure branch no longer overwrites the status.请勿重复提交), which previously discarded the response and never created a submission.CaptchaIsMissing()before every retry POST, so an empty answer sent during the cooldown wait can't make the server swap the 4-digit captcha for an 8-digit one; its message and button restoration are kept intact.fetchcalls in try/catch, so a network error returns an error message instead of escaping the click handler and leaving the button stuck on "正在提交..."..jumbotronon failure instead of the generic error.验证码错误and stops retrying, because the answer is already consumed.GetCaptchaParameter()to the fallback POST body./\d+/instead ofsubstring(2, 6), which truncated 5-digit IDs.package.jsonversions to 3.6.5 and adds the release entry toUpdate.json.Summary by Sourcery
确保已结束比赛的提交能够可靠回退到原题,并在冷却或失败时提供可操作的状态反馈。
Bug Fixes:
Enhancements:
Build:
Chores:
Written for commit 514bd86. Summary will update on new commits.