diff --git a/hydro/error.js b/hydro/error.js index a2cffc692..e522028de 100644 --- a/hydro/error.js +++ b/hydro/error.js @@ -36,6 +36,7 @@ const SystemError = Err('SystemError', HydroError, 'SystemError', 500); const BadRequestError = Err('BadRequestError', UserFacingError, 400); const ForbiddenError = Err('ForbiddenError', UserFacingError, 403); const NotFoundError = Err('NotFoundError', UserFacingError, 404); +const MethodNotAllowedError = Err('MethodNotAllowedError', UserFacingError, 405); const RemoteOnlineJudgeError = Err('RemoteOnlineJudgeError', UserFacingError, 500); const AlreadyVotedError = Err('AlreadyVotedError', ForbiddenError, "You've already voted."); @@ -78,6 +79,8 @@ const ContestNotFoundError = Err('ContestNotFoundError', DocumentNotFoundError); const DiscussionNotFoundError = Err('DiscussionNotFoundError', DocumentNotFoundError, 'Discussion {0} not found.'); const DiscussionNodeNotFoundError = Err('DiscussionNodeNotFoundError', DocumentNotFoundError, 'Discussion node {1} not found.'); +const InvalidOperationError = Err('InvalidOperationError', MethodNotAllowedError); + global.Hydro.error = module.exports = { Err, HydroError, @@ -87,6 +90,8 @@ global.Hydro.error = module.exports = { NotFoundError, LoginError, CsrfTokenError, + MethodNotAllowedError, + InvalidOperationError, UserAlreadyExistError, InvalidTokenError, UserNotFoundError, @@ -129,9 +134,6 @@ class FileTypeNotAllowedError(ValidationError): def message(self): return 'This type of files are not allowed to be uploaded.' -class InvalidOperationError(ForbiddenError): - pass - class InvalidTokenDigestError(ForbiddenError): pass diff --git a/hydro/handler/misc.js b/hydro/handler/misc.js index ac11584a5..d37817204 100644 --- a/hydro/handler/misc.js +++ b/hydro/handler/misc.js @@ -64,6 +64,12 @@ class MarkdownHandler extends Handler { } } +class SockToken extends Handler { + async get() { + this.response.body = { token: this.csrfToken }; + } +} + async function apply() { Route('file_download', '/fs/:docId/:secret', FileDownloadHandler); Route('file_download_with_name', '/fs/:docId/:name/:secret', FileDownloadHandler); @@ -71,6 +77,7 @@ async function apply() { Route('status_update', '/status/update', StatusUpdateHandler); Route('switch_language', '/language/:lang', SwitchLanguageHandler); Route('markdown', '/markdown', MarkdownHandler); + Route('token', '/token', SockToken); } apply.updateStatus = function updateStatus(args) { diff --git a/hydro/handler/training.js b/hydro/handler/training.js index fb6fc017f..48677366a 100644 --- a/hydro/handler/training.js +++ b/hydro/handler/training.js @@ -4,7 +4,7 @@ const paginate = require('../lib/paginate'); const { PERM: { PERM_VIEW_TRAINING, PERM_VIEW_PROBLEM_HIDDEN, PERM_CREATE_TRAINING, - PERM_EDIT_TRAINING, + PERM_EDIT_TRAINING, PERM_EDIT_TRAINING_SELF, }, PRIV: { PRIV_USER_PROFILE, @@ -57,13 +57,7 @@ async function _parseDagJson(domainId, dag) { return parsed; } -class TrainingHandler extends Handler { - async _prepare() { - this.checkPerm(PERM_VIEW_TRAINING); - } -} - -class TrainingMainHandler extends TrainingHandler { +class TrainingMainHandler extends Handler { async get({ domainId, sort, page }) { const qs = sort ? 'sort={0}'.format(sort) : ''; const [tdocs, tpcount] = await paginate( @@ -102,7 +96,7 @@ class TrainingMainHandler extends TrainingHandler { } } -class TrainingDetailHandler extends TrainingHandler { +class TrainingDetailHandler extends Handler { async get({ domainId, tid }) { const tdoc = await training.get(domainId, tid); const pids = training.getPids(tdoc); @@ -162,7 +156,7 @@ class TrainingDetailHandler extends TrainingHandler { } } -class TrainingCreateHandler extends TrainingHandler { +class TrainingCreateHandler extends Handler { async get() { const path = [ ['Hydro', 'homepage'], @@ -201,28 +195,30 @@ class TrainingCreateHandler extends TrainingHandler { } } -class TrainingEditHandler extends TrainingHandler { - async get({ domainId, tid }) { - const tdoc = await training.get(domainId, tid); - if (tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_TRAINING); - const dag = JSON.stringify(tdoc.dag, null, 2); +class TrainingEditHandler extends Handler { + async prepare({ domainId, tid }) { + this.tdoc = await training.get(domainId, tid); + if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_TRAINING); + else this.checkPerm(PERM_EDIT_TRAINING_SELF); + } + + async get({ tid }) { + const dag = JSON.stringify(this.tdoc.dag, null, 2); const path = [ ['Hydro', 'homepage'], ['training_main', 'training_main'], - [tdoc.title, 'training_detail', { tid: tdoc.docId }, true], + [this.tdoc.title, 'training_detail', { tid }, true], ['training_edit', null], ]; this.response.template = 'training_edit.html'; this.response.body = { - tdoc, dag, path, page_name: 'training_edit', + tdoc: this.tdoc, dag, path, page_name: 'training_edit', }; } async post({ domainId, tid, title, content, dag, description, }) { - const tdoc = await training.get(domainId, tid); - if (!this.user._id === tdoc.owner) this.checkPerm(PERM_EDIT_TRAINING); dag = await _parseDagJson(domainId, dag); const pids = training.getPids({ dag }); assert(pids.length, new ValidationError('dag')); @@ -254,9 +250,9 @@ class TrainingEditHandler extends TrainingHandler { } async function apply() { - Route('training_main', '/training', TrainingMainHandler); + Route('training_main', '/training', TrainingMainHandler, PERM_VIEW_TRAINING); Route('training_create', '/training/create', TrainingCreateHandler, PERM_CREATE_TRAINING); - Route('training_detail', '/training/:tid', TrainingDetailHandler); + Route('training_detail', '/training/:tid', TrainingDetailHandler, PERM_VIEW_TRAINING); Route('training_edit', '/training/:tid/edit', TrainingEditHandler); } diff --git a/hydro/service/server.js b/hydro/service/server.js index 24bf37be6..3d7c7ed95 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -22,7 +22,7 @@ const opcount = require('../model/opcount'); const { UserNotFoundError, BlacklistedError, PermissionError, UserFacingError, ValidationError, PrivilegeError, - CsrfTokenError, + CsrfTokenError, InvalidOperationError, MethodNotAllowedError, } = require('../error'); let enableLog = true; @@ -387,10 +387,26 @@ async function handle(ctx, HandlerClass, checker) { domainId: 'system', ...ctx.params, ...ctx.query, ...ctx.request.body, }; h.args = args; + let operation; + if (method === 'post' && ctx.request.body.operation) { + operation = `_${ctx.request.body.operation}` + .replace(/_([a-z])/gm, (s) => s[1].toUpperCase()); + } if (h.___prepare) await h.___prepare(args); if (checker) checker.call(h); - if (method === 'post') await h.checkCsrfToken(args.csrfToken); + if (method === 'post') { + await h.checkCsrfToken(args.csrfToken); + if (operation) { + if (typeof h[`post${operation}`] !== 'function') { + throw new InvalidOperationError(operation); + } + } else if (typeof h.post !== 'function') { + throw new MethodNotAllowedError(method); + } + } else if (typeof h[method] !== 'function') { + throw new MethodNotAllowedError(method); + } let checking = ''; try { @@ -404,6 +420,7 @@ async function handle(ctx, HandlerClass, checker) { 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.prepare) await h.prepare(args);