From 19cf1a53040aea3a1acd0b6ceccd78e6c450d5bc Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 10 Jul 2020 22:54:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20@param=20=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 + hydro/entry/master.ts | 4 +- hydro/entry/worker.ts | 2 +- hydro/error.ts | 7 +- hydro/handler/contest.ts | 121 +++++++++++----- hydro/handler/discussion.ts | 61 +++++--- hydro/handler/domain.ts | 12 +- hydro/handler/homework.ts | 118 ++++++++++----- hydro/handler/import.ts | 13 +- hydro/handler/misc.ts | 21 ++- hydro/handler/problem.ts | 188 +++++++++++++++--------- hydro/handler/record.ts | 25 ++-- hydro/handler/training.ts | 48 +++++-- hydro/handler/user.ts | 41 ++++-- hydro/handler/wiki.ts | 8 +- hydro/lib/validator.ts | 53 ++++--- hydro/model/contest.ts | 8 +- hydro/model/problem.ts | 5 +- hydro/script/blacklist.ts | 1 + hydro/service/server.ts | 278 +++++++++++++++++++----------------- package.json | 16 +-- yarn.lock | 15 +- 22 files changed, 657 insertions(+), 393 deletions(-) diff --git a/README.md b/README.md index 361b6d53f..a7b181c54 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ Hydro 的界面基于 Vijos 二次开发。 如果您认为本项目有价值,欢迎 star 。 +## 联系我们 + +QQ [3402182471](tencent://AddContact/?uin=3402182471&subcmd=all) +Telegram [@webpack_exports_undefined](https://t.me/webpack_exports_undefined) + ## 鸣谢 排名不分先后,按照链接字典序 diff --git a/hydro/entry/master.ts b/hydro/entry/master.ts index 72abc4fa3..19ab5252f 100644 --- a/hydro/entry/master.ts +++ b/hydro/entry/master.ts @@ -17,7 +17,7 @@ export async function load(call) { await call({ entry: 'setup' }); require('../options'); } - const bus = require('../service/bus').default; + const bus = require('../service/bus'); await new Promise((resolve) => { const h = () => { console.log('Database connected'); @@ -32,7 +32,7 @@ export async function load(call) { require('../service/gridfs'); require('../service/monitor'); const server = require('../service/server'); - await server.default.prepare(); + await server.prepare(); await service(pending, fail); for (const i of builtinModel) require(`../model/${i}`); for (const i of builtinHandler) require(`../handler/${i}`); diff --git a/hydro/entry/worker.ts b/hydro/entry/worker.ts index fd49b8d91..989540b32 100644 --- a/hydro/entry/worker.ts +++ b/hydro/entry/worker.ts @@ -27,7 +27,7 @@ export async function load() { for (const i of builtinLib) require(`../lib/${i}`); await lib(pending, fail); require('../service/gridfs'); - const server = require('../service/server').default; + const server = require('../service/server'); await server.prepare(); await service(pending, fail); for (const i of builtinModel) require(`../model/${i}`); diff --git a/hydro/error.ts b/hydro/error.ts index 08a42fcbd..3d7afa267 100644 --- a/hydro/error.ts +++ b/hydro/error.ts @@ -31,6 +31,8 @@ const Err = (name: string, ...info: Array `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }), @@ -117,8 +127,10 @@ class ContestScoreboardDownloadHandler extends ContestDetailHandler { } } -class ContestEditHandler extends ContestDetailHandler { - async prepare() { +class ContestEditHandler extends ContestHandler { + @param('tid', Types.ObjectID) + async prepare(domainId: string, tid: ObjectID) { + this.tdoc = await contest.get(domainId, tid); if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_CONTEST); else this.checkPerm(PERM.PERM_EDIT_CONTEST_SELF); } @@ -149,9 +161,22 @@ class ContestEditHandler extends ContestDetailHandler { }; } - async post({ - domainId, beginAtDate, beginAtTime, duration, title, content, rule, pids, rated = false, - }) { + @param('beginAtDate', Types.Date) + @param('beginAtTime', Types.Time) + @param('duration', Types.Float) + @param('title', Types.String, isTitle) + @param('content', Types.String, isContent) + @param('rule', Types.String) + @param('pids', Types.String) + @param('rated', Types.Boolean, true) + async post( + domainId: string, beginAtDate: string, beginAtTime: string, duration: number, + title: string, content: string, rule: string, _pids: string, rated = false, + ) { + let pids = _pids.split(',').map((i) => { + if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10); + return i; + }); const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); if (!beginAtMoment.isValid()) { throw new ValidationError('beginAtDate', 'beginAtTime'); @@ -172,8 +197,10 @@ class ContestEditHandler extends ContestDetailHandler { } } -class ContestProblemHandler extends ContestDetailHandler { - async prepare({ domainId, tid, pid }) { +class ContestProblemHandler extends ContestHandler { + @param('tid', Types.ObjectID) + @param('pid', Types.UnsignedInt) + async prepare(domainId: string, tid: ObjectID, pid: number) { [this.tdoc, this.pdoc] = await Promise.all([ contest.get(domainId, tid), problem.get(domainId, pid, this.user._id), @@ -193,12 +220,11 @@ class ContestProblemHandler extends ContestDetailHandler { } } - // @ts-ignore - async get({ tid }) { + async get() { const path = [ ['Hydro', 'homepage'], ['contest_main', 'contest_main'], - [this.tdoc.title, 'contest_detail', { tid }, true], + [this.tdoc.title, 'contest_detail', { tid: this.tdoc.docId }, true], [this.pdoc.title, null, null, true], ]; this.response.body = { @@ -213,7 +239,9 @@ class ContestProblemHandler extends ContestDetailHandler { } class ContestProblemSubmitHandler extends ContestProblemHandler { - async get({ domainId, tid, pid }) { + @param('tid', Types.ObjectID) + @param('pid', Types.UnsignedInt) + async prepare(domainId: string, tid: ObjectID, pid: number) { let rdocs = []; if (this.canShowRecord(this.tdoc)) { rdocs = await record.getUserInProblemMulti( @@ -240,9 +268,10 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { }; } - async post({ - domainId, tid, lang, code, - }) { + @param('tid', Types.ObjectID) + @param('lang', Types.String) + @param('code', Types.String) + async post(domainId: string, tid: ObjectID, lang: string, code: string) { await this.limitRate('add_record', 60, 100); const rid = await record.add(domainId, { pid: this.pdoc.docId, @@ -263,8 +292,9 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { } } -class ContestCodeHandler extends ContestDetailHandler { - async get({ domainId, tid }) { +class ContestCodeHandler extends ContestHandler { + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { if (!this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) { this.checkPerm(PERM.PERM_READ_RECORD_CODE); } @@ -313,9 +343,22 @@ class ContestCreateHandler extends ContestHandler { }; } - async post({ - domainId, title, content, rule, beginAtDate, beginAtTime, duration, pids, rated = false, - }) { + @param('beginAtDate', Types.Date) + @param('beginAtTime', Types.Time) + @param('duration', Types.Float) + @param('title', Types.String, isTitle) + @param('content', Types.String, isContent) + @param('rule', Types.String) + @param('pids', Types.String) + @param('rated', Types.Boolean, true) + async post( + domainId: string, beginAtDate: string, beginAtTime: string, duration: number, + title: string, content: string, rule: string, _pids: string, rated = false, + ) { + let pids = _pids.split(',').map((i) => { + if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10); + return i; + }); const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); if (!beginAt.isValid()) { throw new ValidationError('beginAtDate', 'beginAtTime'); diff --git a/hydro/handler/discussion.ts b/hydro/handler/discussion.ts index 586f78034..0872358c6 100644 --- a/hydro/handler/discussion.ts +++ b/hydro/handler/discussion.ts @@ -6,7 +6,10 @@ import * as user from '../model/user'; import * as discussion from '../model/discussion'; import * as document from '../model/document'; import { PERM, PRIV } from '../model/builtin'; -import { Route, Handler } from '../service/server'; +import { + Route, Handler, Types, param, +} from '../service/server'; +import { isTitle, isContent } from '../lib/validator'; export const typeMapper = { problem: document.TYPE_PROBLEM, @@ -22,17 +25,20 @@ class DiscussionHandler extends Handler { }) { this.checkPerm(PERM.PERM_VIEW_DISCUSSION); if (did) { + did = new ObjectID(did); this.ddoc = await discussion.get(domainId, did); if (!this.ddoc) throw new DiscussionNotFoundError(did); type = discussion.typeDisplay[this.ddoc.parentType]; name = this.ddoc.parentId; if (drid) { + drid = new ObjectID(drid); this.drdoc = await discussion.getReply(domainId, drid); if (!this.drdoc) throw new DiscussionNotFoundError(drid); if (!this.drdoc.parentId.equals(this.ddoc._id)) { throw new DocumentNotFoundError(drid); } if (drrid) { + drrid = new ObjectID(drrid); [, this.drrdoc] = await discussion.getTailReply(domainId, drid, drrid); if (!this.drrdoc) throw new DiscussionNotFoundError(drrid); if (!this.drrdoc.parentId.equals(this.drdoc._id)) { @@ -57,7 +63,8 @@ class DiscussionHandler extends Handler { } class DiscussionMainHandler extends Handler { - async get({ domainId, page = 1 }) { + @param('page', Types.UnsignedInt, true) + async get(domainId: string, page = 1) { const [ddocs, dpcount] = await paginate( discussion.getMulti(domainId), page, @@ -83,6 +90,7 @@ class DiscussionNodeHandler extends DiscussionHandler { async get({ domainId, type, name, page = 1, }) { + if (ObjectID.isValid(name)) name = new ObjectID(name); const [ddocs, dpcount] = await paginate( discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name }), page, @@ -146,7 +154,9 @@ class DiscussionCreateHandler extends DiscussionHandler { } class DiscussionDetailHandler extends DiscussionHandler { - async get({ domainId, did, page = 1 }) { + @param('did', Types.ObjectID) + @param('page', Types.UnsignedInt, true) + async get(domainId: string, did: ObjectID, page = 1) { const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE) ? await discussion.getStatus(domainId, did, this.user._id) : null; @@ -181,52 +191,64 @@ class DiscussionDetailHandler extends DiscussionHandler { this.checkPriv(PRIV.PRIV_USER_PROFILE); } - async postReply({ domainId, did, content }) { + @param('did', Types.ObjectID) + @param('content', Types.String, isContent) + async postReply(domainId: string, did: ObjectID, content: string) { this.checkPerm(PERM.PERM_REPLY_DISCUSSION); this.limitRate('add_discussion', 3600, 30); await discussion.addReply(domainId, did, this.user._id, content, this.request.ip); this.back(); } - async postTailReply({ domainId, drid, content }) { + @param('drid', Types.ObjectID) + @param('content', Types.String, isContent) + async postTailReply(domainId: string, drid: ObjectID, content: string) { this.checkPerm(PERM.PERM_REPLY_DISCUSSION); this.limitRate('add_discussion', 3600, 30); await discussion.addTailReply(domainId, drid, this.user._id, content, this.request.ip); this.back(); } - async postEditReply({ domainId, drid, content }) { + @param('drid', Types.ObjectID) + @param('content', Types.String, isContent) + async postEditReply(domainId: string, drid: ObjectID, content: string) { if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY); await discussion.editReply(domainId, drid, content); this.back(); } - async postDeleteReply({ domainId, drid }) { + @param('drid', Types.ObjectID) + async postDeleteReply(domainId: string, drid: ObjectID) { if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY); await discussion.delReply(domainId, drid); this.back(); } - async postEditTailReply({ - domainId, drid, drrid, content, - }) { + @param('drid', Types.ObjectID) + @param('drrid', Types.ObjectID) + @param('content', Types.String, isContent) + async postEditTailReply(domainId: string, drid: ObjectID, drrid: ObjectID, content: string) { if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY); await discussion.editTailReply(domainId, drid, drrid, content); this.back(); } - async postDeleteTailReply({ domainId, drid, drrid }) { + @param('drid', Types.ObjectID) + @param('drrid', Types.ObjectID) + async postDeleteTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) { if (this.drrdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY); await discussion.delTailReply(domainId, drid, drrid); this.back(); } - async postStar({ domainId, did }) { + @param('did', Types.ObjectID) + async postStar(domainId: string, did: ObjectID) { await discussion.setStar(domainId, did, this.user._id, true); this.back({ star: true }); } - async postUnstar({ domainId, did }) { + @param('did', Types.ObjectID) + async postUnstar(domainId: string, did: ObjectID) { await discussion.setStar(domainId, did, this.user._id, false); this.back({ star: false }); } @@ -266,9 +288,13 @@ class DiscussionEditHandler extends DiscussionHandler { this.response.body = { ddoc: this.ddoc, path }; } - async postUpdate({ - domainId, did, title, content, highlight, - }) { + @param('did', Types.ObjectID) + @param('title', Types.String, isTitle) + @param('content', Types.String, isContent) + @param('highlight', Types.Boolean, true) + async postUpdate( + domainId: string, did: ObjectID, title: string, content: string, highlight = false, + ) { if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION); if (highlight && !this.ddoc.highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION); await discussion.edit(domainId, did, title, content, highlight); @@ -276,7 +302,8 @@ class DiscussionEditHandler extends DiscussionHandler { this.response.redirect = this.url('discussion_detail', { did }); } - async postDelete({ domainId, did }) { + @param('did', Types.ObjectID) + async postDelete(domainId: string, did: ObjectID) { if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION); await discussion.del(domainId, did); this.response.body = { type: this.ddoc.parentType, parent: this.ddoc.parentId }; diff --git a/hydro/handler/domain.ts b/hydro/handler/domain.ts index c666e4306..fdd9ceb01 100644 --- a/hydro/handler/domain.ts +++ b/hydro/handler/domain.ts @@ -5,10 +5,14 @@ import * as system from '../model/system'; import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting'; import { PERM, PERMS_BY_FAMILY } from '../model/builtin'; import paginate from '../lib/paginate'; -import { Route, Handler } from '../service/server'; +import { + Route, Handler, Types, param, +} from '../service/server'; +import { isTitle, isUname } from '../lib/validator'; class DomainRankHandler extends Handler { - async get({ domainId, page = 1 }) { + @param('page', Types.UnsignedInt, true) + async get(domainId: string, page = 1) { const [dudocs, upcount, ucount] = await paginate( user.getMultiInDomain(domainId).sort({ rating: -1 }), page, @@ -46,7 +50,9 @@ class DomainCreateHandler extends Handler { this.response.template = 'domain_create.html'; } - async post({ id, name }) { + @param('id', Types.String, isUname) + @param('name', Types.String, isTitle) + async post(domainId: string, id: string, name: string) { await domain.add(id, this.user._id, name); this.response.body = { domainId: id }; this.response.redirect = this.url('homepage', { domainId: id }); diff --git a/hydro/handler/homework.ts b/hydro/handler/homework.ts index 22d9b1acd..ce51b8588 100644 --- a/hydro/handler/homework.ts +++ b/hydro/handler/homework.ts @@ -2,11 +2,14 @@ import yaml from 'js-yaml'; import moment from 'moment-timezone'; import { ObjectID } from 'mongodb'; import AdmZip from 'adm-zip'; +import { isSafeInteger } from 'lodash'; import { ValidationError, HomeworkNotLiveError, ProblemNotFoundError, HomeworkNotAttendedError, } from '../error'; -import { Route, Handler } from '../service/server'; +import { + Route, Handler, Types, param, +} from '../service/server'; import { PERM } from '../model/builtin'; import * as system from '../model/system'; import * as user from '../model/user'; @@ -16,6 +19,7 @@ import * as problem from '../model/problem'; import * as record from '../model/record'; import * as document from '../model/document'; import paginate from '../lib/paginate'; +import { isTitle, isContent } from '../lib/validator'; const HomeworkHandler = contest.ContestHandlerMixin(Handler); @@ -43,7 +47,9 @@ class HomeworkMainHandler extends HomeworkHandler { } class HomeworkDetailHandler extends HomeworkHandler { - async get({ domainId, tid, page = 1 }) { + @param('tid', Types.ObjectID) + @param('page', Types.UnsignedInt, true) + async get(domainId: string, tid: ObjectID, page = 1) { const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); const [tsdoc, pdict] = await Promise.all([ contest.getStatus(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK), @@ -86,7 +92,8 @@ class HomeworkDetailHandler extends HomeworkHandler { }; } - async postAttend({ domainId, tid }) { + @param('tid', Types.ObjectID) + async postAttend(domainId: string, tid: ObjectID) { this.checkPerm(PERM.PERM_ATTEND_HOMEWORK); const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); if (contest.isDone(tdoc)) throw new HomeworkNotLiveError(tdoc.docId); @@ -96,7 +103,9 @@ class HomeworkDetailHandler extends HomeworkHandler { } class HomeworkDetailProblemHandler extends HomeworkHandler { - async prepare({ domainId, tid, pid }) { + @param('tid', Types.ObjectID) + @param('pid', Types.UnsignedInt) + async prepare(domainId: string, tid: ObjectID, pid: number) { this.checkPerm(PERM.PERM_VIEW_PROBLEM); [this.tdoc, this.pdoc, this.tsdoc] = await Promise.all([ contest.get(domainId, tid, document.TYPE_HOMEWORK), @@ -105,7 +114,9 @@ class HomeworkDetailProblemHandler extends HomeworkHandler { ]); } - async get({ domainId, tid, pid }) { + @param('tid', Types.ObjectID) + @param('pid', Types.UnsignedInt) + async get(domainId: string, tid: ObjectID, pid: number) { const udoc = await user.getById(domainId, this.tdoc.owner); const attended = this.tsdoc && this.tsdoc.attend === 1; if (!contest.isDone(this.tdoc)) { @@ -127,7 +138,9 @@ class HomeworkDetailProblemHandler extends HomeworkHandler { } class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler { - async get({ domainId, tid, pid }) { + @param('tid', Types.ObjectID) + @param('pid', Types.UnsignedInt) + async get(domainId: string, tid: ObjectID, pid: number) { const udoc = await user.getById(domainId, this.tdoc.owner); const attended = this.tsdoc && this.tsdoc.attend === 1; if (!attended) throw new HomeworkNotAttendedError(tid); @@ -150,9 +163,11 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler { }; } - async post({ - domainId, tid, pid, code, lang, - }) { + @param('tid', Types.ObjectID) + @param('pid', Types.UnsignedInt) + @param('code', Types.String) + @param('lang', Types.String) + async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) { this.limitRate('add_record', 60, 100); const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK); if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid); @@ -191,38 +206,44 @@ class HomeworkCreateHandler extends HomeworkHandler { }; } - async post({ - domainId, title, content, beginAtDate, beginAtTime, - penaltySinceDate, penaltySinceTime, extensionDays, - penaltyRules, pids, rated = false, - }) { - let beginAt; - let penaltySince; - try { - beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); - } catch (e) { - throw new ValidationError('beginAtDate', 'beginAtTime'); - } - try { - penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone); - } catch (e) { - throw new ValidationError('endAtDate', 'endAtTime'); - } + @param('beginAtDate', Types.Date) + @param('beginAtTime', Types.Time) + @param('penaltySinceDate', Types.Date) + @param('penaltySinceTime', Types.Time) + @param('extensionDays', Types.Float) + @param('penaltyRules', Types.String) + @param('title', Types.String, isTitle) + @param('content', Types.String, isContent) + @param('pids', Types.String) + @param('rated', Types.Boolean, true) + async post( + domainId: string, beginAtDate: string, beginAtTime: string, + penaltySinceDate: string, penaltySinceTime: string, extensionDays: number, + penaltyRules: string, title: string, content: string, _pids: string, rated = false, + ) { + const pids = _pids.split(',').map((i) => { + if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10); + return i; + }); + const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); + if (!beginAt.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime'); + const penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone); + if (!penaltySince.isValid()) throw new ValidationError('endAtDate', 'endAtTime'); const endAt = penaltySince.clone().add(extensionDays, 'days'); if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime'); if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays'); - penaltySince = penaltySince.toDate(); await this.verifyProblems(domainId, pids); const tid = await contest.add(domainId, title, content, this.user._id, 'homework', beginAt.toDate(), endAt.toDate(), pids, rated, - { penaltySince, penaltyRules }, document.TYPE_HOMEWORK); + { penaltySince: penaltySince.toDate(), penaltyRules }, document.TYPE_HOMEWORK); this.response.body = { tid }; this.response.redirect = this.url('homework_detail', { tid }); } } class HomeworkEditHandler extends HomeworkHandler { - async get({ domainId, tid }) { + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK); else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF); @@ -251,11 +272,26 @@ class HomeworkEditHandler extends HomeworkHandler { }; } - async post({ - domainId, title, content, beginAtDate, beginAtTime, - penaltySinceDate, penaltySinceTime, extensionDays, - penaltyRules, pids, tid, - }) { + @param('tid', Types.ObjectID) + @param('beginAtDate', Types.Date) + @param('beginAtTime', Types.Time) + @param('penaltySinceDate', Types.Date) + @param('penaltySinceTime', Types.Time) + @param('extensionDays', Types.Float) + @param('penaltyRules', Types.String) + @param('title', Types.String, isTitle) + @param('content', Types.String, isContent) + @param('pids', Types.String) + @param('rated', Types.Boolean, true) + async post( + domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, + penaltySinceDate: string, penaltySinceTime: string, extensionDays: number, + penaltyRules: string, title: string, content: string, _pids: string, rated = false, + ) { + const pids = _pids.split(',').map((i) => { + if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10); + return i; + }); const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK); else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF); @@ -278,7 +314,7 @@ class HomeworkEditHandler extends HomeworkHandler { endAt = endAt.toDate(); penaltySince = penaltySince.toDate(); await contest.edit(domainId, tid, { - title, content, beginAt, endAt, pids, penaltySince, penaltyRules, + title, content, beginAt, endAt, pids, penaltySince, penaltyRules, rated, }, document.TYPE_HOMEWORK); if (tdoc.beginAt !== beginAt || tdoc.endAt !== endAt @@ -292,7 +328,8 @@ class HomeworkEditHandler extends HomeworkHandler { } class HomeworkScoreboardHandler extends HomeworkHandler { - async get({ domainId, tid }) { + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { const [tdoc, rows, udict] = await this.getScoreboard( domainId, tid, false, document.TYPE_HOMEWORK, ); @@ -310,9 +347,11 @@ class HomeworkScoreboardHandler extends HomeworkHandler { } class HomeworkScoreboardDownloadHandler extends HomeworkHandler { - async get({ domainId, tid, ext }) { + @param('tid', Types.ObjectID) + @param('ext', Types.String) + async get(domainId: string, tid: ObjectID, ext: string) { const getContent = { - csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, + csv: (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }), }; if (!getContent[ext]) throw new ValidationError('ext'); @@ -322,7 +361,8 @@ class HomeworkScoreboardDownloadHandler extends HomeworkHandler { } class HomeworkCodeHandler extends HomeworkHandler { - async get({ domainId, tid }) { + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { this.checkPerm(PERM.PERM_READ_RECORD_CODE); this.limitRate('homework_code', 3600, 60); const [tdoc, tsdocs] = await contest.getAndListStatus( diff --git a/hydro/handler/import.ts b/hydro/handler/import.ts index 8dedff9b8..573eed448 100644 --- a/hydro/handler/import.ts +++ b/hydro/handler/import.ts @@ -4,7 +4,10 @@ import os from 'os'; import { ValidationError } from '../error'; import * as problem from '../model/problem'; import { PERM } from '../model/builtin'; -import { Route, Handler } from '../service/server'; +import { + Route, Handler, Types, param, +} from '../service/server'; +import { isPid } from '../lib/validator'; class ProblemImportHandler extends Handler { async prepare() { @@ -22,9 +25,11 @@ class ProblemImportHandler extends Handler { }; } - async post({ - domainId, url, pid, hidden, remoteType, - }) { + @param('url', Types.String) + @param('pid', Types.String, isPid) + @param('hidden', Types.Boolean) + @param('remoteType', Types.String) + async post(domainId: string, url: string, pid: string, hidden = false, remoteType: string) { if (typeof global.Hydro.lib[`import.${remoteType}`] !== 'function') { throw new ValidationError('remoteType'); } diff --git a/hydro/handler/misc.ts b/hydro/handler/misc.ts index ef35792e3..f8c7c0981 100644 --- a/hydro/handler/misc.ts +++ b/hydro/handler/misc.ts @@ -1,17 +1,23 @@ +import { ObjectID } from 'mongodb'; import { PRIV } from '../model/builtin'; import * as file from '../model/file'; import * as user from '../model/user'; import * as markdown from '../lib/markdown'; import * as db from '../service/db'; -import { Route, Handler } from '../service/server'; +import { + Route, Handler, Types, param, +} from '../service/server'; const coll = db.collection('status'); class FileDownloadHandler extends Handler { - async get({ docId, secret, name }) { + @param('fileId', Types.ObjectID) + @param('secret', Types.String) + @param('name', Types.String, true) + async get(domainId: string, fileId: ObjectID, secret: string, name: string) { if (name) name = Buffer.from(name, 'base64').toString(); - this.response.attachment(name || docId); - this.response.body = await file.getWithSecret(docId, secret); + this.response.attachment(name || fileId); + this.response.body = await file.getWithSecret(fileId, secret); } } @@ -48,7 +54,8 @@ class StatusUpdateHandler extends Handler { } class SwitchLanguageHandler extends Handler { - async get({ lang }) { + @param('lang', Types.String) + async get(domainId: string, lang: string) { if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) { await user.setById(this.user._id, { viewLang: lang }); } else this.session.viewLang = lang; @@ -71,8 +78,8 @@ class SockToken extends Handler { } export async function apply() { - Route('file_download', '/fs/:docId/:secret', FileDownloadHandler); - Route('file_download_with_name', '/fs/:docId/:name/:secret', FileDownloadHandler); + Route('file_download', '/fs/:fileId/:secret', FileDownloadHandler); + Route('file_download_with_name', '/fs/:fileId/:name/:secret', FileDownloadHandler); Route('status', '/status', StatusHandler); Route('status_update', '/status/update', StatusUpdateHandler); Route('switch_language', '/language/:lang', SwitchLanguageHandler); diff --git a/hydro/handler/problem.ts b/hydro/handler/problem.ts index 97e5bd7f1..8aed68703 100644 --- a/hydro/handler/problem.ts +++ b/hydro/handler/problem.ts @@ -1,5 +1,9 @@ +import { isSafeInteger, flatten } from 'lodash'; +import { ObjectID } from 'mongodb'; import paginate from '../lib/paginate'; -import * as validator from '../lib/validator'; +import { + checkPid, isTitle, isContent, isPid, +} from '../lib/validator'; import * as file from '../model/file'; import * as problem from '../model/problem'; import * as record from '../model/record'; @@ -9,19 +13,48 @@ import * as system from '../model/system'; import { PERM, PRIV } from '../model/builtin'; import * as bus from '../service/bus'; import { - Route, Connection, Handler, ConnectionHandler, + Route, Connection, Handler, ConnectionHandler, Types, param, } from '../service/server'; import { NoProblemError, ProblemDataNotFoundError, BadRequestError, SolutionNotFoundError, } from '../error'; +const parseCategory = (value: string) => flatten(value.split('+').map((e) => e.split(','))).map((e) => e.trim()); +const parsePid = (value: string) => (isSafeInteger(value) ? parseInt(value, 10) : value); + class ProblemHandler extends Handler { async __prepare() { this.checkPerm(PERM.PERM_VIEW_PROBLEM); } - async get({ domainId, page = 1, q = '' }) { + async cleanup() { + if (this.response.template === 'problem_main.html' && this.request.json) { + const { + path, page, pcount, ppcount, pdocs, psdict, category, + } = this.response.body; + this.response.body = { + title: this.renderTitle(category), + fragments: (await Promise.all([ + this.renderHTML('partials/problem_list.html', { + page, ppcount, pcount, pdocs, psdict, + }), + this.renderHTML('partials/problem_stat.html', { pcount }), + this.renderHTML('partials/problem_lucky.html', { category }), + this.renderHTML('partials/path.html', { path }), + ])).map((i) => ({ html: i })), + raw: { + path, page, pcount, ppcount, pdocs, psdict, category, + }, + }; + } + } +} + +class ProblemMainHandler extends ProblemHandler { + @param('page', Types.UnsignedInt, true) + @param('q', Types.String, true) + async get(domainId: string, page = 1, q = '') { this.response.template = 'problem_main.html'; const query: any = {}; let psdict = {}; @@ -51,41 +84,23 @@ class ProblemHandler extends Handler { }; } - async postStar({ domainId, pid }) { + @param('pid', Types.UnsignedInt) + async postStar(domainId: string, pid: number) { await problem.setStar(domainId, pid, this.user._id, true); this.back({ star: true }); } - async postUnstar({ domainId, pid }) { + @param('pid', Types.UnsignedInt) + async postUnstar(domainId: string, pid: number) { await problem.setStar(domainId, pid, this.user._id, false); this.back({ star: false }); } - - async cleanup() { - if (this.response.template === 'problem_main.html' && this.request.json) { - const { - path, page, pcount, ppcount, pdocs, psdict, category, - } = this.response.body; - this.response.body = { - title: this.renderTitle(category), - fragments: (await Promise.all([ - this.renderHTML('partials/problem_list.html', { - page, ppcount, pcount, pdocs, psdict, - }), - this.renderHTML('partials/problem_stat.html', { pcount }), - this.renderHTML('partials/problem_lucky.html', { category }), - this.renderHTML('partials/path.html', { path }), - ])).map((i) => ({ html: i })), - raw: { - path, page, pcount, ppcount, pdocs, psdict, category, - }, - }; - } - } } class ProblemCategoryHandler extends ProblemHandler { - async get({ domainId, page = 1, category }) { + @param('page', Types.UnsignedInt, true) + @param('category', Types.String, parseCategory) + async get(domainId: string, page = 1, category: string[]) { this.response.template = 'problem_main.html'; const q: any = { $and: [] }; for (const name of category) { @@ -120,8 +135,9 @@ class ProblemCategoryHandler extends ProblemHandler { } class ProblemRandomHandler extends ProblemHandler { - async get({ domainId, category }) { - const q: any = category[0] ? { $and: [] } : {}; + @param('category', Types.String, true, null, parseCategory) + async get(domainId: string, category: string[]) { + const q: any = category.length ? { $and: [] } : {}; for (const name of category) { if (name) { q.$and.push({ @@ -141,7 +157,8 @@ class ProblemRandomHandler extends ProblemHandler { } class ProblemDetailHandler extends ProblemHandler { - async _prepare({ domainId, pid }) { + @param('pid', Types.String, null, parsePid) + async _prepare(domainId: string, pid: number | string) { this.response.template = 'problem_detail.html'; if (pid) { this.pdoc = await problem.get(domainId, pid, this.user._id); @@ -158,7 +175,7 @@ class ProblemDetailHandler extends ProblemHandler { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async get(args: any) { + async get(...args: any) { this.response.body.path = [ ['Hydro', 'homepage'], ['problem_main', 'problem_main'], @@ -168,9 +185,11 @@ class ProblemDetailHandler extends ProblemHandler { } class ProblemSubmitHandler extends ProblemDetailHandler { - async get({ domainId, pid }) { + @param('pid', Types.String, null, parsePid) + async get(domainId: string, pid: string | number) { this.response.template = 'problem_submit.html'; - const rdocs = await record.getUserInProblemMulti(domainId, this.user._id, this.pdoc.docId) + const rdocs = await record + .getUserInProblemMulti(domainId, this.user._id, this.pdoc.docId) .sort({ _id: -1 }) .limit(10) .toArray(); @@ -188,8 +207,9 @@ class ProblemSubmitHandler extends ProblemDetailHandler { }; } - async post({ domainId }) { - const { lang, code } = this.request.body; + @param('lang', Types.String) + @param('code', Types.String) + async post(domainId: string, lang: string, code: string) { const rid = await record.add(domainId, { uid: this.user._id, lang, code, pid: this.pdoc.docId, }, true); @@ -200,9 +220,10 @@ class ProblemSubmitHandler extends ProblemDetailHandler { } class ProblemPretestHandler extends ProblemDetailHandler { - async post({ - domainId, lang, code, input, - }) { + @param('lang', Types.String) + @param('code', Types.String) + @param('input', Types.String) + async post(domainId: string, lang: string, code: string, input: string) { this.limitRate('add_record', 60, 100); const rid = await record.add(domainId, { uid: this.user._id, lang, code, pid: this.pdoc.docId, input, @@ -213,8 +234,9 @@ class ProblemPretestHandler extends ProblemDetailHandler { } class ProblemPretestConnectionHandler extends ConnectionHandler { - async prepare({ domainId, pid }) { - this.pid = pid.toString(); + @param('pid', Types.String) + async prepare(domainId: string, pid: string) { + this.pid = pid; this.domainId = domainId; bus.subscribe(['record_change'], this, 'onRecordChange'); } @@ -236,7 +258,7 @@ class ProblemPretestConnectionHandler extends ConnectionHandler { } class ProblemStatisticsHandler extends ProblemDetailHandler { - async get({ domainId }) { + async get(domainId: string) { const udoc = await user.getById(domainId, this.pdoc.owner); const path = [ ['problem_main', 'problem_main'], @@ -256,7 +278,8 @@ class ProblemManageHandler extends ProblemDetailHandler { } class ProblemSettingsHandler extends ProblemManageHandler { - async get({ pid }) { + @param('pid', Types.String) + async get(domainId: string, pid: string) { this.response.template = 'problem_settings.html'; this.response.body.path = [ ['Hydro', 'homepage'], @@ -266,15 +289,24 @@ class ProblemSettingsHandler extends ProblemManageHandler { ]; } - async postConfig({ domainId, pid, yaml }) { - await problem.edit(domainId, pid, { config: yaml }); + @param('pid', Types.String, null, parsePid) + @param('yaml', Types.String) + async postConfig(domainId: string, pid: string | number, yaml: string) { + const pdoc = await problem.get(domainId, pid); + await problem.edit(domainId, pdoc.docId, { config: yaml }); this.back(); } - async postSetting({ - domainId, pid, hidden = false, category, tag, - }) { - await problem.edit(domainId, pid, { hidden, category, tag }); + @param('pid', Types.String, null, parsePid) + @param('hidden', Types.Boolean) + @param('category', Types.String, null, parseCategory) + @param('tag', Types.String, null, parseCategory) + async postSetting( + domainId: string, pid: string | number, hidden = false, + category: string[], tag: string[], + ) { + const pdoc = await problem.get(domainId, pid); + await problem.edit(domainId, pdoc.docId, { hidden, category, tag }); this.back(); } } @@ -291,8 +323,10 @@ class ProblemEditHandler extends ProblemManageHandler { this.response.body.page_name = 'problem_edit'; } - async post({ domainId, title, content }) { - const pid = validator.checkPid(this.request.body.pid); + @param('title', Types.String, isTitle) + @param('content', Types.String, isContent) + async post(domainId: string, title: string, content: string) { + const pid = checkPid(this.request.body.pid); const pdoc = await problem.get(domainId, this.request.params.pid); await problem.edit(domainId, pdoc.docId, { title, content, pid }); this.back(); @@ -314,7 +348,9 @@ class ProblemDataUploadHandler extends ProblemManageHandler { async post({ domainId }) { if (!this.request.files.file) throw new BadRequestError(); - await problem.setTestdata(domainId, this.pdoc.docId, this.request.files.file.path); + await problem.setTestdata( + domainId, this.pdoc.docId, this.request.files.file.path, + ); if (this.pdoc.data && typeof this.pdoc.data === 'object') { const f = await file.getMeta(this.pdoc.data); this.md5 = f.md5; @@ -337,7 +373,8 @@ class ProblemDataDownloadHandler extends ProblemDetailHandler { } class ProblemSolutionHandler extends ProblemDetailHandler { - async get({ domainId, page = 1 }) { + @param('page', Types.UnsignedInt, true) + async get(domainId: string, page = 1) { this.response.template = 'problem_solution.html'; this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION); const [psdocs, pcount, pscount] = await paginate( @@ -367,21 +404,22 @@ class ProblemSolutionHandler extends ProblemDetailHandler { } async post({ domainId, psid }) { - if (psid) this.psdoc = await solution.get(domainId, psid); + if (psid) this.psdoc = await solution.get(domainId, new ObjectID(psid)); } - async postSubmit({ domainId, content }) { + @param('content', Types.String, isContent) + async postSubmit(domainId: string, content: string) { this.checkPerm(PERM.PERM_CREATE_PROBLEM_SOLUTION); await solution.add(domainId, this.pdoc.docId, this.user._id, content); this.back(); } - async postEditSolution({ domainId, content }) { + @param('content', Types.String, isContent) + async postEditSolution(domainId: string, content: string) { if (this.psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION); else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF); - this.psdoc = await solution.edit(domainId, this.psdoc.docId, content); - this.ctx.body.psdoc = this.psdoc; - this.back(); + const psdoc = await solution.edit(domainId, this.psdoc.docId, content); + this.back({ psdoc }); } async postDeleteSolution({ domainId }) { @@ -391,16 +429,19 @@ class ProblemSolutionHandler extends ProblemDetailHandler { this.back(); } - async postReply({ domainId, psid, content }) { + @param('psid', Types.ObjectID) + @param('content', Types.String, isContent) + async postReply(domainId: string, psid: ObjectID, content: string) { this.checkPerm(PERM.PERM_REPLY_PROBLEM_SOLUTION); const psdoc = await solution.get(domainId, psid); await solution.reply(domainId, psdoc.docId, this.user._id, content); this.back(); } - async postEditReply({ - domainId, content, psid, psrid, - }) { + @param('psid', Types.ObjectID) + @param('psrid', Types.ObjectID) + @param('content', Types.String, isContent) + async postEditReply(domainId: string, psid: ObjectID, psrid: ObjectID, content: string) { const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid); if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid); if (!(psdoc.owner === this.user._id @@ -414,7 +455,9 @@ class ProblemSolutionHandler extends ProblemDetailHandler { this.back(); } - async postDeleteReply({ domainId, psid, psrid }) { + @param('psid', Types.ObjectID) + @param('psrid', Types.ObjectID) + async postDeleteReply(domainId: string, psid: ObjectID, psrid: ObjectID) { const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid); if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid); if (!(psdoc.owner === this.user._id @@ -442,7 +485,8 @@ class ProblemSolutionHandler extends ProblemDetailHandler { } class ProblemSolutionRawHandler extends ProblemDetailHandler { - async get({ domainId, psid }) { + @param('psid', Types.ObjectID) + async get(domainId: string, psid: ObjectID) { this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION); const psdoc = await solution.get(domainId, psid); this.response.type = 'text/markdown'; @@ -451,7 +495,9 @@ class ProblemSolutionRawHandler extends ProblemDetailHandler { } class ProblemSolutionReplyRawHandler extends ProblemDetailHandler { - async get({ domainId, psid, psrid }) { + @param('psid', Types.ObjectID) + @param('psrid', Types.ObjectID) + async get(domainId: string, psid: ObjectID, psrid: ObjectID) { this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION); const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid); if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid, psrid); @@ -474,9 +520,11 @@ class ProblemCreateHandler extends Handler { }; } - async post({ - domainId, title, pid, content, hidden, - }) { + @param('title', Types.String, isTitle) + @param('pid', Types.String, isPid) + @param('content', Types.String, isContent) + @param('hidden', Types.Boolean) + async post(domainId: string, title: string, pid: string, content: string, hidden = false) { pid = await problem.add(domainId, title, content, this.user._id, { pid, hidden, }); @@ -486,7 +534,7 @@ class ProblemCreateHandler extends Handler { } export async function apply() { - Route('problem_main', '/p', ProblemHandler); + Route('problem_main', '/p', ProblemMainHandler); Route('problem_category', '/p/category/:category', ProblemCategoryHandler); Route('problem_random', '/problem/random', ProblemRandomHandler); Route('problem_detail', '/p/:pid', ProblemDetailHandler); diff --git a/hydro/handler/record.ts b/hydro/handler/record.ts index 0316bebdf..60e75d9ca 100644 --- a/hydro/handler/record.ts +++ b/hydro/handler/record.ts @@ -9,16 +9,19 @@ import * as user from '../model/user'; import paginate from '../lib/paginate'; import * as bus from '../service/bus'; import { - Route, Handler, Connection, ConnectionHandler, + Route, Handler, Connection, ConnectionHandler, Types, param, } from '../service/server'; const RecordHandler = contest.ContestHandlerMixin(Handler); class RecordListHandler extends RecordHandler { - async get({ - domainId, page = 1, pid, tid, uidOrName, - }) { + @param('page', Types.UnsignedInt, true) + @param('pid', Types.String, true) + @param('tid', Types.ObjectID, true) + @param('uidOrName', Types.String, true) + async get(domainId: string, page = 1, pid?: string, tid?: ObjectID, uidOrName?: string) { this.response.template = 'record_main.html'; + if (tid) tid = new ObjectID(tid); const q: any = { tid, hidden: false }; if (uidOrName) { q.$or = [ @@ -54,7 +57,8 @@ class RecordListHandler extends RecordHandler { } class RecordDetailHandler extends RecordHandler { - async get({ domainId, rid }) { + @param('rid', Types.ObjectID) + async get(domainId: string, rid: ObjectID) { this.response.template = 'record_detail.html'; const rdoc = await record.get(domainId, rid); if (rdoc.tid) { @@ -84,7 +88,8 @@ class RecordDetailHandler extends RecordHandler { } class RecordRejudgeHandler extends Handler { - async post({ domainId, rid }) { + @param('rid', Types.ObjectID) + async post(domainId: string, rid: ObjectID) { this.checkPerm(PERM.PERM_REJUDGE); const rdoc = await record.get(domainId, rid); if (rdoc) await record.rejudge(domainId, rid); @@ -95,10 +100,11 @@ class RecordRejudgeHandler extends Handler { const RecordConnectionHandler = contest.ContestHandlerMixin(ConnectionHandler); class RecordMainConnectionHandler extends RecordConnectionHandler { - async prepare({ domainId, tid }) { + @param('tid', Types.ObjectID) + async prepare(domainId: string, tid: ObjectID) { this.domainId = domainId; if (tid) { - const tdoc = await contest.get(domainId, tid, -1); + const tdoc = await contest.get(domainId, new ObjectID(tid), -1); if (this.canShowRecord(tdoc)) this.tid = tid; else { this.close(); @@ -137,7 +143,8 @@ class RecordMainConnectionHandler extends RecordConnectionHandler { } class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(ConnectionHandler) { - async prepare({ domainId, rid }) { + @param('rid', Types.ObjectID) + async prepare(domainId: string, rid: ObjectID) { const rdoc = await record.get(domainId, rid); if (rdoc.tid) { const tdoc = await contest.get(domainId, rdoc.tid, -1); diff --git a/hydro/handler/training.ts b/hydro/handler/training.ts index 4ed016137..1ce84471d 100644 --- a/hydro/handler/training.ts +++ b/hydro/handler/training.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import { ObjectID } from 'mongodb'; import { ValidationError, ProblemNotFoundError } from '../error'; +import { isTitle, isContent, isDescription } from '../lib/validator'; import paginate from '../lib/paginate'; import { PERM, PRIV } from '../model/builtin'; import * as problem from '../model/problem'; @@ -8,7 +9,9 @@ import * as builtin from '../model/builtin'; import * as training from '../model/training'; import * as user from '../model/user'; import * as system from '../model/system'; -import { Route, Handler } from '../service/server'; +import { + Route, Handler, Types, param, +} from '../service/server'; async function _parseDagJson(domainId, dag) { const parsed = []; @@ -51,8 +54,8 @@ async function _parseDagJson(domainId, dag) { } class TrainingMainHandler extends Handler { - async get({ domainId, sort, page }) { - const qs = sort ? 'sort={0}'.format(sort) : ''; + @param('page', Types.UnsignedInt, true) + async get(domainId: string, page = 1) { const [tdocs, tpcount] = await paginate( training.getMulti(domainId).sort('_id', 1), page, @@ -84,13 +87,14 @@ class TrainingMainHandler extends Handler { ]; this.response.template = 'training_main.html'; this.response.body = { - tdocs, page, tpcount, qs, tsdict, tdict, path, + tdocs, page, tpcount, tsdict, tdict, path, }; } } class TrainingDetailHandler extends Handler { - async get({ domainId, tid }) { + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { const tdoc = await training.get(domainId, tid); const pids = training.getPids(tdoc); const [owner, pdict] = await Promise.all([ @@ -141,7 +145,8 @@ class TrainingDetailHandler extends Handler { }; } - async postEnroll({ domainId, tid }) { + @param('tid', Types.ObjectID) + async postEnroll(domainId: string, tid: ObjectID) { this.checkPriv(PRIV.PRIV_USER_PROFILE); const tdoc = await training.get(domainId, tid); await training.enroll(domainId, tdoc.docId, this.user._id); @@ -160,10 +165,15 @@ class TrainingCreateHandler extends Handler { this.response.body = { page_name: 'training_create', path }; } - async post({ - domainId, title, content, dag, description, - }) { - dag = await _parseDagJson(domainId, dag); + @param('title', Types.String, isTitle) + @param('content', Types.String, isContent) + @param('dag', Types.String, isContent) + @param('description', Types.String, isDescription) + async post( + domainId: string, title: string, content: string, + _dag: string, description: string, + ) { + const dag = await _parseDagJson(domainId, _dag); const pids = training.getPids({ dag }); assert(pids.length, new ValidationError('dag')); const pdocs = await problem.getMulti(domainId, { @@ -189,7 +199,8 @@ class TrainingCreateHandler extends Handler { } class TrainingEditHandler extends Handler { - async prepare({ domainId, tid }) { + @param('tid', Types.ObjectID) + async prepare(domainId: string, tid: ObjectID) { this.tdoc = await training.get(domainId, tid); if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_TRAINING); else this.checkPerm(PERM.PERM_EDIT_TRAINING_SELF); @@ -209,10 +220,17 @@ class TrainingEditHandler extends Handler { }; } - async post({ - domainId, tid, title, content, dag, description, - }) { - dag = await _parseDagJson(domainId, dag); + @param('tid', Types.ObjectID) + @param('title', Types.String, isTitle) + @param('content', Types.String, isContent) + @param('dag', Types.String, isContent) + @param('description', Types.String, isDescription) + async post( + domainId: string, tid: ObjectID, + title: string, content: string, + _dag: string, description: string, + ) { + const dag = await _parseDagJson(domainId, _dag); const pids = training.getPids({ dag }); assert(pids.length, new ValidationError('dag')); const pdocs = await problem.getMulti(domainId, { diff --git a/hydro/handler/user.ts b/hydro/handler/user.ts index 5350b9527..aa239e045 100644 --- a/hydro/handler/user.ts +++ b/hydro/handler/user.ts @@ -1,6 +1,8 @@ import superagent from 'superagent'; import moment from 'moment-timezone'; -import { Route, Handler } from '../service/server'; +import { + Route, Handler, Types, param, +} from '../service/server'; import * as user from '../model/user'; import * as token from '../model/token'; import * as record from '../model/record'; @@ -8,6 +10,7 @@ import * as problem from '../model/problem'; import * as task from '../model/task'; import * as system from '../model/system'; import { PERM, PRIV } from '../model/builtin'; +import { isEmail, isPassword, isUname } from '../lib/validator'; import { sendMail } from '../lib/mail'; import * as misc from '../lib/misc'; import { @@ -27,9 +30,10 @@ class UserLoginHandler extends Handler { this.response.template = 'user_login.html'; } - async post({ - domainId, uname, password, rememberme = false, - }) { + @param('uname', Types.String) + @param('password', Types.String) + @param('rememberme', Types.Boolean, true) + async post(domainId: string, uname: string, password: string, rememberme = false) { const udoc = await user.getByUname(domainId, uname); if (!udoc) throw new LoginError(uname); udoc.checkPassword(password); @@ -56,7 +60,8 @@ class UserRegisterHandler extends Handler { this.response.template = 'user_register.html'; } - async post({ mail }) { + @param('mail', Types.String, isEmail) + async post(domainId: string, mail: string) { if (await user.getByEmail('system', mail, true)) throw new UserAlreadyExistError(mail); this.limitRate('send_mail', 3600, 30); const t = await token.add( @@ -85,9 +90,14 @@ class UserRegisterWithCodeHandler extends Handler { this.response.body = { mail }; } - async post({ - code, password, verifyPassword, uname, - }) { + @param('password', Types.String, isPassword) + @param('verifyPassword', Types.String) + @param('uname', Types.String, isUname) + @param('code', Types.String) + async post( + domainId: string, password: string, verifyPassword: string, + uname: string, code: string, + ) { const { mail } = await token.get(code, token.TYPE_REGISTRATION); if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); if (password !== verifyPassword) throw new VerifyPasswordError(); @@ -107,7 +117,8 @@ class UserLostPassHandler extends Handler { this.response.template = 'user_lostpass.html'; } - async post({ mail }) { + @param('mail', Types.String, isEmail) + async post(domainId: string, mail: string) { if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail'); const udoc = await user.getByEmail('system', mail); if (!udoc) throw new UserNotFoundError(mail); @@ -130,7 +141,10 @@ class UserLostPassWithCodeHandler extends Handler { this.response.body = { uname: udoc.uname }; } - async post({ code, password, verifyPassword }) { + @param('code', Types.String) + @param('password', Types.String, isPassword) + @param('verifyPassword', Types.String) + async post(domainId: string, code: string, password: string, verifyPassword: string) { const tdoc = await token.get(code, token.TYPE_LOSTPASS); if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); if (password !== verifyPassword) throw new VerifyPasswordError(); @@ -141,7 +155,8 @@ class UserLostPassWithCodeHandler extends Handler { } class UserDetailHandler extends Handler { - async get({ domainId, uid }) { + @param('uid', Types.Int) + async get(domainId: string, uid: number) { const isSelfProfile = this.user._id === uid; const udoc = await user.getById(domainId, uid, true); const sdoc = await token.getMostRecentSessionByUid(uid); @@ -178,7 +193,9 @@ class UserDeleteHandler extends Handler { } class UserSearchHandler extends Handler { - async get({ domainId, q, exactMatch = false }) { + @param('q', Types.String) + @param('exectMatch', Types.Boolean, true) + async get(domainId: string, q: string, exactMatch = false) { let udocs; if (exactMatch) udocs = []; else udocs = await user.getPrefixList(q, 20); diff --git a/hydro/handler/wiki.ts b/hydro/handler/wiki.ts index ff894afe4..f7d4377aa 100644 --- a/hydro/handler/wiki.ts +++ b/hydro/handler/wiki.ts @@ -1,8 +1,12 @@ import { NotFoundError } from '../error'; -import { Route, Handler } from '../service/server'; +import { + Route, Handler, Types, param, +} from '../service/server'; class WikiHandler extends Handler { - async get({ category = 'wiki', page }) { + @param('category', Types.String, true) + @param('page', Types.String) + async get(domainId: string, category = 'wiki', page: string) { if (!global.Hydro.wiki[category]) throw new NotFoundError(category); if (!global.Hydro.wiki[category][page]) throw new NotFoundError(category, page); const contents = global.Hydro.wiki[category][page]; diff --git a/hydro/lib/validator.ts b/hydro/lib/validator.ts index 096d01484..07ba60923 100644 --- a/hydro/lib/validator.ts +++ b/hydro/lib/validator.ts @@ -8,9 +8,12 @@ const RE_ROLE = /^[_0-9A-Za-z]{1,256}$/i; const RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i; // eslint-disable-next-line consistent-return -function _validate(scheme: any, arg: any, base: string) { +function _validate(scheme: any, arg: any, base: string): void { if (!(scheme instanceof Object)) return _validate({ $type: scheme }, arg, base); - if (scheme.$validate) scheme.$validate(arg); + if (scheme.$validate) { + const result = scheme.$validate(arg); + if (result === false) throw new ValidationError(base); + } if (scheme.$group) _validate(scheme.$group, arg, base); if (scheme.$or) { if (scheme.$or instanceof Array) { @@ -38,11 +41,15 @@ function _validate(scheme: any, arg: any, base: string) { if (scheme.$and) for (const s of scheme.$and) _validate(s, arg, base); if (scheme.$type) { if (typeof scheme.$type === 'string') { - if (!typeof arg === scheme.$type) { - throw new ValidationError(); + if (!scheme.$type.endsWith('?')) { + if (!typeof arg === scheme.$type) { + throw new ValidationError(base); + } + } else if (arg && !scheme.$type.includes(typeof arg)) { + throw new ValidationError(base); } } else if (!(arg instanceof scheme.$type)) { - throw new ValidationError(); + throw new ValidationError(base); } } if (scheme.$eq && scheme.$eq !== arg) throw new ValidationError(base); @@ -54,30 +61,32 @@ function _validate(scheme: any, arg: any, base: string) { if (arg instanceof Object) { _validate(scheme[key], arg[key], `${base}.${key}`); } else { - throw new ValidationError(); + throw new ValidationError(base); } } } } -function descriptor(scheme: any) { - return function desc(target: any, name: string, descriptor: any) { - const originalMethod = descriptor.value; - descriptor.value = function val(...args: any[]) { +function descriptor(scheme: any): Function { + return function desc(target: any, name: string, obj: any) { + const originalMethod = obj.value; + obj.value = function func(...args: any[]) { _validate(scheme, args[0], ''); return originalMethod.call(this, ...args); }; - return descriptor; + return obj; }; } -export function validate(scheme: any, arg: any) { - if (arguments.length === 1) return descriptor(scheme); - return _validate(scheme, arg, ''); +export function validate(scheme: any): Function; +export function validate(scheme: any, arg: any): void; +export function validate(...args: any[]): any { + if (args.length === 1) return descriptor(args[0]); + return _validate(args[0], args[1], ''); } export const isTitle = (s) => s && s.length < 64; -export const checkTitle = (s) => { if (!isTitle(s)) throw new ValidationError('title'); else return s; }; +export const checkTitle = (s) => { if (!(s && s.length < 64)) throw new ValidationError('title'); else return s; }; export const isUid = (s) => RE_UID.test(s); export const checkUid = (s) => { if (!isUid(s)) throw new ValidationError('uid'); else return s; }; export const isUname = (s) => RE_UNAME.test(s); @@ -85,22 +94,22 @@ export const checkUname = (s) => { if (!isUname(s)) throw new ValidationError('u export const isRole = (s) => RE_ROLE.test(s); export const checkRole = (s) => { if (!isRole(s)) throw new ValidationError('role'); else return s; }; export const isPassword = (s) => s.length >= 5; -export const checkPassword = (s) => { if (!isPassword(s)) throw new ValidationError('password'); else return s; }; +export const checkPassword = (s) => { if (!(s && s.length >= 5)) throw new ValidationError('password'); else return s; }; export const isEmail = (s) => RE_MAIL.test(s); -export const checkEmail = (s) => { if (!isEmail(s)) throw new ValidationError('mail'); else return s; }; -export const isContent = (s) => s && s.length < 65536; -export const checkContent = (s) => { if (!isContent(s)) throw new ValidationError('content'); else return s; }; +export const checkEmail = (s) => { if (!RE_MAIL.test(s)) throw new ValidationError('mail'); else return s; }; +export const isContent = (s: any) => s && s.length < 65536; +export const checkContent = (s) => { if (!(s && s.length < 65536)) throw new ValidationError('content'); else return s; }; export const isName = (s) => s && s.length < 256; export const checkName = (s) => { if (!isName(s)) throw new ValidationError('name'); else return s; }; export const isPid = (s) => RE_PID.test(s.toString()); export const checkPid = (s) => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; }; export const isIntro = () => true; export const checkIntro = (s) => { if (!isIntro()) throw new ValidationError('intro'); else return s; }; -export const isDescription = () => true; -export const checkDescription = (s) => { if (!isDescription()) throw new ValidationError('description'); else return s; }; +export const isDescription = (s: any) => s && s.length < 65536; +export const checkDescription = (s) => { if (!(s && s.length < 65536)) throw new ValidationError('description'); else return s; }; global.Hydro.lib.validator = { - validate: _validate, + validate, isTitle, checkTitle, isUid, diff --git a/hydro/model/contest.ts b/hydro/model/contest.ts index fe5c8b44a..f43ec401c 100644 --- a/hydro/model/contest.ts +++ b/hydro/model/contest.ts @@ -511,9 +511,11 @@ export const ContestHandlerMixin = (c) => class extends c { } // eslint-disable-next-line class-methods-use-this - async verifyProblems(domainId: string, pids: number[]) { - await problem.getList(domainId, pids, true); - return pids; + async verifyProblems(domainId: string, pids: Array) { + const pdict = await problem.getList(domainId, pids, true); + const _pids: Set = new Set(); + for (const i in pdict) _pids.add(pdict[i].docId); + return Array.from(_pids); } }; diff --git a/hydro/model/problem.ts b/hydro/model/problem.ts index 7225e7c9f..d5cc54eda 100644 --- a/hydro/model/problem.ts +++ b/hydro/model/problem.ts @@ -116,7 +116,10 @@ export async function random(domainId: string, query: FilterQuery) { } return null; } -export async function getList(domainId: string, pids: number[], getHidden = false, doThrow = true) { +export async function getList( + domainId: string, pids: Array, + getHidden = false, doThrow = true, +) { pids = Array.from(new Set(pids)); const r = {}; const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] }; diff --git a/hydro/script/blacklist.ts b/hydro/script/blacklist.ts index b78aa163c..e284000a2 100644 --- a/hydro/script/blacklist.ts +++ b/hydro/script/blacklist.ts @@ -62,6 +62,7 @@ async function _user( } export async function run({ + // eslint-disable-next-line no-shadow address = null, discuss = null, user = null, dryrun = true, }, report) { if (address) await _address(address, new Set(), new Set(), new Set(), dryrun, report); diff --git a/hydro/service/server.ts b/hydro/service/server.ts index a51be504d..066cae39c 100644 --- a/hydro/service/server.ts +++ b/hydro/service/server.ts @@ -1,31 +1,29 @@ +/* eslint-disable prefer-destructuring */ import assert from 'assert'; import path from 'path'; import os from 'os'; import http from 'http'; -import _ from 'lodash'; +import moment from 'moment-timezone'; +import { isSafeInteger } from 'lodash'; import { ObjectID } from 'mongodb'; import Koa from 'koa'; -import yaml from 'js-yaml'; import morgan from 'koa-morgan'; import Body from 'koa-body'; import Router from 'koa-router'; import cache from 'koa-static-cache'; import sockjs from 'sockjs'; - import { render } from '../lib/template'; -import * as validator from '../lib/validator'; import * as misc from '../lib/misc'; import * as user from '../model/user'; import * as system from '../model/system'; import * as blacklist from '../model/blacklist'; import * as token from '../model/token'; import * as opcount from '../model/opcount'; - -const { +import { UserNotFoundError, BlacklistedError, PermissionError, UserFacingError, ValidationError, PrivilegeError, CsrfTokenError, InvalidOperationError, MethodNotAllowedError, -} = require('../error'); +} from '../error'; let enableLog = true; @@ -33,86 +31,141 @@ const app = new Koa(); let server; const router = new Router(); -const _validateObjectId = (id: string | ObjectID, key: string) => { - if (ObjectID.isValid(id)) return new ObjectID(id); - throw new ValidationError(key); -}; -const _bool = (val: any) => !!val; -const _splitAndTrim = (val: string) => { - const t = val.split(',').map((i) => i.trim().split('+')); - return _.flatten(t); -}; -const _date = (val: string) => { - const d = val.split('-'); - assert(d.length === 3); - return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`; -}; -const _time = (val: string) => { - const t = val.split(':'); - assert(t.length === 2); - return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`; -}; +interface IConstructor { + // @ts-ignore + new(value: any): any; +} +interface IHandler { + // @ts-ignore + new(ctx: Koa.Context): Handler; +} +interface IConnectionHandler { + // @ts-ignore + new(conn: sockjs.Connection): ConnectionHandler; +} +type MethodDecorator = (target: any, name: string, obj: any) => any; +type Converter = IConstructor | ((value: any) => any); +type Validator = (value: any) => boolean; +interface ParamOption { + name: string, + isOptional?: boolean, + convert?: Converter, + validate?: Validator, +} -const validate = { - tid: _validateObjectId, - rid: _validateObjectId, - did: _validateObjectId, - drid: _validateObjectId, - drrid: _validateObjectId, - psid: _validateObjectId, - psrid: _validateObjectId, - docId: _validateObjectId, - mongoId: _validateObjectId, - hidden: _bool, - rated: _bool, - category: _splitAndTrim, - tag: _splitAndTrim, - beginAtDate: _date, - beginAtTime: _time, - pid: (pid: string) => (Number.isSafeInteger(parseInt(pid, 10)) ? parseInt(pid, 10) : pid), - content: validator.checkContent, - title: validator.checkTitle, - uid: (uid: string) => parseInt(validator.checkUid(uid), 10), - password: validator.checkPassword, - mail: validator.checkEmail, - uname: validator.checkUname, - page: (_page: string) => { - let page: number; - if (Number.isSafeInteger(parseInt(_page, 10))) page = parseInt(_page, 10); - if (page <= 0) throw new ValidationError('page'); - return page; - }, - duration: (_duration: string) => { - let duration: number; - if (!Number.isNaN(parseFloat(_duration))) duration = parseFloat(_duration); - if (duration <= 0) throw new ValidationError('duration'); - return duration; - }, - pids: (pids: string) => { - const src = pids.split(',').map((i) => i.trim()); - const d: number[] = []; - for (const i in src) { - if (Number.isSafeInteger(parseInt(src[i], 10))) d[i] = parseInt(src[i], 10); - } - return d; - }, - role: validator.checkRole, - penaltyRules: (penaltyRules) => { - try { - penaltyRules = yaml.safeLoad(penaltyRules); - } catch (e) { - throw new ValidationError('penalty_rules', 'parse error'); - } - assert(typeof penaltyRules === 'object', new ValidationError('penalty_rules', 'invalid format')); - return penaltyRules; - }, - yaml: (input) => { - yaml.safeLoad(input); - return input; - }, -}; +// eslint-disable-next-line no-shadow +export enum Types { String, Int, UnsignedInt, Float, ObjectID, Boolean, Date, Time } -async function prepare() { +const Tools: Array<[Converter, Validator, boolean?]> = [ + [(v) => v.toString(), null], + [(v) => parseInt(v, 10), (v) => isSafeInteger(parseInt(v, 10))], + [(v) => parseInt(v, 10), (v) => parseInt(v, 10) > 0], + [(v) => parseFloat(v), (v) => { + const t = parseFloat(v); + return t && !Number.isNaN(t) && !Number.isFinite(t); + }], + [ObjectID, ObjectID.isValid], + [(v) => !!v, null, true], + [ + (v) => { + const d = v.split('-'); + assert(d.length === 3); + return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`; + }, + (v) => { + const d = v.split('-'); + assert(d.length === 3); + return moment(`${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`).isValid(); + }], + [ + (v) => { + const t = v.split(':'); + assert(t.length === 2); + return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`; + }, + (v) => { + const t = v.split(':'); + assert(t.length === 2); + return moment(`${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`).isValid(); + }, + ], +]; + +export function param(name: string): MethodDecorator; +export function param(name: string, type: Types, validate: Validator): MethodDecorator; +export function param(name: string, type?: Types, isOptional?: boolean): MethodDecorator; +export function param( + name: string, type: Types, validate: null, convert: Converter +): MethodDecorator; +export function param( + name: string, type: Types, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function param( + name: string, type: Types, isOptional?: boolean, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function param( + name: string, ...args: Array +) +export function param(...args: any): MethodDecorator { + let cursor = 0; + let v: ParamOption = null; + let isValidate = true; + const d: ParamOption[] = []; + while (cursor <= args.length) { + if (typeof args[cursor] === 'string') { + v = { name: args[cursor] }; + d.push(v); + isValidate = true; + } else if (typeof args[cursor] === 'number') { + const type = args[cursor]; + if (Tools[type]) { + if (Tools[type][0]) d[d.length - 1].convert = Tools[type][0]; + if (Tools[type][1]) d[d.length - 1].validate = Tools[type][1]; + if (Tools[type][2]) d[d.length - 1].isOptional = Tools[type][2]; + } + } else if (typeof args[cursor] === 'boolean') d[d.length - 1].isOptional = args[cursor]; + else if (!isValidate) { + const I = args[cursor]; + if (typeof I.constructor === 'function') { + d[d.length - 1].convert = (val: any) => new I(val); + } else d[d.length - 1].convert = I; + isValidate = false; + } else d[d.length - 1].validate = args[cursor]; + cursor++; + } + for (let i = 0; i < d.length; i++) { + d[i].isOptional = d[i].isOptional || false; + } + return function desc(target: any, funcName: string, obj: any) { + if (!target.__param) target.__param = {}; + if (!target.__param[funcName]) { + target.__param[funcName] = [{ name: 'domainId', type: 'string' }]; + const originalMethod = obj.value; + obj.value = function func(rawArgs: any) { + const c = []; + const arglist: ParamOption[] = this.__param[funcName]; + for (const item of arglist) { + if (!item.isOptional || rawArgs[item.name]) { + if (!rawArgs[item.name]) throw new ValidationError(item.name); + if (item.validate) { + if (!item.validate(rawArgs[item.name])) { + throw new ValidationError(item.name); + } + } + // @ts-ignore + if (item.convert) c.push(item.convert(rawArgs[item.name])); + else c.push(rawArgs[item.name]); + } else c.push(undefined); + } + return originalMethod.call(this, ...c); + }; + } + target.__param[funcName].splice(1, 0, ...d); + return obj; + }; +} + +export async function prepare() { server = http.createServer(app.callback()); app.keys = await system.get('session.keys'); app.use(cache(path.join(os.tmpdir(), 'hydro', 'public'), { @@ -126,7 +179,7 @@ async function prepare() { })); } -const HandlerMixin = (Class) => class extends Class { +const HandlerMixin = (Class: IConstructor) => class extends Class { async renderHTML(name: string, context: any): Promise { if (enableLog) console.time(name); this.hasPerm = (perm) => this.user.hasPerm(perm); @@ -247,9 +300,8 @@ export const Handler = HandlerMixin(class { user: any; - constructor(ctx: Koa.Context, args: any) { + constructor(ctx: Koa.Context) { this.ctx = ctx; - this.args = args; this.request = { host: ctx.request.host, hostname: ctx.request.hostname, @@ -435,10 +487,12 @@ async function handle(ctx, HandlerClass, checker) { const args = { domainId: 'system', ...ctx.params, ...ctx.query, ...ctx.request.body, }; - const h = new HandlerClass(ctx, args); + const h = new HandlerClass(ctx); + h.args = args; + h.domainId = args.domainId; try { const method = ctx.method.toLowerCase(); - let operation; + let operation: string; if (method === 'post' && ctx.request.body.operation) { operation = `_${ctx.request.body.operation}` .replace(/_([a-z])/gm, (s) => s[1].toUpperCase()); @@ -459,19 +513,6 @@ async function handle(ctx, HandlerClass, checker) { throw new MethodNotAllowedError(method); } - let checking = ''; - try { - for (const key in validate) { - checking = key; - if (args[key]) { - args[key] = validate[key](args[key], key); - } - } - } catch (e) { - if (e instanceof ValidationError) throw e; - throw new ValidationError(`Argument ${checking} check failed`); - } - if (h._prepare) await h._prepare(args); if (h.prepare) await h.prepare(args); @@ -511,7 +552,7 @@ const Checker = (permPrivChecker) => { }; }; -export function Route(name, route, RouteHandler, ...permPrivChecker) { +export function Route(name: string, route: string, RouteHandler: IHandler, ...permPrivChecker) { const checker = Checker(permPrivChecker); router.all(name, route, (ctx) => handle(ctx, RouteHandler, checker)); router.all(`${name}_with_domainId`, `/d/:domainId${route}`, (ctx) => handle(ctx, RouteHandler, checker)); @@ -528,6 +569,8 @@ export const ConnectionHandler = HandlerMixin(class { session: any + args: any + user: any constructor(conn: sockjs.Connection) { @@ -569,11 +612,6 @@ export const ConnectionHandler = HandlerMixin(class { } }); -interface IConnectionHandler { - // @ts-ignore - new(conn: sockjs.Connection): ConnectionHandler; -} - export function Connection( name: string, prefix: string, RouteConnHandler: IConnectionHandler, @@ -588,18 +626,7 @@ export function Connection( h.args = args; await h.init(args); checker.call(h); - let checking = ''; - try { - for (const key in validate) { - checking = key; - if (args[key]) { - args[key] = validate[key](args[key], key); - } - } - } catch (e) { - if (e instanceof ValidationError) throw e; - throw new ValidationError(`Argument ${checking} check failed`); - } + if (h._prepare) await h._prepare(args); if (h.prepare) await h.prepare(args); if (h.message) { @@ -623,11 +650,6 @@ export function Middleware(middleware: Koa.Middleware) { app.use(middleware); } -export function Validate(key: string, func: Function) { - if (validate[key]) validate[key].push(func); - else validate[key] = [func]; -} - export async function start() { const [disableLog, port] = await system.getMany(['server.log', 'server.port']); if (!disableLog) { @@ -640,7 +662,5 @@ export async function start() { } global.Hydro.service.server = { - Handler, ConnectionHandler, Route, Connection, Middleware, Validate, prepare, start, + param, Handler, ConnectionHandler, Route, Connection, Middleware, prepare, start, }; - -export default global.Hydro.service.server; diff --git a/package.json b/package.json index 901ec1645..d0ddd6ea9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.6.0", + "version": "2.7.0", "main": "hydro/loader.js", "repository": "https://github.com/hydro-dev/Hydro.git", "author": "masnn", @@ -16,7 +16,7 @@ "koa-morgan": "^1.0.1", "koa-router": "^8.0.8", "koa-static-cache": "^5.1.3", - "lodash": "^4.17.15", + "lodash": "^4.17.19", "markdown-it": "^11.0.0", "markdown-it-anchor": "^5.3.0", "markdown-it-container": "^3.0.0", @@ -29,6 +29,7 @@ "nodemailer": "^6.4.10", "nunjucks": "^3.2.1", "prismjs": "^1.20.0", + "saslprep": "^1.0.3", "sockjs": "^0.3.20", "superagent": "^5.2.2", "superagent-proxy": "^2.0.0", @@ -44,7 +45,7 @@ "@types/lodash": "^4.14.157", "@types/markdown-it": "^10.0.1", "@types/mongodb": "^3.5.25", - "@types/node": "^14.0.18", + "@types/node": "^14.0.21", "@types/nodemailer": "^6.4.0", "@types/nunjucks": "^3.1.3", "@types/prismjs": "^1.16.1", @@ -66,11 +67,10 @@ "webpack": "^4.42.1" }, "scripts": { - "tsc": "tsc", - "build": "tsc && cp -r hydro/script dist", - "pack:development": "node build/development.js", - "pack:production": "node build/production.js", - "build:all": "cd ui && yarn && yarn build:production && cd .. && yarn && yarn build && yarn pack:production", + "build": "tsc && node build/development.js", + "build:development": "tsc && node build/development.js", + "build:production": "tsc && node build/production.js", + "build:all": "cd ui && yarn && yarn build:production && cd .. && yarn && yarn build:production", "start": "ts-node hydro/loader.ts", "pkg": "pkg .", "lint": "eslint build hydro module tool --ext ts,js --fix", diff --git a/yarn.lock b/yarn.lock index 09ca1b792..a2a42768e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -557,10 +557,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.12.tgz#9c72e865380a7dc99999ea0ef20fc9635b503d20" integrity sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw== -"@types/node@^14.0.18": - version "14.0.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.18.tgz#5111b2285659442f9f95697386a2b42b875bd7e9" - integrity sha512-0Z3nS5acM0cIV4JPzrj9g/GH0Et5vmADWtip3YOXOp1NpOLU8V3KoZDc8ny9c1pe/YSYYzQkAWob6dyV/EWg4g== +"@types/node@^14.0.21": + version "14.0.21" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.21.tgz#c84005e3f43b7e19a7374d47d5c411039da59231" + integrity sha512-kmfWRnh81BrOfQ2Bn3xXCkoB6PLNsJlHhliYvkPa5UL6nmMHkANm358zVpO7TW6CDe9i267pkS/Id65OKL8+Ug== "@types/node@^6.14.4", "@types/node@^6.14.7": version "6.14.10" @@ -4141,6 +4141,11 @@ lodash@^4.17.14, lodash@^4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.17.19: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + logic-solver@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/logic-solver/-/logic-solver-2.0.1.tgz#e9fa47002eb5d8cda7616d41639b97552eb674be" @@ -5582,7 +5587,7 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -saslprep@^1.0.0: +saslprep@^1.0.0, saslprep@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==