fix: Double recursive loops can cause server lockup on deeply nested docs (#5222)

This commit is contained in:
Tom Moor
2023-04-18 19:38:35 -04:00
committed by GitHub
parent bcffd81c9c
commit 1642eb610d
7 changed files with 61 additions and 27 deletions

View File

@@ -49,22 +49,21 @@ export default class ProsemirrorHelper {
static parseMentions(node: Node) {
const mentions: MentionAttrs[] = [];
function findMentions(node: Node) {
node.descendants((node: Node) => {
if (
node.type.name === "mention" &&
!mentions.some((m) => m.id === node.attrs.id)
) {
mentions.push(node.attrs as MentionAttrs);
return false;
}
if (!node.content.size) {
return;
return false;
}
node.content.descendants(findMentions);
}
findMentions(node);
return true;
});
return mentions;
}

View File

@@ -6,6 +6,7 @@ import parseAttachmentIds from "./parseAttachmentIds";
it("should return an empty array with no matches", () => {
expect(parseAttachmentIds(`some random text`).length).toBe(0);
});
it("should not return orphaned UUID's", () => {
const uuid = uuidv4();
expect(
@@ -14,6 +15,7 @@ it("should not return orphaned UUID's", () => {
![caption](/images/${uuid}.png)`).length
).toBe(0);
});
it("should parse attachment ID from markdown", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
@@ -22,6 +24,7 @@ it("should parse attachment ID from markdown", () => {
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse attachment ID from markdown with additional query params", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
@@ -30,6 +33,7 @@ it("should parse attachment ID from markdown with additional query params", () =
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse attachment ID from markdown with fully qualified url", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
@@ -38,6 +42,7 @@ it("should parse attachment ID from markdown with fully qualified url", () => {
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse attachment ID from markdown with title", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
@@ -46,6 +51,7 @@ it("should parse attachment ID from markdown with title", () => {
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse multiple attachment IDs from markdown", () => {
const uuid = uuidv4();
const uuid2 = uuidv4();
@@ -58,6 +64,7 @@ some text
expect(results[0]).toBe(uuid);
expect(results[1]).toBe(uuid2);
});
it("should parse attachment ID from html", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
@@ -66,6 +73,7 @@ it("should parse attachment ID from html", () => {
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse attachment ID from html with fully qualified url", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(

View File

@@ -18,6 +18,22 @@ it("should return an array of document ids", () => {
expect(result[1]).toBe("test-123456");
});
it("should return deeply nested link document ids", () => {
const result = parseDocumentIds(`# Header
[internal](http://app.getoutline.com/doc/test-456733)
More text
- one
- two
- three [internal](/doc/test-123456#heading-anchor)
`);
expect(result.length).toBe(2);
expect(result[0]).toBe("test-456733");
expect(result[1]).toBe("test-123456");
});
it("should not return duplicate document ids", () => {
expect(parseDocumentIds(`# Header`).length).toBe(0);
const result = parseDocumentIds(`# Header

View File

@@ -10,10 +10,10 @@ import { parser } from "@server/editor";
* @returns An array of document identifiers
*/
export default function parseDocumentIds(text: string): string[] {
const value = parser.parse(text);
const doc = parser.parse(text);
const identifiers: string[] = [];
function findLinks(node: Node) {
doc.descendants((node: Node) => {
// get text nodes
if (node.type.name === "text") {
// get marks for text nodes
@@ -28,15 +28,12 @@ export default function parseDocumentIds(text: string): string[] {
}
}
});
return false;
}
if (!node.content.size) {
return;
}
return true;
});
node.content.descendants(findLinks);
}
findLinks(value);
return identifiers;
}

View File

@@ -3,6 +3,7 @@ import parseImages from "./parseImages";
it("should not return non images", () => {
expect(parseImages(`# Header`).length).toBe(0);
});
it("should return an array of images", () => {
const result = parseImages(`# Header
@@ -11,9 +12,22 @@ it("should return an array of images", () => {
expect(result.length).toBe(1);
expect(result[0]).toBe("/attachments/image.png");
});
it("should return deeply nested images", () => {
const result = parseImages(`# Header
- one
- two
- three ![internal](/attachments/image.png)
`);
expect(result.length).toBe(1);
expect(result[0]).toBe("/attachments/image.png");
});
it("should not return non document links", () => {
expect(parseImages(`[google](http://www.google.com)`).length).toBe(0);
});
it("should not return non document relative links", () => {
expect(parseImages(`[relative](/developers)`).length).toBe(0);
});

View File

@@ -2,25 +2,24 @@ import { Node } from "prosemirror-model";
import { parser } from "@server/editor";
export default function parseImages(text: string): string[] {
const value = parser.parse(text);
const doc = parser.parse(text);
const images: string[] = [];
function findImages(node: Node) {
doc.descendants((node: Node) => {
if (node.type.name === "image") {
if (!images.includes(node.attrs.src)) {
images.push(node.attrs.src);
}
return;
return false;
}
if (!node.content.size) {
return;
return false;
}
node.content.descendants(findImages);
}
return true;
});
findImages(value);
return images;
}

View File

@@ -4,7 +4,7 @@ import { EditorView } from "prosemirror-view";
function findPlaceholderLink(doc: Node, href: string) {
let result: { pos: number; node: Node } | undefined;
function findLinks(node: Node, pos = 0) {
doc.descendants((node: Node, pos = 0) => {
// get text nodes
if (node.type.name === "text") {
// get marks for text nodes
@@ -17,16 +17,17 @@ function findPlaceholderLink(doc: Node, href: string) {
}
}
});
return false;
}
if (!node.content.size) {
return;
return false;
}
node.descendants(findLinks);
}
return true;
});
findLinks(doc);
return result;
}