From 779480060355d8b2a018f290048218144a560c53 Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Mon, 6 Feb 2023 09:30:21 +0800 Subject: [PATCH] feat(remote_judger/loj): fetch submission from archive --- remote_judger/src/providers/loj.ts | 59 +++++++- remote_judger/src/vjudge.ts | 37 +++++ web/app/controllers/problem.php | 33 ++++- .../remote_judge/custom_account_validator.php | 32 ++++- web/app/controllers/submission.php | 2 +- web/app/models/UOJRemoteProblem.php | 2 +- web/app/models/UOJSubmissionLikeTrait.php | 33 +++-- web/js/uoj.js | 132 +++++++++++++++++- 8 files changed, 300 insertions(+), 30 deletions(-) diff --git a/remote_judger/src/providers/loj.ts b/remote_judger/src/providers/loj.ts index 793b319..adfcf3b 100644 --- a/remote_judger/src/providers/loj.ts +++ b/remote_judger/src/providers/loj.ts @@ -10,7 +10,7 @@ import { crlf, LF } from 'crlf-normalize'; proxy(superagent); const logger = new Logger('remote/loj'); -const langs_map = { +const LANGS_MAP = { C: { name: 'C (gcc, c11, O2, m64)', info: { @@ -157,7 +157,11 @@ export default class LibreojProvider implements IBasicProvider { } static constructFromAccountData(data) { - throw new Error('Method not implemented.'); + return new this({ + type: 'loj', + handle: data.username, + password: data.token, + }); } get(url: string) { @@ -186,8 +190,7 @@ export default class LibreojProvider implements IBasicProvider { get loggedIn() { return this.get('/auth/getSessionInfo?token=' + this.account.password).then( - res => - res.body.userMeta && res.body.userMeta.username === this.account.handle + res => res.body.userMeta && res.body.userMeta.id ); } @@ -212,7 +215,17 @@ export default class LibreojProvider implements IBasicProvider { next, end ) { - const programType = langs_map[lang] || langs_map['C++']; + if (!(await this.ensureLogin())) { + await end({ + error: true, + status: 'Judgment Failed', + message: 'Login failed', + }); + + return null; + } + + const programType = LANGS_MAP[lang] || LANGS_MAP['C++']; const comment = programType.comment; if (comment) { @@ -255,7 +268,34 @@ export default class LibreojProvider implements IBasicProvider { return body.submissionId; } + async ensureIsOwnSubmission(id: string) { + const user_id = await this.get( + '/auth/getSessionInfo?token=' + this.account.password + ).then(res => res.body.userMeta?.id); + + if (!user_id) return false; + + const submission_user_id = await this.post( + '/submission/getSubmissionDetail' + ) + .send({ submissionId: String(id), locale: 'zh_CN' }) + .retry(3) + .then(res => res.body.meta?.submitter.id); + + return user_id === submission_user_id; + } + async waitForSubmission(problem_id: string, id: string, next, end) { + if (!(await this.ensureLogin())) { + await end({ + error: true, + status: 'Judgment Failed', + message: 'Login failed', + }); + + return null; + } + let i = 0; while (true) { @@ -275,6 +315,15 @@ export default class LibreojProvider implements IBasicProvider { if (error) continue; + if (body.meta.problem.displayId != problem_id) { + return await end({ + id, + error: true, + status: 'Judgment Failed', + message: 'Submission does not match current problem.', + }); + } + if (!body.progress) { await next({ status: 'Waiting for Remote Judge' }); diff --git a/remote_judger/src/vjudge.ts b/remote_judger/src/vjudge.ts index f34440a..b43938d 100644 --- a/remote_judger/src/vjudge.ts +++ b/remote_judger/src/vjudge.ts @@ -182,6 +182,43 @@ class VJudge { } catch (e) { logger.error(e); + await end({ + error: true, + status: 'Judgment Failed', + message: e.message, + }); + } + } else if (config.remote_submit_type == 'archive') { + try { + const provider = this.p_imports[type].constructFromAccountData( + JSON.parse(config.remote_account_data) + ); + + if (!config.remote_submission_id) { + return await end({ + error: true, + status: 'Judgment Failed', + message: 'REMOTE_SUBMISSION_ID is not set.', + }); + } + + if (await provider.ensureIsOwnSubmission(config.remote_submission_id)) { + await provider.waitForSubmission( + problem_id, + config.remote_submission_id, + next, + end + ); + } else { + return await end({ + error: true, + status: 'Judgment Failed', + message: 'Remote submission does not belongs to current user.', + }); + } + } catch (e) { + logger.error(e); + await end({ error: true, status: 'Judgment Failed', diff --git a/web/app/controllers/problem.php b/web/app/controllers/problem.php index f9de13c..cfd085b 100644 --- a/web/app/controllers/problem.php +++ b/web/app/controllers/problem.php @@ -109,13 +109,30 @@ function handleUpload($zip_file_name, $content, $tot_size) { if (UOJProblem::info('type') == 'remote') { $submit_type = in_array($_POST['answer_remote_submit_type'], $remote_provider['submit_type']) ? $_POST['answer_remote_submit_type'] : $remote_provider['submit_type'][0]; + $content['config'][] = ['remote_submit_type', $submit_type]; if ($submit_type != 'bot') { $content['no_rejudge'] = true; $content['config'][] = ['remote_account_data', $_POST['answer_remote_account_data']]; } - $content['config'][] = ['remote_submit_type', $submit_type]; + if ($submit_type == 'archive') { + $content['remote_submission_id'] = $_POST['answer_remote_submission_id']; + $content['config'][] = ['remote_submission_id', $_POST['answer_remote_submission_id']]; + + $content['config'] = array_filter( + $content['config'], + function ($key) { + return !strEndWith($key, '_language'); + }, + ARRAY_FILTER_USE_KEY + ); + + $zip_file = new ZipArchive(); + $zip_file->open(UOJContext::storagePath() . $zip_file_name, ZipArchive::CREATE); + $zip_file->addFromString('answer.code', ''); + $zip_file->close(); + } } UOJSubmission::onUpload($zip_file_name, $content, $tot_size, $is_participating); @@ -217,10 +234,18 @@ if ($pre_submit_check_ret === true && !$no_more_submission) { $remote_oj = UOJProblem::cur()->getExtraConfig('remote_online_judge'); $remote_pid = UOJProblem::cur()->getExtraConfig('remote_problem_id'); $remote_url = UOJRemoteProblem::getProblemRemoteUrl($remote_oj, $remote_pid); - $submit_type = json_encode(UOJRemoteProblem::$providers[$remote_oj]['submit_type']); + $remote_provider = UOJRemoteProblem::$providers[$remote_oj]; + $submit_type = json_encode($remote_provider['submit_type']); - $answer_form->addNoVal('answer_remote_submit_type', ''); - $answer_form->addNoVal('answer_remote_account_data', ''); + $answer_form->add('answer_remote_submit_type', '', function ($opt) use ($remote_provider) { + return in_array($opt, $remote_provider['submit_type']) ? '' : '无效选项'; + }, null); + $answer_form->add('answer_remote_account_data', '', function ($data) { + return json_decode($data) !== null ? '' : '无效数据'; + }, null); + $answer_form->add('answer_remote_submission_id', '', function ($id) { + return validateUInt($id) ? '' : '无效 ID'; + }, null); $answer_form->appendHTML(<<Remote Judge 配置
diff --git a/web/app/controllers/subdomain/api/remote_judge/custom_account_validator.php b/web/app/controllers/subdomain/api/remote_judge/custom_account_validator.php index f655b20..7e1b8d7 100644 --- a/web/app/controllers/subdomain/api/remote_judge/custom_account_validator.php +++ b/web/app/controllers/subdomain/api/remote_judge/custom_account_validator.php @@ -25,14 +25,14 @@ if ($type == 'luogu') { return false; } - if ($curl->responseHeaders['Content-Type'] == 'text/html') { + if (strStartWith($curl->responseHeaders['Content-Type'], 'text/html')) { $sec = $curl->getResponseCookie('sec'); if ($sec) { $curl->setCookie('sec', $sec); $curl->get(UOJRemoteProblem::$providers['luogu']['url'] . '/user/setting?_contentOnly=1'); - if ($curl->responseHeaders['Content-Type'] == 'application/json') { + if (strStartWith($curl->responseHeaders['Content-Type'], 'application/json')) { $res = validateLuogu($curl->response); return true; @@ -42,7 +42,7 @@ if ($type == 'luogu') { } return false; - } else if ($curl->responseHeaders['Content-Type'] == 'application/json') { + } else if (strStartWith($curl->responseHeaders['Content-Type'], 'application/json')) { $res = validateLuogu($curl->response); return true; @@ -50,8 +50,6 @@ if ($type == 'luogu') { return false; }, 3); - - die(json_encode(['ok' => $res === true])); } else if ($type == 'codeforces') { $curl->setFollowLocation(); $curl->setCookie('JSESSIONID', UOJRequest::post('JSESSIONID', 'is_string', '')); @@ -63,7 +61,7 @@ if ($type == 'luogu') { return false; } - if (str_starts_with($curl->responseHeaders['Content-Type'], 'text/html')) { + if (strStartWith($curl->responseHeaders['Content-Type'], 'text/html')) { if (str_contains($curl->response, 'Login into Codeforces')) { return false; } @@ -77,6 +75,28 @@ if ($type == 'luogu') { return true; } + return false; + }, 3); +} else if ($type == 'loj') { + retry_loop(function () use (&$curl, &$res) { + $curl->get('https://api.loj.ac.cn/api/auth/getSessionInfo?token=' . UOJRequest::post('token', 'is_string', '')); + + if ($curl->error) { + return false; + } + + if (strStartWith($curl->responseHeaders['Content-Type'], 'application/json')) { + $response = json_decode(json_encode($curl->response), true); + + if (isset($response['userMeta']) && isset($response['userMeta']['id'])) { + $res = true; + + return true; + } + + return true; + } + return false; }, 3); } else { diff --git a/web/app/controllers/submission.php b/web/app/controllers/submission.php index f89c9dc..4ce2e96 100644 --- a/web/app/controllers/submission.php +++ b/web/app/controllers/submission.php @@ -168,7 +168,7 @@ if ($perm['manager_view']) { ?> -
+
echoContent() ?>
diff --git a/web/app/models/UOJRemoteProblem.php b/web/app/models/UOJRemoteProblem.php index deed5ee..c5addc2 100644 --- a/web/app/models/UOJRemoteProblem.php +++ b/web/app/models/UOJRemoteProblem.php @@ -42,7 +42,7 @@ class UOJRemoteProblem { 'short_name' => 'LOJ', 'url' => 'https://loj.ac', 'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java17', 'Pascal'], - 'submit_type' => ['bot'], + 'submit_type' => ['bot', 'archive'], ], 'luogu' => [ 'name' => '洛谷', diff --git a/web/app/models/UOJSubmissionLikeTrait.php b/web/app/models/UOJSubmissionLikeTrait.php index 5166ff6..4d05980 100644 --- a/web/app/models/UOJSubmissionLikeTrait.php +++ b/web/app/models/UOJSubmissionLikeTrait.php @@ -142,18 +142,33 @@ trait UOJSubmissionLikeTrait { return false; } + if ($content['remote_submission_id']) { + echo << +
+ 远程提交 +
+
+ 远程提交 ID:{$content['remote_submission_id']} +
+
+ EOD; + + return true; + } + $zip_file = new ZipArchive(); if ($zip_file->open(UOJContext::storagePath() . $content['file_name'], ZipArchive::RDONLY) !== true) { echo << -
- 提交内容 -
-
- 木有 -
- - EOD; +
+
+ 提交内容 +
+
+ 木有 +
+
+ EOD; return false; } diff --git a/web/js/uoj.js b/web/js/uoj.js index 9c0f3de..51992b1 100644 --- a/web/js/uoj.js +++ b/web/js/uoj.js @@ -932,11 +932,14 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { return this.each(function() { var input_submit_type_bot_id = 'input-submit_type_bot'; var input_submit_type_my_id = 'input-submit_type_my'; + var input_submit_type_archive_id = 'input-submit_type_archive'; var div_submit_type_bot_id = 'div-submit_type_bot'; var div_submit_type_my_id = 'div-submit_type_my'; + var div_submit_type_archive_id = 'div-submit_type_archive'; var input_submit_type_bot = $(''); var input_submit_type_my = $(''); + var input_submit_type_archive = $(''); var input_my_account_data = $(''); var my_account_validation_status = $('').append('待验证'); @@ -977,10 +980,24 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { .append($('
') .append('将使用您的账号提交本题。') .append('配置方法请查阅 使用教程') + ); + var div_submit_type_archive = $('
') + .append($('
') + .append('将从您给定的提交记录中抓取评测结果。') + .append('配置方法请查阅 使用教程') + ).append( + $('
') + .append($('
').append('')) + .append($('
').append('')) + .append($('
').append($('
').append('请填入远程 OJ 上的提交记录 ID。'))) + ); + var div_account_data = $('
') + .append($('
').append('远程账号信息')) + .append($('
') .append('账号状态:') .append(my_account_validation_status) .append(my_account_validation_btn) - ); + ); if ('localStorage' in window) { var prefer_submit_type = localStorage.getItem('uoj_remote_judge_save_prefer_submit_type__' + oj) || null; @@ -993,31 +1010,64 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { } input_submit_type_bot.click(function() { + div_account_data.hide('fast'); div_submit_type_my.hide('fast'); + div_submit_type_archive.hide('fast'); div_submit_type_bot.show('fast'); + $('#form-group-answer_answer').show('fast'); save_prefer_submit_type('bot'); }); input_submit_type_my.click(function() { div_submit_type_bot.hide('fast'); + div_submit_type_archive.hide('fast'); div_submit_type_my.show('fast'); + div_account_data.show('fast'); + $('#form-group-answer_answer').show('fast'); save_prefer_submit_type('my'); }); + input_submit_type_archive.click(function() { + div_submit_type_bot.hide('fast'); + div_submit_type_my.hide('fast'); + div_submit_type_archive.show('fast'); + div_account_data.show('fast'); + $('#form-group-answer_answer').hide('fast'); + save_prefer_submit_type('archive'); + }); if (submit_type[0] == 'bot') { + div_account_data.hide(); div_submit_type_my.hide(); + div_submit_type_archive.hide(); + div_submit_type_bot.show(); + $('#form-group-answer_answer').show(); input_submit_type_bot[0].checked = true; } else if (submit_type[0] == 'my') { div_submit_type_bot.hide(); + div_submit_type_my.show(); + div_submit_type_archive.hide(); + div_account_data.show(); + $('#form-group-answer_answer').show(); input_submit_type_my[0].checked = true; + } else if (submit_type[0] == 'archive') { + div_submit_type_bot.hide(); + div_submit_type_my.hide(); + div_submit_type_archive.show(); + div_account_data.show(); + $('#form-group-answer_answer').hide(); + input_submit_type_archive[0].checked = true; } if (submit_type.indexOf('bot') == -1) { input_submit_type_bot.attr('disabled', 'disabled'); } else if (prefer_submit_type == 'bot') { + div_account_data.hide(); div_submit_type_my.hide(); + div_submit_type_archive.hide(); div_submit_type_bot.show(); + $('#form-group-answer_answer').show(); input_submit_type_bot[0].checked = true; input_submit_type_my[0].checked = false; + input_submit_type_archive[0].checked = false; } if (submit_type.indexOf('my') == -1) { @@ -1025,8 +1075,25 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { } else if (prefer_submit_type == 'my') { div_submit_type_bot.hide(); div_submit_type_my.show(); + div_submit_type_archive.hide(); + div_account_data.show(); + $('#form-group-answer_answer').show(); input_submit_type_bot[0].checked = false; input_submit_type_my[0].checked = true; + input_submit_type_archive[0].checked = false; + } + + if (submit_type.indexOf('archive') == -1) { + input_submit_type_archive.attr('disabled', 'disabled'); + } else if (prefer_submit_type == 'archive') { + div_submit_type_bot.hide(); + div_submit_type_my.hide(); + div_submit_type_archive.show(); + div_account_data.show(); + $('#form-group-answer_answer').hide(); + input_submit_type_bot[0].checked = false; + input_submit_type_my[0].checked = false; + input_submit_type_archive[0].checked = true; } if (oj == 'luogu') { @@ -1083,7 +1150,7 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { }); } - div_submit_type_my.append( + div_account_data.append( $('
') .append($('
').append('')) .append($('
').append(input_luogu_uid)) @@ -1135,12 +1202,61 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { }); } - div_submit_type_my.append( + div_account_data.append( $('
') .append($('
').append('')) .append($('
').append(input_codeforces_jsessionid)) .append($('
').append($('
').append('请填入 Cookie 中的 JSESSIONID。'))) ).append(input_my_account_data); + } else if (oj == 'loj') { + var loj_account_data = {username: "", token: ""}; + var input_loj_token = $(''); + + if ('localStorage' in window) { + try { + var loj_account_data_str = localStorage.getItem('uoj_remote_judge_loj_account_data'); + if (loj_account_data_str) { + loj_account_data = JSON.parse(loj_account_data_str); + } + } catch (e) {} + + var save_loj_account_data = function() { + localStorage.setItem('uoj_remote_judge_loj_account_data', JSON.stringify(loj_account_data)); + } + } else { + var save_loj_account_data = function() {}; + } + + input_loj_token.change(function() { + loj_account_data.token = $(this).val(); + input_my_account_data.val(JSON.stringify(loj_account_data)); + save_loj_account_data(); + my_account_validation_status.html('待验证'); + }); + + my_account_validation_btn.click(function() { + validate_my_account({ + type: 'loj', + token: input_loj_token.val(), + }); + }); + + input_my_account_data.val(JSON.stringify(loj_account_data)); + input_loj_token.val(loj_account_data.token); + + if (loj_account_data.token) { + validate_my_account({ + type: 'loj', + token: loj_account_data.token, + }); + } + + div_account_data.append( + $('
') + .append($('
').append('')) + .append($('
').append(input_loj_token)) + .append($('
').append($('
').append('请前往 LibreOJ 登录账号,然后输入在控制台中运行 console.log(JSON.parse(localStorage.appState).token) 的输出结果。'))) + ).append(input_my_account_data); } $(this).append( @@ -1152,8 +1268,16 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { $('
') .append(input_submit_type_my) .append($('