Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax

This commit is contained in:
Daniel J. Geiger 2023-08-21 16:09:37 -05:00
commit e4ddd08bb1
261 changed files with 12625 additions and 14661 deletions

View file

@ -3,6 +3,7 @@ import {
ExcalidrawSelectionElement,
ExcalidrawTextElement,
FontFamilyValues,
PointBinding,
StrokeRoundness,
} from "../element/types";
import {
@ -42,6 +43,7 @@ import {
measureTextElement,
} from "../element/textElement";
import { COLOR_PALETTE } from "../colors";
import { normalizeLink } from "./url";
type RestoredAppState = Omit<
AppState,
@ -64,6 +66,7 @@ export const AllowedExcalidrawActiveTools: Record<
eraser: false,
custom: true,
frame: true,
embeddable: true,
hand: true,
};
@ -82,6 +85,13 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
return DEFAULT_FONT_FAMILY;
};
const repairBinding = (binding: PointBinding | null) => {
if (!binding) {
return null;
}
return { ...binding, focus: binding.focus || 0 };
};
const restoreElementWithProperties = <
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
subtype?: ExcalidrawElement["subtype"];
@ -144,7 +154,7 @@ const restoreElementWithProperties = <
? element.boundElementIds.map((id) => ({ type: "arrow", id }))
: element.boundElements ?? [],
updated: element.updated ?? getUpdatedTimestamp(),
link: element.link ?? null,
link: element.link ? normalizeLink(element.link) : null,
locked: element.locked ?? false,
};
@ -257,8 +267,8 @@ const restoreElement = (
(element.type as ExcalidrawElement["type"] | "draw") === "draw"
? "line"
: element.type,
startBinding: element.startBinding,
endBinding: element.endBinding,
startBinding: repairBinding(element.startBinding),
endBinding: repairBinding(element.endBinding),
lastCommittedPoint: null,
startArrowhead,
endArrowhead,
@ -275,6 +285,10 @@ const restoreElement = (
return restoreElementWithProperties(element, {});
case "diamond":
return restoreElementWithProperties(element, {});
case "embeddable":
return restoreElementWithProperties(element, {
validated: undefined,
});
case "frame":
return restoreElementWithProperties(element, {
name: element.name ?? null,
@ -371,6 +385,24 @@ const repairBoundElement = (
}
};
/**
* Remove an element's frameId if its containing frame is non-existent
*
* NOTE mutates elements.
*/
const repairFrameMembership = (
element: Mutable<ExcalidrawElement>,
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
) => {
if (element.frameId) {
const containingFrame = elementsMap.get(element.frameId);
if (!containingFrame) {
element.frameId = null;
}
}
};
export const restoreElements = (
elements: ImportedDataState["elements"],
/** NOTE doesn't serve for reconciliation */
@ -411,6 +443,10 @@ export const restoreElements = (
// repair binding. Mutates elements.
const restoredElementsMap = arrayToMap(restoredElements);
for (const element of restoredElements) {
if (element.frameId) {
repairFrameMembership(element, restoredElementsMap);
}
if (isTextElement(element) && element.containerId) {
repairBoundElement(element, restoredElementsMap);
} else if (element.boundElements) {

30
src/data/url.test.tsx Normal file
View file

@ -0,0 +1,30 @@
import { normalizeLink } from "./url";
describe("normalizeLink", () => {
// NOTE not an extensive XSS test suite, just to check if we're not
// regressing in sanitization
it("should sanitize links", () => {
expect(
// eslint-disable-next-line no-script-url
normalizeLink(`javascript://%0aalert(document.domain)`).startsWith(
// eslint-disable-next-line no-script-url
`javascript:`,
),
).toBe(false);
expect(normalizeLink("ola")).toBe("ola");
expect(normalizeLink(" ola")).toBe("ola");
expect(normalizeLink("https://www.excalidraw.com")).toBe(
"https://www.excalidraw.com",
);
expect(normalizeLink("www.excalidraw.com")).toBe("www.excalidraw.com");
expect(normalizeLink("/ola")).toBe("/ola");
expect(normalizeLink("http://test")).toBe("http://test");
expect(normalizeLink("ftp://test")).toBe("ftp://test");
expect(normalizeLink("file://")).toBe("file://");
expect(normalizeLink("file://")).toBe("file://");
expect(normalizeLink("[test](https://test)")).toBe("[test](https://test)");
expect(normalizeLink("[[test]]")).toBe("[[test]]");
expect(normalizeLink("<test>")).toBe("<test>");
});
});

35
src/data/url.ts Normal file
View file

@ -0,0 +1,35 @@
import { sanitizeUrl } from "@braintree/sanitize-url";
export const normalizeLink = (link: string) => {
link = link.trim();
if (!link) {
return link;
}
return sanitizeUrl(link);
};
export const isLocalLink = (link: string | null) => {
return !!(link?.includes(location.origin) || link?.startsWith("/"));
};
/**
* Returns URL sanitized and safe for usage in places such as
* iframe's src attribute or <a> href attributes.
*/
export const toValidURL = (link: string) => {
link = normalizeLink(link);
// make relative links into fully-qualified urls
if (link.startsWith("/")) {
return `${location.origin}${link}`;
}
try {
new URL(link);
} catch {
// if link does not parse as URL, assume invalid and return blank page
return "about:blank";
}
return link;
};