Skip to content

Commit f87c2cd

Browse files
authored
feat: allow installing libs from excal github (#9041)
1 parent 0bf234f commit f87c2cd

File tree

2 files changed

+139
-12
lines changed

2 files changed

+139
-12
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { validateLibraryUrl } from "./library";
2+
3+
describe("validateLibraryUrl", () => {
4+
it("should validate hostname & pathname", () => {
5+
// valid hostnames
6+
// -------------------------------------------------------------------------
7+
expect(
8+
validateLibraryUrl("https://www.excalidraw.com", ["excalidraw.com"]),
9+
).toBe(true);
10+
expect(
11+
validateLibraryUrl("https://excalidraw.com", ["excalidraw.com"]),
12+
).toBe(true);
13+
expect(
14+
validateLibraryUrl("https://library.excalidraw.com", ["excalidraw.com"]),
15+
).toBe(true);
16+
expect(
17+
validateLibraryUrl("https://library.excalidraw.com", [
18+
"library.excalidraw.com",
19+
]),
20+
).toBe(true);
21+
expect(
22+
validateLibraryUrl("https://excalidraw.com/", ["excalidraw.com/"]),
23+
).toBe(true);
24+
expect(
25+
validateLibraryUrl("https://excalidraw.com", ["excalidraw.com/"]),
26+
).toBe(true);
27+
expect(
28+
validateLibraryUrl("https://excalidraw.com/", ["excalidraw.com"]),
29+
).toBe(true);
30+
31+
// valid pathnames
32+
// -------------------------------------------------------------------------
33+
expect(
34+
validateLibraryUrl("https://excalidraw.com/path", ["excalidraw.com"]),
35+
).toBe(true);
36+
expect(
37+
validateLibraryUrl("https://excalidraw.com/path/", ["excalidraw.com"]),
38+
).toBe(true);
39+
expect(
40+
validateLibraryUrl("https://excalidraw.com/specific/path", [
41+
"excalidraw.com/specific/path",
42+
]),
43+
).toBe(true);
44+
expect(
45+
validateLibraryUrl("https://excalidraw.com/specific/path/", [
46+
"excalidraw.com/specific/path",
47+
]),
48+
).toBe(true);
49+
expect(
50+
validateLibraryUrl("https://excalidraw.com/specific/path", [
51+
"excalidraw.com/specific/path/",
52+
]),
53+
).toBe(true);
54+
expect(
55+
validateLibraryUrl("https://excalidraw.com/specific/path/other", [
56+
"excalidraw.com/specific/path",
57+
]),
58+
).toBe(true);
59+
60+
// invalid hostnames
61+
// -------------------------------------------------------------------------
62+
expect(() =>
63+
validateLibraryUrl("https://xexcalidraw.com", ["excalidraw.com"]),
64+
).toThrow();
65+
expect(() =>
66+
validateLibraryUrl("https://x-excalidraw.com", ["excalidraw.com"]),
67+
).toThrow();
68+
expect(() =>
69+
validateLibraryUrl("https://excalidraw.comx", ["excalidraw.com"]),
70+
).toThrow();
71+
expect(() =>
72+
validateLibraryUrl("https://excalidraw.comx", ["excalidraw.com"]),
73+
).toThrow();
74+
expect(() =>
75+
validateLibraryUrl("https://excalidraw.com.mx", ["excalidraw.com"]),
76+
).toThrow();
77+
// protocol must be https
78+
expect(() =>
79+
validateLibraryUrl("http://excalidraw.com.mx", ["excalidraw.com"]),
80+
).toThrow();
81+
82+
// invalid pathnames
83+
// -------------------------------------------------------------------------
84+
expect(() =>
85+
validateLibraryUrl("https://excalidraw.com/specific/other/path", [
86+
"excalidraw.com/specific/path",
87+
]),
88+
).toThrow();
89+
expect(() =>
90+
validateLibraryUrl("https://excalidraw.com/specific/paths", [
91+
"excalidraw.com/specific/path",
92+
]),
93+
).toThrow();
94+
expect(() =>
95+
validateLibraryUrl("https://excalidraw.com/specific/path-s", [
96+
"excalidraw.com/specific/path",
97+
]),
98+
).toThrow();
99+
expect(() =>
100+
validateLibraryUrl("https://excalidraw.com/some/specific/path", [
101+
"excalidraw.com/specific/path",
102+
]),
103+
).toThrow();
104+
});
105+
});

packages/excalidraw/data/library.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,18 @@ import { Queue } from "../queue";
3636
import { hashElementsVersion, hashString } from "../element";
3737
import { toValidURL } from "./url";
3838

39-
const ALLOWED_LIBRARY_HOSTNAMES = ["excalidraw.com"];
39+
/**
40+
* format: hostname or hostname/pathname
41+
*
42+
* Both hostname and pathname are matched partially,
43+
* hostname from the end, pathname from the start, with subdomain/path
44+
* boundaries
45+
**/
46+
const ALLOWED_LIBRARY_URLS = [
47+
"excalidraw.com",
48+
// when installing from github PRs
49+
"raw.githubusercontent.com/excalidraw/excalidraw-libraries",
50+
];
4051

4152
type LibraryUpdate = {
4253
/** deleted library items since last onLibraryChange event */
@@ -469,26 +480,37 @@ export const distributeLibraryItemsOnSquareGrid = (
469480
return resElements;
470481
};
471482

472-
const validateLibraryUrl = (
483+
export const validateLibraryUrl = (
473484
libraryUrl: string,
474485
/**
475-
* If supplied, takes precedence over the default whitelist.
476-
* Return `true` if the URL is valid.
486+
* @returns `true` if the URL is valid, throws otherwise.
477487
*/
478-
validator?: (libraryUrl: string) => boolean,
479-
): boolean => {
488+
validator:
489+
| ((libraryUrl: string) => boolean)
490+
| string[] = ALLOWED_LIBRARY_URLS,
491+
): true => {
480492
if (
481-
validator
493+
typeof validator === "function"
482494
? validator(libraryUrl)
483-
: ALLOWED_LIBRARY_HOSTNAMES.includes(
484-
new URL(libraryUrl).hostname.split(".").slice(-2).join("."),
485-
)
495+
: validator.some((allowedUrlDef) => {
496+
const allowedUrl = new URL(
497+
`https://${allowedUrlDef.replace(/^https?:\/\//, "")}`,
498+
);
499+
500+
const { hostname, pathname } = new URL(libraryUrl);
501+
502+
return (
503+
new RegExp(`(^|\\.)${allowedUrl.hostname}$`).test(hostname) &&
504+
new RegExp(
505+
`^${allowedUrl.pathname.replace(/\/+$/, "")}(/+|$)`,
506+
).test(pathname)
507+
);
508+
})
486509
) {
487510
return true;
488511
}
489512

490-
console.error(`Invalid or disallowed library URL: "${libraryUrl}"`);
491-
throw new Error("Invalid or disallowed library URL");
513+
throw new Error(`Invalid or disallowed library URL: "${libraryUrl}"`);
492514
};
493515

494516
export const parseLibraryTokensFromUrl = () => {

0 commit comments

Comments
 (0)