diff --git a/remote_judger/package-lock.json b/remote_judger/package-lock.json index d66645f..ee07390 100644 --- a/remote_judger/package-lock.json +++ b/remote_judger/package-lock.json @@ -12,6 +12,7 @@ "crlf-normalize": "^1.0.18", "fs-extra": "^11.1.0", "jsdom": "^21.0.0", + "lodash.flattendeep": "^4.4.0", "math-sum": "^2.0.0", "reggol": "^1.3.4", "superagent": "^8.0.6", @@ -21,6 +22,7 @@ "@types/fs-extra": "^11.0.1", "@types/js-yaml": "^4.0.5", "@types/jsdom": "^20.0.1", + "@types/lodash.flattendeep": "^4.4.7", "@types/node": "^18.11.18", "@types/superagent": "^4.1.16", "@types/superagent-proxy": "^3.0.0", @@ -78,6 +80,21 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "dev": true + }, + "node_modules/@types/lodash.flattendeep": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.7.tgz", + "integrity": "sha512-1h6GW/AeZw/Wej6uxrqgmdTDZX1yFS39lRsXYkg+3kWvOWWrlGCI6H7lXxlUHOzxDT4QeYGmgPpQ3BX9XevzOg==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", @@ -790,6 +807,11 @@ "node": ">= 0.8.0" } }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1567,6 +1589,21 @@ "@types/node": "*" } }, + "@types/lodash": { + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "dev": true + }, + "@types/lodash.flattendeep": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.7.tgz", + "integrity": "sha512-1h6GW/AeZw/Wej6uxrqgmdTDZX1yFS39lRsXYkg+3kWvOWWrlGCI6H7lXxlUHOzxDT4QeYGmgPpQ3BX9XevzOg==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", @@ -2113,6 +2150,11 @@ "type-check": "~0.3.2" } }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", diff --git a/remote_judger/package.json b/remote_judger/package.json index 86ff0e9..3d1d50d 100644 --- a/remote_judger/package.json +++ b/remote_judger/package.json @@ -15,6 +15,7 @@ "crlf-normalize": "^1.0.18", "fs-extra": "^11.1.0", "jsdom": "^21.0.0", + "lodash.flattendeep": "^4.4.0", "math-sum": "^2.0.0", "reggol": "^1.3.4", "superagent": "^8.0.6", @@ -24,6 +25,7 @@ "@types/fs-extra": "^11.0.1", "@types/js-yaml": "^4.0.5", "@types/jsdom": "^20.0.1", + "@types/lodash.flattendeep": "^4.4.7", "@types/node": "^18.11.18", "@types/superagent": "^4.1.16", "@types/superagent-proxy": "^3.0.0", diff --git a/remote_judger/src/daemon.ts b/remote_judger/src/daemon.ts index 0e88968..bfa52c5 100644 --- a/remote_judger/src/daemon.ts +++ b/remote_judger/src/daemon.ts @@ -7,6 +7,7 @@ import * as TIME from './utils/time'; import { apply } from './vjudge'; import path from 'path'; import child from 'child_process'; +import htmlspecialchars from './utils/htmlspecialchars'; proxy(superagent); @@ -146,7 +147,8 @@ export default async function daemon(config: UOJConfig) { config.remote_problem_id, config.answer_language, code, - judge_time + judge_time, + config ); } catch (err) { await request('/submit', { @@ -157,7 +159,7 @@ export default async function daemon(config: UOJConfig) { status: 'Judged', score: 0, error: 'Judgment Failed', - details: `No details.`, + details: `${htmlspecialchars(err.message)}`, }), judge_time, }); diff --git a/remote_judger/src/interface.ts b/remote_judger/src/interface.ts index 77af4c5..579d507 100644 --- a/remote_judger/src/interface.ts +++ b/remote_judger/src/interface.ts @@ -1,8 +1,8 @@ export interface RemoteAccount { type: string; cookie?: string[]; - handle: string; - password: string; + handle?: string; + password?: string; endpoint?: string; proxy?: string; } diff --git a/remote_judger/src/providers/atcoder.ts b/remote_judger/src/providers/atcoder.ts index f96bde7..757de3a 100644 --- a/remote_judger/src/providers/atcoder.ts +++ b/remote_judger/src/providers/atcoder.ts @@ -73,6 +73,10 @@ export default class AtcoderProvider implements IBasicProvider { this.account.endpoint ||= 'https://atcoder.jp'; } + static constructFromAccountData(data) { + throw new Error('Method not implemented.'); + } + cookie: string[] = ['language=en']; csrf: string; diff --git a/remote_judger/src/providers/codeforces.ts b/remote_judger/src/providers/codeforces.ts index 1131c4b..76eb301 100644 --- a/remote_judger/src/providers/codeforces.ts +++ b/remote_judger/src/providers/codeforces.ts @@ -87,6 +87,10 @@ export default class CodeforcesProvider implements IBasicProvider { this.account.endpoint ||= 'https://codeforces.com'; } + static constructFromAccountData(data) { + throw new Error('Method not implemented.'); + } + cookie: string[] = []; csrf: string; diff --git a/remote_judger/src/providers/loj.ts b/remote_judger/src/providers/loj.ts index cd79213..793b319 100644 --- a/remote_judger/src/providers/loj.ts +++ b/remote_judger/src/providers/loj.ts @@ -156,6 +156,10 @@ export default class LibreojProvider implements IBasicProvider { this.account.endpoint ||= 'https://api.loj.ac.cn/api'; } + static constructFromAccountData(data) { + throw new Error('Method not implemented.'); + } + get(url: string) { logger.debug('get', url); if (!url.includes('//')) url = `${this.account.endpoint}${url}`; diff --git a/remote_judger/src/providers/luogu.ts b/remote_judger/src/providers/luogu.ts new file mode 100644 index 0000000..11902cb --- /dev/null +++ b/remote_judger/src/providers/luogu.ts @@ -0,0 +1,350 @@ +import { JSDOM } from 'jsdom'; +import superagent from 'superagent'; +import proxy from 'superagent-proxy'; +import Logger from '../utils/logger'; +import { IBasicProvider, RemoteAccount, USER_AGENT } from '../interface'; +import sleep from '../utils/sleep'; +import flattenDeep from 'lodash.flattendeep'; +import htmlspecialchars from '../utils/htmlspecialchars'; + +proxy(superagent); +const logger = new Logger('remote/luogu'); + +const STATUS_MAP = [ + 'Waiting', // WAITING, + 'Judging', // JUDGING, + 'Compile Error', // CE + 'Output Limit Exceeded', // OLE + 'Memory Limit Exceeded', // MLE + 'Time Limit Exceeded', // TLE + 'Wrong Answer', // WA + 'Runtime Error', // RE + 0, + 0, + 0, + 'Judgment Failed', // UKE + 'Accepted', // AC + 0, + 'Wrong Answer', // WA +]; + +const LANGS_MAP = { + C: { + id: 2, + name: 'C', + comment: '//', + }, + 'C++98': { + id: 3, + name: 'C++98', + comment: '//', + }, + 'C++11': { + id: 4, + name: 'C++11', + comment: '//', + }, + 'C++': { + id: 11, + name: 'C++14', + comment: '//', + }, + 'C++17': { + id: 12, + name: 'C++17', + comment: '//', + }, + 'C++20': { + id: 27, + name: 'C++20', + comment: '//', + }, + Python3: { + id: 7, + name: 'Python 3', + comment: '#', + }, + Java8: { + id: 8, + name: 'Java 8', + comment: '//', + }, + Pascal: { + id: 1, + name: 'Pascal', + comment: '//', + }, +}; + +function buildLuoguTestCaseInfoBlock(test) { + let res = ''; + + res += ``; + res += `${htmlspecialchars(test.description || '')}`; + res += ''; + + return res; +} + +export default class LuoguProvider implements IBasicProvider { + constructor(public account: RemoteAccount) { + if (account.cookie) this.cookie = account.cookie; + } + + static constructFromAccountData(data) { + return new this({ + type: 'luogu', + cookie: Object.entries(data).map(([key, value]) => `${key}=${value}`), + }); + } + + cookie: string[] = []; + csrf: string; + + get(url: string) { + logger.debug('get', url, this.cookie); + + if (!url.includes('//')) + url = `${this.account.endpoint || 'https://www.luogu.com.cn'}${url}`; + + const req = superagent + .get(url) + .set('Cookie', this.cookie) + .set('User-Agent', USER_AGENT); + + if (this.account.proxy) return req.proxy(this.account.proxy); + + return req; + } + + async safeGet(url: string) { + const res = await this.get(url); + + if (res.text.startsWith(' + EOD); + } + $answer_form->extra_validator = $submission_extra_validator; $answer_form->succ_href = $is_participating ? '/contest/' . UOJContest::info('id') . '/submissions' : '/submissions'; $answer_form->runAtServer(); diff --git a/web/app/controllers/problem_statement_manage.php b/web/app/controllers/problem_statement_manage.php index 3ccc75b..3b3353a 100644 --- a/web/app/controllers/problem_statement_manage.php +++ b/web/app/controllers/problem_statement_manage.php @@ -118,7 +118,7 @@ if (UOJProblem::info('type') == 'remote') { EOD); $re_crawl_form->config['submit_button']['text'] = '重新爬取'; - $re_crawl_form->handle = function () use ($remote_online_judge, $remote_problem_id, $remote_provider) { + $re_crawl_form->handle = function () use ($remote_online_judge, $remote_problem_id) { try { $data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id); } catch (Exception $e) { @@ -134,16 +134,9 @@ if (UOJProblem::info('type') == 'remote') { $data['difficulty'] = UOJProblem::info('difficulty'); } - $submission_requirement = [ - [ - "name" => "answer", - "type" => "source code", - "file_name" => "answer.code", - "languages" => $remote_provider['languages'], - ] - ]; + $submission_requirement = UOJRemoteProblem::getSubmissionRequirements($remote_online_judge); $enc_submission_requirement = json_encode($submission_requirement); - + $extra_config = [ 'remote_online_judge' => $remote_online_judge, 'remote_problem_id' => $remote_problem_id, diff --git a/web/app/libs/uoj-form-lib.php b/web/app/libs/uoj-form-lib.php index aef5409..ddde655 100644 --- a/web/app/libs/uoj-form-lib.php +++ b/web/app/libs/uoj-form-lib.php @@ -46,6 +46,7 @@ function newAddDelCmdForm($form_name, $validate, $handle, $final = null) { function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle) { $form = new UOJForm($form_name); + foreach ($requirement as $req) { if ($req['type'] == "source code") { $languages = UOJLang::getAvailableLanguages(isset($req['languages']) ? $req['languages'] : null); @@ -74,6 +75,7 @@ function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle $content = []; $content['file_name'] = $zip_file_name; $content['config'] = []; + foreach ($requirement as $req) { if ($req['type'] == "source code") { $content['config'][] = ["{$req['name']}_language", $_POST["{$form_name}_{$req['name']}_language"]]; diff --git a/web/app/libs/uoj-html-lib.php b/web/app/libs/uoj-html-lib.php index 0fa2ddb..c874368 100644 --- a/web/app/libs/uoj-html-lib.php +++ b/web/app/libs/uoj-html-lib.php @@ -403,7 +403,7 @@ function echoSubmissionContent($submission, $requirement) { echo ''; echo ''; echo ''; - } elseif ($req['type'] == "text") { + } else if ($req['type'] == "text") { $file_content = $zip_file->getFromName("{$req['file_name']}", 504); $file_content = strOmit($file_content, 500); $file_content = uojTextEncode($file_content, array('allow_CR' => true, 'html_escape' => true)); diff --git a/web/app/libs/uoj-validate-lib.php b/web/app/libs/uoj-validate-lib.php index 426cc92..82b2d95 100644 --- a/web/app/libs/uoj-validate-lib.php +++ b/web/app/libs/uoj-validate-lib.php @@ -79,3 +79,7 @@ function is_short_string($str) { function validateCodeforcesProblemId($str) { return preg_match('/(|GYM)[1-9][0-9]{0,5}[A-Z][1-9]?/', $str) !== true; } + +function validateLuoguProblemId($str) { + return preg_match('/P[1-9][0-9]{4,5}/', $str) !== true; +} diff --git a/web/app/models/UOJForm.php b/web/app/models/UOJForm.php index 55882e4..ad3a541 100644 --- a/web/app/models/UOJForm.php +++ b/web/app/models/UOJForm.php @@ -665,6 +665,7 @@ class UOJForm { EOD; } else { echo <<form_name}").addClass('disabled'); return ok; EOD; } diff --git a/web/app/models/UOJRemoteProblem.php b/web/app/models/UOJRemoteProblem.php index 11e5fe8..636984a 100644 --- a/web/app/models/UOJRemoteProblem.php +++ b/web/app/models/UOJRemoteProblem.php @@ -14,6 +14,7 @@ class UOJRemoteProblem { 'ограничение по времени на тест', ], 'languages' => ['C', 'C++', 'C++17', 'C++20', 'Java17', 'Pascal', 'Python2', 'Python3'], + 'submit_type' => ['bot'], ], 'atcoder' => [ 'name' => 'AtCoder', @@ -24,6 +25,7 @@ class UOJRemoteProblem { '指定されたタスクが見つかりません', ], 'languages' => ['C', 'C++', 'Java11', 'Python3', 'Pascal'], + 'submit_type' => ['bot'], ], 'uoj' => [ 'name' => 'UniversalOJ', @@ -33,12 +35,21 @@ class UOJRemoteProblem { '未找到该页面', ], 'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java8', 'Java11', 'Java17', 'Pascal'], + 'submit_type' => ['bot'], ], 'loj' => [ 'name' => 'LibreOJ', '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'], + ], + 'luogu' => [ + 'name' => '洛谷', + 'short_name' => '洛谷', + 'url' => 'https://www.luogu.com.cn', + 'languages' => ['C', 'C++98', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Java8', 'Pascal'], + 'submit_type' => ['my'], ], ]; @@ -90,6 +101,10 @@ class UOJRemoteProblem { return static::$providers['loj']['url'] . '/p/' . $id; } + static function getLuoguProblemUrl($id) { + return static::$providers['luogu']['url'] . '/problem/' . $id; + } + static function getCodeforcesProblemBasicInfoFromHtml($id, $html) { $remote_provider = static::$providers['codeforces']; @@ -394,6 +409,72 @@ class UOJRemoteProblem { ]; } + static function getLuoguProblemBasicInfo($id) { + $remote_provider = static::$providers['luogu']; + $res = static::curl_get(static::getLuoguProblemUrl($id) . '?_contentOnly=1'); + + if (!$res) return null; + + // Convert stdClass to array + $res = json_decode(json_encode($res['response']), true); + + if (!isset($res['code']) || $res['code'] != 200) return null; + + $problem = $res['currentData']['problem']; + $statement = ''; + + if ($problem['background']) { + $statement .= "\n### 题目背景\n\n"; + $statement .= $problem['background'] . "\n"; + } + + $statement .= "\n### 题目描述\n\n"; + $statement .= $problem['description'] . "\n"; + + $statement .= "\n### 输入格式\n\n"; + $statement .= $problem['inputFormat'] . "\n"; + + $statement .= "\n### 输出格式\n\n"; + $statement .= $problem['outputFormat'] . "\n"; + + $statement .= "\n### 输入输出样例\n\n"; + + foreach ($problem['samples'] as $id => $sample) { + $display_sample_id = $id + 1; + + $statement .= "\n#### 样例输入 #{$display_sample_id}\n\n"; + $statement .= "\n```text\n{$sample[0]}\n```\n\n"; + + $statement .= "\n#### 样例输出 #{$display_sample_id}\n\n"; + $statement .= "\n```text\n{$sample[1]}\n```\n\n"; + } + + $statement .= "\n### 说明/提示\n\n"; + $statement .= $problem['hint'] . "\n"; + + return [ + 'type' => 'html', + 'title' => "【{$remote_provider['short_name']}{$problem['pid']}】{$problem['title']}", + 'time_limit' => (float)max($problem['limits']['time']) / 1000.0, + 'memory_limit' => (float)max($problem['limits']['memory']) / 1024.0, + 'difficulty' => -1, + 'statement' => HTML::parsedown()->text($statement), + ]; + } + + public static function getSubmissionRequirements($oj) { + $remote_provider = UOJRemoteProblem::$providers[$oj]; + + return [ + [ + "name" => "answer", + "type" => "source code", + "file_name" => "answer.code", + "languages" => $remote_provider['languages'], + ] + ]; + } + public static function getProblemRemoteUrl($oj, $id) { if ($oj === 'codeforces') { return static::getCodeforcesProblemUrl($id); @@ -403,6 +484,8 @@ class UOJRemoteProblem { return static::getUojProblemUrl($id); } else if ($oj === 'loj') { return static::getLojProblemUrl($id); + } else if ($oj === 'luogu') { + return static::getLuoguProblemUrl($id); } return null; @@ -418,6 +501,8 @@ class UOJRemoteProblem { return static::getUojProblemBasicInfo($id); } else if ($oj === 'loj') { return static::getLojProblemBasicInfo($id); + } else if ($oj === 'luogu') { + return static::getLuoguProblemBasicInfo($id); } return null; diff --git a/web/app/models/UOJSubmission.php b/web/app/models/UOJSubmission.php index 8f5e341..6ae9327 100644 --- a/web/app/models/UOJSubmission.php +++ b/web/app/models/UOJSubmission.php @@ -418,6 +418,10 @@ class UOJSubmission { } public function userCanRejudge(array $user = null) { + if ($this->getContent('no_rejudge')) { + return false; + } + if (isSuperUser($user)) { return true; } diff --git a/web/js/uoj.js b/web/js/uoj.js index 41bf66a..3e1f1c2 100644 --- a/web/js/uoj.js +++ b/web/js/uoj.js @@ -947,6 +947,110 @@ $.fn.text_file_form_group = function(name, text) { }); } +// remote judge submit type group +$.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 div_submit_type_bot_id = 'div-submit_type_bot'; + var div_submit_type_my_id = 'div-submit_type_my'; + + var input_submit_type_bot = $(''); + var input_submit_type_my = $(''); + var input_my_account_data = $(''); + + var div_submit_type_bot = $('
') + .append('
将使用公用账号提交本题。
'); + var div_submit_type_my = $('
') + .append('
将使用您的账号提交本题。
'); + + input_submit_type_bot.click(function() { + div_submit_type_my.hide('fast'); + div_submit_type_bot.show('fast'); + }); + input_submit_type_my.click(function() { + div_submit_type_bot.hide('fast'); + div_submit_type_my.show('fast'); + }); + + if (submit_type[0] == 'bot') { + div_submit_type_my.hide(); + input_submit_type_bot[0].checked = true; + } else if (submit_type[0] == 'my') { + div_submit_type_bot.hide(); + input_submit_type_my[0].checked = true; + } + + if (submit_type.indexOf('bot') == -1) { + input_submit_type_bot.attr('disabled', 'disabled'); + } + if (submit_type.indexOf('my') == -1) { + input_submit_type_my.attr('disabled', 'disabled'); + } + + if (oj == 'luogu') { + var luogu_account_data = {"_uid": "", "__client_id": ""}; + var input_luogu_uid = $(''); + var input_luogu_client_id = $(''); + + if ('localStorage' in window) { + try { + var luogu_account_data_str = localStorage.getItem('uoj_remote_judge_luogu_account_data'); + if (luogu_account_data_str) { + luogu_account_data = JSON.parse(luogu_account_data_str); + } + } catch (e) {} + + var save_luogu_account_data = function() { + localStorage.setItem('uoj_remote_judge_luogu_account_data', JSON.stringify(luogu_account_data)); + } + } else { + var save_luogu_account_data = function() {}; + } + + input_luogu_uid.change(function() { + luogu_account_data._uid = $(this).val(); + input_my_account_data.val(JSON.stringify(luogu_account_data)); + save_luogu_account_data(); + }); + + input_luogu_client_id.change(function() { + luogu_account_data.__client_id = $(this).val(); + input_my_account_data.val(JSON.stringify(luogu_account_data)); + save_luogu_account_data(); + }); + + input_my_account_data.val(JSON.stringify(luogu_account_data)); + input_luogu_uid.val(luogu_account_data._uid); + input_luogu_client_id.val(luogu_account_data.__client_id); + + div_submit_type_my.append( + $('
') + .append($('
').append('')) + .append($('
').append(input_luogu_uid)) + .append($('
').append($('
').append('请填入 Cookie 中的 _uid。'))) + ).append( + $('
') + .append($('
').append('')) + .append($('
').append(input_luogu_client_id)) + .append($('
').append($('
').append('请填入 Cookie 中的 __client_id。'))) + ).append(input_my_account_data); + } + + $(this).append( + $('
').append( + $('
') + .append(input_submit_type_bot) + .append($('