fix: Double recursive loops can cause server lockup on deeply nested docs (#5222)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
`).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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 
|
||||
`);
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user