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

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

No files matched your search

+39 -22
View File
@@ -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();
}
}
+7 -4
View File
@@ -4,7 +4,7 @@ declare global {
interface String {
format: (...args: Array<any>) => 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;
};
+4 -1
View File
@@ -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];
}
+5 -3
View File
@@ -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<string> {
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,
+51 -42
View File
@@ -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<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 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<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, {
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,
};
+17
View File
@@ -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) {