From 13e3e0ec9859d93f1de4fe74ba40d9efafa630ef Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 13 Jul 2020 17:09:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=BF=BB=E8=AF=91=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BF=BB=E8=AF=91=E7=BC=BA=E5=A4=B1=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO | 4 + hydro/handler/home.ts | 61 +++++++----- hydro/lib/i18n.ts | 11 ++- hydro/model/contest.ts | 5 +- hydro/model/token.ts | 8 +- hydro/service/server.ts | 93 ++++++++++--------- hydro/utils.ts | 17 ++++ locales/zh_CN.yaml | 10 +- package.json | 4 +- templates/components/comments_discussion.html | 6 +- templates/components/comments_solution.html | 8 +- templates/discussion_detail.html | 18 +++- templates/home_security.html | 1 - templates/layout/html5.html | 2 +- tool/checkTranslation.js | 61 ++++++++++++ tool/install.js | 2 +- ui/pages/home_messages.page.js | 2 +- ui/pages/judge_playground.page.js | 2 +- yarn.lock | 17 ++++ 19 files changed, 242 insertions(+), 90 deletions(-) create mode 100644 TODO create mode 100644 tool/checkTranslation.js diff --git a/TODO b/TODO new file mode 100644 index 000000000..1e45ee9bd --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +@requireCsrfToken +消息提醒 +bug修复 +使用BigInt管理权限 \ No newline at end of file diff --git a/hydro/handler/home.ts b/hydro/handler/home.ts index 1dab44970..e60d389e9 100644 --- a/hydro/handler/home.ts +++ b/hydro/handler/home.ts @@ -1,14 +1,17 @@ +import { ObjectID } from 'mongodb'; import { VerifyPasswordError, UserAlreadyExistError, InvalidTokenError, NotFoundError, UserNotFoundError, + PermissionError, } from '../error'; import * as bus from '../service/bus'; import { - Route, Connection, Handler, ConnectionHandler, + Route, Connection, Handler, ConnectionHandler, param, Types, } from '../service/server'; import * as misc from '../lib/misc'; import md5 from '../lib/md5'; +import * as mail from '../lib/mail'; import * as contest from '../model/contest'; import * as message from '../model/message'; import * as document from '../model/document'; @@ -21,6 +24,7 @@ import * as discussion from '../model/discussion'; import * as token from '../model/token'; import * as training from '../model/training'; import { PERM, PRIV } from '../model/builtin'; +import { isContent, isPassword, isEmail } from '../lib/validator'; const { geoip, useragent } = global.Hydro.lib; @@ -133,29 +137,38 @@ class HomeSecurityHandler extends Handler { if (useragent) this.response.body.icon = useragent.icon; } - async postChangePassword({ current, password, verifyPassword }) { + @param('current', Types.String) + @param('password', Types.String, isPassword) + @param('verifyPassword', Types.String) + async postChangePassword(current: string, password: string, verifyPassword: string) { if (password !== verifyPassword) throw new VerifyPasswordError(); await user.changePassword(this.user._id, current, password); this.back(); } - async postChangeMail({ domainId, currentPassword, mail }) { + @param('currentPassword', Types.String) + @param('mail', Types.String, isEmail) + async postChangeMail(domainId: string, currentPassword: string, email: string) { this.limitRate('send_mail', 3600, 30); this.user.checkPassword(currentPassword); - const udoc = await user.getByEmail(domainId, mail); - if (udoc) throw new UserAlreadyExistError(mail); + const udoc = await user.getByEmail(domainId, email); + if (udoc) throw new UserAlreadyExistError(email); const [code] = await token.add( token.TYPE_CHANGEMAIL, await system.get('changemail_token_expire_seconds'), - { uid: this.user._id, mail }, + { uid: this.user._id, email }, ); - await mail.sendMail(mail, 'Change Email', 'user_changemail_mail.html', { - url: this.url('user_changemail_with_code', { code }), uname: this.user.uname, + const m = await this.renderHTML('user_changemail_mail.html', { + path: `home/changeMail/${code}`, + uname: this.user.uname, + url_prefix: await system.get('server.url'), }); + await mail.sendMail(email, 'Change Email', 'user_changemail_mail', m); this.response.template = 'user_changemail_mail_sent.html'; } - async postDeleteToken({ tokenDigest }) { + @param('tokenDigest', Types.String) + async postDeleteToken(domainId: string, tokenDigest: string) { const sessions = await token.getSessionListByUid(this.user._id); for (const session of sessions) { if (tokenDigest === md5(session._id)) { @@ -174,9 +187,8 @@ class HomeSecurityHandler extends Handler { } class HomeSettingsHandler extends Handler { - async get({ category }) { - // eslint-disable-next-line prefer-destructuring - category = category[0]; // Category would be splitted into array + @param('category', Types.String) + async get(domainId: string, category: string) { const path = [ ['Hydro', 'homepage'], [`home_${category}`, null], @@ -195,7 +207,7 @@ class HomeSettingsHandler extends Handler { } else throw new NotFoundError(); } - async post(args) { + async post(args: any) { const $set = {}; for (const key in args) { if (setting.SETTINGS_BY_KEY[key] && !setting.SETTINGS_BY_KEY[key].disabled) { @@ -208,15 +220,16 @@ class HomeSettingsHandler extends Handler { } class UserChangemailWithCodeHandler extends Handler { - async get({ domainId, code }) { + @param('code', Types.String) + async get(domainId: string, code: string) { const tdoc = await token.get(code, token.TYPE_CHANGEMAIL); if (!tdoc || tdoc.uid !== this.user._id) { throw new InvalidTokenError(code); } - const udoc = await user.getByEmail(domainId, tdoc.mail); - if (udoc) throw new UserAlreadyExistError(tdoc.mail); + const udoc = await user.getByEmail(domainId, tdoc.email); + if (udoc) throw new UserAlreadyExistError(tdoc.email); await Promise.all([ - user.setEmail(this.user._id, tdoc.mail), + user.setEmail(this.user._id, tdoc.email), token.del(code, token.TYPE_CHANGEMAIL), ]); this.response.redirect = this.url('home_security'); @@ -252,20 +265,24 @@ class HomeMessagesHandler extends Handler { this.response.template = 'home_messages.html'; } - async postSend({ uid, content }) { + @param('uid', Types.Int) + @param('content', Types.String, isContent) + async postSend(domainId: string, uid: number, content: string) { const udoc = await user.getById('system', uid); if (!udoc) throw new UserNotFoundError(uid); const mdoc = await message.send(this.user._id, uid, content); // TODO(twd2): improve here: projection if (this.user._id !== uid) { - await bus.publish(`user_message-${uid}`, { mdoc, udoc }); + bus.publish(`user_message-${uid}`, { mdoc, udoc }); } this.back({ mdoc, udoc }); } - async postDeleteMessage({ messageId }) { - // TODO limitations - await message.del(messageId); + @param('messageId', Types.ObjectID) + async postDeleteMessage(domainId: string, messageId: ObjectID) { + const msg = await message.get(messageId); + if ([msg.from, msg.to].includes(this.user._id)) await message.del(messageId); + else throw new PermissionError(); this.back(); } } diff --git a/hydro/lib/i18n.ts b/hydro/lib/i18n.ts index da36b2eb5..f337d97fb 100644 --- a/hydro/lib/i18n.ts +++ b/hydro/lib/i18n.ts @@ -4,7 +4,7 @@ declare global { interface String { format: (...args: Array) => string; rawformat: (object: any) => string; - translate: (language: string | undefined) => string; + translate: (...languages: string[]) => string; } } @@ -35,9 +35,12 @@ String.prototype.rawformat = function rawFormat(object) { return [res[0], object, res[1]].join(); }; -String.prototype.translate = function translate(language = 'zh_CN') { - if (locales[language]) { - if (locales[language][this] !== undefined) return locales[language][this]; +String.prototype.translate = function translate(...languages: string[]) { + if (languages.length == 0) languages = ['zh_CN']; + for (const language of languages) { + if (locales[language]) { + if (locales[language][this] !== undefined) return locales[language][this]; + } } return this; }; diff --git a/hydro/model/contest.ts b/hydro/model/contest.ts index eb104a61b..756364a00 100644 --- a/hydro/model/contest.ts +++ b/hydro/model/contest.ts @@ -510,7 +510,10 @@ export const ContestHandlerMixin = (c) => class extends c { problem.getList(domainId, tdoc.pids, true), ]); const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs); - const rows = RULES[tdoc.rule].scoreboard(isExport, (str) => (str ? str.toString().translate(this.user.language) : ''), tdoc, rankedTsdocs, udict, pdict); + const rows = RULES[tdoc.rule].scoreboard( + isExport, this.translate.bind(this), + tdoc, rankedTsdocs, udict, pdict, + ); return [tdoc, rows, udict]; } diff --git a/hydro/model/token.ts b/hydro/model/token.ts index a51826a15..f029a7d19 100644 --- a/hydro/model/token.ts +++ b/hydro/model/token.ts @@ -4,7 +4,7 @@ import * as db from '../service/db'; const coll = db.collection('token'); export const TYPE_SESSION = 0; -export const TYPE_CSRF_TOKEN = 1; +export const TYPE_TOKEN = 1; export const TYPE_REGISTRATION = 2; export const TYPE_CHANGEMAIL = 3; export const TYPE_OAUTH = 4; @@ -62,7 +62,9 @@ export async function del(tokenId: string, tokenType: number) { return !!result.deletedCount; } -export async function createOrUpdate(tokenType: number, expireSeconds: number, data: object) { +export async function createOrUpdate( + tokenType: number, expireSeconds: number, data: any, +): Promise { const d = await coll.findOne({ tokenType, ...data }); if (!d) { const res = await add(tokenType, expireSeconds, data); @@ -87,7 +89,7 @@ export function delByUid(uid: number) { global.Hydro.model.token = { TYPE_SESSION, TYPE_CHANGEMAIL, - TYPE_CSRF_TOKEN, + TYPE_TOKEN, TYPE_OAUTH, TYPE_REGISTRATION, TYPE_LOSTPASS, diff --git a/hydro/service/server.ts b/hydro/service/server.ts index e0f245625..e50701b59 100644 --- a/hydro/service/server.ts +++ b/hydro/service/server.ts @@ -24,6 +24,9 @@ import { UserFacingError, ValidationError, PrivilegeError, CsrfTokenError, InvalidOperationError, MethodNotAllowedError, } from '../error'; +import hash from '../lib/hash.hydro'; +import { lrucache } from '../utils'; +import { Udoc } from '../interface'; let enableLog = true; @@ -158,6 +161,17 @@ export function param(name: string, ...args: any): MethodDecorator { }; } +export function requireCsrfToken(target: any, funcName: string, obj: any) { + const originalMethod = obj.value; + obj.value = async function func(...args: any[]) { + if (this.getCsrfToken(this.session._id) !== this.args.csrfToken) { + throw new CsrfTokenError(this.args.csrfToken); + } + return await originalMethod.call(this, ...args); + }; + return obj; +} + export async function prepare() { server = http.createServer(app.callback()); app.keys = await system.get('session.keys'); @@ -208,7 +222,7 @@ export class Handler { csrfToken: string; - user: any; + user: Udoc; constructor(ctx: Koa.Context) { this.ctx = ctx; @@ -242,18 +256,22 @@ export class Handler { this.session = {}; } + @lrucache + // eslint-disable-next-line class-methods-use-this + getCsrfToken(id: string) { + return hash('csrf_token', id); + } + async renderHTML(name: string, context: any): Promise { if (enableLog) console.time(name); - // FIXME fix this ugly hack - // @ts-ignore - this._user = { ...this.user, gravatar: misc.gravatar(this.user.gravatar, 128) }; - const res = await render(name, Object.assign(context, { + const UserContext = { ...this.user, gravatar: misc.gravatar(this.user.gravatar, 128) }; + const res = await render(name, { handler: this, - // @ts-ignore - url: (...args) => this.url(...args), - _: (str: string) => (str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''), - user: this.user, - })); + UserContext, + url: this.url.bind(this), + _: this.translate.bind(this), + ...context, + }); if (enableLog) console.timeEnd(name); return res; } @@ -262,11 +280,12 @@ export class Handler { await opcount.inc(op, this.request.ip, periodSecs, maxOperations); } - translate(str) { - return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''; + translate(str: string) { + if (!str) return ''; + return str.toString().translate(this.user.viewLang, this.session.viewLang); } - renderTitle(str) { + renderTitle(str: string) { return `${this.translate(str)} - Hydro`; } @@ -304,7 +323,7 @@ export class Handler { } } - url(name, kwargs = {}) { + url(name: string, kwargs = {}) { let res = '#'; const args: any = { ...kwargs }; try { @@ -324,7 +343,6 @@ export class Handler { } async render(name: string, context: any) { - // @ts-ignore this.response.body = await this.renderHTML(name, context); this.response.type = 'text/html'; } @@ -352,14 +370,6 @@ export class Handler { if (bdoc) throw new BlacklistedError(this.request.ip); } - async getCsrfToken() { - this.csrfToken = await token.createOrUpdate(token.TYPE_CSRF_TOKEN, 600, { - path: this.request.path, - uid: this.session.uid, - }); - this.UIContext.csrfToken = this.csrfToken; - } - async init({ domainId }) { const xff = await system.get('server.xff'); if (xff) this.request.ip = this.request.headers[xff.toLowerCase()]; @@ -367,15 +377,13 @@ export class Handler { this.getSession(), this.getBdoc(), ]); - [this.user] = await Promise.all([ + [this.user, this.UIContext.token] = await Promise.all([ user.getById(domainId, this.session.uid), - this.getCsrfToken(), + token.createOrUpdate( + token.TYPE_TOKEN, 300, { uid: this.session.uid }, + ), ]); - } - - async checkCsrfToken(csrfToken: any) { - const sdoc = await token.get(csrfToken, token.TYPE_CSRF_TOKEN, false); - if (!sdoc || sdoc.uid !== this.user._id) throw new CsrfTokenError(csrfToken); + this.csrfToken = this.getCsrfToken(this.session._id || String.random(32)); } async finish() { @@ -494,7 +502,6 @@ async function handle(ctx, HandlerClass, checker) { await h.init(args); if (checker) checker.call(h); if (method === 'post') { - await h.checkCsrfToken(args.csrfToken); if (operation) { if (typeof h[`post${operation}`] !== 'function') { throw new InvalidOperationError(operation); @@ -580,18 +587,11 @@ export class ConnectionHandler { } async renderHTML(name: string, context: any): Promise { - if (enableLog) console.time(name); - // FIXME fix this ugly hack - // @ts-ignore - this._user = { ...this.user, gravatar: misc.gravatar(this.user.gravatar, 128) }; const res = await render(name, Object.assign(context, { handler: this, - // @ts-ignore - url: (...args) => this.url(...args), - _: (str: string) => (str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''), - user: this.user, + url: this.url.bind(this), + _: this.translate.bind(this), })); - if (enableLog) console.timeEnd(name); return res; } @@ -675,7 +675,7 @@ export class ConnectionHandler { async init({ domainId }) { try { - this.session = await token.get(this.request.params.token, token.TYPE_CSRF_TOKEN, true); + this.session = await token.get(this.request.params.token, token.TYPE_TOKEN, true); } catch (e) { this.session = { uid: 0 }; } @@ -736,5 +736,14 @@ export async function start() { } global.Hydro.service.server = { - Types, param, Handler, ConnectionHandler, Route, Connection, Middleware, prepare, start, + Types, + param, + requireCsrfToken, + Handler, + ConnectionHandler, + Route, + Connection, + Middleware, + prepare, + start, }; diff --git a/hydro/utils.ts b/hydro/utils.ts index 74c67b752..f99a6a856 100644 --- a/hydro/utils.ts +++ b/hydro/utils.ts @@ -1,8 +1,10 @@ import fs from 'fs'; +import { serialize } from 'v8'; import cluster from 'cluster'; import path from 'path'; import * as superagent from 'superagent'; import proxy from 'superagent-proxy'; +import Lru from 'lru-cache'; declare global { interface StringConstructor { @@ -24,6 +26,21 @@ declare global { } } +const lru = new Lru(1000); + +export function lrucache(target: any, funcName: string, obj: any) { + const originalMethod = obj.value; + obj.value = function func(...args: any[]) { + const key = serialize({ funcName, args }); + let val = lru.get(key); + if (val) return val; + val = originalMethod.call(this, ...args); + lru.set(key, val); + return val; + }; + return obj; +} + proxy(superagent); if (!cluster.worker) { diff --git a/locales/zh_CN.yaml b/locales/zh_CN.yaml index f9883f08b..6b4be313e 100644 --- a/locales/zh_CN.yaml +++ b/locales/zh_CN.yaml @@ -41,6 +41,7 @@ __langname: 简体中文 "What's file?": 什么是文件? (All Problems): (所有题目) (None): (无) +(Not changed): 未更改 Aborted: 已放弃 About Hydro: 关于 Hydro About Markdown: 关于 Markdown @@ -337,9 +338,10 @@ hour(s): 小时 If enabled, source code will be emailed to you after the submission is accepted.: 如果启用,在您通过题目后,源代码会以 Email 的形式发送给您。 If left blank, the built-in template of the corresponding language will be used.: 若留空,则将使用对应语言的内置代码模板。 Images: 图片 +Import from: 导入自 Import problem from syzoj.: 从运行 syzoj 系统的 OJ 导入题目。 Import Problem: 导入题目 -import: 导入 +Import: 导入 In 1 day: 一天后 In 1 month: 一个月后 In 1 week: 一周后 @@ -378,6 +380,7 @@ Last Submit At: 最后递交于 Last Update At: 最后活动时间 LaTeX Expressions: LaTeX 表达式 Learn More: 了解更多 +Leave blank to use numeric pid.: 留空来使用数字题号。 Limitations: 限制 Links: 链接 List View: 列表视图 @@ -388,6 +391,8 @@ Login to Attend Contest: 登录后参加比赛 Login to Claim Homework: 登录后认领作业 Login to Create a Discussion: 登录后创建讨论 Login to Submit: 登录后递交 +Login with Github: 使用 Github 登录 +Login with Google: 使用 Google 登录 Login: 登录 Logout All Sessions: 注销所有会话 Logout This Session: 注销该会话 @@ -639,6 +644,7 @@ Sorry, there is no problem in the problem set: 题库中没有题目 Sorry, there is no training plan.: 目前没有训练计划。 Sorry: 对不起 Sort by: 排序 +Source type: 来源类型 Star Topic: 收藏讨论 Star: 收藏 Start at: 开始于 @@ -663,6 +669,7 @@ Technical Information: 技术信息 Temporary session: 临时会话 Terms of Service: 服务条款 Test data comes from: 测试数据来自 +Testdata Config: 测试数据配置 Text: 文本 The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。 The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 加入此域的邀请码。您只能使用字母和数字,并且不能长于 64 个字符。 @@ -681,6 +688,7 @@ This homework is not open and you cannot view problems.: 该作业还未到开 This homework is not open.: 该作业还未开放递交。 This is the current session: 这是当前的会话 This message will be presented to those whose submissions are accepted.: 这条消息将会被展示给通过此题的用户。 +This page needs JavaScript to work.: 这个页面需要 Javascript。 This section cannot be challenged at present, so please complete the following sections first: 该章节目前不可挑战,请先完成以下章节 This type of files are not allowed to be uploaded.: 不允许上传这种文件。 Time (Seconds): 耗时 (秒) diff --git a/package.json b/package.json index 23b081625..ebee4a7d3 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "hydrooj", - "version": "2.7.2", + "version": "2.7.3", "main": "hydro/loader.ts", "repository": "https://github.com/hydro-dev/Hydro.git", "author": "masnn", "license": "GPL-3.0-only", "dependencies": { "@hydrooj/markdown-it-katex": "^2.1.1", + "@types/lru-cache": "^5.1.0", "adm-zip": "^0.4.16", "ansi_up": "^4.0.4", "detect-browser": "^5.1.1", @@ -17,6 +18,7 @@ "koa-router": "^8.0.8", "koa-static-cache": "^5.1.3", "lodash": "^4.17.19", + "lru-cache": "^6.0.0", "markdown-it": "^11.0.0", "markdown-it-anchor": "^5.3.0", "markdown-it-container": "^3.0.0", diff --git a/templates/components/comments_discussion.html b/templates/components/comments_discussion.html index fcefbe302..c2e1cdb8c 100644 --- a/templates/components/comments_discussion.html +++ b/templates/components/comments_discussion.html @@ -121,12 +121,12 @@ data-form="{{ {operation: reply_post_op, drid: doc._id}|json }}" > {% endif %} - {% if doc.owner == handler.state.user._id or handler.user.hasPerm(comment_edit_perm) %} + {% if (handler.user.hasPerm(comment_edit_self_perm) and doc.owner == handler.user._id) or handler.user.hasPerm(comment_edit_perm) %} {% endif %} - {% if doc.owner == handler.state.user._id or handler.user.hasPerm(comment_delete_perm) %} + {% if (handler.user.hasPerm(comment_delete_self_perm) and doc.owner == handler.user._id) or handler.user.hasPerm(comment_delete_perm) %} @@ -139,7 +139,7 @@
    {% for rdoc in doc['reply'] %} - {{ reply(mode_create, udict[rdoc.owner], doc, rdoc) }} + {{ reply(false, udict[rdoc.owner], doc, rdoc) }} {% endfor %}
diff --git a/templates/components/comments_solution.html b/templates/components/comments_solution.html index e1e49238e..fc01f9548 100644 --- a/templates/components/comments_solution.html +++ b/templates/components/comments_solution.html @@ -118,12 +118,12 @@ data-form="{{ {operation: reply_post_op, psid: doc._id}|json }}" > {% endif %} - {% if doc.owner == handler.state.user._id or handler.user.hasPerm(comment_edit_perm) %} + {% if (handler.user.hasPerm(comment_edit_self_perm) and doc.owner == handler.user._id) or handler.user.hasPerm(comment_edit_perm) %} {% endif %} - {% if doc.owner == handler.state.user._id or handler.user.hasPerm(comment_delete_perm) %} + {% if (handler.user.hasPerm(comment_delete_self_perm) and doc.owner == handler.user._id) or handler.user.hasPerm(comment_delete_perm) %} @@ -148,12 +148,12 @@ {% if handler.user.hasPerm(reply_post_perm) %} {% endif %} - {% if doc.owner == handler.state.user._id or handler.user.hasPerm(reply_edit_perm) %} + {% if (handler.user.hasPerm(reply_edit_self_perm) and doc.owner == handler.user._id) or handler.user.hasPerm(reply_edit_perm) %} {% endif %} - {% if doc.owner == handler.state.user._id or handler.user.hasPerm(reply_delete_perm) %} + {% if (handler.user.hasPerm(reply_delete_self_perm) and doc.owner == handler.user._id) or handler.user.hasPerm(reply_delete_perm) %} diff --git a/templates/discussion_detail.html b/templates/discussion_detail.html index a443b66b2..f5abadbb2 100644 --- a/templates/discussion_detail.html +++ b/templates/discussion_detail.html @@ -63,12 +63,22 @@ udict = udict, comment_ref = 'drid', reply_ref = 'drrid', + comment_post_op = 'reply', + reply_post_op = 'tail_reply', + comment_edit_op = 'edit_reply', + comment_delete_op = 'delete_reply', + reply_edit_op = 'edit_tail_reply', + reply_delete_op = 'delete_tail_reply', comment_post_perm = perm.PERM_REPLY_DISCUSSION, - comment_edit_perm = perm.PERM_NONE if ddoc.owner == handler.user._id else perm.PERM_EDIT_DISCUSSION_REPLY, - comment_delete_perm = perm.PERM_NONE if ddoc.owner == handler.user._id else perm.PERM_DELETE_DISCUSSION_REPLY, + comment_edit_perm = perm.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION if ddoc.owner == handler.user._id else perm.PERM_EDIT_DISCUSSION_REPLY, + comment_delete_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION if ddoc.owner == handler.user._id else perm.PERM_DELETE_DISCUSSION_REPLY, + comment_edit_self_perm = perm.PERM_EDIT_DISCUSSION_REPLY_SELF, + comment_delete_self_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF, reply_post_perm = perm.PERM_REPLY_DISCUSSION, - reply_edit_perm = perm.PERM_NONE if ddoc.owner == handler.user._id else perm.PERM_EDIT_DISCUSSION_REPLY, - reply_delete_perm = perm.PERM_NONE if ddoc.owner == handler.user._id else perm.PERM_DELETE_DISCUSSION_REPLY + reply_edit_perm = perm.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION if ddoc.owner == handler.user._id else perm.PERM_EDIT_DISCUSSION_REPLY, + reply_delete_perm = perm.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION if ddoc.owner == handler.user._id else perm.PERM_DELETE_DISCUSSION_REPLY, + reply_edit_self_perm = perm.PERM_EDIT_DISCUSSION_REPLY_SELF, + reply_delete_self_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF ) }} {{ paginator.render(page, pcount) }} {% if drcount == 0 %} diff --git a/templates/home_security.html b/templates/home_security.html index 61c379cf6..d4669250c 100644 --- a/templates/home_security.html +++ b/templates/home_security.html @@ -97,7 +97,6 @@

{{ _('Location') }}: {{ _(session.updateGeoip.display) }} ({{ session.updateIp }})

{{ _('Operating System') }}: {{ session.updateUa.os }}

{{ _('Browser') }}: {{ session.updateUa.name }} {{ session.updateUa.version }}

- {% if session.isCurrent %}

{{ _('This is the current session') }}

{% endif %} diff --git a/templates/layout/html5.html b/templates/layout/html5.html index 46f670096..431735cd1 100644 --- a/templates/layout/html5.html +++ b/templates/layout/html5.html @@ -30,7 +30,7 @@ var _htmlNode = document.documentElement; _htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs'); var UiContext = {{ handler.UIContext|json|safe }}; - var UserContext = {{ handler._user|json|safe }}; + var UserContext = {{ currentUser|json|safe }}; {% endif %} diff --git a/tool/checkTranslation.js b/tool/checkTranslation.js new file mode 100644 index 000000000..dbbeae4a4 --- /dev/null +++ b/tool/checkTranslation.js @@ -0,0 +1,61 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const IGNORE_CHECK = ['en']; +const RE_TEXT = /_\(['"]([\s\S])*?['"]\)/gmi; +const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates'); +const LOCALE_ROOT = path.resolve(__dirname, '..', 'locales'); + +const texts = {}; +let currentFile = ''; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function _(str) { + if (!texts[str]) texts[str] = [currentFile]; + else texts[str].push(currentFile); +} + +function scan(folder, relative = '') { + const files = fs.readdirSync(folder); + for (const file of files) { + const p = path.join(folder, file); + if (fs.statSync(p).isDirectory()) { + scan(p, path.join(relative, file)); + } else { + currentFile = path.join(relative, file); + const f = fs.readFileSync(p).toString(); + f.replace(RE_TEXT, (substr) => { + try { + // eslint-disable-next-line no-eval + eval(substr); + } catch (e) { + console.error('Cannot parse: ', substr, ' in file ', p); + } + }); + } + } +} + +scan(TEMPLATE_ROOT); + +const result = {}; + +const locales = fs.readdirSync(LOCALE_ROOT); +for (const locale of locales) { + if (!IGNORE_CHECK.includes(locale.split('.')[0])) { + const p = path.join(LOCALE_ROOT, locale); + const f = fs.readFileSync(p).toString(); + const l = yaml.safeLoad(f); + for (const str in texts) { + if (!l[str]) { + if (result[str]) result[str].locale.push(locale); + else result[str] = { str, source: texts[str], locale: [locale] }; + } + } + } +} + +console.log(`${Object.keys(result).length} translations missing.`); +fs.writeFileSync(path.join(__dirname, '..', '__result.json'), JSON.stringify(result, null, 2)); +console.log(`result wrote to ${path.resolve(__dirname, '..', '__result.json')}`); diff --git a/tool/install.js b/tool/install.js index f2f515b44..50701cf5b 100644 --- a/tool/install.js +++ b/tool/install.js @@ -5,7 +5,7 @@ async function install() { if (!global.Hydro) throw new Error('Data missing'); if (!fs.existsSync('hydro')) fs.mkdirSync('hydro'); const hydro = JSON.parse(zlib.gunzipSync(global.Hydro).toString()); - fs.writeFileSync('development.js', hydro.app); + fs.writeFileSync('app.js', hydro.app); for (const i in hydro.modules) { fs.writeFileSync(`${i}.hydro`, Buffer.from(hydro.modules[i], 'base64')); } diff --git a/ui/pages/home_messages.page.js b/ui/pages/home_messages.page.js index c5ac62cf7..50d0f4091 100644 --- a/ui/pages/home_messages.page.js +++ b/ui/pages/home_messages.page.js @@ -32,7 +32,7 @@ const page = new NamedPage('home_messages', () => { reduxStore = store; - const sock = new SockJs(`/home/messages-conn?token=${UiContext.csrfToken}`); + const sock = new SockJs(`/home/messages-conn?token=${UiContext.token}`); sock.onmessage = (message) => { const msg = JSON.parse(message.data); store.dispatch({ diff --git a/ui/pages/judge_playground.page.js b/ui/pages/judge_playground.page.js index 7ea87a7ec..334ce0cf3 100644 --- a/ui/pages/judge_playground.page.js +++ b/ui/pages/judge_playground.page.js @@ -5,7 +5,7 @@ import * as recordEnum from 'vj/constant/record'; const page = new NamedPage('judge_playground', async () => { const { default: SockJs } = await import('sockjs-client'); - const sock = new SockJs(`/judge/consume-conn?token=${UiContext.csrfToken}`); + const sock = new SockJs(`/judge/consume-conn?token=${UiContext.token}`); sock.onopen = () => { const div = $('
').appendTo('#messages'); $('

Connection opened.

') diff --git a/yarn.lock b/yarn.lock index 13b2a0fa1..ad59d66fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -501,6 +501,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8" integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ== +"@types/lru-cache@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" + integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w== + "@types/markdown-it@^10.0.1": version "10.0.1" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.1.tgz#94e252ab689c8e9ceb9aff2946e0a458390105eb" @@ -4165,6 +4170,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + macos-release@^2.2.0: version "2.4.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.0.tgz#837b39fc01785c3584f103c5599e0f0c8068b49e" @@ -7136,6 +7148,11 @@ yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yaml@^1.9.2: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"