feat: Remote Judge for Luogu (#34)
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Baoshuo Ren 2023-02-03 13:59:31 +08:00 committed by GitHub
commit dfc57f13d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 779 additions and 92 deletions

View File

@ -12,6 +12,7 @@
"crlf-normalize": "^1.0.18", "crlf-normalize": "^1.0.18",
"fs-extra": "^11.1.0", "fs-extra": "^11.1.0",
"jsdom": "^21.0.0", "jsdom": "^21.0.0",
"lodash.flattendeep": "^4.4.0",
"math-sum": "^2.0.0", "math-sum": "^2.0.0",
"reggol": "^1.3.4", "reggol": "^1.3.4",
"superagent": "^8.0.6", "superagent": "^8.0.6",
@ -21,6 +22,7 @@
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/jsdom": "^20.0.1", "@types/jsdom": "^20.0.1",
"@types/lodash.flattendeep": "^4.4.7",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/superagent": "^4.1.16", "@types/superagent": "^4.1.16",
"@types/superagent-proxy": "^3.0.0", "@types/superagent-proxy": "^3.0.0",
@ -78,6 +80,21 @@
"@types/node": "*" "@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": { "node_modules/@types/node": {
"version": "18.11.18", "version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
@ -790,6 +807,11 @@
"node": ">= 0.8.0" "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": { "node_modules/lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -1567,6 +1589,21 @@
"@types/node": "*" "@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": { "@types/node": {
"version": "18.11.18", "version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
@ -2113,6 +2150,11 @@
"type-check": "~0.3.2" "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": { "lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",

View File

@ -15,6 +15,7 @@
"crlf-normalize": "^1.0.18", "crlf-normalize": "^1.0.18",
"fs-extra": "^11.1.0", "fs-extra": "^11.1.0",
"jsdom": "^21.0.0", "jsdom": "^21.0.0",
"lodash.flattendeep": "^4.4.0",
"math-sum": "^2.0.0", "math-sum": "^2.0.0",
"reggol": "^1.3.4", "reggol": "^1.3.4",
"superagent": "^8.0.6", "superagent": "^8.0.6",
@ -24,6 +25,7 @@
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/jsdom": "^20.0.1", "@types/jsdom": "^20.0.1",
"@types/lodash.flattendeep": "^4.4.7",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/superagent": "^4.1.16", "@types/superagent": "^4.1.16",
"@types/superagent-proxy": "^3.0.0", "@types/superagent-proxy": "^3.0.0",

View File

@ -7,6 +7,7 @@ import * as TIME from './utils/time';
import { apply } from './vjudge'; import { apply } from './vjudge';
import path from 'path'; import path from 'path';
import child from 'child_process'; import child from 'child_process';
import htmlspecialchars from './utils/htmlspecialchars';
proxy(superagent); proxy(superagent);
@ -146,7 +147,8 @@ export default async function daemon(config: UOJConfig) {
config.remote_problem_id, config.remote_problem_id,
config.answer_language, config.answer_language,
code, code,
judge_time judge_time,
config
); );
} catch (err) { } catch (err) {
await request('/submit', { await request('/submit', {
@ -157,7 +159,7 @@ export default async function daemon(config: UOJConfig) {
status: 'Judged', status: 'Judged',
score: 0, score: 0,
error: 'Judgment Failed', error: 'Judgment Failed',
details: `<error>No details.</error>`, details: `<error>${htmlspecialchars(err.message)}</error>`,
}), }),
judge_time, judge_time,
}); });

View File

@ -1,8 +1,8 @@
export interface RemoteAccount { export interface RemoteAccount {
type: string; type: string;
cookie?: string[]; cookie?: string[];
handle: string; handle?: string;
password: string; password?: string;
endpoint?: string; endpoint?: string;
proxy?: string; proxy?: string;
} }

View File

@ -73,6 +73,10 @@ export default class AtcoderProvider implements IBasicProvider {
this.account.endpoint ||= 'https://atcoder.jp'; this.account.endpoint ||= 'https://atcoder.jp';
} }
static constructFromAccountData(data) {
throw new Error('Method not implemented.');
}
cookie: string[] = ['language=en']; cookie: string[] = ['language=en'];
csrf: string; csrf: string;

View File

@ -87,6 +87,10 @@ export default class CodeforcesProvider implements IBasicProvider {
this.account.endpoint ||= 'https://codeforces.com'; this.account.endpoint ||= 'https://codeforces.com';
} }
static constructFromAccountData(data) {
throw new Error('Method not implemented.');
}
cookie: string[] = []; cookie: string[] = [];
csrf: string; csrf: string;

View File

@ -156,6 +156,10 @@ export default class LibreojProvider implements IBasicProvider {
this.account.endpoint ||= 'https://api.loj.ac.cn/api'; this.account.endpoint ||= 'https://api.loj.ac.cn/api';
} }
static constructFromAccountData(data) {
throw new Error('Method not implemented.');
}
get(url: string) { get(url: string) {
logger.debug('get', url); logger.debug('get', url);
if (!url.includes('//')) url = `${this.account.endpoint}${url}`; if (!url.includes('//')) url = `${this.account.endpoint}${url}`;

View File

@ -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 += `<test num="${test.id + 1}" info="${STATUS_MAP[test.status]}" time="${
test.time || -1
}" memory="${test.memory || -1}" score="${test.score || ''}">`;
res += `<res>${htmlspecialchars(test.description || '')}</res>`;
res += '</test>';
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('<html><script>document.location.reload()')) {
const sec = this.getCookie.call(
{ cookie: res.header['set-cookie'] },
'sec'
);
this.setCookie('sec', sec);
logger.debug('sec', sec);
return await this.get(url);
}
return res;
}
post(url: string) {
logger.debug('post', url, this.cookie);
if (!url.includes('//'))
url = `${this.account.endpoint || 'https://www.luogu.com.cn'}${url}`;
const req = superagent
.post(url)
.set('Cookie', this.cookie)
.set('x-csrf-token', this.csrf)
.set('User-Agent', USER_AGENT)
.set('x-requested-with', 'XMLHttpRequest')
.set('origin', 'https://www.luogu.com.cn');
if (this.account.proxy) return req.proxy(this.account.proxy);
return req;
}
getCookie(target: string) {
return this.cookie
.find(i => i.startsWith(`${target}=`))
?.split('=')[1]
?.split(';')[0];
}
setCookie(target: string, value: string) {
this.cookie = this.cookie.filter(i => !i.startsWith(`${target}=`));
this.cookie.push(`${target}=${value}`);
}
async getCsrfToken(url: string) {
let { text: html } = await this.safeGet(url);
const $dom = new JSDOM(html);
this.csrf = $dom.window.document
.querySelector('meta[name="csrf-token"]')
.getAttribute('content');
logger.info('csrf-token=', this.csrf);
}
get loggedIn() {
return this.safeGet('/user/setting?_contentOnly=1').then(
({ body }) => body.currentTemplate !== 'AuthLogin'
);
}
async ensureLogin() {
if (await this.loggedIn) {
await this.getCsrfToken('/user/setting');
return true;
}
logger.info('retry login');
// TODO login;
return false;
}
async submitProblem(
id: string,
lang: string,
code: string,
submissionId: number,
next,
end
) {
if (!(await this.ensureLogin())) {
end({
error: true,
status: 'Judgment Failed',
message: 'Login failed',
});
return null;
}
if (code.length < 10) {
end({
error: true,
status: 'Compile Error',
message: 'Code too short',
});
return null;
}
const programType = LANGS_MAP[lang] || LANGS_MAP['C++'];
const comment = programType.comment;
if (comment) {
const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`;
if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`;
else if (comment instanceof Array)
code = `${comment[0]} ${msg} ${comment[1]}\n${code}`;
}
const result = await this.post(`/fe/api/problem/submit/${id}`)
.set('referer', `https://www.luogu.com.cn/problem/${id}`)
.send({
code,
lang: programType.id,
enableO2: 1,
});
logger.info('RecordID:', result.body.rid);
return result.body.rid;
}
async waitForSubmission(problem_id: string, id: string, next, end) {
let fail = 0;
let count = 0;
while (count < 120 && fail < 5) {
await sleep(1500);
count++;
try {
const { body } = await this.safeGet(`/record/${id}?_contentOnly=1`);
const data = body.currentData.record;
if (
data.detail.compileResult &&
data.detail.compileResult.success === false
) {
return await end({
error: true,
id: `R${id}`,
status: 'Compile Error',
message: data.detail.compileResult.message,
});
}
logger.info('Fetched with length', JSON.stringify(body).length);
const total = flattenDeep(
Object.entries(body.currentData.testCaseGroup || {}).map(o => o[1])
).length;
if (!data.detail.judgeResult?.subtasks) continue;
await next({
status: `Judging (${
data.detail.judgeResult?.finishedCaseCount || '?'
}/${total})`,
});
if (data.status < 2) continue;
logger.info('RecordID:', id, 'done');
const status = STATUS_MAP[data.status];
let details = '';
details += `<info-block>REMOTE_SUBMISSION_ID = ${id}\nVERDICT = ${status}</info-block>`;
if (data.detail.judgeResult.subtasks.length === 1) {
details += Object.entries(
data.detail.judgeResult.subtasks[0].testCases
)
.map(o => o[1])
.map(buildLuoguTestCaseInfoBlock)
.join('\n');
} else {
details += Object.entries(data.detail.judgeResult.subtasks)
.map(o => o[1])
.map(
(subtask: any, index) =>
`<subtask num="${index}" info="${
STATUS_MAP[subtask.status]
}" time="${subtask.time || -1}" memory="${
subtask.memory || -1
}" score="${subtask.score || ''}">${Object.entries(
subtask.testCases
)
.map(o => o[1])
.map(buildLuoguTestCaseInfoBlock)
.join('\n')}</subtask>`
)
.join('\n');
}
return await end({
id: `R${id}`,
status,
score:
status === 'Accepted'
? 100
: (data.score / data.problem.fullScore) * 100,
time: data.time,
memory: data.memory,
details: `<div>${details}</div>`,
});
} catch (e) {
logger.error(e);
fail++;
}
}
return await end({
error: true,
id: `R${id}`,
status: 'Judgment Failed',
message: 'Failed to fetch submission details.',
});
}
}

View File

@ -99,6 +99,10 @@ export default class UOJProvider implements IBasicProvider {
if (account.cookie) this.cookie = account.cookie; if (account.cookie) this.cookie = account.cookie;
} }
static constructFromAccountData(data) {
throw new Error('Method not implemented.');
}
cookie: string[] = []; cookie: string[] = [];
csrf: string; csrf: string;

View File

@ -8,11 +8,7 @@ const logger = new Logger('vjudge');
class AccountService { class AccountService {
api: IBasicProvider; api: IBasicProvider;
constructor( constructor(public Provider: BasicProvider, public account: RemoteAccount) {
public Provider: BasicProvider,
public account: RemoteAccount,
private request: any
) {
this.api = new Provider(account); this.api = new Provider(account);
this.main().catch(e => this.main().catch(e =>
logger.error(`Error occured in ${account.type}/${account.handle}`, e) logger.error(`Error occured in ${account.type}/${account.handle}`, e)
@ -24,7 +20,81 @@ class AccountService {
problem_id: string, problem_id: string,
language: string, language: string,
code: string, code: string,
judge_time: string next,
end
) {
try {
const rid = await this.api.submitProblem(
problem_id,
language,
code,
id,
next,
end
);
if (!rid) return;
await this.api.waitForSubmission(problem_id, rid, next, end);
} catch (e) {
logger.error(e);
await end({ error: true, status: 'Judgment Failed', message: e.message });
}
}
async login() {
const login = await this.api.ensureLogin();
if (login === true) {
logger.info(`${this.account.type}/${this.account.handle}: logged in`);
return true;
}
logger.warn(
`${this.account.type}/${this.account.handle}: login fail`,
login || ''
);
return false;
}
async main() {
const res = await this.login();
if (!res) return;
setInterval(() => this.login(), Time.hour);
}
}
class VJudge {
private p_imports: Record<string, any> = {};
private providers: Record<string, AccountService> = {};
constructor(private request: any) {}
async importProvider(type: string) {
if (this.p_imports[type]) throw new Error(`duplicate provider ${type}`);
const provider = await import(`./providers/${type}`);
this.p_imports[type] = provider.default;
}
async addProvider(type: string) {
if (this.p_imports[type]) throw new Error(`duplicate provider ${type}`);
const provider = await import(`./providers/${type}`);
const account = provider.getAccountInfoFromEnv();
if (!account) throw new Error(`no account info for ${type}`);
this.p_imports[type] = provider.default;
this.providers[type] = new AccountService(provider.default, account);
}
async judge(
id: number,
type: string,
problem_id: string,
language: string,
code: string,
judge_time: string,
config
) { ) {
const next = async payload => { const next = async payload => {
return await this.request('/submit', { return await this.request('/submit', {
@ -69,16 +139,35 @@ class AccountService {
details: details:
payload.details || payload.details ||
'<div>' + '<div>' +
`<info-block>ID = ${payload.id || 'None'}</info-block>` + `<info-block>REMOTE_SUBMISSION_ID = ${
`<info-block>VERDICT = ${payload.status}</info-block>` + payload.id || 'None'
}\nVERDICT = ${payload.status}</info-block>` +
'</div>', '</div>',
}), }),
judge_time, judge_time,
}); });
}; };
if (!config.remote_submit_type || config.remote_submit_type == 'bot') {
if (!this.providers[type]) throw new Error(`No provider ${type}`);
await this.providers[type].judge(
id,
problem_id,
language,
code,
next,
end
);
} else if (config.remote_submit_type == 'my') {
if (!this.p_imports[type]) throw new Error(`No provider ${type}`);
try { try {
const rid = await this.api.submitProblem( const provider = this.p_imports[type].constructFromAccountData(
JSON.parse(config.remote_account_data)
);
const rid = await provider.submitProblem(
problem_id, problem_id,
language, language,
code, code,
@ -89,63 +178,21 @@ class AccountService {
if (!rid) return; if (!rid) return;
await this.api.waitForSubmission(problem_id, rid, next, end); await provider.waitForSubmission(problem_id, rid, next, end);
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
await end({ error: true, status: 'Judgment Failed', message: e.message });
await end({
error: true,
status: 'Judgment Failed',
message: e.message,
});
} }
} } else {
throw new Error(
async login() { 'Unsupported remote submit type: ' + config.remote_submit_type
const login = await this.api.ensureLogin();
if (login === true) {
logger.info(`${this.account.type}/${this.account.handle}: logged in`);
return true;
}
logger.warn(
`${this.account.type}/${this.account.handle}: login fail`,
login || ''
);
return false;
}
async main() {
const res = await this.login();
if (!res) return;
setInterval(() => this.login(), Time.hour);
}
}
class VJudge {
private providers: Record<string, AccountService> = {};
constructor(private request: any) {}
async addProvider(type: string) {
if (this.providers[type]) throw new Error(`duplicate provider ${type}`);
const provider = await import(`./providers/${type}`);
const account = provider.getAccountInfoFromEnv();
if (!account) throw new Error(`no account info for ${type}`);
this.providers[type] = new AccountService(
provider.default,
account,
this.request
); );
} }
async judge(
id: number,
type: string,
problem_id: string,
language: string,
code: string,
judge_time: string
) {
if (!this.providers[type]) throw new Error(`no provider ${type}`);
await this.providers[type].judge(id, problem_id, language, code, judge_time);
} }
} }
@ -156,6 +203,7 @@ export async function apply(request: any) {
await vjudge.addProvider('atcoder'); await vjudge.addProvider('atcoder');
await vjudge.addProvider('uoj'); await vjudge.addProvider('uoj');
await vjudge.addProvider('loj'); await vjudge.addProvider('loj');
await vjudge.importProvider('luogu');
return vjudge; return vjudge;
} }

View File

@ -51,6 +51,16 @@ $new_remote_problem_form->addInput('remote_problem_id', [
$vdata['remote_problem_id'] = $id; $vdata['remote_problem_id'] = $id;
return '';
} else if ($remote_oj === 'luogu') {
$id = trim(strtoupper($id));
if (!validateLuoguProblemId($id)) {
return '不合法的题目 ID';
}
$vdata['remote_problem_id'] = $id;
return ''; return '';
} }
@ -73,14 +83,7 @@ $new_remote_problem_form->handle = function (&$vdata) {
UOJResponse::page500('题目抓取失败,可能是题目不存在或者没有题面!如果题目没有问题,请稍后再试。<a href="">返回</a>'); UOJResponse::page500('题目抓取失败,可能是题目不存在或者没有题面!如果题目没有问题,请稍后再试。<a href="">返回</a>');
} }
$submission_requirement = [ $submission_requirement = UOJRemoteProblem::getSubmissionRequirements($remote_online_judge);
[
"name" => "answer",
"type" => "source code",
"file_name" => "answer.code",
"languages" => $remote_provider['languages'],
]
];
$enc_submission_requirement = json_encode($submission_requirement); $enc_submission_requirement = json_encode($submission_requirement);
$extra_config = [ $extra_config = [

View File

@ -104,6 +104,19 @@ $custom_test_enabled = $custom_test_requirement && $pre_submit_check_ret === tru
function handleUpload($zip_file_name, $content, $tot_size) { function handleUpload($zip_file_name, $content, $tot_size) {
global $is_participating; global $is_participating;
$remote_oj = UOJProblem::cur()->getExtraConfig('remote_online_judge');
$remote_provider = UOJRemoteProblem::$providers[$remote_oj];
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];
if ($submit_type != 'bot') {
$content['no_rejudge'] = true;
}
$content['config'][] = ['remote_submit_type', $submit_type];
$content['config'][] = ['remote_account_data', $_POST['answer_remote_account_data']];
}
UOJSubmission::onUpload($zip_file_name, $content, $tot_size, $is_participating); UOJSubmission::onUpload($zip_file_name, $content, $tot_size, $is_participating);
} }
function handleCustomTestUpload($zip_file_name, $content, $tot_size) { function handleCustomTestUpload($zip_file_name, $content, $tot_size) {
@ -182,7 +195,7 @@ if ($pre_submit_check_ret === true && !$no_more_submission) {
if (UOJProblem::cur()->userCanUploadSubmissionViaZip(Auth::user())) { if (UOJProblem::cur()->userCanUploadSubmissionViaZip(Auth::user())) {
$zip_answer_form = newZipSubmissionForm( $zip_answer_form = newZipSubmissionForm(
'zip-answer', 'zip_answer',
$submission_requirement, $submission_requirement,
'FS::randomAvailableSubmissionFileName', 'FS::randomAvailableSubmissionFileName',
'handleUpload' 'handleUpload'
@ -198,6 +211,24 @@ if ($pre_submit_check_ret === true && !$no_more_submission) {
'FS::randomAvailableSubmissionFileName', 'FS::randomAvailableSubmissionFileName',
'handleUpload' 'handleUpload'
); );
if (UOJProblem::info('type') == 'remote') {
$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']);
$answer_form->addNoVal('answer_remote_submit_type', '');
$answer_form->addNoVal('answer_remote_account_data', '');
$answer_form->appendHTML(<<<EOD
<h5>Remote Judge 配置</h5>
<div class="" id="answer-remote_submit_group"></div>
<script>
$('#answer-remote_submit_group').remote_submit_type_group("{$remote_oj}", "{$remote_pid}", "{$remote_url}", {$submit_type});
</script>
EOD);
}
$answer_form->extra_validator = $submission_extra_validator; $answer_form->extra_validator = $submission_extra_validator;
$answer_form->succ_href = $is_participating ? '/contest/' . UOJContest::info('id') . '/submissions' : '/submissions'; $answer_form->succ_href = $is_participating ? '/contest/' . UOJContest::info('id') . '/submissions' : '/submissions';
$answer_form->runAtServer(); $answer_form->runAtServer();

View File

@ -118,7 +118,7 @@ if (UOJProblem::info('type') == 'remote') {
</ul> </ul>
EOD); EOD);
$re_crawl_form->config['submit_button']['text'] = '重新爬取'; $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 { try {
$data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id); $data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id);
} catch (Exception $e) { } catch (Exception $e) {
@ -134,14 +134,7 @@ if (UOJProblem::info('type') == 'remote') {
$data['difficulty'] = UOJProblem::info('difficulty'); $data['difficulty'] = UOJProblem::info('difficulty');
} }
$submission_requirement = [ $submission_requirement = UOJRemoteProblem::getSubmissionRequirements($remote_online_judge);
[
"name" => "answer",
"type" => "source code",
"file_name" => "answer.code",
"languages" => $remote_provider['languages'],
]
];
$enc_submission_requirement = json_encode($submission_requirement); $enc_submission_requirement = json_encode($submission_requirement);
$extra_config = [ $extra_config = [

View File

@ -46,6 +46,7 @@ function newAddDelCmdForm($form_name, $validate, $handle, $final = null) {
function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle) { function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle) {
$form = new UOJForm($form_name); $form = new UOJForm($form_name);
foreach ($requirement as $req) { foreach ($requirement as $req) {
if ($req['type'] == "source code") { if ($req['type'] == "source code") {
$languages = UOJLang::getAvailableLanguages(isset($req['languages']) ? $req['languages'] : null); $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 = [];
$content['file_name'] = $zip_file_name; $content['file_name'] = $zip_file_name;
$content['config'] = []; $content['config'] = [];
foreach ($requirement as $req) { foreach ($requirement as $req) {
if ($req['type'] == "source code") { if ($req['type'] == "source code") {
$content['config'][] = ["{$req['name']}_language", $_POST["{$form_name}_{$req['name']}_language"]]; $content['config'][] = ["{$req['name']}_language", $_POST["{$form_name}_{$req['name']}_language"]];

View File

@ -79,3 +79,7 @@ function is_short_string($str) {
function validateCodeforcesProblemId($str) { function validateCodeforcesProblemId($str) {
return preg_match('/(|GYM)[1-9][0-9]{0,5}[A-Z][1-9]?/', $str) !== true; 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;
}

View File

@ -665,6 +665,7 @@ class UOJForm {
EOD; EOD;
} else { } else {
echo <<<EOD echo <<<EOD
$("#button-submit-{$this->form_name}").addClass('disabled');
return ok; return ok;
EOD; EOD;
} }

View File

@ -14,6 +14,7 @@ class UOJRemoteProblem {
'ограничение по времени на тест', 'ограничение по времени на тест',
], ],
'languages' => ['C', 'C++', 'C++17', 'C++20', 'Java17', 'Pascal', 'Python2', 'Python3'], 'languages' => ['C', 'C++', 'C++17', 'C++20', 'Java17', 'Pascal', 'Python2', 'Python3'],
'submit_type' => ['bot'],
], ],
'atcoder' => [ 'atcoder' => [
'name' => 'AtCoder', 'name' => 'AtCoder',
@ -24,6 +25,7 @@ class UOJRemoteProblem {
'指定されたタスクが見つかりません', '指定されたタスクが見つかりません',
], ],
'languages' => ['C', 'C++', 'Java11', 'Python3', 'Pascal'], 'languages' => ['C', 'C++', 'Java11', 'Python3', 'Pascal'],
'submit_type' => ['bot'],
], ],
'uoj' => [ 'uoj' => [
'name' => 'UniversalOJ', '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'], 'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java8', 'Java11', 'Java17', 'Pascal'],
'submit_type' => ['bot'],
], ],
'loj' => [ 'loj' => [
'name' => 'LibreOJ', 'name' => 'LibreOJ',
'short_name' => 'LOJ', 'short_name' => 'LOJ',
'url' => 'https://loj.ac', 'url' => 'https://loj.ac',
'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java17', 'Pascal'], '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; return static::$providers['loj']['url'] . '/p/' . $id;
} }
static function getLuoguProblemUrl($id) {
return static::$providers['luogu']['url'] . '/problem/' . $id;
}
static function getCodeforcesProblemBasicInfoFromHtml($id, $html) { static function getCodeforcesProblemBasicInfoFromHtml($id, $html) {
$remote_provider = static::$providers['codeforces']; $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) { public static function getProblemRemoteUrl($oj, $id) {
if ($oj === 'codeforces') { if ($oj === 'codeforces') {
return static::getCodeforcesProblemUrl($id); return static::getCodeforcesProblemUrl($id);
@ -403,6 +484,8 @@ class UOJRemoteProblem {
return static::getUojProblemUrl($id); return static::getUojProblemUrl($id);
} else if ($oj === 'loj') { } else if ($oj === 'loj') {
return static::getLojProblemUrl($id); return static::getLojProblemUrl($id);
} else if ($oj === 'luogu') {
return static::getLuoguProblemUrl($id);
} }
return null; return null;
@ -418,6 +501,8 @@ class UOJRemoteProblem {
return static::getUojProblemBasicInfo($id); return static::getUojProblemBasicInfo($id);
} else if ($oj === 'loj') { } else if ($oj === 'loj') {
return static::getLojProblemBasicInfo($id); return static::getLojProblemBasicInfo($id);
} else if ($oj === 'luogu') {
return static::getLuoguProblemBasicInfo($id);
} }
return null; return null;

View File

@ -418,6 +418,10 @@ class UOJSubmission {
} }
public function userCanRejudge(array $user = null) { public function userCanRejudge(array $user = null) {
if ($this->getContent('no_rejudge')) {
return false;
}
if (isSuperUser($user)) { if (isSuperUser($user)) {
return true; return true;
} }

View File

@ -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 = $('<input class="form-check-input" type="radio" name="answer_remote_submit_type" id="' + input_submit_type_bot_id + '" value="bot" />');
var input_submit_type_my = $('<input class="form-check-input" type="radio" name="answer_remote_submit_type" id="' + input_submit_type_my_id + '" value="my" />');
var input_my_account_data = $('<input type="hidden" name="answer_remote_account_data" value="" />');
var div_submit_type_bot = $('<div id="' + div_submit_type_bot_id + '" />')
.append('<div class="mt-3">将使用公用账号提交本题。</div>');
var div_submit_type_my = $('<div id="' + div_submit_type_my_id + '" />')
.append('<div class="mt-3">将使用您的账号提交本题。</div>');
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 = $('<input class="form-control" type="text" name="luogu_uid" id="input-luogu_uid" />');
var input_luogu_client_id = $('<input class="form-control" type="text" name="luogu_client_id" id="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(
$('<div class="row mt-3" />')
.append($('<div class="col-sm-2" />').append('<label for="input-luogu_uid" class="form-col-label">_uid</label>'))
.append($('<div class="col-sm-4" />').append(input_luogu_uid))
.append($('<div class="col-sm-6" />').append($('<div class="form-text" />').append('请填入 Cookie 中的 <code>_uid</code>。')))
).append(
$('<div class="row mt-3" />')
.append($('<div class="col-sm-2" />').append('<label for="input-luogu_client_id" class="form-col-label">__client_id</label>'))
.append($('<div class="col-sm-4" />').append(input_luogu_client_id))
.append($('<div class="col-sm-6" />').append($('<div class="form-text" />').append('请填入 Cookie 中的 <code>__client_id</code>。')))
).append(input_my_account_data);
}
$(this).append(
$('<div class="mt-3" />').append(
$('<div class="form-check d-inline-block" />')
.append(input_submit_type_bot)
.append($('<label class="form-check-label" for="' + input_submit_type_bot_id + '" />').append(' 公用账号'))
).append(
$('<div class="form-check d-inline-block ms-3" />')
.append(input_submit_type_my)
.append($('<label class="form-check-label" for="' + input_submit_type_my_id + '" />').append(' 自有账号'))
)
).append(div_submit_type_bot).append(div_submit_type_my);
});
}
// custom test // custom test
function custom_test_onsubmit(response_text, div_result, url) { function custom_test_onsubmit(response_text, div_result, url) {
if (response_text != '') { if (response_text != '') {