diff --git a/hydro/development.js b/hydro/development.js index c704553cd..85e7a7223 100644 --- a/hydro/development.js +++ b/hydro/development.js @@ -35,13 +35,12 @@ async function run() { }); require('./service/gridfs'); let server = require('./service/server'); - require('./handler/ui'); - require('./handler/base'); require('./handler/home'); require('./handler/problem'); require('./handler/record'); require('./handler/judge'); require('./handler/user'); + require('./handler/contest'); server.start(); } run().catch(e => { diff --git a/hydro/handler/base.js b/hydro/handler/base.js deleted file mode 100644 index 4baa58ca0..000000000 --- a/hydro/handler/base.js +++ /dev/null @@ -1,73 +0,0 @@ -const - { MIDDLEWARE } = require('../service/server.js'), - user = require('../model/user'), - blacklist = require('../model/blacklist'), - token = require('../model/token'), - options = require('../options'), - { - UserNotFoundError, - BlacklistedError, - PermissionError - } = require('../error'); - -MIDDLEWARE(async (ctx, next) => { - try { - let sid = ctx.cookies.get('sid'); - let save = ctx.cookies.get('save'); - let tokenType, expireSeconds; - tokenType = token.TYPE_SESSION; - if (save) expireSeconds = options.session.saved_expire_seconds; - else expireSeconds = options.session.unsaved_expire_seconds; - ctx.session = sid ? - await token.update(sid, tokenType, expireSeconds, Object.assign({ - update_ip: ctx.request.ip, - update_ua: ctx.request.headers['user-agent'] || '' - })) : { uid: 1 }; - if (!ctx.session) ctx.session = { uid: 1 }; - let bdoc = await blacklist.get(ctx.request.ip); - if (bdoc) throw new BlacklistedError(ctx.request.ip); - ctx.state.user = await user.getById(ctx.session.uid); - if (!ctx.state.user) throw new UserNotFoundError(ctx.session.uid); - ctx.checkPerm = function (perm) { - for (let i in arguments) { - if (arguments[i] instanceof Array) { - let p = false; - for (let j in arguments) - if (ctx.state.user.hasPerm(arguments[i][j])) { - p = true; - break; - } - if (!p) throw new PermissionError([arguments[i]]); - } else { - if (ctx.state.user.hasPerm(arguments[i])) continue; - else throw new PermissionError([[arguments[i]]]); - } - } - }; - ctx.back = () => ctx.redirect(ctx.request.headers.referer || '/'); - ctx.csrf_token = (await token.add(token.TYPE_CSRF_TOKEN, 600, ctx.path))[0]; - await next(); - if (ctx.session.sid) - await token.update(ctx.session.sid, tokenType, expireSeconds, Object.assign({ - updateIp: ctx.request.ip, - updateUa: ctx.request.headers['user-agent'] || '' - })); - else - [ctx.session.sid] = await token.add(tokenType, expireSeconds, Object.assign({ - create_ip: ctx.request.ip, - create_ua: ctx.request.headers['user-agent'] || '', - update_ip: ctx.request.ip, - update_ua: ctx.request.headers['user-agent'] || '' - }, ctx.session)); - let cookie = { secure: options.session.secure }; - if (save) { - cookie.expires = ctx.session.expireAt, cookie.maxAge = expireSeconds; - ctx.cookies.set('save', 'true', cookie); - } - ctx.cookies.set('sid', ctx.session.sid, cookie); - } catch (e) { - console.error(e.message, e.params); - console.error(e.stack); - ctx.body = { error: { message: e.message, params: e.params, stack: e.stack } }; - } -}, true); \ No newline at end of file diff --git a/hydro/handler/contest.js b/hydro/handler/contest.js new file mode 100644 index 000000000..a7761bbc0 --- /dev/null +++ b/hydro/handler/contest.js @@ -0,0 +1,146 @@ +const + { ContestNotLiveError, ValidationError, ProblemNotFoundError } = require('../error'), + { PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD, PERM_CREATE_CONTEST } = require('../permission'), + { constants } = require('../options'), + paginate = require('../lib/paginate'), + contest = require('../model/contest'), + problem = require('../model/problem'), + record = require('../model/record'), + user = require('../model/user'), + { Route, Handler } = require('../service/server'); + + +class ContestHandler extends Handler { + canViewHiddenScoreboard() { + return this.hasPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + } + canShowRecord(tdoc, allowPermOverride = true) { + if (contest.RULES[tdoc.rule].showRecordFunc(tdoc, new Date())) return true; + if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true; + return false; + } + canShowScoreboard(tdoc, allowPermOverride = true) { + if (contest.RULES[tdoc.rule].showScoreboardFunc(tdoc, new Date())) return true; + if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true; + return false; + } + async verifyProblems(pids) { + let pdocs = await problem.getMulti({ _id: { $in: pids } }).sort({ doc_id: 1 }).toArray(); + if (pids.length != pdocs.length) + for (let pid of pids) { + let p = false; + for (let pdoc of pdocs) + if (pid == pdoc._id) { + p = true; + break; + } + if (!p) throw new ProblemNotFoundError(pid); + } + return pids; + } +} +class ContestListHandler extends ContestHandler { + async get({ rule = 0, page = 1 }) { + this.response.template = 'contest_main.html'; + let tdocs, qs, tpcount; + if (!rule) { + tdocs = contest.getMulti(); + qs = ''; + } else { + if (!contest.CONTEST_RULES.includes(rule)) throw new ValidationError('rule'); + tdocs = contest.getMulti({ rule }); + qs = 'rule={0}'.format(rule); + } + [tdocs, tpcount] = await paginate(tdocs, page, constants.CONTEST_PER_PAGE); + let tids = []; + for (let tdoc of tdocs) tids.push(tdoc._id); + let tsdict = await contest.getListStatus(this.uid, tids); + this.response.body = { + page, tpcount, qs, rule, tdocs, tsdict + }; + } +} +class ContestDetailHandler extends ContestHandler { + async _prepare({ tid }) { + this.tdoc = await contest.get(tid); + } + async get({ page = 1 }) { + this.response.template = 'contest_detail.html'; + let [tsdoc, pdict] = await Promise.all([ + contest.getStatus(this.tdoc._id, this.uid), + problem.getList(this.tdoc.pids) + ]); + let psdict = {}, rdict = {}, attended; + if (tsdoc) { + attended = tsdoc.attend == 1; + for (let pdetail in tsdoc.detail || []) + psdict[pdetail['pid']] = pdetail; + if (this.canShowRecord(this.tdoc)) { + let q = []; + for (let i in psdict) q.push(psdict[i].rid); + rdict = await record.getList(q, { getHidden: true }); + } else + for (let i in psdict) + rdict[psdict[i].rid] = { _id: psdict[i].rid }; + } else attended = false; + let udict = await user.get_dict([this.tdoc.owner]); + let path = [ + ['contest_main', '/c'], + [this.tdoc['title'], null, true] + ]; + this.response.body = { + path, tdoc: this.tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page + }; + } + async post_attend() { + if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); + await contest.attend(this.tdoc._id, this.uid); + this.back(); + } +} +class ContestProblemHandler extends ContestDetailHandler { + constructor(ctx) { + super(ctx); + this.response.template = ''; + } +} +class ContestCreateHandler extends ContestHandler { + async prepare() { + this.checkPerm(PERM_CREATE_CONTEST); + } + async get() { + this.response.template = 'contest_edit.html'; + let rules = {}; + for (let i in contest.RULES) + rules[i] = contest.RULES[i].TEXT; + let now = new Date(); + let ts = now.getTime(); + ts = ts - ts % (15 * 60 * 1000) + 15 * 60 * 1000; + let dt = new Date(ts); + this.response.body = { + rules, page_name: 'contest_create', + date_text: `${dt.getFullYear()}-${dt.getMonth()}-${dt.getDate()}`, + time_text: `${dt.getHours()}-${dt.getMinutes()}`, + pids: '1000, 1001' + }; + } + async post({ title, content, rule, beginAtDate, beginAtTime, duration, pids }) { + let beginAt, endAt; + try { + beginAt = Date.parse(`${beginAtDate}T${beginAtTime}+8:00`); + } catch (e) { + throw new ValidationError('beginAtDate', 'beginAtTime'); + } + endAt = new Date(beginAt + duration * 3600 * 1000); + if (beginAt >= endAt) throw new ValidationError('duration'); + await this.verifyProblems(pids); + let tid = await contest.add(title, content, this.user._id, rule, beginAt, endAt, pids); + this.response.body = { tid }; + this.response.redirect = `/c/${tid}`; + } +} + +Route('/c', ContestListHandler); +Route('/c/:cid', ContestDetailHandler); +Route('/c/:cid/p/:pid', ContestProblemHandler); +Route('/contest/create', ContestCreateHandler); diff --git a/hydro/handler/home.js b/hydro/handler/home.js index a1c235cfe..ed384fcbf 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -1,5 +1,9 @@ -const { GET } = require('../service/server'); +const { Route, Handler } = require('../service/server'); -GET('/', async ctx => { - ctx.body = {}; -}); +class HomeHandler extends Handler { + async get() { + this.response.body = {}; + } +} + +Route('/', HomeHandler); diff --git a/hydro/handler/judge.js b/hydro/handler/judge.js index 16b7001c9..3ea79bb96 100644 --- a/hydro/handler/judge.js +++ b/hydro/handler/judge.js @@ -1,12 +1,10 @@ const - { requirePerm } = require('./tools'), - { sleep } = require('../utils'), { PERM_JUDGE } = require('../permission'), record = require('../model/record'), problem = require('../model/problem'), bus = require('../service/bus'), queue = require('../service/queue'), - { GET, POST, SOCKET } = require('../service/server'); + { Route, Handler, Connection, ConnectionHandler } = require('../service/server'); queue.assert('judge'); @@ -57,45 +55,45 @@ async function end(body) { bus.publish('record_change', rdoc); } -GET('/judge/noop', requirePerm(PERM_JUDGE), async ctx => { - ctx.body = {}; -}); -GET('/judge/fetch', requirePerm(PERM_JUDGE), async ctx => { - let rid = await queue.get('judge', false); - if (rid) { - let rdoc = await record.get(rid); - let pdoc = await problem.getById(rdoc.pid); - let task = { - event: 'judge', - rid, type: 0, - pid: rdoc.pid, - data: pdoc.data, - lang: rdoc.lang, - code: rdoc.code - }; - ctx.body = { task }; +class JudgeHandler extends Handler { + async prepare(){ + this.checkPerm(PERM_JUDGE); } - else ctx.body = {}; -}); -SOCKET('/judge/conn', [requirePerm(PERM_JUDGE)], async conn => { - let isOpen = true, processing = null; - conn.on('close', async () => { - isOpen = false; - if (processing) { - await record.reset(processing); - queue.push('judge', processing); + async get() { + let rid = await queue.get('judge', false); + if (rid) { + let rdoc = await record.get(rid); + let pdoc = await problem.getById(rdoc.pid); + let task = { + event: 'judge', + rid, type: 0, + pid: rdoc.pid, + data: pdoc.data, + lang: rdoc.lang, + code: rdoc.code + }; + this.response.body = { task }; } - }); - conn.on('data', async message => { - message = JSON.parse(message); - if (message.key == 'next') await next(message); - else if (message.key == 'end') { - await end(Object.assign({ judger: conn.state.user._id }, message)); - processing = null; - } - }); - while (isOpen) { - if (!processing) { + else this.response.body = {}; + } + async post_next() { + await next(this.request.body); + this.response.body = {}; + } + async post_end() { + await end(this.request.body); + this.response.body = {}; + } +} +class JudgeConnectionHandler extends ConnectionHandler { + async prepare() { + this.checkPerm(PERM_JUDGE); + } + async message(msg) { + if (msg.key == 'next') await next(msg); + else if (msg.key == 'end') { + await end({ judger: this.user._id, ...msg }); + this.processing = null; let rid = await queue.get('judge'); let rdoc = await record.get(rid); let pdoc = await problem.getById(rdoc.pid); @@ -107,16 +105,17 @@ SOCKET('/judge/conn', [requirePerm(PERM_JUDGE)], async conn => { lang: rdoc.lang, code: rdoc.code }; - conn.write(JSON.stringify({ task })); - processing = task.rid; - } else await sleep(100); + this.send({ task }); + this.processing = task.rid; + } } -}); -POST('/judge/next', requirePerm(PERM_JUDGE), async ctx => { - await next(ctx.request.body); - ctx.body = {}; -}); -POST('/judge/end', requirePerm(PERM_JUDGE), async ctx => { - await end(Object.assign({ judger: ctx.state.user._id }, ctx.request.body)); - ctx.body = {}; -}); + async cleanup() { + if (this.processing) { + await record.reset(this.processing); + queue.push('judge', this.processing); + } + } +} + +Route('/judge', JudgeHandler); +Connection('/judge/conn', JudgeConnectionHandler); diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index 2f170ae4a..a2889f070 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -7,42 +7,31 @@ const user = require('../model/user'), solution = require('../model/solution'), system = require('../model/system'), - { ROUTE } = require('../service/server'), + { Route, Handler } = require('../service/server'), gridfs = require('../service/gridfs'), queue = require('../service/queue'), { NoProblemError, ProblemDataNotFoundError, BadRequestError, SolutionNotFoundError } = require('../error'), { constants } = require('../options'), - { - PERM_VIEW_PROBLEM, - PERM_VIEW_PROBLEM_HIDDEN, - PERM_SUBMIT_PROBLEM, - PERM_CREATE_PROBLEM, - PERM_READ_PROBLEM_DATA, - PERM_EDIT_PROBLEM, - PERM_JUDGE, - PERM_VIEW_PROBLEM_SOLUTION, - PERM_CREATE_PROBLEM_SOLUTION, - PERM_EDIT_PROBLEM_SOLUTION, - PERM_DELETE_PROBLEM_SOLUTION, - PERM_EDIT_PROBLEM_SOLUTION_REPLY, - PERM_REPLY_PROBLEM_SOLUTION - } = require('../permission'); + { PERM_VIEW_PROBLEM, PERM_VIEW_PROBLEM_HIDDEN, PERM_SUBMIT_PROBLEM, + PERM_CREATE_PROBLEM, PERM_READ_PROBLEM_DATA, PERM_EDIT_PROBLEM, + PERM_JUDGE, PERM_VIEW_PROBLEM_SOLUTION, PERM_CREATE_PROBLEM_SOLUTION, + PERM_EDIT_PROBLEM_SOLUTION, PERM_DELETE_PROBLEM_SOLUTION, PERM_EDIT_PROBLEM_SOLUTION_REPLY, + PERM_REPLY_PROBLEM_SOLUTION } = require('../permission'); queue.assert('judge'); -class ProblemHandler { - constructor(ctx) { - this.ctx = ctx; - this.ctx.templateName = 'problem_main.html'; - this.ctx.checkPerm(PERM_VIEW_PROBLEM); +class ProblemHandler extends Handler { + async _prepare() { + this.checkPerm(PERM_VIEW_PROBLEM); } async get({ page = 1, category = null }) { + this.response.template = 'problem_main.html'; let q = {}; if (category) q.category = category; - if (!this.ctx.state.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; + if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; let pdocs = await problem.getMany(q, { pid: 1 }, page, constants.PROBLEM_PER_PAGE); - this.ctx.body = { + this.response.body = { path: [ ['Hydro', '/'], ['problem_main', null] @@ -53,15 +42,12 @@ class ProblemHandler { } class ProblemRandomHandler extends ProblemHandler { - constructor(ctx) { - super(ctx); - } async get() { let q = {}; - if (!this.ctx.state.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; + if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; let pid = await problem.random(q); if (!pid) throw new NoProblemError(); - this.ctx.body = { pid }; + this.response.body = { pid }; } } @@ -70,20 +56,20 @@ class ProblemDetailHandler extends ProblemHandler { super(ctx); this.uid = ctx.state.user._id; this.pid = ctx.params.pid; - this.ctx.templateName = 'problem_detail.html'; + this.response.template = 'problem_detail.html'; } async _prepare() { if (this.pid) this.pdoc = await problem.get(this); - if (this.pdoc.hidden && this.pdoc.owner != this.uid) this.ctx.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + if (this.pdoc.hidden && this.pdoc.owner != this.uid) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); if (this.pdoc) this.udoc = await user.getById(this.pdoc.owner); - this.ctx.body = { + this.response.body = { pdoc: this.pdoc, udoc: this.udoc, title: (this.pdoc || {}).title || '' }; } async get() { - this.ctx.body.path = [ + this.response.body.path = [ ['Hydro', '/'], ['problem_main', '/p'], [this.pdoc.title, null, true] @@ -92,14 +78,13 @@ class ProblemDetailHandler extends ProblemHandler { } class ProblemSubmitHandler extends ProblemDetailHandler { - constructor(ctx) { - super(ctx); - this.ctx.templateName = 'problem_submit.html'; - this.ctx.checkPerm(PERM_SUBMIT_PROBLEM); + async prepare() { + this.checkPerm(PERM_SUBMIT_PROBLEM); } async get() { + this.response.template = 'problem_submit.html'; let rdocs = await record.getUserInProblemMulti(this.uid, this.pdoc._id).sort({ _id: -1 }).limit(10).toArray(); - this.ctx.body = { + this.response.body = { path: [ ['Hydro', '/'], ['problem_main', '/p'], @@ -113,26 +98,26 @@ class ProblemSubmitHandler extends ProblemDetailHandler { }; } async post() { - let { lang, code } = this.ctx.request.body; + let { lang, code } = this.request.body; let rid = await record.add({ uid: this.uid, lang, code, pid: this.pdoc._id }); await queue.push('judge', rid); - this.ctx.state.user.nSubmit++; - this.ctx.body = { rid }; - this.ctx.setRedirect = `/r/${rid}`; + this.user.nSubmit++; + this.response.body = { rid }; + this.response.redirect = `/r/${rid}`; } } -class ProblemSettingsHandler extends ProblemDetailHandler { - constructor(ctx) { - super(ctx); - this.ctx.templateName = 'problem_settings.html'; - } +class ProblemManageHandler extends ProblemDetailHandler { async prepare() { - if (this.pdoc.owner != this.uid) this.ctx.checkPerm(PERM_EDIT_PROBLEM); + if (this.pdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM); } +} + +class ProblemSettingsHandler extends ProblemManageHandler { async get() { + this.response.template = 'problem_settings.html'; this.ctx.body.path = [ ['Hydro', '/'], ['problem_main', '/p'], @@ -142,57 +127,44 @@ class ProblemSettingsHandler extends ProblemDetailHandler { } async post() { // TODO(masnn) - this.ctx.back(); + this.back(); } } -class ProblemEditHandler extends ProblemDetailHandler { - constructor(ctx) { - super(ctx); - this.ctx.templateName = 'problem_edit.html'; - } - async prepare() { - if (this.pdoc.owner != this.uid) this.ctx.checkPerm(PERM_EDIT_PROBLEM); - } +class ProblemEditHandler extends ProblemManageHandler { async get() { - this.ctx.body.path = [ + this.response.template = 'problem_edit.html'; + this.response.body.path = [ ['Hydro', '/'], ['problem_main', '/p'], - [this.pdoc.title, `/p/${this.ctx.params.pid}`, true], + [this.pdoc.title, `/p/${this.request.params.pid}`, true], ['problem_edit', null] ]; - this.ctx.page_name = 'problem_edit'; + this.response.body.page_name = 'problem_edit'; } - async post() { - let title = validator.checkTitle(this.ctx.request.body.title); - let content = validator.checkContent(this.ctx.request.body.content); - let pid = validator.checkPid(this.ctx.request.body.pid); - let pdoc = await problem.get({ pid: this.ctx.params.pid }); - await problem.edit(pdoc._id, { - title, content, pid - }); - this.ctx.setRedirect = `/p/${pid}`; + async post({ title, content }) { + let pid = validator.checkPid(this.request.body.pid); + let pdoc = await problem.get({ pid: this.params.pid }); + await problem.edit(pdoc._id, { title, content, pid }); + this.response.redirect = `/p/${pid}`; } } -class ProblemDataUploadHandler extends ProblemDetailHandler { +class ProblemDataUploadHandler extends ProblemManageHandler { constructor(ctx) { super(ctx); - this.ctx.templateName = 'problem_upload.html'; + this.response.template = 'problem_upload.html'; } - async prepare() { - if (this.pdoc.owner != this.uid) this.ctx.checkPerm(PERM_EDIT_PROBLEM); + async get() { if (this.pdoc.data && typeof this.pdoc.data == 'object') { let files = await gridfs.find({ _id: this.pdoc.data }).toArray(); this.md5 = files[0].md5; } - } - async get() { - this.ctx.body.md5 = this.md5; + this.response.body.md5 = this.md5; } async post() { - if (!this.ctx.request.files.file) throw new BadRequestError(); - const r = fs.createReadStream(this.ctx.request.files.file.path); + if (!this.request.files.file) throw new BadRequestError(); + const r = fs.createReadStream(this.request.files.file.path); let f = gridfs.openUploadStream('data.zip'); await new Promise((resolve, reject) => { r.pipe(f); @@ -206,136 +178,117 @@ class ProblemDataUploadHandler extends ProblemDetailHandler { let files = await gridfs.find({ _id: this.pdoc.data }).toArray(); this.md5 = files[0].md5; } - this.ctx.body.md5 = this.md5; + this.response.body.md5 = this.md5; } } class ProblemDataDownloadHandler extends ProblemDetailHandler { - constructor(ctx) { - super(ctx); - } async get({ pid }) { - if (this.uid != this.pdoc.owner) this.ctx.checkPerm([PERM_READ_PROBLEM_DATA, PERM_JUDGE]); + if (this.uid != this.pdoc.owner) this.checkPerm([PERM_READ_PROBLEM_DATA, PERM_JUDGE]); if (!this.pdoc.data) throw new ProblemDataNotFoundError(pid); else if (typeof this.pdoc.data == 'string') this.ctx.setRedirect = this.pdoc.data.split('from:')[1]; - this.ctx.attachment(`${this.pdoc.title}.zip`); - this.ctx.body = gridfs.openDownloadStream(this.pdoc.data); + this.response.attachment(`${this.pdoc.title}.zip`); + this.response.body = gridfs.openDownloadStream(this.pdoc.data); } } class ProblemSolutionHandler extends ProblemDetailHandler { - constructor(ctx) { - super(ctx); - ctx.templateName = 'problem_solution.html'; - } - async get() { - let page = this.ctx.query.page || 1; - this.ctx.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); + async get({ page = 1 }) { + this.response.template = 'problem_solution.html'; + this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); let [psdocs, pcount, pscount] = await paginate(solution.getMulti(this.pdoc._id), page, constants.SOLUTION_PER_PAGE); let uids = [this.pdoc.owner], docids = []; for (let psdoc of psdocs) { docids.push(psdoc._id); uids.push(psdoc.owner); - if (psdoc.reply.length) { - for (let psrdoc of psdoc.reply) { + if (psdoc.reply.length) + for (let psrdoc of psdoc.reply) uids.push(psrdoc.owner); - } - } } let udict = await user.getList(uids); let pssdict = solution.getListStatus(docids, this.uid); - this.ctx.body.path = [ + let path = [ ['problem_main', '/p'], [this.pdoc.title, `/p/${this.pdoc.pid}`, true], ['problem_solution', null] ]; - this.ctx.body = Object.assign(this.ctx.body, { - psdocs, page, pcount, pscount, udict, pssdict - }); + this.response.body = { + path, psdocs, page, pcount, pscount, udict, pssdict + }; } async post({ psid }) { if (psid) this.psdoc = await solution.get(psid); } async post_submit({ content }) { - this.ctx.checkPerm(PERM_CREATE_PROBLEM_SOLUTION); + this.checkPerm(PERM_CREATE_PROBLEM_SOLUTION); await solution.add(this.pdoc._id, this.uid, content); - this.ctx.back(); + this.back(); } async post_edit_solution({ content }) { - if (this.psdoc.owner != this.uid) this.ctx.checkPerm(PERM_EDIT_PROBLEM_SOLUTION); + if (this.psdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION); this.psdoc = await solution.edit(this.psdoc._id, content); this.ctx.body.psdoc = this.psdoc; - this.ctx.back(); + this.back(); } async post_delete_solution() { - if (this.psdoc.owner != this.uid) this.ctx.checkPerm(PERM_DELETE_PROBLEM_SOLUTION); + if (this.psdoc.owner != this.uid) this.checkPerm(PERM_DELETE_PROBLEM_SOLUTION); await solution.del(this.psdoc._id); - this.ctx.back(); + this.back(); } async post_reply({ psid, content }) { - this.ctx.checkPerm(PERM_REPLY_PROBLEM_SOLUTION); + this.checkPerm(PERM_REPLY_PROBLEM_SOLUTION); let psdoc = await solution.get(psid); await solution.reply(psdoc._id, this.uid, content); } - async post_edit_reply({ content }) { - let { psid, psrid } = this.ctx.request.body; + async post_edit_reply({ content, psid, psrid }) { let [psdoc, psrdoc] = await solution.getReply(psid, psrid); if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid); - if (psrdoc.owner != this.uid) this.ctx.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); + if (psrdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); await solution.editReply(psid, psrid, content); } async post_delete_reply({ psid, psrid }) { let [psdoc, psrdoc] = await solution.getReply(psid, psrid); if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid); - if (psrdoc.owner != this.uid) this.ctx.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); + if (psrdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); await solution.delReply(psid, psrid); - this.ctx.back(); + this.back(); } async post_upvote() { let [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, 1); - this.ctx.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; - if (!this.ctx.preferJson) this.ctx.back(); + this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; + if (!this.preferJson) this.back(); } async post_downvote() { let [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, -1); - this.ctx.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; - if (!this.ctx.preferJson) this.ctx.back(); + this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; + if (!this.preferJson) this.back(); } } class ProblemSolutionRawHandler extends ProblemDetailHandler { - constructor(ctx) { - super(ctx); - ctx.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); - } async get({ psid }) { + this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); let psdoc = await solution.get(psid); - this.ctx.response.type = 'text/markdown'; - this.ctx.body = psdoc.content; + this.response.type = 'text/markdown'; + this.response.body = psdoc.content; } } class ProblemSolutionReplyRawHandler extends ProblemDetailHandler { - constructor(ctx) { - super(ctx); - ctx.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); - } async get({ psid }) { + this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); let [psdoc, psrdoc] = await solution.getReply(psid); if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid); - this.ctx.response.type = 'text/markdown'; - this.ctx.body = psrdoc.content; + this.response.type = 'text/markdown'; + this.response.body = psrdoc.content; } } -class ProblemCreateHandler { - constructor(ctx) { - this.ctx = ctx; - this.ctx.templateName = 'problem_edit.html'; - this.ctx.checkPerm(PERM_CREATE_PROBLEM); - } +class ProblemCreateHandler extends Handler { async get() { - this.ctx.body = { + this.response.template = 'problem_edit.html'; + this.checkPerm(PERM_CREATE_PROBLEM); + this.response.body = { path: [ ['Hydro', '/'], ['problem_main', '/p'], @@ -343,28 +296,27 @@ class ProblemCreateHandler { ], page_name: 'problem_create' }; } - async post() { - let { title, pid, content, hidden } = this.ctx.request.body; + async post({ title, pid, content, hidden }) { validator.checkPid(pid); pid = pid || await system.incPidCounter(); - await problem.add({ title, content, owner: this.ctx.state.user._id, pid, hidden }); - this.ctx.body = { pid }; - this.ctx.setRedirect = `/p/${pid}/settings`; + await problem.add({ title, content, owner: this.user._id, pid, hidden }); + this.response.body = { pid }; + this.response.redirect = `/p/${pid}/settings`; } } -ROUTE('/p', ProblemHandler); -ROUTE('/problem/random', ProblemRandomHandler); -ROUTE('/p/:pid', ProblemDetailHandler); -ROUTE('/p/:pid/submit', ProblemSubmitHandler); -ROUTE('/p/:pid/settings', ProblemSettingsHandler); -ROUTE('/p/:pid/edit', ProblemEditHandler); -ROUTE('/p/:pid/upload', ProblemDataUploadHandler); -ROUTE('/p/:pid/data', ProblemDataDownloadHandler); -ROUTE('/p/:pid/solution', ProblemSolutionHandler); -ROUTE('/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler); -ROUTE('/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler); -ROUTE('/problem/create', ProblemCreateHandler); +Route('/p', ProblemHandler); +Route('/problem/random', ProblemRandomHandler); +Route('/p/:pid', ProblemDetailHandler); +Route('/p/:pid/submit', ProblemSubmitHandler); +Route('/p/:pid/settings', ProblemSettingsHandler); +Route('/p/:pid/edit', ProblemEditHandler); +Route('/p/:pid/upload', ProblemDataUploadHandler); +Route('/p/:pid/data', ProblemDataDownloadHandler); +Route('/p/:pid/solution', ProblemSolutionHandler); +Route('/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler); +Route('/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler); +Route('/problem/create', ProblemCreateHandler); /* diff --git a/hydro/handler/record.js b/hydro/handler/record.js index 815829889..28883b0f3 100644 --- a/hydro/handler/record.js +++ b/hydro/handler/record.js @@ -1,99 +1,107 @@ const - bson = require('bson'), { constants } = require('../options'), { PERM_READ_RECORD_CODE, PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD, PERM_REJUDGE, PERM_VIEW_PROBLEM_HIDDEN } = require('../permission'), - { requirePerm } = require('../handler/tools'), problem = require('../model/problem'), record = require('../model/record'), user = require('../model/user'), bus = require('../service/bus'), queue = require('../service/queue'), - { GET, POST, SOCKET } = require('../service/server'); + { Route, Handler, Connection, ConnectionHandler } = require('../service/server'); -GET('/r', async ctx => { - ctx.templateName = 'record_main.html'; - let q = {}, - page = ctx.query.page || 1; - let rdocs = await record.getMany(q, { _id: -1 }, page, constants.RECORD_PER_PAGE); - let pdict = {}, udict = {}; - for (let rdoc of rdocs) { - udict[rdoc.uid] = await user.getById(rdoc.uid); - pdict[rdoc.pid] = await problem.get({ pid: rdoc.pid, uid: ctx.state.user._id }); +class RecordListHandler extends Handler { + async get({ page = 1 }) { + this.response.template = 'record_main.html'; + let q = {}; + let rdocs = await record.getMany(q, { _id: -1 }, page, constants.RECORD_PER_PAGE); + let pdict = {}, udict = {}; + for (let rdoc of rdocs) { + udict[rdoc.uid] = await user.getById(rdoc.uid); + pdict[rdoc.pid] = await problem.get({ pid: rdoc.pid, uid: this.user._id }); + } + this.response.body = { + path: [ + ['Hydro', '/'], + ['record_main', null] + ], + page, rdocs, pdict, udict + }; } - ctx.body = { - path: [ - ['Hydro', '/'], - ['record_main', null] - ], - page, rdocs, pdict, udict - }; -}); -SOCKET('/record-conn', [], conn => { - let tid = conn.params.tid; - async function onRecordChange(data) { - let rdoc = data.value; - if (rdoc.tid && rdoc.tid.toString() != tid) return; - let [udoc, pdoc] = await Promise.all([user.getById(rdoc.uid), problem.get({ pid: rdoc.pid })]); - if (pdoc.hidden && !conn.state.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null; - conn.send({ html: await conn.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) }); +} +class RecordDetailHandler extends Handler { + async get({ rid }) { + this.response.template = 'record_detail.html'; + let rdoc = await record.get(rid); + if (rdoc.hidden) this.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + if (rdoc.uid != this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null; + this.response.body = { + path: [ + ['Hydro', '/'], + ['record_detail', null] + ], + rdoc, show_status: true + }; } - bus.subscribe(['record_change'], onRecordChange); - conn.on('data', async message => { - console.log(message); - let { rids } = JSON.parse(message); - for (let rid of rids) { +} +class RecordRejudgeHandler extends Handler { + async post({ rid }) { + this.checkPerm(PERM_REJUDGE); + let rdoc = await record.get(rid); + if (rdoc) { + await record.reset(rid); + await queue.push('judge', rid); + } + this.response.back(); + } +} +class RecordConnectionHandler extends ConnectionHandler { + async prepare() { + bus.subscribe(['record_change'], this.onRecordChange); + } + async message(msg) { + for (let rid of msg.rids) { let rdoc = await record.get(rid); - await onRecordChange({ value: rdoc }); + await this.onRecordChange({ value: rdoc }); } - }); - conn.on('close', () => { - bus.unsubscribe(['record_change'], onRecordChange); - }); -}); -GET('/r/:rid', async ctx => { - ctx.templateName = 'record_detail.html'; - let uid = ctx.state.user._id, rid = new bson.ObjectID(ctx.params.rid); - let rdoc = await record.get(rid); - if (rdoc.hidden) ctx.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); - if (rdoc.uid != uid && !ctx.state.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null; - ctx.body = { - path: [ - ['Hydro', '/'], - ['record_detail', null] - ], - rdoc, show_status: true - }; -}); -SOCKET('/record-detail-conn', [], async conn => { - let rdoc = await record.get(conn.params.rid); - if (rdoc.tid) - if (!await conn.rdoc_contest_visible(rdoc)) { - conn.close(); - return; - } - async function onRecordChange(data) { + } + async cleanup() { + bus.unsubscribe(['record_change'], this.onRecordChange); + } + async onRecordChange(data) { let rdoc = data.value; - if (rdoc._id.toString() != conn.params.rid) return; - conn.send({ - status_html: await conn.renderHTML('record_detail_status.html', { rdoc }), - summary_html: await conn.renderHTML('record_detail_summary.html', { rdoc }) + if (rdoc.tid && rdoc.tid.toString() != this.tid) return; + let [udoc, pdoc] = await Promise.all([user.getById(rdoc.uid), problem.get({ pid: rdoc.pid })]); + if (pdoc.hidden && !this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null; + this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) }); + } +} +class RecordDetailConnectionHandler extends ConnectionHandler { + async prepare({ rid }) { + let rdoc = await record.get(rid); + if (rdoc.tid) + if (!await this.rdocContestVisible(rdoc)) { + this.close(); + return; + } + this.rid = rid; + bus.subscribe(['record_change'], this.onRecordChange); + this.onRecordChange({ value: rdoc }); + } + async onRecordChange(data) { + let rdoc = data.value; + if (rdoc._id.toString() != this.rid) return; + this.send({ + status_html: await this.renderHTML('record_detail_status.html', { rdoc }), + summary_html: await this.renderHTML('record_detail_summary.html', { rdoc }) }); } - bus.subscribe(['record_change'], onRecordChange); - onRecordChange({ value: rdoc }); - conn.on('close', () => { - bus.unsubscribe(['record_change'], onRecordChange); - }); -}); -POST('/r/:rid/rejudge', requirePerm(PERM_REJUDGE), async ctx => { - let uid = ctx.state.user._id, rid = new bson.ObjectID(ctx.params.rid); - let rdoc = await record.get(rid); - if (rdoc.hidden) ctx.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); - if (rdoc.uid != uid && !ctx.state.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null; - if (rdoc) { - await record.reset(rid); - await queue.push('judge', rid); + async cleanup() { + bus.unsubscribe(['record_change'], this.onRecordChange); } - ctx.back(); -}); +} + +Route('/r', RecordListHandler); +Route('/r/:rid', RecordDetailHandler); +Route('/r/:rid/rejudge', RecordRejudgeHandler); +Connection('/record-conn', RecordConnectionHandler); +Connection('/record-detail-conn', RecordDetailConnectionHandler); diff --git a/hydro/handler/tools.js b/hydro/handler/tools.js index 386eb4ed3..1390135c5 100644 --- a/hydro/handler/tools.js +++ b/hydro/handler/tools.js @@ -22,10 +22,5 @@ module.exports = { await next(); }; }, - limitRate(op, period_secs, max_operations) { - return async (ctx, next) => { - await opcount.inc(op, ctx.request.ip, period_secs, max_operations); - await next(); - }; - } -}; \ No newline at end of file + +}; diff --git a/hydro/handler/ui.js b/hydro/handler/ui.js deleted file mode 100644 index a8a9f015c..000000000 --- a/hydro/handler/ui.js +++ /dev/null @@ -1,123 +0,0 @@ -const - path = require('path'), - staticCache = require('koa-static-cache'), - nunjucks = require('nunjucks'), - hljs = require('highlight.js'), - MarkdownIt = require('markdown-it'), - katex = require('markdown-it-katex'), - options = require('../options'), - perm = require('../permission'), - builtin = require('../model/builtin'), - misc=require('../lib/misc'), - { MIDDLEWARE } = require('../service/server'), - { NotFoundError } = require('../error'); - -class Markdown extends MarkdownIt { - constructor() { - super({ - linkify: true, - highlight: function (str, lang) { - if (lang && hljs.getLanguage(lang)) - try { - return hljs.highlight(lang, str).value; - } catch (__) { } // eslint-disable-line no-empty - return ''; - } - }); - this.linkify.tlds('.py', false); - this.use(katex); - } -} -const md = new Markdown(); -class Nunjucks extends nunjucks.Environment { - constructor() { - super( - new nunjucks.FileSystemLoader(path.resolve(options.template.path)), - { autoescape: true, trimBlocks: true } - ); - this.addFilter('json', function (self) { - return JSON.stringify(self); - }, false); - this.addFilter('assign', function (self, data) { - return Object.assign(self, data); - }); - this.addFilter('markdown', function (self) { - return md.render(self); - }); - this.addFilter('gravatar_url', misc.gravatar_url); - this.addFilter('format_size', misc.format_size); - this.addGlobal('typeof', o => typeof o); - this.addGlobal('console', console); - this.addGlobal('static_url', str => `/${str}`); - this.addGlobal('reverse_url', str => str); - this.addGlobal('datetime_span', misc.datetime_span); - this.addGlobal('paginate', misc.paginate); - this.addGlobal('perm', perm); - this.addGlobal('builtin', builtin); - this.addGlobal('status', builtin.STATUS); - } -} -let env = new Nunjucks(); -MIDDLEWARE(staticCache(path.join(process.cwd(), '.uibuild'), { - maxAge: 365 * 24 * 60 * 60 -})); -MIDDLEWARE(async (ctx, next) => { - ctx.render_title = str => str; - ctx.UIContext = { - cdn_prefix: '/', - url_prefix: '/' - }; - ctx.preferJson = (ctx.request.headers['accept'] || '').includes('application/json'); - ctx.renderHTML = (name, context) => { - ctx.user = ctx.state.user; - ctx.translate = str => { - if (!str) return ''; - return str.toString().translate(ctx.state.user.language); - }; - ctx.has_perm = perm => ctx.state.user.hasPerm(perm); - return new Promise((resolve, reject) => { - env.render(name, Object.assign(ctx.state, context, { - handler: ctx, - _: ctx.translate, - user: ctx.state.user - }), (error, res) => { - if (error) reject(error); - else resolve(res); - }); - }); - }; - ctx.render = async (name, context) => { - ctx.body = await ctx.renderHTML(name, context); - ctx.response.type = 'text/html'; - }; - try { - try { - await next(); - if (ctx.setRedirect) { - ctx.response.type = 'application/octet-stream'; - ctx.redirect(ctx.setRedirect); - } else if (ctx.body || ctx.templateName) { - if (ctx.query.noTemplate || ctx.preferJson) return; - if (ctx.query.template || ctx.templateName) { - ctx.body = ctx.body || {}; - Object.assign(ctx.body, JSON.parse(ctx.query.data || '{}')); - await ctx.render(ctx.query.template || ctx.templateName, ctx.body); - } - } else { - throw new NotFoundError(); - } - } catch (e) { - let error; - if (ctx.body && ctx.body.error) error = ctx.body.error; - else error = e; - if (error instanceof NotFoundError) ctx.status = 404; - if (error.toString().startsWith('NotFoundError')) console.log(error); - if (error.toString().startsWith('Template render error')) throw error; - if (ctx.preferJson) ctx.body = { error }; - else await ctx.render('error.html', { error }); - } - } catch (error) { - if (ctx.preferJson) ctx.body = { error }; - else await ctx.render('bsod.html', { error }); - } -}, true); \ No newline at end of file diff --git a/hydro/handler/user.js b/hydro/handler/user.js index ae327bbc9..7034d88d1 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -1,122 +1,136 @@ const - { GET, POST, ROUTE } = require('../service/server.js'), + { Route, Handler } = require('../service/server.js'), user = require('../model/user'), token = require('../model/token'), system = require('../model/system'), - mail = require('../lib/mail'), + { sendMail } = require('../lib/mail'), misc = require('../lib/misc'), validator = require('../lib/validator'), options = require('../options'), { PERM_REGISTER_USER, PERM_LOGGEDIN } = require('../permission'), - { requirePerm, limitRate } = require('./tools'), - { UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, UserNotFoundError, LoginError } = require('../error'); + { UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, + UserNotFoundError, LoginError, SystemError } = require('../error'); -GET('/user', async ctx => { - let udoc = await user.getById(ctx.session.uid); - ctx.templateName = 'user_detail.html'; - ctx.body = { udoc }; -}); -GET('/login', async ctx => { - ctx.templateName = 'user_login.html'; -}); -POST('/login', async ctx => { - let { uname, password, rememberme = false } = ctx.request.body; - let udoc = await user.getByUname(uname); - if (!udoc) throw new LoginError(uname); - if (udoc) udoc.checkPassword(password); - await user.setById(udoc._id, { loginat: new Date(), loginip: ctx.request.ip }); - udoc.salt = ''; - udoc.password = ''; - console.log(udoc); - ctx.session.uid = udoc._id; - ctx.session.rememberme = rememberme; - ctx.body = {}; - let referer = ctx.request.headers.referer || '/'; - ctx.setRedirect = referer.endsWith('/login') ? '/' : referer; -}); -POST('/logout', requirePerm(PERM_LOGGEDIN), async ctx => { - ctx.session = { uid: 1 }; - ctx.body = {}; -}); -GET('/register', requirePerm(PERM_REGISTER_USER), async ctx => { - ctx.templateName = 'user_register.html'; -}); -GET('/register/:code', requirePerm(PERM_REGISTER_USER), async ctx => { - ctx.templateName = 'user_register_with_code.html'; - let code = ctx.params.code; - let { email } = await token.get(code, token.TYPE_REGISTRATION); - if (!email) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); - ctx.body = { email }; -}); -POST('/register/:code', requirePerm(PERM_REGISTER_USER), async ctx => { - let code = ctx.params.code; - let { password, verify_password, uname } = ctx.request.body; - let { email } = await token.get(code, token.TYPE_REGISTRATION); - if (!email) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); - if (password != verify_password) throw new VerifyPasswordError(); - let uid = await system.incUserCounter(); - await user.create({ uid, uname, password, email, regip: ctx.request.ip }); - await token.delete(code, token.TYPE_REGISTRATION); - ctx.session.uid = uid; - ctx.body = {}; - ctx.setRedirect = '/'; -}); - -if (options.smtp.user) { - POST('/register', requirePerm(PERM_REGISTER_USER), limitRate('send_mail', 3600, 30), async ctx => { - let email = ctx.request.body.mail; - validator.checkEmail(email); - if (await user.getByEmail(email, true)) throw new UserAlreadyExistError(email); - let rid = await token.add(token.TYPE_REGISTRATION, options.registration_token_expire_seconds, { email }); - let m = await ctx.render('user_register_mail', { url: `/register/${rid}` }, true); - await mail.sendMail(email, 'Sign Up', 'user_register_mail', m); - ctx.body = {}; - }); - POST('/lostpass', limitRate('send_mail', 3600, 30), async ctx => { - let email = ctx.request.body.mail; - validator.checkEmail(email); - let udoc = await user.getByEmail(email); - if (!udoc) throw new UserNotFoundError(email); +class UserLoginHandler extends Handler { + async get() { + this.response.template = 'user_login.html'; + } + async post({ uname, password, rememberme = false }) { + let udoc = await user.getByUname(uname); + if (!udoc) throw new LoginError(uname); + if (udoc) udoc.checkPassword(password); + await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip }); + udoc.salt = ''; + udoc.password = ''; + this.session.uid = udoc._id; + this.session.rememberme = rememberme; + this.response.body = {}; + let referer = this.request.headers.referer || '/'; + this.response.redirect = referer.endsWith('/login') ? '/' : referer; + } +} +class UserLogoutHandler extends Handler { + constructor(ctx) { + super(ctx); + this.checkPerm(PERM_LOGGEDIN); + } + async get() { + this.response.template = 'user_logout.html'; + } + async post() { + this.session = { uid: 1 }; + this.response.body = {}; + } +} +class UserRegisterHandler extends Handler { + constructor(ctx) { + super(ctx); + this.checkPerm(PERM_REGISTER_USER); + } + async get() { + this.response.template = 'user_register.html'; + } + async post({ mail }) { + validator.checkEmail(mail); + if (await user.getByEmail(mail, true)) throw new UserAlreadyExistError(mail); + this.limitRate('send_mail', 3600, 30); + let t = await token.add(token.TYPE_REGISTRATION, options.registration_token_expire_seconds, { mail }); + if (options.smtp.user) { + let m = await this.renderHTML('user_register_mail', { url: `/register/${t}` }); + await sendMail(mail, 'Sign Up', 'user_register_mail', m); + this.response.body = {}; + this.response.template = 'user_register_mail_sent.html'; + } else { + this.response.redirect = `/register/${t[0]}`; + } + } +} +class UserRegisterWithCodeHandler extends Handler { + constructor(ctx) { + super(ctx); + this.checkPerm(PERM_REGISTER_USER); + } + async get({ code }) { + this.response.template = 'user_register_with_code.html'; + let { mail } = await token.get(code, token.TYPE_REGISTRATION); + if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); + this.response.body = { mail }; + } + async post({ code, password, verify_password, uname }) { + let { mail } = await token.get(code, token.TYPE_REGISTRATION); + if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); + if (password != verify_password) throw new VerifyPasswordError(); + let uid = await system.incUserCounter(); + await user.create({ uid, uname, password, mail, regip: this.request.ip }); + await token.delete(code, token.TYPE_REGISTRATION); + this.session.uid = uid; + this.response.body = {}; + this.response.redirect = '/'; + } +} +class UserLostPassHandler extends Handler { + constructor(ctx) { + if (!options.smtp.user) throw new SystemError('Cannot send mail'); + super(ctx); + } + async get() { + this.response.template = 'user_lostpass.html'; + } + async post({ mail }) { + validator.checkEmail(mail); + let udoc = await user.getByEmail(mail); + if (!udoc) throw new UserNotFoundError(mail); let tid = await token.add( token.TYPE_LOSTPASS, options.lostpass_token_expire_seconds, { uid: udoc._id } ); - let m = await ctx.render('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname }, true); - await mail.sendMail(email, 'Lost Password', 'user_lostpass_mail', m); - ctx.body = {}; - }); - GET('/lostpass/:code', async ctx => { - let code = ctx.params.code; + let m = await this.renderHTML('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname }); + await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m); + this.response.body = {}; + this.response.template = 'user_lostpass_mail_sent.html'; + } +} +class UserLostPassWithCodeHandler extends Handler { + async get({ code }) { let tdoc = await token.get(code, token.TYPE_LOSTPASS); if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); let udoc = await user.getById(tdoc.uid); - ctx.body = { uname: udoc.uname }; - }); - POST('/lostpass/:code', async ctx => { - let code = ctx.params.code; - let password = ctx.request.body.password; - let verify_password = ctx.request.body.verify_password; + this.response.body = { uname: udoc.uname }; + } + async post({ code, password, verify_password }) { let tdoc = await token.get(code, token.TYPE_LOSTPASS); if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); if (password != verify_password) throw new VerifyPasswordError(); await user.setPassword(tdoc.uid, password); await token.delete(code, token.TYPE_LOSTPASS); - ctx.redirect('/'); - }); -} else - POST('/register', requirePerm(PERM_REGISTER_USER), limitRate('send_mail', 3600, 60), async ctx => { - let email = ctx.request.body.mail; - validator.checkEmail(email); - if (await user.getByEmail(email, true)) throw new UserAlreadyExistError(email); - let t = await token.add(token.TYPE_REGISTRATION, options.session.registration_token_expire_seconds, { email }); - ctx.setRedirect = `/register/${t[0]}`; - }); - -ROUTE('/user/:uid', class UserDetailHandler { + this.response.redirect = '/'; + } +} +class UserDetailHandler extends Handler { constructor(ctx) { - this.ctx = ctx; - this.ctx.templateName = 'user_detail.html'; + super(ctx); + this.response.template = 'user_detail.html'; } async get({ uid }) { let isSelfProfile = this.ctx.state.user._id == uid; @@ -125,12 +139,8 @@ ROUTE('/user/:uid', class UserDetailHandler { let sdoc = await token.getMostRecentSessionByUid(uid); this.ctx.body = { isSelfProfile, udoc, sdoc }; } -}); - -ROUTE('/user/search', class UserSearchHandler { - constructor(ctx) { - this.ctx = ctx; - } +} +class UserSearchHandler extends Handler { async get({ q, exact_match = false }) { let udocs; if (exact_match) udocs = []; @@ -146,4 +156,13 @@ ROUTE('/user/search', class UserSearchHandler { udocs[i].gravatar_url = misc.gravatar_url[udocs[i].gravatar]; this.ctx.body = { udocs }; } -}); +} + +Route('/login', UserLoginHandler); +Route('/register', UserRegisterHandler); +Route('/register/:code', UserRegisterWithCodeHandler); +Route('/logout', UserLogoutHandler); +Route('/lostpass', UserLostPassHandler); +Route('/lostpass/:code', UserLostPassWithCodeHandler); +Route('/user/:uid', UserDetailHandler); +Route('/user/search', UserSearchHandler); diff --git a/hydro/lib/i18n.js b/hydro/lib/i18n.js index 495b20d35..9ea37547a 100644 --- a/hydro/lib/i18n.js +++ b/hydro/lib/i18n.js @@ -10,12 +10,12 @@ String.prototype.format = function (args) { if (arguments.length == 1 && typeof (args) == 'object') { for (var key in args) if (args[key] != undefined) { - let reg = new RegExp('({' + key + '})', 'g'); + let reg = new RegExp('(\\{' + key + '\\})', 'g'); result = result.replace(reg, args[key]); } } else for (var i = 0; i < arguments.length; i++) if (arguments[i] != undefined) { - let reg = new RegExp('({)' + i + '(})', 'g'); + let reg = new RegExp('(\\{)' + i + '(\\})', 'g'); result = result.replace(reg, arguments[i]); } } diff --git a/hydro/lib/markdown.js b/hydro/lib/markdown.js new file mode 100644 index 000000000..0f972ac2e --- /dev/null +++ b/hydro/lib/markdown.js @@ -0,0 +1,22 @@ +const + hljs = require('highlight.js'), + MarkdownIt = require('markdown-it'), + katex = require('markdown-it-katex'); + +class Markdown extends MarkdownIt { + constructor() { + super({ + linkify: true, + highlight: function (str, lang) { + if (lang && hljs.getLanguage(lang)) + try { + return hljs.highlight(lang, str).value; + } catch (__) { } // eslint-disable-line no-empty + return ''; + } + }); + this.linkify.tlds('.py', false); + this.use(katex); + } +} +module.exports = new Markdown(); diff --git a/hydro/lib/template.js b/hydro/lib/template.js new file mode 100644 index 000000000..0231e79bc --- /dev/null +++ b/hydro/lib/template.js @@ -0,0 +1,41 @@ +const + path = require('path'), + nunjucks = require('nunjucks'), + markdown = require('./markdown'), + options = require('../options'), + perm = require('../permission'), + builtin = require('../model/builtin'), + model = require('../model'), + misc = require('../lib/misc'); + +class Nunjucks extends nunjucks.Environment { + constructor() { + super( + new nunjucks.FileSystemLoader(path.resolve(options.template.path)), + { autoescape: true, trimBlocks: true } + ); + this.addFilter('json', function (self) { + return JSON.stringify(self); + }, false); + this.addFilter('assign', function (self, data) { + return Object.assign(self, data); + }); + this.addFilter('markdown', function (self) { + return markdown.render(self); + }); + this.addFilter('gravatar_url', misc.gravatar_url); + this.addFilter('format_size', misc.format_size); + this.addGlobal('typeof', o => typeof o); + this.addGlobal('console', console); + this.addGlobal('static_url', str => `/${str}`); + this.addGlobal('reverse_url', str => str); + this.addGlobal('datetime_span', misc.datetime_span); + this.addGlobal('paginate', misc.paginate); + this.addGlobal('perm', perm); + this.addGlobal('status', builtin.STATUS); + this.addGlobal('model', model); + } +} +let env = new Nunjucks(); + +module.exports = env; \ No newline at end of file diff --git a/hydro/model/contest.js b/hydro/model/contest.js index 7e5697608..bc6eeca4a 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -1,18 +1,15 @@ const validator = require('../lib/validator'), { ValidationError, ContestNotFoundError } = require('../error'), - RULE_HOMEWORK = require('../module/contest/homework'), - RULE_OI = require('../module/contest/oi'), - RULE_ACM = require('../module/contest/acm'), db = require('../service/db.js'), coll = db.collection('contest'), coll_status = db.collection('contest.status'); const RULES = { - homework: RULE_HOMEWORK, - oi: RULE_OI, - acm: RULE_ACM + homework: require('../module/contest/homework'), + oi: require('../module/contest/oi'), + acm: require('../module/contest/acm') }; module.exports = { @@ -28,26 +25,26 @@ module.exports = { return await coll.insertOne(data); }, count: query => coll.find(query).count(), - async edit(cid, $set) { + async edit(tid, $set) { if ($set.title) validator.checkTitle($set.title); if ($set.content) validator.checkIntro($set.content); if ($set.rule) if (!this.RULES[$set.rule]) throw new ValidationError('rule'); if ($set.beginAt && $set.endAt) if ($set.beginAt >= $set.endAt) throw new ValidationError('beginAt', 'endAt'); - let cdoc = await coll.findOne({ cid }); - if (!cdoc) throw new ContestNotFoundError(cid); - this.RULES[$set.rule || cdoc.rule].check(Object.assign(cdoc, $set)); - await coll.findOneAndUpdate({ cid }, { $set }); - return cdoc; - }, - async get( cid) { - let tdoc = await coll.findOne({ cid }); - if (!tdoc) throw new ContestNotFoundError(cid); + let tdoc = await coll.findOne({ tid }); + if (!tdoc) throw new ContestNotFoundError(tid); + this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); + await coll.findOneAndUpdate({ tid }, { $set }); return tdoc; }, - get_multi: (query, fields) => coll.find(query, { fields }), - get_multi_status: (query, fields) => coll_status.find(query, { fields }), + async get(tid) { + let tdoc = await coll.findOne({ tid }); + if (!tdoc) throw new ContestNotFoundError(tid); + return tdoc; + }, + getMulti: query => coll.find(query), + getMultiStatus: query => coll_status.find(query), async get_random_id(query) { let pdocs = coll.find(query); let pcount = await pdocs.count(); @@ -56,8 +53,43 @@ module.exports = { return pdoc.pid; } else return null; }, - get_status: (cid, uid, fields) => coll_status.findOne({ cid, uid }, { fields }), - set_status: (cid, uid, $set) => coll_status.findOneAndUpdate({ cid, uid }, { $set }) + getStatus: (tid, uid) => coll_status.findOne({ tid, uid }), + setStatus: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set }), + async getListStatus(uid, tids) { + let r = {}; + for (let tid of tids) r[tid] = await this.getStatus(tid, uid); + return r; + }, + is_new(tdoc, days = 1) { + let now = new Date().getTime(); + let readyAt = tdoc.beginAt.getTime(); + return (now < readyAt - days * 24 * 3600 * 1000); + }, + is_upcoming(tdoc, days = 1) { + let now = new Date().getTime(); + let readyAt = tdoc.beginAt.getTime(); + return (now > readyAt + days * 24 * 3600 * 1000 && now < tdoc.beginAt); + }, + is_not_started: tdoc => new Date() < tdoc.beginAt, + is_ongoing(tdoc) { + let now = new Date(); + return (tdoc.beginAt <= now && now < tdoc.endAt); + }, + is_done: tdoc => tdoc.endAt <= new Date(), + status_text: tdoc => + this.is_new(tdoc) + ? 'New' + : this.is_upcoming(tdoc) + ? 'Ready (☆▽☆)' + : this.is_ongoing(tdoc) + ? 'Live...' + : 'Done', + get_status: tdoc => + this.is_not_started(tdoc) + ? 'not_started' + : this.is_ongoing(tdoc) + ? 'ongoing' + : 'finished', }; /* @@ -275,9 +307,9 @@ class ContestVisibilityMixin(object): if self.domainId != tdoc['domainId']: return False if tdoc['doc_type'] == document.TYPE_CONTEST: - return self.has_perm(builtin.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD) + return self.hasPerm(builtin.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD) elif tdoc['doc_type'] == document.TYPE_HOMEWORK: - return self.has_perm(builtin.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD) + return self.hasPerm(builtin.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD) else: return False diff --git a/hydro/model/index.js b/hydro/model/index.js new file mode 100644 index 000000000..20361c900 --- /dev/null +++ b/hydro/model/index.js @@ -0,0 +1,13 @@ +module.exports = { + blacklist: require('./blacklist'), + builtin: require('./builtin'), + contest: require('./contest'), + opcount: require('./opcount'), + problem: require('./problem'), + record: require('./record'), + solution: require('./solution'), + system: require('./system'), + token: require('./token'), + training: require('./training'), + user: require('./user') +}; diff --git a/hydro/model/problem.js b/hydro/model/problem.js index 78efde183..6a7afeb68 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -63,6 +63,9 @@ async function getById(_id) { async function getMany(query, sort, page, limit) { return await coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray(); } +function getMulti(query) { + return coll.find(query); +} async function edit(_id, $set) { if ($set.title) validator.checkTitle($set.title); if ($set.content) validator.checkContent($set.content); @@ -90,5 +93,6 @@ module.exports = { edit, count, random, - getById + getById, + getMulti }; \ No newline at end of file diff --git a/hydro/model/token.js b/hydro/model/token.js index 78ecfe509..e01a6277a 100644 --- a/hydro/model/token.js +++ b/hydro/model/token.js @@ -66,7 +66,7 @@ module.exports = { return !!result.deletedCount; }, async getMostRecentSessionByUid(uid) { - return await coll.findOne({ data: { uid }, token_type: this.TYPE_SESSION }, { sort: { updateAt: -1 } }); + return await coll.findOne({ uid, token_type: this.TYPE_SESSION }, { sort: { updateAt: -1 } }); }, init: () => Promise.all([ coll.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true }), diff --git a/hydro/module/contest/acm.js b/hydro/module/contest/acm.js index 6bcc6bba8..612a01017 100644 --- a/hydro/module/contest/acm.js +++ b/hydro/module/contest/acm.js @@ -1,4 +1,5 @@ module.exports = { + TEXT: 'ACM/ICPC', check: () => { }, stat: (tdoc, journal) => { let naccept = {}, effective = {}, detail = [], accept = 0, time = 0; diff --git a/hydro/module/contest/homework.js b/hydro/module/contest/homework.js index dbb2c4dbf..64e43a2b7 100644 --- a/hydro/module/contest/homework.js +++ b/hydro/module/contest/homework.js @@ -1,5 +1,6 @@ const { ValidationError } = require('../../error'); module.exports = { + TEXT: 'Assignment', check(data) { if (!data.penalty_since) throw new ValidationError('penalty_since'); if (data.penalty_since < data.begin_at) throw new ValidationError('penalty_since', 'begin_at'); diff --git a/hydro/module/contest/oi.js b/hydro/module/contest/oi.js index 0be8accf1..1a4c8c402 100644 --- a/hydro/module/contest/oi.js +++ b/hydro/module/contest/oi.js @@ -1,4 +1,5 @@ module.exports = { + TEXT: 'OI', check: () => { }, stat: (tdoc, journal) => { let detail = {}, score = 0; diff --git a/hydro/options.js b/hydro/options.js index a92a92531..ea0f3cb36 100644 --- a/hydro/options.js +++ b/hydro/options.js @@ -40,7 +40,8 @@ let options = { constants: { PROBLEM_PER_PAGE: 100, RECORD_PER_PAGE: 100, - SOLUTION_PER_PAGE: 20 + SOLUTION_PER_PAGE: 20, + CONTEST_PER_PAGE: 20 } }; diff --git a/hydro/service/server.js b/hydro/service/server.js index 671aea1ac..212b2a4d6 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -1,17 +1,29 @@ const + path = require('path'), + { ObjectID } = require('bson'), Koa = require('koa'), morgan = require('koa-morgan'), Body = require('koa-body'), Router = require('koa-router'), + cache = require('koa-static-cache'), sockjs = require('sockjs'), - validator = require('../lib/validator'); + validator = require('../lib/validator'), + template = require('../lib/template'), + user = require('../model/user'), + blacklist = require('../model/blacklist'), + token = require('../model/token'), + opcount = require('../model/opcount'), + { UserNotFoundError, BlacklistedError, PermissionError, + NotFoundError } = require('../error'); const options = require('../options'); - let http = options.listen.https ? require('https') : require('http'); let app = new Koa(); let server = http.createServer(app.callback()); app.keys = options.session.keys; +app.use(cache(path.join(process.cwd(), '.uibuild'), { + maxAge: 365 * 24 * 60 * 60 +})); app.use(Body({ multipart: true, formidable: { @@ -19,121 +31,302 @@ app.use(Body({ } })); let router = new Router(); -let m = []; -/** - * @param {import('koa').Middleware} middleware - * @param {boolean} installForSocket - */ -function MIDDLEWARE(middleware, installForSocket = false) { - app.use(middleware); - if (installForSocket) m.push(middleware); + +class Handler { + /** + * @param {import('koa').Context} ctx + */ + constructor(ctx) { + this.ctx = ctx; + this.request = { + ip: ctx.request.ip, + headers: ctx.request.headers, + cookies: ctx.cookies, + body: ctx.request.body, + files: ctx.request.files, + query: ctx.query, + path: ctx.path + }; + this.response = { + body: '', + type: '', + status: 404, + template: null, + redirect: null, + attachment: name => ctx.attachment(name) + }; + this.UIContext = { + cdn_prefix: '/', + url_prefix: '/' + }; + this._handler = {}; + } + renderHTML(name, context) { + console.time(name); + this.hasPerm = perm => this.user.hasPerm(perm); + return new Promise((resolve, reject) => { + template.render(name, Object.assign(context, { + handler: this, + _: str => str ? str.toString().translate(this.user.language) : '', + user: this.user + }), (error, res) => { + console.timeEnd(name); + if (error) reject(error); + else resolve(res); + }); + }); + } + async render(name, context) { + this.response.body = await this.renderHTML(name, context); + this.response.type = 'text/html'; + } + render_title(str) { + return str; + } + checkPerm(perm) { + for (let i in arguments) { + if (arguments[i] instanceof Array) { + let p = false; + for (let j in arguments) + if (this.user.hasPerm(arguments[i][j])) { + p = true; + break; + } + if (!p) throw new PermissionError([arguments[i]]); + } else { + if (this.user.hasPerm(arguments[i])) continue; + else throw new PermissionError([[arguments[i]]]); + } + } + } + async limitRate(op, period_secs, max_operations) { + await opcount.inc(op, this.request.ip, period_secs, max_operations); + } + back() { + this.ctx.redirect(this.request.headers.referer || '/'); + } + async ___prepare() { + this._handler.sid = this.request.cookies.get('sid'); + this._handler.save = this.request.cookies.get('save'); + this._handler.tokenType = token.TYPE_SESSION; + if (this._handler.save) this._handler.expireSeconds = options.session.saved_expire_seconds; + else this._handler.expireSeconds = options.session.unsaved_expire_seconds; + this.session = this._handler.sid ? + await token.update(this._handler.sid, this._handler.tokenType, this._handler.expireSeconds, { + update_ip: this.request.ip, + update_ua: this.request.headers['user-agent'] || '' + }) : { uid: 1 }; + if (!this.session) this.session = { uid: 1 }; + let bdoc = await blacklist.get(this.request.ip); + if (bdoc) throw new BlacklistedError(this.request.ip); + this.user = await user.getById(this.session.uid); + console.log(this.user, this.session.uid); + if (!this.user) throw new UserNotFoundError(this.session.uid); + this.csrf_token = (await token.add(token.TYPE_CSRF_TOKEN, 600, this.request.path))[0]; + this.preferJson = (this.request.headers['accept'] || '').includes('application/json'); + } + async ___cleanup() { + try { + await this.renderBody(); + } catch (e) { + try { + let error; + if (this.response.body && this.response.body.error) error = this.response.body.error; + else error = e; + if (error instanceof NotFoundError) this.response.status = 404; + if (error.name == 'Template render error') throw error; + if (this.preferJson) this.response.body = { error }; + else await this.render('error.html', { error }); + } catch (error) { + if (this.preferJson) this.response.body = { error }; + else await this.render('bsod.html', { error }); + } + } + await this.putResponse(); + await this.saveCookie(); + } + async renderBody() { + if (this.response.body || this.response.template) { + if (this.request.query.noTemplate || this.preferJson) return; + if (this.request.query.template || this.response.template) { + this.response.body = this.response.body || {}; + await this.render(this.request.query.template || this.response.template, this.response.body); + } + } + } + async putResponse() { + if (this.response.redirect) { + this.response.type = 'application/octet-stream'; + this.redirect(this.response.redirect); + } + if (this.response.body) this.ctx.body = this.response.body; + if (this.response.type) this.ctx.response.type = this.response.type; + if (this.response.status) this.ctx.response.status = this.response.status; + } + async saveCookie() { + if (this.session.sid) + await token.update(this.session.sid, this._handler.tokenType, this._handler.expireSeconds, { + updateIp: this.request.ip, + updateUa: this.request.headers['user-agent'] || '' + }); + else + [this.session.sid] = await token.add(this._handler.tokenType, this._handler.expireSeconds, { + createIp: this.request.ip, + createUa: this.request.headers['user-agent'] || '', + updateIp: this.request.ip, + updateUa: this.request.headers['user-agent'] || '', + ...this.session + }); + let cookie = { secure: options.session.secure }; + if (this._handler.save) { + cookie.expires = this.session.expireAt, cookie.maxAge = this._handler.expireSeconds; + this.request.cookies.set('save', 'true', cookie); + } + this.ctx.cookies.set('sid', this.session.sid, cookie); + } + async onerror(e) { + console.error(e.message, e.params); + console.error(e.stack); + await this.___cleanup(); + } } -/** - * @param {string} route - * @param {...import('koa').Middleware} handler - */ -function GET(route, ...handler) { - router.get(route, ...handler); -} -/** - * @param {string} route - * @param {...import('koa').Middleware} handler - */ -function POST(route, ...handler) { - router.post(route, ...handler); -} -/** - * @param {string} route - * @param {...import('koa').Middleware} handler - */ -function ALL(route, ...handler) { - router.all(route, ...handler); -} -/** - * @param {string} route - * @param {class} handler - */ -function ROUTE(route, handler) { +function Route(route, handler) { router.all(route, async (ctx) => { let h = new handler(ctx); - let method = ctx.method.toLowerCase(); - let args = Object.assign({}, ctx.params, ctx.query, ctx.request.body); - if (args.content) validator.checkContent(args.content); - if (args.title) validator.checkContent(args.title); - if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); - if (h._prepare) await h._prepare(args); - if (h.prepare) await h.prepare(args); - if (h[`_${method}`]) await h[`_${method}`](args); - if (h[method]) await h[method](args); - console.log(ctx.request.body); - if (method == 'post' && ctx.request.body.operation) { - if (h[`${method}_${ctx.request.body.operation}`]) - await h[`${method}_${ctx.request.body.operation}`](args); + try { + let method = ctx.method.toLowerCase(); + let args = Object.assign({}, ctx.params, ctx.query, ctx.request.body); + + if (args.content) validator.checkContent(args.content); + if (args.title) validator.checkContent(args.title); + if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); + if (args.page) args.page = parseInt(args.page); + if (args.rid) args.rid = new ObjectID(args.rid); + if (args.tid) args.tid = new ObjectID(args.tid); + + if (h.___prepare) await h.___prepare(args); + if (h.__prepare) await h.__prepare(args); + if (h._prepare) await h._prepare(args); + if (h.prepare) await h.prepare(args); + + if (h[`___${method}`]) await h[`___${method}`](args); + if (h[`__${method}`]) await h[`__${method}`](args); + if (h[`_${method}`]) await h[`_${method}`](args); + if (h[method]) await h[method](args); + + console.log(ctx.request.body); + if (method == 'post' && ctx.request.body.operation) { + if (h[`${method}_${ctx.request.body.operation}`]) + await h[`${method}_${ctx.request.body.operation}`](args); + } + + if (h.cleanup) await h.cleanup(args); + if (h._cleanup) await h._cleanup(args); + if (h.__cleanup) await h.__cleanup(args); + if (h.___cleanup) await h.___cleanup(args); + } catch (e) { + if (h.onerror) await h.onerror(e); } }); } -/** - * @callback SockJSHandler - * @param {import('sockjs').Connection} conn - */ -/** - * @param {string} prefix - * @param {SockJSHandler} handler - */ -function SOCKET(prefix, middlewares, handler) { - const sock = sockjs.createServer({ prefix }); - sock.on('connection', async conn => { - try { - conn.cookies = { +class ConnectionHandler { + /** + * @param {import('sockjs').Connection} conn + */ + constructor(conn) { + this.conn = conn; + this.request = { + cookies: { get(name) { return conn.cookies[name]; }, set() { } - }; - conn.state = {}; - conn.params = {}; - conn.request = { - headers: conn.headers - }; - let p = (conn.url.split('?')[1] || '').split('&'); - for (let i in p) p[i] = p[i].split('='); - for (let i in p) conn.params[p[i][0]] = decodeURIComponent(p[i][1]); - await new Promise((resolve, reject) => { - conn.once('data', msg => { - for (let i of msg.split(';')) { - i = i.trim().split('='); - conn.cookies[i[0]] = i[1]; - } - resolve(); - }); - setTimeout(reject, 5000); + }, + params: {}, + headers: conn.headers + }; + let p = (conn.url.split('?')[1] || '').split('&'); + for (let i in p) p[i] = p[i].split('='); + for (let i in p) conn.params[p[i][0]] = decodeURIComponent(p[i][1]); + } + renderHTML(name, context) { + console.time(name); + this.hasPerm = perm => this.user.hasPerm(perm); + return new Promise((resolve, reject) => { + template.render(name, Object.assign(context, { + handler: this, + _: this.translate, + user: this.user + }), (error, res) => { + console.timeEnd(name); + if (error) reject(error); + else resolve(res); + }); + }); + } + send(data) { + this.conn.write(JSON.stringify(data)); + } + close(code, reason) { + this.conn.close(code, reason); + } + async ___prepare() { + await new Promise((resolve, reject) => { + this.conn.once('data', msg => { + for (let i of msg.split(';')) { + i = i.trim().split('='); + this.request.cookies[i[0]] = i[1]; + } + resolve(); + }); + setTimeout(reject, 5000); + }); + this._handler.sid = this.request.cookies.get('sid'); + this.session = this._handler.sid ? await token.get(this._handler.sid, token.TYPE_SESSION) : { uid: 1 }; + if (!this.session) this.session = { uid: 1 }; + let bdoc = await blacklist.get(this.request.ip); + if (bdoc) throw new BlacklistedError(this.request.ip); + this.user = await user.getById(this.session.uid); + if (!this.user) throw new UserNotFoundError(this.session.uid); + } +} +function Connection(prefix, handler) { + const sock = sockjs.createServer({ prefix }); + sock.on('connection', async conn => { + let h = new handler(conn); + try { + let args = Object.assign({}, h.conn.params); + + if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); + if (args.page) args.page = parseInt(args.page); + if (args.rid) args.rid = new ObjectID(args.rid); + if (args.tid) args.tid = new ObjectID(args.tid); + + if (h.___prepare) await h.___prepare(args); + if (h.__prepare) await h.__prepare(args); + if (h._prepare) await h._prepare(args); + if (h.prepare) await h.prepare(args); + if (h.message) conn.on('data', data => { + h.message(JSON.parse(data)); + }); + conn.on('close', async () => { + if (h.cleanup) await h.cleanup(args); + if (h._cleanup) await h._cleanup(args); + if (h.__cleanup) await h.__cleanup(args); + if (h.___cleanup) await h.___cleanup(args); }); - for (let i of m) - await new Promise((resolve, reject) => { - i(conn, resolve).catch(reject); - }); - for (let i of middlewares) - await new Promise((resolve, reject) => { - i(conn, resolve).catch(reject); - }); - conn.send = data => { - conn.write(JSON.stringify(data)); - }; - handler(conn); } catch (e) { - console.error(e); + if (h.onerror) await h.onerror(e); } }); sock.installHandlers(server); } -exports.ROUTE = ROUTE; -exports.MIDDLEWARE = MIDDLEWARE; -exports.GET = GET; -exports.POST = POST; -exports.ALL = ALL; -exports.SOCKET = SOCKET; +exports.Handler = Handler; +exports.ConnectionHandler = ConnectionHandler; +exports.Route = Route; +exports.Connection = Connection; exports.start = function start() { app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); app.use(router.routes()).use(router.allowedMethods()); diff --git a/locales/zh_CN.yaml b/locales/zh_CN.yaml index e86f1d863..f98c007b7 100644 --- a/locales/zh_CN.yaml +++ b/locales/zh_CN.yaml @@ -688,7 +688,7 @@ Recent Submissions: 最近递交 Oh, the user hasn't submitted yet!: 这个用户还没有交过题 _(:зゝ∠)_ Oh, the user hasn't created any discussions yet!: 这个用户还没有发布过讨论 Recently Created Discussions: 最近创建的讨论 -Oh, the user doesn't have any contributions!: 啊哦,这个用户还没贡献过题目和题解~ +Oh, the user doesn't have any contributions!: 啊哦,这个用户还没贡献过题目~ Recent Solutions: 最近编写的题解 Most Upvoted Solutions: 最被赞同的题解 The user is too lazy to leave something here...: 该用户太懒,这里啥也没写 (´・ω・`) diff --git a/templates/components/comments_discussion.html b/templates/components/comments_discussion.html index 9d51fb90f..fbaf09e46 100644 --- a/templates/components/comments_discussion.html +++ b/templates/components/comments_discussion.html @@ -28,15 +28,15 @@ {% macro reply_operations(doc, rdoc) %}
-{% if handler.has_perm(comment_post_perm) %} +{% if handler.hasPerm(comment_post_perm) %}-
@@ -116,17 +116,17 @@
@ {{ datetime_span(doc['_id'])|safe }}
- {% if handler.has_perm(reply_post_perm) %}
+ {% if handler.hasPerm(reply_post_perm) %}
{% endif %}
- {% if doc.owner == handler.state.user._id or handler.has_perm(comment_edit_perm) %}
+ {% if doc.owner == handler.state.user._id or handler.hasPerm(comment_edit_perm) %}
{% endif %}
- {% if doc.owner == handler.state.user._id or handler.has_perm(comment_delete_perm) %}
+ {% if doc.owner == handler.state.user._id or handler.hasPerm(comment_delete_perm) %}
diff --git a/templates/components/comments_solution.html b/templates/components/comments_solution.html
index 66d857a2c..4d67224a8 100644
--- a/templates/components/comments_solution.html
+++ b/templates/components/comments_solution.html
@@ -30,7 +30,7 @@
reply_delete_op = 'delete_tail_reply'
) %}
-{% if handler.has_perm(comment_post_perm) %} +{% if handler.hasPerm(comment_post_perm) %}-
@@ -85,7 +85,7 @@
- {% if handler.has_perm(perm.PERM_VOTE_PROBLEM_SOLUTION) %}
+ {% if handler.hasPerm(perm.PERM_VOTE_PROBLEM_SOLUTION) %}
{{ doc.vote }}
- {% if handler.has_perm(reply_post_perm) %}
+ {% if handler.hasPerm(reply_post_perm) %}
{% endif %}
- {% if doc.owner == handler.state.user._id or handler.has_perm(comment_edit_perm) %}
+ {% if doc.owner == handler.state.user._id or handler.hasPerm(comment_edit_perm) %}
{% endif %}
- {% if doc.owner == handler.state.user._id or handler.has_perm(comment_delete_perm) %}
+ {% if doc.owner == handler.state.user._id or handler.hasPerm(comment_delete_perm) %}
@@ -145,15 +145,15 @@
{{ user.render_inline(udoc, badge=false) }} @ {{ datetime_span(rdoc['_id'])|safe }}
- {% if handler.has_perm(reply_post_perm) %}
+ {% if handler.hasPerm(reply_post_perm) %}
{% endif %}
- {% if doc.owner == handler.state.user._id or handler.has_perm(reply_edit_perm) %}
+ {% if doc.owner == handler.state.user._id or handler.hasPerm(reply_edit_perm) %}
{% endif %}
- {% if doc.owner == handler.state.user._id or handler.has_perm(reply_delete_perm) %}
+ {% if doc.owner == handler.state.user._id or handler.hasPerm(reply_delete_perm) %}
diff --git a/templates/components/form.html b/templates/components/form.html
index 8a208b1ae..9b59fa1df 100644
--- a/templates/components/form.html
+++ b/templates/components/form.html
@@ -1,12 +1,12 @@
{% macro form_begin(args) %}
{% set label = args.label %}
{% set columns = args.columns %}
-{% set last_column = args.last_column or true %}
+{% set last_column = args.last_column|default(true) %}
{% set required = args.required or false %}
{% set hotkeys = args.hotkeys or none %}
-{% set row = args.row or true %}
+{% set row = args.row|default(true) %}
{% set no_label = args.no_label or false %}
-{% set label_wrap = args.label_wrap or true %}
+{% set label_wrap = args.label_wrap|default(true) %}
{% if row %}
{% endif %}
@@ -28,9 +28,9 @@
{% macro form_end(args) %}
{% set help_text = args.help_text or none %}
-{% set row = args.row or true %}
+{% set row = args.row|default(true) %}
{% set no_label = args.no_label or false %}
-{% set label_wrap = args.label_wrap or true %}
+{% set label_wrap = args.label_wrap|default(true) %}
{% if not no_label and label_wrap %}
{% endif %}
diff --git a/templates/components/user.html b/templates/components/user.html
index ccae04b7e..7ed8daa34 100644
--- a/templates/components/user.html
+++ b/templates/components/user.html
@@ -13,7 +13,7 @@
{% if badge %}
LV {{ udoc['level']|default(0) }}
-{% if modbadge and handler.has_perm(perm.PERM_MOD_BADGE) %}
+{% if modbadge and handler.hasPerm(perm.PERM_MOD_BADGE) %}
MOD
{% endif %}
{% endif %}
diff --git a/templates/contest_detail.html b/templates/contest_detail.html
index ccf378b16..23755862b 100644
--- a/templates/contest_detail.html
+++ b/templates/contest_detail.html
@@ -24,7 +24,7 @@
- {% if handler.has_perm(perm.PERM_LOGGEDIN) %}
+ {% if handler.hasPerm(perm.PERM_LOGGEDIN) %}
{% endif %}
@@ -32,7 +32,7 @@
- {% if handler.has_perm(perm.PERM_LOGGEDIN) %}
+ {% if handler.hasPerm(perm.PERM_LOGGEDIN) %}
{{ _('Status') }}
{{ _('Last Submit At') }}
{% endif %}
@@ -42,7 +42,7 @@
{% for pid in tdoc['pids'] %}
- {% if handler.has_perm(perm.PERM_LOGGEDIN) %}
+ {% if handler.hasPerm(perm.PERM_LOGGEDIN) %}
{% if psdict[pid] and psdict[pid]['rid'] %}
{% with rdoc=rdict[psdict[pid]['rid']] %}
{% if handler.can_show_record(tdoc) %}
diff --git a/templates/contest_edit.html b/templates/contest_edit.html
index a662518e7..6a4457ee0 100644
--- a/templates/contest_edit.html
+++ b/templates/contest_edit.html
@@ -6,15 +6,63 @@