Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dc7fe15f2 | |||
| 133b9a7277 |
@@ -130,6 +130,86 @@ const parseGoogleDriveVideoLink = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isGoogleMapsURL = (url: string): boolean => {
|
||||||
|
try {
|
||||||
|
const { hostname, pathname } = new URL(
|
||||||
|
url.startsWith("http") ? url : `https://${url}`,
|
||||||
|
);
|
||||||
|
const bareHostname = hostname.replace(/^www\./, "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
(bareHostname === "google.com" || bareHostname === "maps.google.com") &&
|
||||||
|
(pathname === "/maps" || pathname.startsWith("/maps/"))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGoogleMapsZoom = (zoomOrDistance: string): string | null => {
|
||||||
|
const match = zoomOrDistance.match(/^(\d+(?:\.\d+)?)(z|km|m)$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const value = Number(match[1]);
|
||||||
|
if (match[2] === "z") {
|
||||||
|
return `${Math.round(value)}`;
|
||||||
|
}
|
||||||
|
const meters = value * (match[2] === "km" ? 1000 : 1);
|
||||||
|
return `${Math.max(
|
||||||
|
3,
|
||||||
|
Math.min(21, Math.round(16 - Math.log2(meters / 500))),
|
||||||
|
)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseGoogleMapsLink = (url: string): string | null => {
|
||||||
|
if (!isGoogleMapsURL(url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`);
|
||||||
|
|
||||||
|
if (
|
||||||
|
urlObj.pathname.startsWith("/maps/embed") ||
|
||||||
|
urlObj.searchParams.get("output") === "embed"
|
||||||
|
) {
|
||||||
|
return urlObj.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, lat, lng, zoomOrDistance] =
|
||||||
|
urlObj.pathname.match(
|
||||||
|
/@(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),([^/?#,]+)/,
|
||||||
|
) || [];
|
||||||
|
const place = urlObj.pathname.match(/^\/maps\/place\/([^/]+)/)?.[1];
|
||||||
|
const query =
|
||||||
|
urlObj.searchParams.get("q") ||
|
||||||
|
(place ? decodeURIComponent(place).replace(/\+/g, " ") : null) ||
|
||||||
|
(lat && lng ? `${lat},${lng}` : null);
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedURL = new URL("https://www.google.com/maps");
|
||||||
|
embedURL.searchParams.set("q", query);
|
||||||
|
embedURL.searchParams.set("output", "embed");
|
||||||
|
|
||||||
|
if (lat && lng) {
|
||||||
|
embedURL.searchParams.set("ll", `${lat},${lng}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoom = zoomOrDistance ? getGoogleMapsZoom(zoomOrDistance) : null;
|
||||||
|
if (zoom) {
|
||||||
|
embedURL.searchParams.set("z", zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
return embedURL.toString();
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const ALLOWED_DOMAINS = new Set([
|
const ALLOWED_DOMAINS = new Set([
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
"youtu.be",
|
"youtu.be",
|
||||||
@@ -147,6 +227,8 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"giphy.com",
|
"giphy.com",
|
||||||
"reddit.com",
|
"reddit.com",
|
||||||
"forms.microsoft.com",
|
"forms.microsoft.com",
|
||||||
|
"wikipedia.org",
|
||||||
|
"*.wikipedia.org",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ALLOW_SAME_ORIGIN = new Set([
|
const ALLOW_SAME_ORIGIN = new Set([
|
||||||
@@ -278,6 +360,27 @@ export const getEmbedLink = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isGoogleMapsURL(link)) {
|
||||||
|
const googleMapsLink = parseGoogleMapsLink(link);
|
||||||
|
if (googleMapsLink) {
|
||||||
|
link = googleMapsLink;
|
||||||
|
aspectRatio = { w: 600, h: 450 };
|
||||||
|
embeddedLinkCache.set(originalLink, {
|
||||||
|
link,
|
||||||
|
intrinsicSize: aspectRatio,
|
||||||
|
type,
|
||||||
|
sandbox: { allowSameOrigin },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
link,
|
||||||
|
intrinsicSize: aspectRatio,
|
||||||
|
type,
|
||||||
|
sandbox: { allowSameOrigin },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const figmaLink = link.match(RE_FIGMA);
|
const figmaLink = link.match(RE_FIGMA);
|
||||||
if (figmaLink) {
|
if (figmaLink) {
|
||||||
type = "generic";
|
type = "generic";
|
||||||
@@ -506,6 +609,7 @@ export const embeddableURLValidator = (
|
|||||||
if (!url) {
|
if (!url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validateEmbeddable != null) {
|
if (validateEmbeddable != null) {
|
||||||
if (typeof validateEmbeddable === "function") {
|
if (typeof validateEmbeddable === "function") {
|
||||||
const ret = validateEmbeddable(url);
|
const ret = validateEmbeddable(url);
|
||||||
@@ -531,5 +635,5 @@ export const embeddableURLValidator = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!matchHostname(url, ALLOWED_DOMAINS);
|
return isGoogleMapsURL(url) || !!matchHostname(url, ALLOWED_DOMAINS);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { embeddableURLValidator, getEmbedLink } from "../src/embeddable";
|
import {
|
||||||
|
embeddableURLValidator,
|
||||||
|
getEmbedLink,
|
||||||
|
maybeParseEmbedSrc,
|
||||||
|
} from "../src/embeddable";
|
||||||
|
|
||||||
describe("YouTube timestamp parsing", () => {
|
describe("YouTube timestamp parsing", () => {
|
||||||
it("should parse YouTube URLs with timestamp in seconds", () => {
|
it("should parse YouTube URLs with timestamp in seconds", () => {
|
||||||
@@ -231,3 +235,62 @@ describe("Google Drive video embedding", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Google Maps embedding", () => {
|
||||||
|
const regularUrl =
|
||||||
|
"https://www.google.com/maps/place/26-432+Jab%C5%82onica,+Poland/@51.356302,20.797168,1921m/data=!3m2!1e3!4b1!4m15!1m8!3m7!1s0x47186c0e0e7578fd:0xe80d19a1ef6ad853!2zMjctMTAwIEnFgsW8YSwgUG9sYW5k!3b1!8m2!3d51.16305!4d21.23991!16zL20vMGM1ZnJ3!3m5!1s0x47184db43a4a5df9:0x6a2b8e648f9dc694!8m2!3d51.3562959!4d20.8023178!16s%2Fm%2F04q6t9r?entry=ttu";
|
||||||
|
const officialEmbedSrc =
|
||||||
|
"https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d8363.540754033738!2d20.79716795156659!3d51.356301987021546!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x47184db43a4a5df9%3A0x6a2b8e648f9dc694!2s26-432%20Jab%C5%82onica%2C%20Poland!5e1!3m2!1sen!2scz!4v1778159513974!5m2!1sen!2scz";
|
||||||
|
|
||||||
|
it("should preserve official Google Maps embed links", () => {
|
||||||
|
const parsedSrc = maybeParseEmbedSrc(
|
||||||
|
`<iframe src="${officialEmbedSrc}" width="600" height="450"></iframe>`,
|
||||||
|
);
|
||||||
|
const result = getEmbedLink(parsedSrc);
|
||||||
|
|
||||||
|
expect(embeddableURLValidator(parsedSrc, undefined)).toBe(true);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result?.type).toBe("generic");
|
||||||
|
if (result?.type === "generic") {
|
||||||
|
expect(result.link).toBe(officialEmbedSrc);
|
||||||
|
}
|
||||||
|
expect(result?.intrinsicSize).toEqual({ w: 600, h: 450 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should normalize regular Google Maps place links", () => {
|
||||||
|
const result = getEmbedLink(regularUrl);
|
||||||
|
|
||||||
|
expect(embeddableURLValidator(regularUrl, undefined)).toBe(true);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result?.type).toBe("generic");
|
||||||
|
if (result?.type !== "generic") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedURL = new URL(result.link);
|
||||||
|
expect(embedURL.origin).toBe("https://www.google.com");
|
||||||
|
expect(embedURL.pathname).toBe("/maps");
|
||||||
|
expect(embedURL.searchParams.get("q")).toBe(
|
||||||
|
decodeURIComponent("26-432%20Jab%C5%82onica%2C%20Poland"),
|
||||||
|
);
|
||||||
|
expect(embedURL.searchParams.get("output")).toBe("embed");
|
||||||
|
expect(embedURL.searchParams.get("ll")).toBe("51.356302,20.797168");
|
||||||
|
expect(embedURL.searchParams.get("z")).toBe("14");
|
||||||
|
expect(result.intrinsicSize).toEqual({ w: 600, h: 450 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject non-Maps Google pages and fail closed for unsupported Maps pages", () => {
|
||||||
|
expect(
|
||||||
|
embeddableURLValidator("https://www.google.com/search?q=maps", undefined),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
const unsupportedMapsUrl = "https://www.google.com/maps/about/";
|
||||||
|
expect(embeddableURLValidator(unsupportedMapsUrl, undefined)).toBe(true);
|
||||||
|
expect(getEmbedLink(unsupportedMapsUrl)).toBe(null);
|
||||||
|
|
||||||
|
const malformedMapsUrl = `https://www.google.com/maps/@${"0,0,".repeat(
|
||||||
|
1000,
|
||||||
|
)}`;
|
||||||
|
expect(embeddableURLValidator(malformedMapsUrl, undefined)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1840,8 +1840,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
src={
|
src={
|
||||||
src?.type !== "document" ? src?.link ?? "" : undefined
|
src?.type !== "document" ? src?.link ?? "" : undefined
|
||||||
}
|
}
|
||||||
// https://stackoverflow.com/q/18470015
|
|
||||||
scrolling="no"
|
|
||||||
referrerPolicy="no-referrer-when-downgrade"
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
title="Excalidraw Embedded Content"
|
title="Excalidraw Embedded Content"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
|||||||
Reference in New Issue
Block a user