完善翻译,添加翻译缺失检查工具

This commit is contained in:
undefined committed 2020-07-13 17:09:17 +08:00
1 parent 82c193b5ee
commit 13e3e0ec98
19 files changed
+240 -88

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
@requireCsrfToken
消息提醒
bug修复
使用BigInt管理权限
+39 -22
View File
@@ -1,14 +1,17 @@
import { ObjectID } from 'mongodb';
import { import {
VerifyPasswordError, UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, UserAlreadyExistError, InvalidTokenError,
NotFoundError, NotFoundError,
UserNotFoundError, UserNotFoundError,
PermissionError,
} from '../error'; } from '../error';
import * as bus from '../service/bus'; import * as bus from '../service/bus';
import { import {
Route, Connection, Handler, ConnectionHandler, Route, Connection, Handler, ConnectionHandler, param, Types,
} from '../service/server'; } from '../service/server';
import * as misc from '../lib/misc'; import * as misc from '../lib/misc';
import md5 from '../lib/md5'; import md5 from '../lib/md5';
import * as mail from '../lib/mail';
import * as contest from '../model/contest'; import * as contest from '../model/contest';
import * as message from '../model/message'; import * as message from '../model/message';
import * as document from '../model/document'; import * as document from '../model/document';
@@ -21,6 +24,7 @@ import * as discussion from '../model/discussion';
import * as token from '../model/token'; import * as token from '../model/token';
import * as training from '../model/training'; import * as training from '../model/training';
import { PERM, PRIV } from '../model/builtin'; import { PERM, PRIV } from '../model/builtin';
import { isContent, isPassword, isEmail } from '../lib/validator';
const { geoip, useragent } = global.Hydro.lib; const { geoip, useragent } = global.Hydro.lib;
@@ -133,29 +137,38 @@ class HomeSecurityHandler extends Handler {
if (useragent) this.response.body.icon = useragent.icon; 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(); if (password !== verifyPassword) throw new VerifyPasswordError();
await user.changePassword(this.user._id, current, password); await user.changePassword(this.user._id, current, password);
this.back(); 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.limitRate('send_mail', 3600, 30);
this.user.checkPassword(currentPassword); this.user.checkPassword(currentPassword);
const udoc = await user.getByEmail(domainId, mail); const udoc = await user.getByEmail(domainId, email);
if (udoc) throw new UserAlreadyExistError(mail); if (udoc) throw new UserAlreadyExistError(email);
const [code] = await token.add( const [code] = await token.add(
token.TYPE_CHANGEMAIL, token.TYPE_CHANGEMAIL,
await system.get('changemail_token_expire_seconds'), 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', { const m = await this.renderHTML('user_changemail_mail.html', {
url: this.url('user_changemail_with_code', { code }), uname: this.user.uname, 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'; 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); const sessions = await token.getSessionListByUid(this.user._id);
for (const session of sessions) { for (const session of sessions) {
if (tokenDigest === md5(session._id)) { if (tokenDigest === md5(session._id)) {
@@ -174,9 +187,8 @@ class HomeSecurityHandler extends Handler {
} }
class HomeSettingsHandler extends Handler { class HomeSettingsHandler extends Handler {
async get({ category }) { @param('category', Types.String)
// eslint-disable-next-line prefer-destructuring async get(domainId: string, category: string) {
category = category[0]; // Category would be splitted into array
const path = [ const path = [
['Hydro', 'homepage'], ['Hydro', 'homepage'],
[`home_${category}`, null], [`home_${category}`, null],
@@ -195,7 +207,7 @@ class HomeSettingsHandler extends Handler {
} else throw new NotFoundError(); } else throw new NotFoundError();
} }
async post(args) { async post(args: any) {
const $set = {}; const $set = {};
for (const key in args) { for (const key in args) {
if (setting.SETTINGS_BY_KEY[key] && !setting.SETTINGS_BY_KEY[key].disabled) { if (setting.SETTINGS_BY_KEY[key] && !setting.SETTINGS_BY_KEY[key].disabled) {
@@ -208,15 +220,16 @@ class HomeSettingsHandler extends Handler {
} }
class UserChangemailWithCodeHandler 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); const tdoc = await token.get(code, token.TYPE_CHANGEMAIL);
if (!tdoc || tdoc.uid !== this.user._id) { if (!tdoc || tdoc.uid !== this.user._id) {
throw new InvalidTokenError(code); throw new InvalidTokenError(code);
} }
const udoc = await user.getByEmail(domainId, tdoc.mail); const udoc = await user.getByEmail(domainId, tdoc.email);
if (udoc) throw new UserAlreadyExistError(tdoc.mail); if (udoc) throw new UserAlreadyExistError(tdoc.email);
await Promise.all([ await Promise.all([
user.setEmail(this.user._id, tdoc.mail), user.setEmail(this.user._id, tdoc.email),
token.del(code, token.TYPE_CHANGEMAIL), token.del(code, token.TYPE_CHANGEMAIL),
]); ]);
this.response.redirect = this.url('home_security'); this.response.redirect = this.url('home_security');
@@ -252,20 +265,24 @@ class HomeMessagesHandler extends Handler {
this.response.template = 'home_messages.html'; 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); const udoc = await user.getById('system', uid);
if (!udoc) throw new UserNotFoundError(uid); if (!udoc) throw new UserNotFoundError(uid);
const mdoc = await message.send(this.user._id, uid, content); const mdoc = await message.send(this.user._id, uid, content);
// TODO(twd2): improve here: projection // TODO(twd2): improve here: projection
if (this.user._id !== uid) { if (this.user._id !== uid) {
await bus.publish(`user_message-${uid}`, { mdoc, udoc }); bus.publish(`user_message-${uid}`, { mdoc, udoc });
} }
this.back({ mdoc, udoc }); this.back({ mdoc, udoc });
} }
async postDeleteMessage({ messageId }) { @param('messageId', Types.ObjectID)
// TODO limitations async postDeleteMessage(domainId: string, messageId: ObjectID) {
await message.del(messageId); 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(); this.back();
} }
} }
+5 -2
View File
@@ -4,7 +4,7 @@ declare global {
interface String { interface String {
format: (...args: Array<any>) => string; format: (...args: Array<any>) => string;
rawformat: (object: any) => string; rawformat: (object: any) => string;
translate: (language: string | undefined) => string; translate: (...languages: string[]) => string;
} }
} }
@@ -35,10 +35,13 @@ String.prototype.rawformat = function rawFormat(object) {
return [res[0], object, res[1]].join(); return [res[0], object, res[1]].join();
}; };
String.prototype.translate = function translate(language = 'zh_CN') { 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]) {
if (locales[language][this] !== undefined) return locales[language][this]; if (locales[language][this] !== undefined) return locales[language][this];
} }
}
return this; return this;
}; };
+4 -1
View File
@@ -510,7 +510,10 @@ export const ContestHandlerMixin = (c) => class extends c {
problem.getList(domainId, tdoc.pids, true), problem.getList(domainId, tdoc.pids, true),
]); ]);
const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs); 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]; return [tdoc, rows, udict];
} }
+5 -3
View File
@@ -4,7 +4,7 @@ import * as db from '../service/db';
const coll = db.collection('token'); const coll = db.collection('token');
export const TYPE_SESSION = 0; export const TYPE_SESSION = 0;
export const TYPE_CSRF_TOKEN = 1; export const TYPE_TOKEN = 1;
export const TYPE_REGISTRATION = 2; export const TYPE_REGISTRATION = 2;
export const TYPE_CHANGEMAIL = 3; export const TYPE_CHANGEMAIL = 3;
export const TYPE_OAUTH = 4; export const TYPE_OAUTH = 4;
@@ -62,7 +62,9 @@ export async function del(tokenId: string, tokenType: number) {
return !!result.deletedCount; return !!result.deletedCount;
} }
export async function createOrUpdate(tokenType: number, expireSeconds: number, data: object) { export async function createOrUpdate(
tokenType: number, expireSeconds: number, data: any,
): Promise<string> {
const d = await coll.findOne({ tokenType, ...data }); const d = await coll.findOne({ tokenType, ...data });
if (!d) { if (!d) {
const res = await add(tokenType, expireSeconds, data); const res = await add(tokenType, expireSeconds, data);
@@ -87,7 +89,7 @@ export function delByUid(uid: number) {
global.Hydro.model.token = { global.Hydro.model.token = {
TYPE_SESSION, TYPE_SESSION,
TYPE_CHANGEMAIL, TYPE_CHANGEMAIL,
TYPE_CSRF_TOKEN, TYPE_TOKEN,
TYPE_OAUTH, TYPE_OAUTH,
TYPE_REGISTRATION, TYPE_REGISTRATION,
TYPE_LOSTPASS, TYPE_LOSTPASS,
+51 -42
View File
@@ -24,6 +24,9 @@ import {
UserFacingError, ValidationError, PrivilegeError, UserFacingError, ValidationError, PrivilegeError,
CsrfTokenError, InvalidOperationError, MethodNotAllowedError, CsrfTokenError, InvalidOperationError, MethodNotAllowedError,
} from '../error'; } from '../error';
import hash from '../lib/hash.hydro';
import { lrucache } from '../utils';
import { Udoc } from '../interface';
let enableLog = true; 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() { export async function prepare() {
server = http.createServer(app.callback()); server = http.createServer(app.callback());
app.keys = await system.get('session.keys'); app.keys = await system.get('session.keys');
@@ -208,7 +222,7 @@ export class Handler {
csrfToken: string; csrfToken: string;
user: any; user: Udoc;
constructor(ctx: Koa.Context) { constructor(ctx: Koa.Context) {
this.ctx = ctx; this.ctx = ctx;
@@ -242,18 +256,22 @@ export class Handler {
this.session = {}; 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<string> { async renderHTML(name: string, context: any): Promise<string> {
if (enableLog) console.time(name); if (enableLog) console.time(name);
// FIXME fix this ugly hack const UserContext = { ...this.user, gravatar: misc.gravatar(this.user.gravatar, 128) };
// @ts-ignore const res = await render(name, {
this._user = { ...this.user, gravatar: misc.gravatar(this.user.gravatar, 128) };
const res = await render(name, Object.assign(context, {
handler: this, handler: this,
// @ts-ignore UserContext,
url: (...args) => this.url(...args), url: this.url.bind(this),
_: (str: string) => (str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''), _: this.translate.bind(this),
user: this.user, ...context,
})); });
if (enableLog) console.timeEnd(name); if (enableLog) console.timeEnd(name);
return res; return res;
} }
@@ -262,11 +280,12 @@ export class Handler {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations); await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
} }
translate(str) { translate(str: string) {
return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''; if (!str) return '';
return str.toString().translate(this.user.viewLang, this.session.viewLang);
} }
renderTitle(str) { renderTitle(str: string) {
return `${this.translate(str)} - Hydro`; return `${this.translate(str)} - Hydro`;
} }
@@ -304,7 +323,7 @@ export class Handler {
} }
} }
url(name, kwargs = {}) { url(name: string, kwargs = {}) {
let res = '#'; let res = '#';
const args: any = { ...kwargs }; const args: any = { ...kwargs };
try { try {
@@ -324,7 +343,6 @@ export class Handler {
} }
async render(name: string, context: any) { async render(name: string, context: any) {
// @ts-ignore
this.response.body = await this.renderHTML(name, context); this.response.body = await this.renderHTML(name, context);
this.response.type = 'text/html'; this.response.type = 'text/html';
} }
@@ -352,14 +370,6 @@ export class Handler {
if (bdoc) throw new BlacklistedError(this.request.ip); 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 }) { async init({ domainId }) {
const xff = await system.get('server.xff'); const xff = await system.get('server.xff');
if (xff) this.request.ip = this.request.headers[xff.toLowerCase()]; if (xff) this.request.ip = this.request.headers[xff.toLowerCase()];
@@ -367,15 +377,13 @@ export class Handler {
this.getSession(), this.getSession(),
this.getBdoc(), this.getBdoc(),
]); ]);
[this.user] = await Promise.all([ [this.user, this.UIContext.token] = await Promise.all([
user.getById(domainId, this.session.uid), user.getById(domainId, this.session.uid),
this.getCsrfToken(), token.createOrUpdate(
token.TYPE_TOKEN, 300, { uid: this.session.uid },
),
]); ]);
} this.csrfToken = this.getCsrfToken(this.session._id || String.random(32));
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);
} }
async finish() { async finish() {
@@ -494,7 +502,6 @@ async function handle(ctx, HandlerClass, checker) {
await h.init(args); await h.init(args);
if (checker) checker.call(h); if (checker) checker.call(h);
if (method === 'post') { if (method === 'post') {
await h.checkCsrfToken(args.csrfToken);
if (operation) { if (operation) {
if (typeof h[`post${operation}`] !== 'function') { if (typeof h[`post${operation}`] !== 'function') {
throw new InvalidOperationError(operation); throw new InvalidOperationError(operation);
@@ -580,18 +587,11 @@ export class ConnectionHandler {
} }
async renderHTML(name: string, context: any): Promise<string> { async renderHTML(name: string, context: any): Promise<string> {
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 res = await render(name, Object.assign(context, {
handler: this, handler: this,
// @ts-ignore url: this.url.bind(this),
url: (...args) => this.url(...args), _: this.translate.bind(this),
_: (str: string) => (str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''),
user: this.user,
})); }));
if (enableLog) console.timeEnd(name);
return res; return res;
} }
@@ -675,7 +675,7 @@ export class ConnectionHandler {
async init({ domainId }) { async init({ domainId }) {
try { 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) { } catch (e) {
this.session = { uid: 0 }; this.session = { uid: 0 };
} }
@@ -736,5 +736,14 @@ export async function start() {
} }
global.Hydro.service.server = { global.Hydro.service.server = {
Types, param, Handler, ConnectionHandler, Route, Connection, Middleware, prepare, start, Types,
param,
requireCsrfToken,
Handler,
ConnectionHandler,
Route,
Connection,
Middleware,
prepare,
start,
}; };
+17
View File
@@ -1,8 +1,10 @@
import fs from 'fs'; import fs from 'fs';
import { serialize } from 'v8';
import cluster from 'cluster'; import cluster from 'cluster';
import path from 'path'; import path from 'path';
import * as superagent from 'superagent'; import * as superagent from 'superagent';
import proxy from 'superagent-proxy'; import proxy from 'superagent-proxy';
import Lru from 'lru-cache';
declare global { declare global {
interface StringConstructor { 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); proxy(superagent);
if (!cluster.worker) { if (!cluster.worker) {
+9 -1
View File
@@ -41,6 +41,7 @@ __langname: 简体中文
"What's file?": 什么是文件? "What's file?": 什么是文件?
(All Problems): (所有题目) (All Problems): (所有题目)
(None): (无) (None): (无)
(Not changed): 未更改
Aborted: 已放弃 Aborted: 已放弃
About Hydro: 关于 Hydro About Hydro: 关于 Hydro
About Markdown: 关于 Markdown 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 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.: 若留空,则将使用对应语言的内置代码模板。 If left blank, the built-in template of the corresponding language will be used.: 若留空,则将使用对应语言的内置代码模板。
Images: 图片 Images: 图片
Import from: 导入自
Import problem from syzoj.: 从运行 syzoj 系统的 OJ 导入题目。 Import problem from syzoj.: 从运行 syzoj 系统的 OJ 导入题目。
Import Problem: 导入题目 Import Problem: 导入题目
import: 导入 Import: 导入
In 1 day: 一天后 In 1 day: 一天后
In 1 month: 一个月后 In 1 month: 一个月后
In 1 week: 一周后 In 1 week: 一周后
@@ -378,6 +380,7 @@ Last Submit At: 最后递交于
Last Update At: 最后活动时间 Last Update At: 最后活动时间
LaTeX Expressions: LaTeX 表达式 LaTeX Expressions: LaTeX 表达式
Learn More: 了解更多 Learn More: 了解更多
Leave blank to use numeric pid.: 留空来使用数字题号。
Limitations: 限制 Limitations: 限制
Links: 链接 Links: 链接
List View: 列表视图 List View: 列表视图
@@ -388,6 +391,8 @@ Login to Attend Contest: 登录后参加比赛
Login to Claim Homework: 登录后认领作业 Login to Claim Homework: 登录后认领作业
Login to Create a Discussion: 登录后创建讨论 Login to Create a Discussion: 登录后创建讨论
Login to Submit: 登录后递交 Login to Submit: 登录后递交
Login with Github: 使用 Github 登录
Login with Google: 使用 Google 登录
Login: 登录 Login: 登录
Logout All Sessions: 注销所有会话 Logout All Sessions: 注销所有会话
Logout This Session: 注销该会话 Logout This Session: 注销该会话
@@ -639,6 +644,7 @@ Sorry, there is no problem in the problem set: 题库中没有题目
Sorry, there is no training plan.: 目前没有训练计划。 Sorry, there is no training plan.: 目前没有训练计划。
Sorry: 对不起 Sorry: 对不起
Sort by: 排序 Sort by: 排序
Source type: 来源类型
Star Topic: 收藏讨论 Star Topic: 收藏讨论
Star: 收藏 Star: 收藏
Start at: 开始于 Start at: 开始于
@@ -663,6 +669,7 @@ Technical Information: 技术信息
Temporary session: 临时会话 Temporary session: 临时会话
Terms of Service: 服务条款 Terms of Service: 服务条款
Test data comes from: 测试数据来自 Test data comes from: 测试数据来自
Testdata Config: 测试数据配置
Text: 文本 Text: 文本
The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。 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 个字符。 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 homework is not open.: 该作业还未开放递交。
This is the current session: 这是当前的会话 This is the current session: 这是当前的会话
This message will be presented to those whose submissions are accepted.: 这条消息将会被展示给通过此题的用户。 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 section cannot be challenged at present, so please complete the following sections first: 该章节目前不可挑战,请先完成以下章节
This type of files are not allowed to be uploaded.: 不允许上传这种文件。 This type of files are not allowed to be uploaded.: 不允许上传这种文件。
Time (Seconds): 耗时 (秒) Time (Seconds): 耗时 (秒)
+3 -1
View File
@@ -1,12 +1,13 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "2.7.2", "version": "2.7.3",
"main": "hydro/loader.ts", "main": "hydro/loader.ts",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",
"author": "masnn", "author": "masnn",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"@hydrooj/markdown-it-katex": "^2.1.1", "@hydrooj/markdown-it-katex": "^2.1.1",
"@types/lru-cache": "^5.1.0",
"adm-zip": "^0.4.16", "adm-zip": "^0.4.16",
"ansi_up": "^4.0.4", "ansi_up": "^4.0.4",
"detect-browser": "^5.1.1", "detect-browser": "^5.1.1",
@@ -17,6 +18,7 @@
"koa-router": "^8.0.8", "koa-router": "^8.0.8",
"koa-static-cache": "^5.1.3", "koa-static-cache": "^5.1.3",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"lru-cache": "^6.0.0",
"markdown-it": "^11.0.0", "markdown-it": "^11.0.0",
"markdown-it-anchor": "^5.3.0", "markdown-it-anchor": "^5.3.0",
"markdown-it-container": "^3.0.0", "markdown-it-container": "^3.0.0",
@@ -121,12 +121,12 @@
data-form="{{ {operation: reply_post_op, drid: doc._id}|json }}" data-form="{{ {operation: reply_post_op, drid: doc._id}|json }}"
><span class="icon icon-reply"></span></a> ><span class="icon icon-reply"></span></a>
{% endif %} {% 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) %}
<a href="javascript:;" data-tooltip="{{ _('Edit') }}" name="dczcomments__op-edit-comment" <a href="javascript:;" data-tooltip="{{ _('Edit') }}" name="dczcomments__op-edit-comment"
data-form="{{ {operation: comment_edit_op, drid: doc._id}|json }}" data-form="{{ {operation: comment_edit_op, drid: doc._id}|json }}"
><span class="icon icon-edit"></span></a> ><span class="icon icon-edit"></span></a>
{% endif %} {% 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) %}
<a href="javascript:;" data-tooltip="{{ _('Delete') }}" name="dczcomments__op-delete-comment" <a href="javascript:;" data-tooltip="{{ _('Delete') }}" name="dczcomments__op-delete-comment"
data-form="{{ {operation: comment_delete_op, drid: doc._id}|json }}" data-form="{{ {operation: comment_delete_op, drid: doc._id}|json }}"
><span class="icon icon-delete"></span></a> ><span class="icon icon-delete"></span></a>
@@ -139,7 +139,7 @@
<div class="commentbox-edit-target"></div> <div class="commentbox-edit-target"></div>
<ul class="dczcomments__replies commentbox-reply-target"> <ul class="dczcomments__replies commentbox-reply-target">
{% for rdoc in doc['reply'] %} {% for rdoc in doc['reply'] %}
{{ reply(mode_create, udict[rdoc.owner], doc, rdoc) }} {{ reply(false, udict[rdoc.owner], doc, rdoc) }}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
+4 -4
View File
@@ -118,12 +118,12 @@
data-form="{{ {operation: reply_post_op, psid: doc._id}|json }}" data-form="{{ {operation: reply_post_op, psid: doc._id}|json }}"
><span class="icon icon-reply"></span></a> ><span class="icon icon-reply"></span></a>
{% endif %} {% 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) %}
<a href="javascript:;" data-tooltip="{{ _('Edit') }}" name="dczcomments__op-edit-comment" <a href="javascript:;" data-tooltip="{{ _('Edit') }}" name="dczcomments__op-edit-comment"
data-form="{{ {operation: comment_edit_op, psid: doc._id}|json }}" data-form="{{ {operation: comment_edit_op, psid: doc._id}|json }}"
><span class="icon icon-edit"></span></a> ><span class="icon icon-edit"></span></a>
{% endif %} {% 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) %}
<a href="javascript:;" data-tooltip="{{ _('Delete') }}" name="dczcomments__op-delete-comment" <a href="javascript:;" data-tooltip="{{ _('Delete') }}" name="dczcomments__op-delete-comment"
data-form="{{ {operation: comment_delete_op, psid: doc._id}|json }}" data-form="{{ {operation: comment_delete_op, psid: doc._id}|json }}"
><span class="icon icon-delete"></span></a> ><span class="icon icon-delete"></span></a>
@@ -148,12 +148,12 @@
{% if handler.user.hasPerm(reply_post_perm) %} {% if handler.user.hasPerm(reply_post_perm) %}
<a href="javascript:;" data-tooltip="{{ _('Reply') }}" name="dczcomments__op-reply-reply"><span class="icon icon-reply"></span></a> <a href="javascript:;" data-tooltip="{{ _('Reply') }}" name="dczcomments__op-reply-reply"><span class="icon icon-reply"></span></a>
{% endif %} {% 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) %}
<a href="javascript:;" data-tooltip="{{ _('Edit') }}" name="dczcomments__op-edit-reply" <a href="javascript:;" data-tooltip="{{ _('Edit') }}" name="dczcomments__op-edit-reply"
data-form="{{ {operation: reply_edit_op, psid: doc._id, psrid: rdoc._id}|json }}" data-form="{{ {operation: reply_edit_op, psid: doc._id, psrid: rdoc._id}|json }}"
><span class="icon icon-edit"></span></a> ><span class="icon icon-edit"></span></a>
{% endif %} {% 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) %}
<a href="javascript:;" data-tooltip="{{ _('Delete') }}" name="dczcomments__op-delete-reply" <a href="javascript:;" data-tooltip="{{ _('Delete') }}" name="dczcomments__op-delete-reply"
data-form="{{ {operation: reply_delete_op, psid: doc._id, psrid: rdoc._id}|json }}" data-form="{{ {operation: reply_delete_op, psid: doc._id, psrid: rdoc._id}|json }}"
><span class="icon icon-delete"></span></a> ><span class="icon icon-delete"></span></a>
+14 -4
View File
@@ -63,12 +63,22 @@
udict = udict, udict = udict,
comment_ref = 'drid', comment_ref = 'drid',
reply_ref = 'drrid', 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_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_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_NONE if ddoc.owner == handler.user._id else perm.PERM_DELETE_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_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_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_NONE if ddoc.owner == handler.user._id else perm.PERM_DELETE_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) }} {{ paginator.render(page, pcount) }}
{% if drcount == 0 %} {% if drcount == 0 %}
-1
View File
@@ -97,7 +97,6 @@
<p>{{ _('Location') }}: {{ _(session.updateGeoip.display) }} ({{ session.updateIp }})</p> <p>{{ _('Location') }}: {{ _(session.updateGeoip.display) }} ({{ session.updateIp }})</p>
<p>{{ _('Operating System') }}: {{ session.updateUa.os }}</p> <p>{{ _('Operating System') }}: {{ session.updateUa.os }}</p>
<p>{{ _('Browser') }}: {{ session.updateUa.name }} {{ session.updateUa.version }}</p> <p>{{ _('Browser') }}: {{ session.updateUa.name }} {{ session.updateUa.version }}</p>
<!-- {{ _('User-Agent') }}: {{ session['update_ua']['str'] }} -->
{% if session.isCurrent %} {% if session.isCurrent %}
<p class="sessionlist__current-session"><span class="icon icon-check"></span> {{ _('This is the current session') }}</p> <p class="sessionlist__current-session"><span class="icon icon-check"></span> {{ _('This is the current session') }}</p>
{% endif %} {% endif %}
+1 -1
View File
@@ -30,7 +30,7 @@
var _htmlNode = document.documentElement; var _htmlNode = document.documentElement;
_htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs'); _htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs');
var UiContext = {{ handler.UIContext|json|safe }}; var UiContext = {{ handler.UIContext|json|safe }};
var UserContext = {{ handler._user|json|safe }}; var UserContext = {{ currentUser|json|safe }};
</script> </script>
{% endif %} {% endif %}
</head> </head>
+61
View File
@@ -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')}`);
+1 -1
View File
@@ -5,7 +5,7 @@ async function install() {
if (!global.Hydro) throw new Error('Data missing'); if (!global.Hydro) throw new Error('Data missing');
if (!fs.existsSync('hydro')) fs.mkdirSync('hydro'); if (!fs.existsSync('hydro')) fs.mkdirSync('hydro');
const hydro = JSON.parse(zlib.gunzipSync(global.Hydro).toString()); 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) { for (const i in hydro.modules) {
fs.writeFileSync(`${i}.hydro`, Buffer.from(hydro.modules[i], 'base64')); fs.writeFileSync(`${i}.hydro`, Buffer.from(hydro.modules[i], 'base64'));
} }
+1 -1
View File
@@ -32,7 +32,7 @@ const page = new NamedPage('home_messages', () => {
reduxStore = store; 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) => { sock.onmessage = (message) => {
const msg = JSON.parse(message.data); const msg = JSON.parse(message.data);
store.dispatch({ store.dispatch({
+1 -1
View File
@@ -5,7 +5,7 @@ import * as recordEnum from 'vj/constant/record';
const page = new NamedPage('judge_playground', async () => { const page = new NamedPage('judge_playground', async () => {
const { default: SockJs } = await import('sockjs-client'); 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 = () => { sock.onopen = () => {
const div = $('<div class="section visible">').appendTo('#messages'); const div = $('<div class="section visible">').appendTo('#messages');
$('<div class="section__header"><h1 class="section__title">Connection opened.</h1></div>') $('<div class="section__header"><h1 class="section__title">Connection opened.</h1></div>')
+17
View File
@@ -501,6 +501,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8"
integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ== 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": "@types/markdown-it@^10.0.1":
version "10.0.1" version "10.0.1"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.1.tgz#94e252ab689c8e9ceb9aff2946e0a458390105eb" 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: dependencies:
yallist "^3.0.2" 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: macos-release@^2.2.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.0.tgz#837b39fc01785c3584f103c5599e0f0c8068b49e" 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" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== 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: yaml@^1.9.2:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"