diff --git a/package.json b/package.json index a4b287c54..6d1077036 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint": "eslint packages --ext ts --fix", "pub": "node build/publish", "jest": "node build/jest", - "debug": "node --async-stack-traces --trace-deprecation --enable-source-maps build/start", + "debug": "node --async-stack-traces --trace-deprecation --enable-source-maps build/start --debug", "start": "node build/start", "clean": "zsh -c \"rm -r tsconfig.build.tsbuildinfo ./packages/*/tsconfig.tsbuildinfo ./packages/*/dist ./.coverage\"" }, diff --git a/packages/hydrooj/TODO b/packages/hydrooj/TODO index 93b420c63..51a23ef05 100644 --- a/packages/hydrooj/TODO +++ b/packages/hydrooj/TODO @@ -9,5 +9,6 @@ message.markAsRead 评测队列优先级系统 讨论浏览数量显示 解离mail模块 -@param @get @post @arg -extend problem settings \ No newline at end of file +存储系统抽象化 +extend problem settings ++oplog \ No newline at end of file diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 5c3003771..fe83348f3 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.13.16", + "version": "2.13.17", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/handler/discussion.ts b/packages/hydrooj/src/handler/discussion.ts index ca871eea9..fd5f784c5 100644 --- a/packages/hydrooj/src/handler/discussion.ts +++ b/packages/hydrooj/src/handler/discussion.ts @@ -1,6 +1,6 @@ import { ObjectID } from 'mongodb'; import { isSafeInteger } from 'lodash'; -import { DiscussionNotFoundError, DocumentNotFoundError } from '../error'; +import { DiscussionNotFoundError, DocumentNotFoundError, PermissionError } from '../error'; import { Mdoc, Drdoc, Ddoc, Drrdoc, } from '../interface'; @@ -242,11 +242,8 @@ class DiscussionDetailHandler extends DiscussionHandler { @param('drid', Types.ObjectID) @param('content', Types.String, isContent) async postEditReply(domainId: string, drid: ObjectID, content: string) { - if (!(this.ddoc.owner === this.user._id - && this.user.hasPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION))) { - if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY); - else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF); - } + this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF); + if (this.drdoc.owner !== this.user._id) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF); await discussion.editReply(domainId, drid, content); this.back(); } @@ -267,7 +264,8 @@ class DiscussionDetailHandler extends DiscussionHandler { @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); + this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF); + if (this.drdoc.owner !== this.user._id) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF); await discussion.editTailReply(domainId, drid, drrid, content); this.back(); } diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 1743b7d18..8191313eb 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -556,7 +556,7 @@ export class ProblemSolutionHandler extends ProblemDetailHandler { if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid); if (!(psrdoc.owner === this.user._id && this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF))) { - this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY); + throw new PermissionError(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF); } await solution.editReply(domainId, psid, psrid, content); this.back(); diff --git a/packages/hydrooj/src/logger.ts b/packages/hydrooj/src/logger.ts index f5d16631f..2583ca3a9 100644 --- a/packages/hydrooj/src/logger.ts +++ b/packages/hydrooj/src/logger.ts @@ -140,4 +140,6 @@ export class Logger { } } +// This fails in jest +// @ts-ignore global.Hydro.Logger = { Logger }; diff --git a/packages/hydrooj/src/model/builtin.ts b/packages/hydrooj/src/model/builtin.ts index ee662d3fc..95ca9cc49 100644 --- a/packages/hydrooj/src/model/builtin.ts +++ b/packages/hydrooj/src/model/builtin.ts @@ -37,7 +37,6 @@ export const PERM = { PERM_DELETE_PROBLEM_SOLUTION: 1n << 20n, PERM_DELETE_PROBLEM_SOLUTION_SELF: 1n << 21n, PERM_REPLY_PROBLEM_SOLUTION: 1n << 22n, - PERM_EDIT_PROBLEM_SOLUTION_REPLY: 1n << 23n, PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF: 1n << 24n, PERM_DELETE_PROBLEM_SOLUTION_REPLY: 1n << 25n, PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF: 1n << 26n, @@ -52,9 +51,7 @@ export const PERM = { PERM_DELETE_DISCUSSION: 1n << 32n, PERM_DELETE_DISCUSSION_SELF: 1n << 33n, PERM_REPLY_DISCUSSION: 1n << 34n, - PERM_EDIT_DISCUSSION_REPLY: 1n << 35n, PERM_EDIT_DISCUSSION_REPLY_SELF: 1n << 36n, - PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 37n, PERM_DELETE_DISCUSSION_REPLY: 1n << 38n, PERM_DELETE_DISCUSSION_REPLY_SELF: 1n << 39n, PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 40n, @@ -118,7 +115,6 @@ export const PERMS = [ Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION, 'Delete problem solutions'), Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF, 'Delete own problem solutions'), Permission('perm_problem_solution', PERM.PERM_REPLY_PROBLEM_SOLUTION, 'Reply problem solutions'), - Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY, 'Edit problem solution replies'), Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF, 'Edit own problem solution replies'), Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY, 'Delete problem solution replies'), Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF, 'Delete own problem solution replies'), @@ -130,9 +126,7 @@ export const PERMS = [ Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION, 'Delete discussions'), Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_SELF, 'Delete own discussions'), Permission('perm_discussion', PERM.PERM_REPLY_DISCUSSION, 'Reply discussions'), - Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_REPLY, 'Edit discussion replies'), Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_REPLY_SELF, 'Edit own discussion replies'), - Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION, 'Edit discussion replies of own discussion'), Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_REPLY, 'Delete discussion replies'), Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_REPLY_SELF, 'Delete own discussion replies'), Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION, 'Delete discussion replies of own discussion'), @@ -627,6 +621,7 @@ export const CONSTANT = { global.Hydro.model.builtin = { Permission, registerLanguage, + CONSTANT, PERM, PERMS, PERMS_BY_FAMILY, diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index 59729c4ab..a61d7e41c 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -1,7 +1,7 @@ /* eslint-disable prefer-destructuring */ import assert from 'assert'; import { PassThrough } from 'stream'; -import path from 'path'; +import { resolve } from 'path'; import os from 'os'; import http from 'http'; import moment from 'moment-timezone'; @@ -43,6 +43,7 @@ type Converter = (value: any) => any; type Validator = (value: any) => boolean; interface ParamOption { name: string, + source: 'all' | 'get' | 'post' | 'route', isOptional?: boolean, convert?: Converter, validate?: Validator, @@ -130,59 +131,52 @@ export const Types: Types = { ], }; -export function param(name: string, type: Type, validate: Validator): MethodDecorator; -export function param(name: string, type?: Type, isOptional?: boolean): MethodDecorator; -export function param( - name: string, type: Type, validate: null, convert: Converter -): MethodDecorator; -export function param( - name: string, type: Type, validate?: Validator, convert?: Converter, -): MethodDecorator; -export function param( - name: string, type: Type, isOptional?: boolean, validate?: Validator, convert?: Converter, -): MethodDecorator; -export function param( - name: string, ...args: Array -): MethodDecorator; -export function param(name: string, ...args: any): MethodDecorator { +function _buildParam(name: string, source: 'get' | 'post' | 'all' | 'route', ...args: Array) { let cursor = 0; - const v: ParamOption = { name }; + const v: ParamOption = { name, source }; let isValidate = true; while (cursor < args.length) { - if (args[cursor] instanceof Array) { - const type = args[cursor]; + const current = args[cursor]; + if (current instanceof Array) { + const type = current; if (type[0]) v.convert = type[0]; if (type[1]) v.validate = type[1]; if (type[2]) v.isOptional = type[2]; - } else if (typeof args[cursor] === 'boolean') v.isOptional = args[cursor]; + } else if (typeof current === 'boolean') v.isOptional = current; else if (isValidate) { - if (args[cursor] !== null) v.validate = args[cursor]; + if (current !== null) v.validate = current; isValidate = false; - } else { - v.convert = args[cursor]; - } + } else v.convert = current; cursor++; } - return function desc(target: any, funcName: string, obj: any) { + return v; +} + +function _descriptor(v: ParamOption) { + return function desc(this: Handler, target: any, funcName: string, obj: any) { if (!target.__param) target.__param = {}; if (!target.__param[target.constructor.name]) target.__param[target.constructor.name] = {}; if (!target.__param[target.constructor.name][funcName]) { - target.__param[target.constructor.name][funcName] = [{ name: 'domainId', type: 'string' }]; + target.__param[target.constructor.name][funcName] = [{ name: 'domainId', type: 'string', source: 'route' }]; const originalMethod = obj.value; - obj.value = function validate(rawArgs: any, ...extra: any[]) { - if (typeof rawArgs === 'string' || extra.length) return originalMethod.call(this, rawArgs, ...extra); + obj.value = function validate(this: Handler, rawArgs: any, ...extra: any[]) { + if (typeof rawArgs !== 'object' || extra.length) return originalMethod.call(this, rawArgs, ...extra); const c = []; const arglist: ParamOption[] = this.__param[target.constructor.name][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); - } - } - if (item.convert) c.push(item.convert(rawArgs[item.name])); - else c.push(rawArgs[item.name]); + const src = item.source === 'all' + ? rawArgs + : item.source === 'get' + ? this.request.query + : item.source === 'route' + ? this.request.params + : this.request.body; + const value = src[item.name]; + if (!item.isOptional || value) { + if (!value) throw new ValidationError(item.name); + if (item.validate && !item.validate(value)) throw new ValidationError(item.name); + if (item.convert) c.push(item.convert(value)); + else c.push(value); } else c.push(undefined); } return originalMethod.call(this, ...c); @@ -193,6 +187,70 @@ export function param(name: string, ...args: any): MethodDecorator { }; } +export function get( + name: string, type: Type, validate: null, convert: Converter +): MethodDecorator; +export function get( + name: string, type: Type, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function get( + name: string, type?: Type, isOptional?: boolean, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function get( + name: string, ...args: Array +): MethodDecorator; +export function get(name: string, ...args: any): MethodDecorator { + return _descriptor(_buildParam(name, 'get', ...args)); +} + +export function post( + name: string, type: Type, validate: null, convert: Converter +): MethodDecorator; +export function post( + name: string, type?: Type, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function post( + name: string, type?: Type, isOptional?: boolean, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function post( + name: string, ...args: Array +): MethodDecorator; +export function post(name: string, ...args: any): MethodDecorator { + return _descriptor(_buildParam(name, 'post', ...args)); +} + +export function route( + name: string, type: Type, validate: null, convert: Converter +): MethodDecorator; +export function route( + name: string, type?: Type, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function route( + name: string, type?: Type, isOptional?: boolean, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function route( + name: string, ...args: Array +): MethodDecorator; +export function route(name: string, ...args: any): MethodDecorator { + return _descriptor(_buildParam(name, 'route', ...args)); +} + +export function param( + name: string, type: Type, validate: null, convert: Converter +): MethodDecorator; +export function param( + name: string, type?: Type, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function param( + name: string, type?: Type, isOptional?: boolean, validate?: Validator, convert?: Converter, +): MethodDecorator; +export function param( + name: string, ...args: Array +): MethodDecorator; +export function param(name: string, ...args: any): MethodDecorator { + return _descriptor(_buildParam(name, 'all', ...args)); +} + export function requireCsrfToken(target: any, funcName: string, obj: any) { const originalMethod = obj.value; obj.value = async function checkCsrfToken(...args: any[]) { @@ -211,7 +269,7 @@ export async function prepare() { maxAge: 0, })); } else { - app.use(cache(path.join(os.tmpdir(), 'hydro', 'public'), { + app.use(cache(resolve(os.tmpdir(), 'hydro', 'public'), { maxAge: 365 * 24 * 60 * 60, })); } @@ -265,6 +323,8 @@ export class Handler { loginMethods: any; + __param: Record; + constructor(ctx: Koa.Context) { this.ctx = ctx; this.request = { @@ -624,10 +684,10 @@ const Checker = (permPrivChecker) => { }; }; -export function Route(name: string, route: string, RouteHandler: any, ...permPrivChecker) { +export function Route(name: string, path: string, RouteHandler: any, ...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)); + router.all(name, path, (ctx) => handle(ctx, RouteHandler, checker)); + router.all(`${name}_with_domainId`, `/d/:domainId${path}`, (ctx) => handle(ctx, RouteHandler, checker)); } export class ConnectionHandler { @@ -772,10 +832,8 @@ export function Connection( try { const args = { domainId: 'system', ...h.request.params }; h.args = args; - const cookie = await new Promise((resolve) => { - conn.once('data', (c) => { - resolve(c); - }); + const cookie = await new Promise((r) => { + conn.once('data', r); }); args.cookie = cookie; await h.init(args); @@ -842,6 +900,9 @@ global.Hydro.service.server = { app, server, router, + get, + post, + route, param, requireCsrfToken, Handler,