enchancements
This commit is contained in:
parent
aa3a30b6ad
commit
e610d4e551
|
|
@ -15,17 +15,22 @@ const ROOT = process.env.OPENPENCIL_WEB_ROOT ?? join(import.meta.dir, 'dist')
|
|||
const PORT = Number(process.env.OPENPENCIL_WEB_PORT ?? 3100)
|
||||
const HOST = process.env.OPENPENCIL_WEB_HOST ?? '0.0.0.0'
|
||||
|
||||
// W4C fork delta: same-origin proxy for the CORS-hostile online-font catalog
|
||||
// (https://fonts.google.com/metadata/fonts). The browser app (which is a W4C
|
||||
// fork) rewrites those requests to /__font-proxy?url=... so they stay
|
||||
// same-origin; we forward them upstream here. Mirrors vite/font-proxy.ts.
|
||||
// W4C fork delta: same-origin proxy for CORS-hostile online-font endpoints
|
||||
// (https://fonts.google.com/metadata/fonts and https://fonts.googleapis.com/css2).
|
||||
// The browser app (which is a W4C fork) rewrites those requests to
|
||||
// /__font-proxy?url=... so they stay same-origin; we forward them upstream here.
|
||||
// Mirrors vite/font-proxy.ts.
|
||||
const FONT_PROXY_PREFIX = '/__font-proxy'
|
||||
const PROXIED_FONT_HOSTS = ['fonts.google.com']
|
||||
const PROXIED_FONT_HOSTS = ['fonts.google.com', 'fonts.googleapis.com']
|
||||
const FONT_PROXY_USER_AGENT =
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36'
|
||||
|
||||
async function handleFontProxy(req, res, hostHeader) {
|
||||
const target = new URL(req.url ?? '/', `http://${hostHeader}`).searchParams.get('url')
|
||||
const url = new URL(req.url ?? '/', `http://${hostHeader}`)
|
||||
const target = url.searchParams.get('url')
|
||||
// The browser forwards the original `user-agent` here because it cannot send
|
||||
// one cross-origin; unifont uses it to pick the glyph format (ttf/otf/woff2).
|
||||
const ua = url.searchParams.get('ua')
|
||||
if (!target) {
|
||||
res.writeHead(400)
|
||||
res.end('missing url')
|
||||
|
|
@ -47,7 +52,7 @@ async function handleFontProxy(req, res, hostHeader) {
|
|||
try {
|
||||
const upstream = await fetch(targetUrl, {
|
||||
headers: {
|
||||
'user-agent': FONT_PROXY_USER_AGENT,
|
||||
'user-agent': ua ?? FONT_PROXY_USER_AGENT,
|
||||
accept: 'application/json, text/plain, */*'
|
||||
},
|
||||
redirect: 'follow'
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
import { renderNodesToImage } from '@open-pencil/core/io/formats/raster'
|
||||
import type { SceneGraph } from '@open-pencil/scene-graph'
|
||||
|
||||
import { isCrossOriginSubFrame } from '@/app/document/io/browser'
|
||||
import type { ExportOptions } from '@/app/document/export/types'
|
||||
import { isTauri } from '@/app/tauri/env'
|
||||
|
||||
|
|
@ -158,7 +159,7 @@ export async function saveExportedFile(
|
|||
return
|
||||
}
|
||||
|
||||
if (window.showSaveFilePicker) {
|
||||
if (window.showSaveFilePicker && !isCrossOriginSubFrame()) {
|
||||
try {
|
||||
const handle = await window.showSaveFilePicker({
|
||||
suggestedName: fileName,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,24 @@ export function createDocumentViewportActions(editor: ViewportEditor, viewportSi
|
|||
return { setViewportSize, fitCurrentPageToViewport }
|
||||
}
|
||||
|
||||
// The File System Access pickers (showSaveFilePicker/showOpenFilePicker) and
|
||||
// window.prompt are blocked by browsers inside cross-origin sub-frames
|
||||
// (Chrome: "Cross origin sub frames aren't allowed to show a file picker").
|
||||
// Detect that context so callers can fall back to a plain download / file
|
||||
// input instead of surfacing the picker error.
|
||||
export function isCrossOriginSubFrame(): boolean {
|
||||
const top = window.top
|
||||
if (!top || top === window) return false
|
||||
try {
|
||||
// Same-origin sub-frame: the top window is reachable and pickers are allowed.
|
||||
void top.location.href
|
||||
return false
|
||||
} catch {
|
||||
// Cross-origin sub-frame: top navigation is blocked, pickers are not allowed.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadBlob(data: Uint8Array, filename: string, mime: string) {
|
||||
const blob = new Blob([data.buffer as ArrayBuffer], { type: mime })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { EditorState } from '@open-pencil/core/editor'
|
||||
import { dialogMessages } from '@open-pencil/vue'
|
||||
|
||||
import { downloadBlob } from '@/app/document/io/browser'
|
||||
import { downloadBlob, isCrossOriginSubFrame } from '@/app/document/io/browser'
|
||||
import { documentNameFromFigPath } from '@/app/document/io/names'
|
||||
import { chooseBrowserFigSaveHandle, chooseTauriFigSavePath } from '@/app/document/io/save-targets'
|
||||
import type { DocumentSourceAccess } from '@/app/document/io/types'
|
||||
|
|
@ -84,7 +84,9 @@ export function createSaveActions({
|
|||
return
|
||||
}
|
||||
|
||||
if (window.showSaveFilePicker) {
|
||||
const crossOrigin = isCrossOriginSubFrame()
|
||||
|
||||
if (!crossOrigin && window.showSaveFilePicker) {
|
||||
const handle = await chooseBrowserFigSaveHandle()
|
||||
if (!handle) return
|
||||
setStorageBinding(null)
|
||||
|
|
@ -96,7 +98,12 @@ export function createSaveActions({
|
|||
return
|
||||
}
|
||||
|
||||
const filename = prompt(dialogMessages.get().saveAsPrompt, getDownloadName() ?? 'Untitled.fig')
|
||||
// Browsers without a usable File System Access picker fall back to a name
|
||||
// prompt + download. In a cross-origin sub-frame window.prompt is also
|
||||
// blocked, so download with the current/default name instead.
|
||||
const filename = crossOrigin
|
||||
? getDownloadName() ?? 'Untitled.fig'
|
||||
: prompt(dialogMessages.get().saveAsPrompt, getDownloadName() ?? 'Untitled.fig')
|
||||
if (!filename) return
|
||||
setStorageBinding(null)
|
||||
setDownloadName(filename)
|
||||
|
|
|
|||
|
|
@ -71,17 +71,21 @@ function configureTauriFontCache() {
|
|||
fontManager.setHostFontLoader(loadSystemFont)
|
||||
}
|
||||
|
||||
// W4C fork delta: relay CORS-hostile online-font catalog requests through a
|
||||
// same-origin endpoint so the browser app can build the Google Fonts catalog.
|
||||
// unifont's `google` provider reads `https://fonts.google.com/metadata/fonts`,
|
||||
// which never sends `Access-Control-Allow-Origin` and sets `cross-origin-resource-policy:
|
||||
// same-site`, so it cannot be read from any non-Google origin. The browser here
|
||||
// rewrites those to `/__font-proxy?url=...` (served by a Vite dev plugin and by
|
||||
// docker/static-server.mjs), which keeps the request same-origin while the server
|
||||
// forwards it upstream. Glyph files (fonts.gstatic.com) already send CORS headers
|
||||
// and are fetched directly.
|
||||
// W4C fork delta: relay CORS-hostile online-font requests through a same-origin
|
||||
// endpoint so the browser app can build the Google Fonts catalog and resolve
|
||||
// font faces. Two Google hosts are CORS-hostile from the browser:
|
||||
// - `https://fonts.google.com/metadata/fonts` — never sends
|
||||
// `Access-Control-Allow-Origin` and sets `cross-origin-resource-policy:
|
||||
// same-site`, so it cannot be read from any non-Google origin.
|
||||
// - `https://fonts.googleapis.com/css2` — its OPTIONS preflight response
|
||||
// carries no CORS headers, so some browsers (notably Firefox) block the
|
||||
// font-discovery fetch outright.
|
||||
// The browser here rewrites both to `/__font-proxy?url=...` (served by the Vite
|
||||
// dev plugin and by docker/static-server.mjs), which keeps the request
|
||||
// same-origin while the server forwards it upstream. Glyph files
|
||||
// (fonts.gstatic.com) already send CORS headers and are fetched directly.
|
||||
const FONT_PROXY_PREFIX = '/__font-proxy'
|
||||
const PROXIED_FONT_HOSTS = new Set(['fonts.google.com'])
|
||||
const PROXIED_FONT_HOSTS = new Set(['fonts.google.com', 'fonts.googleapis.com'])
|
||||
|
||||
// Capture the genuine native fetch once, at module load. `withFetchProxy` in
|
||||
// the core font resolver temporarily swaps `globalThis.fetch` for a wrapper
|
||||
|
|
@ -100,7 +104,20 @@ function createBrowserFontFetch(): (url: string, init?: RequestInit) => Promise<
|
|||
console.warn(`[fonts] invalid URL skipped for font proxy: ${url}`)
|
||||
}
|
||||
if (PROXIED_FONT_HOSTS.has(host)) {
|
||||
return nativeFetch(`${FONT_PROXY_PREFIX}?url=${encodeURIComponent(url)}`, init)
|
||||
// Browsers forbid sending a `user-agent` request header, but the font
|
||||
// provider (unifont) uses it to choose the glyph format (ttf/otf/woff2).
|
||||
// Surface it as a query param so the proxy can forward the original UA and
|
||||
// the provider still gets the format it asked for (CanvasKit is tested on
|
||||
// ttf/otf). The static/vite proxy reads it back and falls back to a default.
|
||||
let ua = ''
|
||||
try {
|
||||
ua = new Headers(init?.headers).get('user-agent') ?? ''
|
||||
} catch {
|
||||
// Invalid/omitted headers — no UA to forward.
|
||||
console.warn(`[fonts] could not read user-agent for font proxy: ${url}`)
|
||||
}
|
||||
const uaQuery = ua ? `&ua=${encodeURIComponent(ua)}` : ''
|
||||
return nativeFetch(`${FONT_PROXY_PREFIX}?url=${encodeURIComponent(url)}${uaQuery}`, init)
|
||||
}
|
||||
return nativeFetch(url, init)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useFileDialog } from '@vueuse/core'
|
||||
|
||||
import { setOpenPencilOpenFileHandler } from '@/app/browser-bridge'
|
||||
import { resolveBrowserFileURL } from '@/app/document/io/browser'
|
||||
import { resolveBrowserFileURL, isCrossOriginSubFrame } from '@/app/document/io/browser'
|
||||
import { notificationMessages } from '@/app/i18n/notifications'
|
||||
import { toast } from '@/app/shell/ui'
|
||||
import { openFileInNewTab } from '@/app/tabs'
|
||||
|
|
@ -80,7 +80,7 @@ export async function openFileDialog() {
|
|||
return
|
||||
}
|
||||
|
||||
if (window.showOpenFilePicker) {
|
||||
if (window.showOpenFilePicker && !isCrossOriginSubFrame()) {
|
||||
try {
|
||||
const handles = await window.showOpenFilePicker({
|
||||
multiple: true,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,10 @@ onMounted(async () => {
|
|||
// unsaved work comes back on every open without an extra prompt.
|
||||
const autoRestore = route.query.recover === 'auto'
|
||||
if (autoRestore && snapshots.value.length > 0) {
|
||||
for (const snapshot of [...snapshots.value]) {
|
||||
// restore() reassigns snapshots.value, so iterate the array as it was
|
||||
// when listed — the original reference is not mutated.
|
||||
const pending = snapshots.value
|
||||
for (const snapshot of pending) {
|
||||
await restore(snapshot)
|
||||
}
|
||||
return
|
||||
|
|
|
|||
|
|
@ -35,6 +35,17 @@ async function installGoogleFontsMock(page: Page, families = ['Inter', 'OpenPenc
|
|||
if (typeof input === 'string') url = input
|
||||
else if (input instanceof URL) url = input.href
|
||||
else url = input.url
|
||||
// css2 font discovery is relayed through the same-origin /__font-proxy in
|
||||
// the browser fork. Decode the upstream target so the mock stays hermetic;
|
||||
// deliberately leave the metadata proxy untouched so the fetch counter
|
||||
// below keeps meaning "the google catalog was loaded from a mock".
|
||||
if (url.startsWith('/__font-proxy?url=')) {
|
||||
const target = new URL(url, 'http://local').searchParams.get('url')
|
||||
if (target) {
|
||||
const decoded = new URL(decodeURIComponent(target), 'http://local').href
|
||||
if (decoded.startsWith('https://fonts.googleapis.com/css2')) url = decoded
|
||||
}
|
||||
}
|
||||
if (url.startsWith('https://fonts.openpencil.test/')) {
|
||||
win.__googleFontPreviewFetchCount = (win.__googleFontPreviewFetchCount ?? 0) + 1
|
||||
return new Response(new ArrayBuffer(8), { status: 200 })
|
||||
|
|
|
|||
|
|
@ -1,24 +1,33 @@
|
|||
import type { Plugin } from 'vite'
|
||||
|
||||
// Dev-only same-origin proxy for CORS-hostile online-font catalog endpoints.
|
||||
// Dev-only same-origin proxy for CORS-hostile online-font endpoints.
|
||||
//
|
||||
// unifont's `google` provider reads `https://fonts.google.com/metadata/fonts` to
|
||||
// build its family catalog. That endpoint never sends `Access-Control-Allow-Origin`
|
||||
// build its family catalog and `https://fonts.googleapis.com/css2` to resolve
|
||||
// font faces. The metadata endpoint never sends `Access-Control-Allow-Origin`
|
||||
// and sets `cross-origin-resource-policy: same-site`, so a browser app served on
|
||||
// its own origin (any origin that is not Google) cannot read it directly — the
|
||||
// provider fails to initialize with "Could not initialize provider `google`".
|
||||
// The css2 endpoint's OPTIONS preflight carries no CORS headers either, so some
|
||||
// browsers (notably Firefox) block the font-discovery fetch.
|
||||
//
|
||||
// The browser relays such requests here instead (see `createFontProxyFetch()` in
|
||||
// The browser relays such requests here instead (see `createBrowserFontFetch()` in
|
||||
// src/app/editor/fonts), which keeps the request same-origin and forwards it
|
||||
// server-side to Google. This is a W4C fork delta; upstream openpencil does not
|
||||
// ship it because Tauri/goes through the Rust command that is not CORS-bound.
|
||||
// ship it because Tauri goes through the Rust command that is not CORS-bound.
|
||||
|
||||
export const FONT_PROXY_PREFIX = '/__font-proxy'
|
||||
|
||||
// Only these hosts are CORS-hostile from the browser; everything else
|
||||
// (notably fonts.gstatic.com glyph files) already sends CORS headers and is
|
||||
// fetched directly by the browser.
|
||||
const PROXIED_HOSTS = new Set(['fonts.google.com'])
|
||||
// - fonts.google.com — metadata never sends CORS headers.
|
||||
// - fonts.googleapis.com — css2 font-discovery OPTIONS preflight returns no
|
||||
// CORS headers, so Firefox blocks the discovery fetch unless it is proxied.
|
||||
const PROXIED_HOSTS = new Set(['fonts.google.com', 'fonts.googleapis.com'])
|
||||
|
||||
const FONT_PROXY_USER_AGENT =
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36'
|
||||
|
||||
export function createDevFontProxyPlugin(): Plugin {
|
||||
return {
|
||||
|
|
@ -43,6 +52,9 @@ export function createDevFontProxyPlugin(): Plugin {
|
|||
res.end('missing url')
|
||||
return
|
||||
}
|
||||
// The browser forwards the original `user-agent` here because it cannot
|
||||
// send one cross-origin; unifont uses it to pick the glyph format.
|
||||
const ua = new URL(req.url ?? '/', 'http://dev').searchParams.get('ua')
|
||||
|
||||
let targetURL: URL
|
||||
try {
|
||||
|
|
@ -61,8 +73,7 @@ export function createDevFontProxyPlugin(): Plugin {
|
|||
try {
|
||||
const upstream = await fetch(targetURL, {
|
||||
headers: {
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36',
|
||||
'user-agent': ua ?? FONT_PROXY_USER_AGENT,
|
||||
accept: 'application/json, text/plain, */*',
|
||||
},
|
||||
redirect: 'follow',
|
||||
|
|
|
|||
Loading…
Reference in a new issue