diff --git a/Update.json b/Update.json index c9667873..942f091a 100644 --- a/Update.json +++ b/Update.json @@ -3727,6 +3727,17 @@ } ], "Notes": "### 修复提交界面不显示验证码\n\n评测队列繁忙时 XMOJ 会启用图片验证码,但提交界面是脚本自行渲染的,服务端的验证码字段被丢弃,导致提交静默失败。现在提交界面会显示验证码图片与输入框(点击图片可更换),并在提交时携带验证码。\n\n另新增 `AutoCaptcha` 开关(默认开启):在浏览器本地用模板匹配识别 4 位数字验证码,不联网、不调用任何 AI 服务。把握不大时会留空由您填写,不会填错。" + }, + "3.6.5": { + "UpdateDate": 1789778010947, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1021, + "Description": "fix: 比赛结束后回退提交不再被静默丢弃" + } + ], + "Notes": "修复比赛结束后自动向原题提交的回退功能。此前回退提交的响应被丢弃,遇到提交冷却时会静默失败并显示\"提交失败\",状态页里也没有任何提交记录。" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index 37322f82..1f59a730 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -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 // @namespace https://github/langningchen @@ -4473,6 +4473,122 @@ async function main() { RefreshCaptcha(""); } + const ShowSubmitStatus = (Message) => { + ErrorElement.style.display = "block"; + ErrorMessage.style.color = "red"; + try { _xmoj_disposeErrorMessageEditors(); } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + ErrorMessage.innerText = Message; + console.log(Message); + }; + + // Credit: https://github.com/boomzero/quicksubmit/blob/main/index.ts + // Also licensed under GPL-3.0 + // The contest is over, so submit.php refuses the cid+pid submission. Look up the + // real problem number on the contest page and submit to that problem instead. + // Returns {Success, Message}; Success means a submission record was created. + 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 + // has no catch, leaving 提交 stuck on 正在提交... with the error box still hidden. + let ContestResponse = undefined; + let ContestPage = ""; + try { + ContestResponse = await fetch("https://www.xmoj.tech/contest.php?cid=" + ContestID); + ContestPage = await ContestResponse.text(); + } catch (e) { + console.error(e); + return {Success: false, Message: "无法读取比赛页面,未能找到原题题号!"}; + } + if (ContestResponse.status !== 200 || ContestPage.indexOf("比赛尚未开始或私有,不能查看题目。") !== -1) { + console.error("Failed to get contest page!"); + return {Success: false, Message: "无法读取比赛页面,未能找到原题题号!"}; + } + let RealPID = undefined; + try { + const ContestDocument = new DOMParser().parseFromString(ContestPage, "text/html"); + const ProblemTable = ContestDocument.querySelector("#problemset > tbody"); + if (ProblemTable === null) { + console.error("Failed to find the problem list of the contest!"); + return {Success: false, Message: "无法解析比赛题目列表,未能找到原题题号!"}; + } + const ContestProblems = []; + for (let i = 0; i < ProblemTable.rows.length; i++) { + // The 题号 cell is padded with newlines and tabs; a fixed substring(2, 6) + // truncates any problem number that is not exactly four digits. + 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); + console.log("Real PID:", RealPID); + } + } catch (e) { + console.error(e); + return {Success: false, Message: "无法解析比赛题目列表,未能找到原题题号!"}; + } + if (RealPID === undefined || RealPID === "") { + return {Success: false, Message: "无法确定原题题号,请手动前往原题提交!"}; + } + // XMOJ rejects anything submitted within a few seconds of the previous submission + // with 请勿重复提交, so wait the cooldown out instead of silently dropping the code. + for (let Attempt = 0; Attempt < 5; Attempt++) { + // The captcha field stays editable while we fetch contest.php and while we wait + // out a cooldown, so re-check it before every POST rather than trusting the + // check the 提交 handler did. Sending a blank answer would burn the session. + // CaptchaIsMissing() already shows its own message and restores the button. + if (CaptchaIsMissing()) return {Success: false, Handled: true, Message: ""}; + ReportStatus("比赛已结束, 正在尝试向题目 " + RealPID + " 提交"); + let SubmitPage = ""; + try { + const SubmitResponse = await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": location.href, + "method": "POST", + "body": "id=" + RealPID + "&language=1&" + "source=" + encodeURIComponent(Source) + O2Switch + GetCaptchaParameter() + }); + if (SubmitResponse.redirected) { + location.href = SubmitResponse.url; + return {Success: true, Message: ""}; + } + SubmitPage = await SubmitResponse.text(); + } catch (e) { + console.error(e); + return {Success: false, Message: "向题目 " + RealPID + " 提交失败!网络错误,请稍后重试!"}; + } + if (UtilityEnabled("DebugMode")) { + console.log("Direct submission response:", SubmitPage); + } + // Retrying cannot help here: the answer that was sent has already been spent. + if (SubmitPage.indexOf("验证码错误") !== -1) { + await RefreshCaptcha(""); + document.querySelector("#vcode").focus(); + return {Success: false, Message: "验证码错误!请填写上方的验证码后重新提交。"}; + } + if (SubmitPage.indexOf("请勿重复提交") === -1) { + let ServerMessage = ""; + try { + const MessageElement = new DOMParser().parseFromString(SubmitPage, "text/html").querySelector(".jumbotron"); + if (MessageElement !== null) ServerMessage = MessageElement.textContent.trim(); + } catch (e) { + console.error(e); + } + return {Success: false, Message: "向题目 " + RealPID + " 提交失败!" + (ServerMessage === "" ? "请关闭脚本后重试!" : ServerMessage)}; + } + ReportStatus("提交过于频繁, 3 秒后重新尝试向题目 " + RealPID + " 提交"); + await new Promise((Resolve) => setTimeout(Resolve, 3000)); + } + return {Success: false, Message: "向题目 " + RealPID + " 提交失败!提交过于频繁,请稍后手动重试!"}; + } + PassCheck.addEventListener("click", async () => { // This is the request that actually reaches submit.php, so the captcha is checked // here as well as in the 提交 handler above. @@ -4515,76 +4631,18 @@ async function main() { document.querySelector("#vcode").focus(); return; } - if (text.indexOf("没有这个比赛!") !== -1 && new URL(location.href).searchParams.get("pid") !== null) { - // Credit: https://github.com/boomzero/quicksubmit/blob/main/index.ts - // Also licensed under GPL-3.0 - const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + new URL(location.href).searchParams.get("cid")); - const res = await contestReq.text(); - if ( - contestReq.status !== 200 || - res.indexOf("比赛尚未开始或私有,不能查看题目。") !== -1 - ) { - console.error(`Failed to get contest page!`); - return; - } - const parser = new DOMParser(); - const dom = parser.parseFromString(res, "text/html"); - const contestProblems = []; - const rows = (dom.querySelector( - "#problemset > tbody", - )).rows; - for (let i = 0; i < rows.length; i++) { - contestProblems.push( - rows[i].children[1].textContent.substring(2, 6).replaceAll( - "\t", - "", - ), - ); - } - rPID = contestProblems[new URL(location.href).searchParams.get("pid")]; - if (UtilityEnabled("DebugMode")) { - console.log("Contest Problems:", contestProblems); - console.log("Real PID:", rPID); - } - ErrorElement.style.display = "block"; - ErrorMessage.style.color = "red"; - try { _xmoj_disposeErrorMessageEditors(); } catch (e) { - console.error(e); - if (UtilityEnabled("DebugMode")) { - SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); - } - } - ErrorMessage.innerText = "比赛已结束, 正在尝试向题目 " + rPID + " 提交"; - console.log("比赛已结束, 正在尝试向题目 " + rPID + " 提交"); - let o2Switch = "&enable_O2=on"; - if (!document.querySelector("#enable_O2").checked) o2Switch = ""; - await fetch("https://www.xmoj.tech/submit.php", { - "headers": { - "content-type": "application/x-www-form-urlencoded" - }, - "referrer": location.href, - "method": "POST", - "body": "id=" + rPID + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + GetCaptchaParameter() - }).then(async (Response) => { - if (Response.redirected) { - location.href = Response.url; - } - console.log(await Response.text()); - }); - - } if (UtilityEnabled("DebugMode")) { console.log("Submission failed! Response:", text); } - ErrorElement.style.display = "block"; - ErrorMessage.style.color = "red"; - try { _xmoj_disposeErrorMessageEditors(); } catch (e) { - console.error(e); - if (UtilityEnabled("DebugMode")) { - SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + let FailMessage = "提交失败!请关闭脚本后重试!"; + if (text.indexOf("没有这个比赛!") !== -1 && SearchParams.get("pid") !== null) { + const FallbackResult = await SubmitToEndedContestProblem(CodeMirrorElement.getValue(), o2Switch, ShowSubmitStatus); + if (FallbackResult.Success || FallbackResult.Handled) { + return; } + FailMessage = FallbackResult.Message; } - ErrorMessage.innerText = "提交失败!请关闭脚本后重试!"; + ShowSubmitStatus(FailMessage); Submit.disabled = false; Submit.value = "提交"; } diff --git a/package.json b/package.json index 8537fedb..c50f9fb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.6.4", + "version": "3.6.5", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": {