diff --git a/app/models/Comment.ts b/app/models/Comment.ts index f7a35df35..1d7b05ae3 100644 --- a/app/models/Comment.ts +++ b/app/models/Comment.ts @@ -1,6 +1,7 @@ import { subSeconds } from "date-fns"; import { computed, observable } from "mobx"; import { now } from "mobx-utils"; +import type { ProsemirrorData } from "@shared/types"; import User from "~/models/User"; import BaseModel from "./BaseModel"; import Field from "./decorators/Field"; @@ -22,7 +23,7 @@ class Comment extends BaseModel { */ @Field @observable - data: Record; + data: ProsemirrorData; /** * If this comment is a reply then the parent comment will be set, otherwise diff --git a/server/models/Comment.ts b/server/models/Comment.ts index e6ac04fb4..8a65f5eb4 100644 --- a/server/models/Comment.ts +++ b/server/models/Comment.ts @@ -5,12 +5,16 @@ import { Column, Table, Scopes, + Length, DefaultScope, } from "sequelize-typescript"; +import type { ProsemirrorData } from "@shared/types"; +import { CommentValidation } from "@shared/validations"; import Document from "./Document"; import User from "./User"; import ParanoidModel from "./base/ParanoidModel"; import Fix from "./decorators/Fix"; +import TextLength from "./validators/TextLength"; @DefaultScope(() => ({ include: [ @@ -35,8 +39,16 @@ import Fix from "./decorators/Fix"; @Table({ tableName: "comments", modelName: "comment" }) @Fix class Comment extends ParanoidModel { + @TextLength({ + max: CommentValidation.maxLength, + msg: `Comment must be less than ${CommentValidation.maxLength} characters`, + }) + @Length({ + max: CommentValidation.maxLength * 10, + msg: `Comment data is too large`, + }) @Column(DataType.JSONB) - data: Record; + data: ProsemirrorData; // associations diff --git a/server/models/validators/TextLength.ts b/server/models/validators/TextLength.ts new file mode 100644 index 000000000..f212f566d --- /dev/null +++ b/server/models/validators/TextLength.ts @@ -0,0 +1,36 @@ +import { size } from "lodash"; +import { Node } from "prosemirror-model"; +import { addAttributeOptions } from "sequelize-typescript"; +import { ProsemirrorData } from "@shared/types"; +import ProsemirrorHelper from "@shared/utils/ProsemirrorHelper"; +import { schema } from "@server/editor"; + +/** + * A decorator that validates the size of the text within a prosemirror data + * object, taking into account unicode characters of variable lengths. + */ +export default function TextLength({ + msg, + min = 0, + max, +}: { + msg?: string; + min?: number; + max: number; +}): (target: any, propertyName: string) => void { + return (target: any, propertyName: string) => + addAttributeOptions(target, propertyName, { + validate: { + validLength(value: ProsemirrorData) { + const text = ProsemirrorHelper.toPlainText( + Node.fromJSON(schema, value), + schema + ); + + if (size(text) > max || size(text) < min) { + throw new Error(msg); + } + }, + }, + }); +} diff --git a/shared/types.ts b/shared/types.ts index a7752acd8..9c2e8f9a5 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -208,3 +208,6 @@ export const NotificationEventDefaults = { [NotificationEventType.Features]: true, [NotificationEventType.ExportCompleted]: true, }; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ProsemirrorData = Record;