fix(diplomacy): purify stored letter HTML
This commit is contained in:
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import type { GamePrisma } from '@sammo-ts/infra';
|
||||
|
||||
import { purifyDiplomacyHtml } from '../../security/diplomacyHtml.js';
|
||||
import { accessAuthedInputProcedure, accessAuthedProcedure, router } from '../../trpc.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
import { assertNationAccess, resolveNationPermission } from '../nation/shared.js';
|
||||
@@ -56,7 +57,8 @@ export const diplomacyRouter = router({
|
||||
const src = asRecord(aux.src);
|
||||
const dest = asRecord(aux.dest);
|
||||
const stateOpt = typeof aux.state_opt === 'string' ? aux.state_opt : null;
|
||||
const detail = permission < 3 && letter.textDetail ? '(권한이 부족합니다)' : letter.textDetail;
|
||||
const detail =
|
||||
permission < 3 && letter.textDetail ? '(권한이 부족합니다)' : purifyDiplomacyHtml(letter.textDetail);
|
||||
const reason = asRecord(aux.reason);
|
||||
|
||||
return {
|
||||
@@ -80,7 +82,7 @@ export const diplomacyRouter = router({
|
||||
prevId: letter.prevId,
|
||||
state: mapLetterState(letter.state),
|
||||
stateOpt,
|
||||
brief: letter.textBrief,
|
||||
brief: purifyDiplomacyHtml(letter.textBrief),
|
||||
detail,
|
||||
date: letter.date.toISOString(),
|
||||
reason: {
|
||||
@@ -207,8 +209,8 @@ export const diplomacyRouter = router({
|
||||
destNationId: destNation.id,
|
||||
prevId,
|
||||
state: 'PROPOSED',
|
||||
textBrief: input.brief,
|
||||
textDetail: input.detail,
|
||||
textBrief: purifyDiplomacyHtml(input.brief),
|
||||
textDetail: purifyDiplomacyHtml(input.detail),
|
||||
srcSignerId: me.id,
|
||||
aux: aux as GamePrisma.InputJsonValue,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
const options: sanitizeHtml.IOptions = {
|
||||
allowedTags: [
|
||||
'p',
|
||||
'br',
|
||||
'strong',
|
||||
'b',
|
||||
'em',
|
||||
'i',
|
||||
'u',
|
||||
's',
|
||||
'strike',
|
||||
'blockquote',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'code',
|
||||
'pre',
|
||||
'hr',
|
||||
'a',
|
||||
'img',
|
||||
],
|
||||
allowedAttributes: {
|
||||
a: ['href', 'target', 'rel', 'title'],
|
||||
img: ['src', 'alt', 'title', 'width', 'height'],
|
||||
},
|
||||
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
|
||||
allowedSchemesByTag: {
|
||||
a: ['http', 'https', 'mailto', 'tel'],
|
||||
img: ['http', 'https'],
|
||||
},
|
||||
allowProtocolRelative: false,
|
||||
transformTags: {
|
||||
a: (tagName, attribs) => {
|
||||
const target = attribs.target === '_blank' || attribs.target === '_self' ? attribs.target : undefined;
|
||||
const { target: _target, rel: _rel, ...rest } = attribs;
|
||||
return {
|
||||
tagName,
|
||||
attribs: target
|
||||
? {
|
||||
...rest,
|
||||
target,
|
||||
...(target === '_blank' ? { rel: 'noopener noreferrer nofollow' } : {}),
|
||||
}
|
||||
: rest,
|
||||
};
|
||||
},
|
||||
},
|
||||
exclusiveFilter: (frame) => frame.tag === 'img' && !frame.attribs.src,
|
||||
};
|
||||
|
||||
/**
|
||||
* Canonicalizes the HTML emitted by the diplomacy Tiptap editors. Writes are
|
||||
* purified before persistence and reads are purified again for legacy rows.
|
||||
*/
|
||||
export const purifyDiplomacyHtml = (value: string | null | undefined): string => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return sanitizeHtml(value, options);
|
||||
};
|
||||
Reference in New Issue
Block a user