feat: allow installing libs from excal github (#9041)

This commit is contained in:
David Luzar 2025-01-23 16:50:47 +01:00 committed by GitHub
parent 0bf234fcc9
commit f87c2cde09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 139 additions and 12 deletions

View file

@ -0,0 +1,105 @@
import { validateLibraryUrl } from "./library";
describe("validateLibraryUrl", () => {
it("should validate hostname & pathname", () => {
// valid hostnames
// -------------------------------------------------------------------------
expect(
validateLibraryUrl("https://www.excalidraw.com", ["excalidraw.com"]),
).toBe(true);
expect(
validateLibraryUrl("https://excalidraw.com", ["excalidraw.com"]),
).toBe(true);
expect(
validateLibraryUrl("https://library.excalidraw.com", ["excalidraw.com"]),
).toBe(true);
expect(
validateLibraryUrl("https://library.excalidraw.com", [
"library.excalidraw.com",
]),
).toBe(true);
expect(
validateLibraryUrl("https://excalidraw.com/", ["excalidraw.com/"]),
).toBe(true);
expect(
validateLibraryUrl("https://excalidraw.com", ["excalidraw.com/"]),
).toBe(true);
expect(
validateLibraryUrl("https://excalidraw.com/", ["excalidraw.com"]),
).toBe(true);
// valid pathnames
// -------------------------------------------------------------------------
expect(
validateLibraryUrl("https://excalidraw.com/path", ["excalidraw.com"]),
).toBe(true);
expect(
validateLibraryUrl("https://excalidraw.com/path/", ["excalidraw.com"]),
).toBe(true);
expect(
validateLibraryUrl("https://excalidraw.com/specific/path", [
"excalidraw.com/specific/path",
]),
).toBe(true);
expect(
validateLibraryUrl("https://excalidraw.com/specific/path/", [
"excalidraw.com/specific/path",
]),
).toBe(true);
expect(
validateLibraryUrl("https://excalidraw.com/specific/path", [
"excalidraw.com/specific/path/",
]),
).toBe(true);
expect(
validateLibraryUrl("https://excalidraw.com/specific/path/other", [
"excalidraw.com/specific/path",
]),
).toBe(true);
// invalid hostnames
// -------------------------------------------------------------------------
expect(() =>
validateLibraryUrl("https://xexcalidraw.com", ["excalidraw.com"]),
).toThrow();
expect(() =>
validateLibraryUrl("https://x-excalidraw.com", ["excalidraw.com"]),
).toThrow();
expect(() =>
validateLibraryUrl("https://excalidraw.comx", ["excalidraw.com"]),
).toThrow();
expect(() =>
validateLibraryUrl("https://excalidraw.comx", ["excalidraw.com"]),
).toThrow();
expect(() =>
validateLibraryUrl("https://excalidraw.com.mx", ["excalidraw.com"]),
).toThrow();
// protocol must be https
expect(() =>
validateLibraryUrl("http://excalidraw.com.mx", ["excalidraw.com"]),
).toThrow();
// invalid pathnames
// -------------------------------------------------------------------------
expect(() =>
validateLibraryUrl("https://excalidraw.com/specific/other/path", [
"excalidraw.com/specific/path",
]),
).toThrow();
expect(() =>
validateLibraryUrl("https://excalidraw.com/specific/paths", [
"excalidraw.com/specific/path",
]),
).toThrow();
expect(() =>
validateLibraryUrl("https://excalidraw.com/specific/path-s", [
"excalidraw.com/specific/path",
]),
).toThrow();
expect(() =>
validateLibraryUrl("https://excalidraw.com/some/specific/path", [
"excalidraw.com/specific/path",
]),
).toThrow();
});
});

View file

@ -36,7 +36,18 @@ import { Queue } from "../queue";
import { hashElementsVersion, hashString } from "../element"; import { hashElementsVersion, hashString } from "../element";
import { toValidURL } from "./url"; import { toValidURL } from "./url";
const ALLOWED_LIBRARY_HOSTNAMES = ["excalidraw.com"]; /**
* format: hostname or hostname/pathname
*
* Both hostname and pathname are matched partially,
* hostname from the end, pathname from the start, with subdomain/path
* boundaries
**/
const ALLOWED_LIBRARY_URLS = [
"excalidraw.com",
// when installing from github PRs
"raw.githubusercontent.com/excalidraw/excalidraw-libraries",
];
type LibraryUpdate = { type LibraryUpdate = {
/** deleted library items since last onLibraryChange event */ /** deleted library items since last onLibraryChange event */
@ -469,26 +480,37 @@ export const distributeLibraryItemsOnSquareGrid = (
return resElements; return resElements;
}; };
const validateLibraryUrl = ( export const validateLibraryUrl = (
libraryUrl: string, libraryUrl: string,
/** /**
* If supplied, takes precedence over the default whitelist. * @returns `true` if the URL is valid, throws otherwise.
* Return `true` if the URL is valid.
*/ */
validator?: (libraryUrl: string) => boolean, validator:
): boolean => { | ((libraryUrl: string) => boolean)
| string[] = ALLOWED_LIBRARY_URLS,
): true => {
if ( if (
validator typeof validator === "function"
? validator(libraryUrl) ? validator(libraryUrl)
: ALLOWED_LIBRARY_HOSTNAMES.includes( : validator.some((allowedUrlDef) => {
new URL(libraryUrl).hostname.split(".").slice(-2).join("."), const allowedUrl = new URL(
) `https://${allowedUrlDef.replace(/^https?:\/\//, "")}`,
);
const { hostname, pathname } = new URL(libraryUrl);
return (
new RegExp(`(^|\\.)${allowedUrl.hostname}$`).test(hostname) &&
new RegExp(
`^${allowedUrl.pathname.replace(/\/+$/, "")}(/+|$)`,
).test(pathname)
);
})
) { ) {
return true; return true;
} }
console.error(`Invalid or disallowed library URL: "${libraryUrl}"`); throw new Error(`Invalid or disallowed library URL: "${libraryUrl}"`);
throw new Error("Invalid or disallowed library URL");
}; };
export const parseLibraryTokensFromUrl = () => { export const parseLibraryTokensFromUrl = () => {