Disallow empty comments and comments with only whitespaces (#7156)
* fix: disallow empty comments * fix: avoid full traversal to validate comment * fix: text * fix:review * fix: review
This commit is contained in:
@@ -176,7 +176,116 @@ describe("#comments.create", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const anotherRes = await server.post("/api/comments.create", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
documentId: document.id,
|
||||||
|
data: {
|
||||||
|
type: "doc",
|
||||||
|
content: [{ type: "paragraph" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(res.status).toEqual(400);
|
expect(res.status).toEqual(400);
|
||||||
|
expect(anotherRes.status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow comments containing only whitespaces", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({ teamId: team.id });
|
||||||
|
const document = await buildDocument({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/comments.create", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
documentId: document.id,
|
||||||
|
data: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
content: [{ type: "text", text: " \n\r\n" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow adding images to comments", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({ teamId: team.id });
|
||||||
|
const document = await buildDocument({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/comments.create", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
documentId: document.id,
|
||||||
|
data: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
attrs: {
|
||||||
|
src: "https://example.com/image.png",
|
||||||
|
alt: "Example image",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow adding images from internal sources", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({ teamId: team.id });
|
||||||
|
const document = await buildDocument({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/comments.create", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
documentId: document.id,
|
||||||
|
data: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
attrs: {
|
||||||
|
src: "/api/attachments.redirect?id=1401323b-c4e2-40de-b172-e1668ec89111",
|
||||||
|
alt: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow invalid comment data", async () => {
|
it("should not allow invalid comment data", async () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import formidable from "formidable";
|
|||||||
import { Node } from "prosemirror-model";
|
import { Node } from "prosemirror-model";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ProsemirrorData as TProsemirrorData } from "@shared/types";
|
import { ProsemirrorData as TProsemirrorData } from "@shared/types";
|
||||||
|
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
|
||||||
import { schema } from "@server/editor";
|
import { schema } from "@server/editor";
|
||||||
|
|
||||||
export const BaseSchema = z.object({
|
export const BaseSchema = z.object({
|
||||||
@@ -14,7 +15,7 @@ export const ProsemirrorSchema = z.custom<TProsemirrorData>((val) => {
|
|||||||
try {
|
try {
|
||||||
const node = Node.fromJSON(schema, val);
|
const node = Node.fromJSON(schema, val);
|
||||||
node.check();
|
node.check();
|
||||||
return true;
|
return !ProsemirrorHelper.isEmpty(node, schema);
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ export default class Image extends SimpleImage {
|
|||||||
["p", { class: "caption" }, 0],
|
["p", { class: "caption" }, 0],
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
toPlainText: (node) =>
|
||||||
|
node.attrs.alt ? `(image: ${node.attrs.alt})` : "(image)",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,8 +142,35 @@ export class ProsemirrorHelper {
|
|||||||
*
|
*
|
||||||
* @returns True if the editor is empty
|
* @returns True if the editor is empty
|
||||||
*/
|
*/
|
||||||
static isEmpty(doc: Node) {
|
static isEmpty(doc: Node, schema?: Schema) {
|
||||||
return !doc || doc.textContent.trim() === "";
|
if (!schema) {
|
||||||
|
return !doc || doc.textContent.trim() === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const textSerializers = Object.fromEntries(
|
||||||
|
Object.entries(schema.nodes)
|
||||||
|
.filter(([, node]) => node.spec.toPlainText)
|
||||||
|
.map(([name, node]) => [name, node.spec.toPlainText])
|
||||||
|
);
|
||||||
|
|
||||||
|
let empty = true;
|
||||||
|
doc.descendants((child: Node) => {
|
||||||
|
// If we've already found non-empty data, we can stop descending further
|
||||||
|
if (!empty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toPlainText = textSerializers[child.type.name];
|
||||||
|
if (toPlainText) {
|
||||||
|
empty = !toPlainText(child).trim();
|
||||||
|
} else if (child.isText) {
|
||||||
|
empty = !child.text?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user