diff --git a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts index a0c161312fbf0..3da845d011a61 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -14,7 +14,6 @@ export class WebviewProtocolProvider extends Disposable { private static validWebviewFilePaths = new Map([ ['/index.html', 'index.html'], ['/fake.html', 'fake.html'], - ['/main.js', 'main.js'], ['/service-worker.js', 'service-worker.js'], ]); diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 6885c2488cdc8..625c46f31153c 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -12,7 +12,1136 @@ - + diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 3f2ade8478f25..457447af38e54 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -4,7 +4,8 @@ - + - + diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js deleted file mode 100644 index 74384c1550f4a..0000000000000 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ /dev/null @@ -1,1133 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check - -/// - -const isSafari = ( - navigator.vendor && navigator.vendor.indexOf('Apple') > -1 && - navigator.userAgent && - navigator.userAgent.indexOf('CriOS') === -1 && - navigator.userAgent.indexOf('FxiOS') === -1 -); - -const isFirefox = ( - navigator.userAgent && - navigator.userAgent.indexOf('Firefox') >= 0 -); - -const searchParams = new URL(location.toString()).searchParams; -const ID = searchParams.get('id'); -const webviewOrigin = searchParams.get('origin'); -const onElectron = searchParams.get('platform') === 'electron'; -const expectedWorkerVersion = parseInt(searchParams.get('swVersion')); - -/** - * Use polling to track focus of main webview and iframes within the webview - * - * @param {Object} handlers - * @param {() => void} handlers.onFocus - * @param {() => void} handlers.onBlur - */ -const trackFocus = ({ onFocus, onBlur }) => { - const interval = 250; - let isFocused = document.hasFocus(); - setInterval(() => { - const isCurrentlyFocused = document.hasFocus(); - if (isCurrentlyFocused === isFocused) { - return; - } - isFocused = isCurrentlyFocused; - if (isCurrentlyFocused) { - onFocus(); - } else { - onBlur(); - } - }, interval); -}; - -const getActiveFrame = () => { - return /** @type {HTMLIFrameElement | undefined} */ (document.getElementById('active-frame')); -}; - -const getPendingFrame = () => { - return /** @type {HTMLIFrameElement | undefined} */ (document.getElementById('pending-frame')); -}; - -/** - * @template T - * @param {T | undefined | null} obj - * @return {T} - */ -function assertIsDefined(obj) { - if (typeof obj === 'undefined' || obj === null) { - throw new Error('Found unexpected null'); - } - return obj; -} - -const vscodePostMessageFuncName = '__vscode_post_message__'; - -const defaultStyles = document.createElement('style'); -defaultStyles.id = '_defaultStyles'; -defaultStyles.textContent = ` - html { - scrollbar-color: var(--vscode-scrollbarSlider-background) var(--vscode-editor-background); - } - - body { - background-color: transparent; - color: var(--vscode-editor-foreground); - font-family: var(--vscode-font-family); - font-weight: var(--vscode-font-weight); - font-size: var(--vscode-font-size); - margin: 0; - padding: 0 20px; - } - - img { - max-width: 100%; - max-height: 100%; - } - - a, a code { - color: var(--vscode-textLink-foreground); - } - - a:hover { - color: var(--vscode-textLink-activeForeground); - } - - a:focus, - input:focus, - select:focus, - textarea:focus { - outline: 1px solid -webkit-focus-ring-color; - outline-offset: -1px; - } - - code { - color: var(--vscode-textPreformat-foreground); - } - - blockquote { - background: var(--vscode-textBlockQuote-background); - border-color: var(--vscode-textBlockQuote-border); - } - - kbd { - color: var(--vscode-editor-foreground); - border-radius: 3px; - vertical-align: middle; - padding: 1px 3px; - - background-color: hsla(0,0%,50%,.17); - border: 1px solid rgba(71,71,71,.4); - border-bottom-color: rgba(88,88,88,.4); - box-shadow: inset 0 -1px 0 rgba(88,88,88,.4); - } - .vscode-light kbd { - background-color: hsla(0,0%,87%,.5); - border: 1px solid hsla(0,0%,80%,.7); - border-bottom-color: hsla(0,0%,73%,.7); - box-shadow: inset 0 -1px 0 hsla(0,0%,73%,.7); - } - - ::-webkit-scrollbar { - width: 10px; - height: 10px; - } - - ::-webkit-scrollbar-corner { - background-color: var(--vscode-editor-background); - } - - ::-webkit-scrollbar-thumb { - background-color: var(--vscode-scrollbarSlider-background); - } - ::-webkit-scrollbar-thumb:hover { - background-color: var(--vscode-scrollbarSlider-hoverBackground); - } - ::-webkit-scrollbar-thumb:active { - background-color: var(--vscode-scrollbarSlider-activeBackground); - } - ::highlight(find-highlight) { - background-color: var(--vscode-editor-findMatchHighlightBackground); - } - ::highlight(current-find-highlight) { - background-color: var(--vscode-editor-findMatchBackground); - }`; - -/** - * @param {boolean} allowMultipleAPIAcquire - * @param {*} [state] - * @return {string} - */ -function getVsCodeApiScript(allowMultipleAPIAcquire, state) { - const encodedState = state ? encodeURIComponent(state) : undefined; - return /* js */` - globalThis.acquireVsCodeApi = (function() { - const originalPostMessage = window.parent['${vscodePostMessageFuncName}'].bind(window.parent); - const doPostMessage = (channel, data, transfer) => { - originalPostMessage(channel, data, transfer); - }; - - let acquired = false; - - let state = ${state ? `JSON.parse(decodeURIComponent("${encodedState}"))` : undefined}; - - return () => { - if (acquired && !${allowMultipleAPIAcquire}) { - throw new Error('An instance of the VS Code API has already been acquired'); - } - acquired = true; - return Object.freeze({ - postMessage: function(message, transfer) { - doPostMessage('onmessage', { message, transfer }, transfer); - }, - setState: function(newState) { - state = newState; - doPostMessage('do-update-state', JSON.stringify(newState)); - return newState; - }, - getState: function() { - return state; - } - }); - }; - })(); - delete window.parent; - delete window.top; - delete window.frameElement; - `; -} - -/** @type {Promise} */ -const workerReady = new Promise((resolve, reject) => { - if (!areServiceWorkersEnabled()) { - return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.')); - } - - const swPath = `service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`; - navigator.serviceWorker.register(swPath) - .then(() => navigator.serviceWorker.ready) - .then(async registration => { - /** - * @param {MessageEvent} event - */ - const versionHandler = async (event) => { - if (event.data.channel !== 'version') { - return; - } - - navigator.serviceWorker.removeEventListener('message', versionHandler); - if (event.data.version === expectedWorkerVersion) { - return resolve(); - } else { - console.log(`Found unexpected service worker version. Found: ${event.data.version}. Expected: ${expectedWorkerVersion}`); - console.log(`Attempting to reload service worker`); - - // If we have the wrong version, try once (and only once) to unregister and re-register - // Note that `.update` doesn't seem to work desktop electron at the moment so we use - // `unregister` and `register` here. - return registration.unregister() - .then(() => navigator.serviceWorker.register(swPath)) - .then(() => navigator.serviceWorker.ready) - .finally(() => { resolve(); }); - } - }; - navigator.serviceWorker.addEventListener('message', versionHandler); - - const postVersionMessage = (/** @type {ServiceWorker} */ controller) => { - controller.postMessage({ channel: 'version' }); - }; - - // At this point, either the service worker is ready and - // became our controller, or we need to wait for it. - // Note that navigator.serviceWorker.controller could be a - // controller from a previously loaded service worker. - const currentController = navigator.serviceWorker.controller; - if (currentController?.scriptURL.endsWith(swPath)) { - // service worker already loaded & ready to receive messages - postVersionMessage(currentController); - } else { - // either there's no controlling service worker, or it's an old one: - // wait for it to change before posting the message - const onControllerChange = () => { - navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange); - postVersionMessage(navigator.serviceWorker.controller); - }; - navigator.serviceWorker.addEventListener('controllerchange', onControllerChange); - } - }).catch(error => { - reject(new Error(`Could not register service workers: ${error}.`)); - }); -}); - -const hostMessaging = new class HostMessaging { - - constructor() { - this.channel = new MessageChannel(); - - /** @type {Map void>>} */ - this.handlers = new Map(); - - this.channel.port1.onmessage = (e) => { - const channel = e.data.channel; - const handlers = this.handlers.get(channel); - if (handlers) { - for (const handler of handlers) { - handler(e, e.data.args); - } - } else { - console.log('no handler for ', e); - } - }; - } - - /** - * @param {string} channel - * @param {any} data - * @param {any} [transfer] - */ - postMessage(channel, data, transfer) { - this.channel.port1.postMessage({ channel, data }, transfer); - } - - /** - * @param {string} channel - * @param {(event: MessageEvent, data: any) => void} handler - */ - onMessage(channel, handler) { - let handlers = this.handlers.get(channel); - if (!handlers) { - handlers = []; - this.handlers.set(channel, handlers); - } - handlers.push(handler); - } - - async signalReady() { - const start = (/** @type {string} */ parentOrigin) => { - window.parent.postMessage({ target: ID, channel: 'webview-ready', data: {} }, parentOrigin, [this.channel.port2]); - }; - - const parentOrigin = searchParams.get('parentOrigin'); - - const hostname = location.hostname; - - if (!crypto.subtle) { - // cannot validate, not running in a secure context - throw new Error(`Cannot validate in current context!`); - } - - // Here the `parentOriginHash()` function from `src/vs/workbench/common/webview.ts` is inlined - // compute a sha-256 composed of `parentOrigin` and `salt` converted to base 32 - let parentOriginHash; - try { - const strData = JSON.stringify({ parentOrigin, salt: webviewOrigin }); - const encoder = new TextEncoder(); - const arrData = encoder.encode(strData); - const hash = await crypto.subtle.digest('sha-256', arrData); - const hashArray = Array.from(new Uint8Array(hash)); - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - // sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32 - parentOriginHash = BigInt(`0x${hashHex}`).toString(32).padStart(52, '0'); - } catch (err) { - throw err instanceof Error ? err : new Error(String(err)); - } - - if (hostname === parentOriginHash || hostname.startsWith(parentOriginHash + '.')) { - // validation succeeded! - return start(parentOrigin); - } - - throw new Error(`Expected '${parentOriginHash}' as hostname or subdomain!`); - } -}(); - -const unloadMonitor = new class { - - constructor() { - this.confirmBeforeClose = 'keyboardOnly'; - this.isModifierKeyDown = false; - - hostMessaging.onMessage('set-confirm-before-close', (_e, /** @type {string} */ data) => { - this.confirmBeforeClose = data; - }); - - hostMessaging.onMessage('content', (_e, /** @type {any} */ data) => { - this.confirmBeforeClose = data.confirmBeforeClose; - }); - - window.addEventListener('beforeunload', (event) => { - if (onElectron) { - return; - } - - switch (this.confirmBeforeClose) { - case 'always': { - event.preventDefault(); - event.returnValue = ''; - return ''; - } - case 'never': { - break; - } - case 'keyboardOnly': - default: { - if (this.isModifierKeyDown) { - event.preventDefault(); - event.returnValue = ''; - return ''; - } - break; - } - } - }); - } - - onIframeLoaded(/** @type {HTMLIFrameElement} */ frame) { - frame.contentWindow.addEventListener('keydown', e => { - this.isModifierKeyDown = e.metaKey || e.ctrlKey || e.altKey; - }); - - frame.contentWindow.addEventListener('keyup', () => { - this.isModifierKeyDown = false; - }); - } -}; - -// state -let firstLoad = true; -/** @type {any} */ -let loadTimeout; -let styleVersion = 0; - -/** @type {Array<{ readonly message: any, transfer?: ArrayBuffer[] }>} */ -let pendingMessages = []; - -const initData = { - /** @type {number | undefined} */ - initialScrollProgress: undefined, - - /** @type {{ [key: string]: string } | undefined} */ - styles: undefined, - - /** @type {string | undefined} */ - activeTheme: undefined, - - /** @type {string | undefined} */ - themeName: undefined, - - /** @type {boolean} */ - screenReader: false, - - /** @type {boolean} */ - reduceMotion: false, -}; - -hostMessaging.onMessage('did-load-resource', (_event, data) => { - navigator.serviceWorker.ready.then(registration => { - assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); - }); -}); - -hostMessaging.onMessage('did-load-localhost', (_event, data) => { - navigator.serviceWorker.ready.then(registration => { - assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data }); - }); -}); - -navigator.serviceWorker.addEventListener('message', event => { - switch (event.data.channel) { - case 'load-resource': - case 'load-localhost': - hostMessaging.postMessage(event.data.channel, event.data); - return; - } -}); -/** - * @param {HTMLDocument?} document - * @param {HTMLElement?} body - */ -const applyStyles = (document, body) => { - if (!document) { - return; - } - - if (body) { - body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast', 'vscode-reduce-motion', 'vscode-using-screen-reader'); - if (initData.activeTheme) { - body.classList.add(initData.activeTheme); - } - - if (initData.reduceMotion) { - body.classList.add('vscode-reduce-motion'); - } - - if (initData.screenReader) { - body.classList.add('vscode-using-screen-reader'); - } - - body.dataset.vscodeThemeKind = initData.activeTheme; - body.dataset.vscodeThemeName = initData.themeName || ''; - } - - if (initData.styles) { - const documentStyle = document.documentElement.style; - - // Remove stale properties - for (let i = documentStyle.length - 1; i >= 0; i--) { - const property = documentStyle[i]; - - // Don't remove properties that the webview might have added separately - if (property && property.startsWith('--vscode-')) { - documentStyle.removeProperty(property); - } - } - - // Re-add new properties - for (const variable of Object.keys(initData.styles)) { - documentStyle.setProperty(`--${variable}`, initData.styles[variable]); - } - } -}; - -/** - * @param {MouseEvent} event - */ -const handleInnerClick = (event) => { - if (!event?.view?.document) { - return; - } - - const baseElement = event.view.document.querySelector('base'); - - for (const pathElement of event.composedPath()) { - /** @type {any} */ - const node = pathElement; - if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { - if (node.getAttribute('href') === '#') { - event.view.scrollTo(0, 0); - } else if (node.hash && (node.getAttribute('href') === node.hash || (baseElement && node.href === baseElement.href + node.hash))) { - const fragment = node.hash.slice(1); - const scrollTarget = event.view.document.getElementById(fragment) ?? event.view.document.getElementById(decodeURIComponent(fragment)); - scrollTarget?.scrollIntoView(); - } else { - hostMessaging.postMessage('did-click-link', node.href.baseVal || node.href); - } - event.preventDefault(); - return; - } - } -}; - -/** - * @param {MouseEvent} event - */ -const handleAuxClick = (event) => { - // Prevent middle clicks opening a broken link in the browser - if (!event?.view?.document) { - return; - } - - if (event.button === 1) { - for (const pathElement of event.composedPath()) { - /** @type {any} */ - const node = pathElement; - if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { - event.preventDefault(); - return; - } - } - } -}; - -/** - * @param {KeyboardEvent} e - */ -const handleInnerKeydown = (e) => { - // If the keypress would trigger a browser event, such as copy or paste, - // make sure we block the browser from dispatching it. Instead VS Code - // handles these events and will dispatch a copy/paste back to the webview - // if needed - if (isUndoRedo(e) || isPrint(e) || isFindEvent(e)) { - e.preventDefault(); - } else if (isCopyPasteOrCut(e)) { - if (onElectron) { - e.preventDefault(); - } else { - return; // let the browser handle this - } - } - - hostMessaging.postMessage('did-keydown', { - key: e.key, - keyCode: e.keyCode, - code: e.code, - shiftKey: e.shiftKey, - altKey: e.altKey, - ctrlKey: e.ctrlKey, - metaKey: e.metaKey, - repeat: e.repeat - }); -}; -/** - * @param {KeyboardEvent} e - */ -const handleInnerUp = (e) => { - hostMessaging.postMessage('did-keyup', { - key: e.key, - keyCode: e.keyCode, - code: e.code, - shiftKey: e.shiftKey, - altKey: e.altKey, - ctrlKey: e.ctrlKey, - metaKey: e.metaKey, - repeat: e.repeat - }); -}; - -/** - * @param {KeyboardEvent} e - * @return {boolean} - */ -function isCopyPasteOrCut(e) { - const hasMeta = e.ctrlKey || e.metaKey; - const shiftInsert = e.shiftKey && e.key.toLowerCase() === 'insert'; - return (hasMeta && ['c', 'v', 'x'].includes(e.key.toLowerCase())) || shiftInsert; -} - -/** - * @param {KeyboardEvent} e - * @return {boolean} - */ -function isUndoRedo(e) { - const hasMeta = e.ctrlKey || e.metaKey; - return hasMeta && ['z', 'y'].includes(e.key.toLowerCase()); -} - -/** - * @param {KeyboardEvent} e - * @return {boolean} - */ -function isPrint(e) { - const hasMeta = e.ctrlKey || e.metaKey; - return hasMeta && e.key.toLowerCase() === 'p'; -} - -/** - * @param {KeyboardEvent} e - * @return {boolean} - */ -function isFindEvent(e) { - const hasMeta = e.ctrlKey || e.metaKey; - return hasMeta && e.key.toLowerCase() === 'f'; -} - -let isHandlingScroll = false; - -/** - * @param {WheelEvent} event - */ -const handleWheel = (event) => { - if (isHandlingScroll) { - return; - } - - hostMessaging.postMessage('did-scroll-wheel', { - deltaMode: event.deltaMode, - deltaX: event.deltaX, - deltaY: event.deltaY, - deltaZ: event.deltaZ, - detail: event.detail, - type: event.type - }); -}; - -/** - * @param {Event} event - */ -const handleInnerScroll = (event) => { - if (isHandlingScroll) { - return; - } - - const target = /** @type {HTMLDocument | null} */ (event.target); - const currentTarget = /** @type {Window | null} */ (event.currentTarget); - if (!currentTarget || !target?.body) { - return; - } - - const progress = currentTarget.scrollY / target.body.clientHeight; - if (isNaN(progress)) { - return; - } - - isHandlingScroll = true; - window.requestAnimationFrame(() => { - try { - hostMessaging.postMessage('did-scroll', progress); - } catch (e) { - // noop - } - isHandlingScroll = false; - }); -}; - -function handleInnerDragStartEvent(/** @type {DragEvent} */ e) { - if (e.defaultPrevented) { - // Extension code has already handled this event - return; - } - - if (!e.dataTransfer || e.shiftKey) { - return; - } - - // Only handle drags from outside editor for now - if (e.dataTransfer.items.length && Array.prototype.every.call(e.dataTransfer.items, item => item.kind === 'file')) { - hostMessaging.postMessage('drag-start'); - } -} - -/** - * @param {() => void} callback - */ -function onDomReady(callback) { - if (document.readyState === 'interactive' || document.readyState === 'complete') { - callback(); - } else { - document.addEventListener('DOMContentLoaded', callback); - } -} - -function areServiceWorkersEnabled() { - try { - return !!navigator.serviceWorker; - } catch (e) { - return false; - } -} - -/** - * @typedef {{ - * contents: string; - * options: { - * readonly allowScripts: boolean; - * readonly allowForms: boolean; - * readonly allowMultipleAPIAcquire: boolean; - * } - * state: any; - * cspSource: string; - * }} ContentUpdateData - */ - -/** - * @param {ContentUpdateData} data - * @return {string} - */ -function toContentHtml(data) { - const options = data.options; - const text = data.contents; - const newDocument = new DOMParser().parseFromString(text, 'text/html'); - - newDocument.querySelectorAll('a').forEach(a => { - if (!a.title) { - const href = a.getAttribute('href'); - if (typeof href === 'string') { - a.title = href; - } - } - }); - - // Set default aria role - if (!newDocument.body.hasAttribute('role')) { - newDocument.body.setAttribute('role', 'document'); - } - - // Inject default script - if (options.allowScripts) { - const defaultScript = newDocument.createElement('script'); - defaultScript.id = '_vscodeApiScript'; - defaultScript.textContent = getVsCodeApiScript(options.allowMultipleAPIAcquire, data.state); - newDocument.head.prepend(defaultScript); - } - - // Inject default styles - newDocument.head.prepend(defaultStyles.cloneNode(true)); - - applyStyles(newDocument, newDocument.body); - - // Strip out unsupported http-equiv tags - for (const metaElement of Array.from(newDocument.querySelectorAll('meta'))) { - const httpEquiv = metaElement.getAttribute('http-equiv'); - if (httpEquiv && !/^(content-security-policy|default-style|content-type)$/i.test(httpEquiv)) { - console.warn(`Removing unsupported meta http-equiv: ${httpEquiv}`); - metaElement.remove(); - } - } - - // Check for CSP - const csp = newDocument.querySelector('meta[http-equiv="Content-Security-Policy"]'); - if (!csp) { - hostMessaging.postMessage('no-csp-found'); - } else { - try { - // Attempt to rewrite CSPs that hardcode old-style resource endpoint - const cspContent = csp.getAttribute('content'); - if (cspContent) { - const newCsp = cspContent.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, data.cspSource); - csp.setAttribute('content', newCsp); - } - } catch (e) { - console.error(`Could not rewrite csp: ${e}`); - } - } - - // set DOCTYPE for newDocument explicitly as DOMParser.parseFromString strips it off - // and DOCTYPE is needed in the iframe to ensure that the user agent stylesheet is correctly overridden - return '\n' + newDocument.documentElement.outerHTML; -} - -onDomReady(() => { - if (!document.body) { - return; - } - - hostMessaging.onMessage('styles', (_event, data) => { - ++styleVersion; - - initData.styles = data.styles; - initData.activeTheme = data.activeTheme; - initData.themeName = data.themeName; - initData.reduceMotion = data.reduceMotion; - initData.screenReader = data.screenReader; - - const target = getActiveFrame(); - if (!target) { - return; - } - - if (target.contentDocument) { - applyStyles(target.contentDocument, target.contentDocument.body); - } - }); - - // propagate focus - hostMessaging.onMessage('focus', () => { - const activeFrame = getActiveFrame(); - if (!activeFrame || !activeFrame.contentWindow) { - // Focus the top level webview instead - window.focus(); - return; - } - - if (document.activeElement === activeFrame) { - // We are already focused on the iframe (or one of its children) so no need - // to refocus. - return; - } - - activeFrame.contentWindow.focus(); - }); - - // update iframe-contents - let updateId = 0; - hostMessaging.onMessage('content', async (_event, /** @type {ContentUpdateData} */ data) => { - const currentUpdateId = ++updateId; - try { - await workerReady; - } catch (e) { - console.error(`Webview fatal error: ${e}`); - hostMessaging.postMessage('fatal-error', { message: e + '' }); - return; - } - - if (currentUpdateId !== updateId) { - return; - } - - const options = data.options; - const newDocument = toContentHtml(data); - - const initialStyleVersion = styleVersion; - - const frame = getActiveFrame(); - const wasFirstLoad = firstLoad; - // keep current scrollY around and use later - /** @type {(body: HTMLElement, window: Window) => void} */ - let setInitialScrollPosition; - if (firstLoad) { - firstLoad = false; - setInitialScrollPosition = (body, window) => { - if (typeof initData.initialScrollProgress === 'number' && !isNaN(initData.initialScrollProgress)) { - if (window.scrollY === 0) { - window.scroll(0, body.clientHeight * initData.initialScrollProgress); - } - } - }; - } else { - const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? assertIsDefined(frame.contentWindow).scrollY : 0; - setInitialScrollPosition = (body, window) => { - if (window.scrollY === 0) { - window.scroll(0, scrollY); - } - }; - } - - // Clean up old pending frames and set current one as new one - const previousPendingFrame = getPendingFrame(); - if (previousPendingFrame) { - previousPendingFrame.setAttribute('id', ''); - document.body.removeChild(previousPendingFrame); - } - if (!wasFirstLoad) { - pendingMessages = []; - } - - const newFrame = document.createElement('iframe'); - newFrame.setAttribute('id', 'pending-frame'); - newFrame.setAttribute('frameborder', '0'); - - const sandboxRules = new Set(['allow-same-origin', 'allow-pointer-lock']); - if (options.allowScripts) { - sandboxRules.add('allow-scripts'); - sandboxRules.add('allow-downloads'); - } - if (options.allowForms) { - sandboxRules.add('allow-forms'); - } - newFrame.setAttribute('sandbox', Array.from(sandboxRules).join(' ')); - if (!isFirefox) { - newFrame.setAttribute('allow', options.allowScripts ? 'clipboard-read; clipboard-write;' : ''); - } - // We should just be able to use srcdoc, but I wasn't - // seeing the service worker applying properly. - // Fake load an empty on the correct origin and then write real html - // into it to get around this. - newFrame.src = `./fake.html?id=${ID}`; - - newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden'; - document.body.appendChild(newFrame); - - /** - * @param {Document} contentDocument - */ - function onFrameLoaded(contentDocument) { - // Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=978325 - setTimeout(() => { - contentDocument.open(); - contentDocument.write(newDocument); - contentDocument.close(); - hookupOnLoadHandlers(newFrame); - - if (initialStyleVersion !== styleVersion) { - applyStyles(contentDocument, contentDocument.body); - } - }, 0); - } - - if (!options.allowScripts && isSafari) { - // On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired: https://bugs.webkit.org/show_bug.cgi?id=33604 - // Use polling instead. - const interval = setInterval(() => { - // If the frame is no longer mounted, loading has stopped - if (!newFrame.parentElement) { - clearInterval(interval); - return; - } - - const contentDocument = assertIsDefined(newFrame.contentDocument); - if (contentDocument.location.pathname.endsWith('/fake.html') && contentDocument.readyState !== 'loading') { - clearInterval(interval); - onFrameLoaded(contentDocument); - } - }, 10); - } else { - assertIsDefined(newFrame.contentWindow).addEventListener('DOMContentLoaded', e => { - const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; - onFrameLoaded(assertIsDefined(contentDocument)); - }); - } - - /** - * @param {Document} contentDocument - * @param {Window} contentWindow - */ - const onLoad = (contentDocument, contentWindow) => { - if (contentDocument && contentDocument.body) { - // Workaround for https://github.com/microsoft/vscode/issues/12865 - // check new scrollY and reset if necessary - setInitialScrollPosition(contentDocument.body, contentWindow); - } - - const newFrame = getPendingFrame(); - if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { - const wasFocused = document.hasFocus(); - const oldActiveFrame = getActiveFrame(); - if (oldActiveFrame) { - document.body.removeChild(oldActiveFrame); - } - // Styles may have changed since we created the element. Make sure we re-style - if (initialStyleVersion !== styleVersion) { - applyStyles(newFrame.contentDocument, newFrame.contentDocument.body); - } - newFrame.setAttribute('id', 'active-frame'); - newFrame.style.visibility = 'visible'; - - contentWindow.addEventListener('scroll', handleInnerScroll); - contentWindow.addEventListener('wheel', handleWheel); - - if (wasFocused) { - contentWindow.focus(); - } - - pendingMessages.forEach((message) => { - contentWindow.postMessage(message.message, window.origin, message.transfer); - }); - pendingMessages = []; - } - }; - - /** - * @param {HTMLIFrameElement} newFrame - */ - function hookupOnLoadHandlers(newFrame) { - clearTimeout(loadTimeout); - loadTimeout = undefined; - loadTimeout = setTimeout(() => { - clearTimeout(loadTimeout); - loadTimeout = undefined; - onLoad(assertIsDefined(newFrame.contentDocument), assertIsDefined(newFrame.contentWindow)); - }, 200); - - const contentWindow = assertIsDefined(newFrame.contentWindow); - - contentWindow.addEventListener('load', function (e) { - const contentDocument = /** @type {Document} */ (e.target); - - if (loadTimeout) { - clearTimeout(loadTimeout); - loadTimeout = undefined; - onLoad(contentDocument, this); - } - }); - - // Bubble out various events - contentWindow.addEventListener('click', handleInnerClick); - contentWindow.addEventListener('auxclick', handleAuxClick); - contentWindow.addEventListener('keydown', handleInnerKeydown); - contentWindow.addEventListener('keyup', handleInnerUp); - contentWindow.addEventListener('contextmenu', e => { - if (e.defaultPrevented) { - // Extension code has already handled this event - return; - } - - e.preventDefault(); - hostMessaging.postMessage('did-context-menu', { - clientX: e.clientX, - clientY: e.clientY, - }); - }); - - contentWindow.addEventListener('dragenter', handleInnerDragStartEvent); - contentWindow.addEventListener('dragover', handleInnerDragStartEvent); - - unloadMonitor.onIframeLoaded(newFrame); - } - }); - - // Forward message to the embedded iframe - hostMessaging.onMessage('message', (_event, /** @type {{message: any, transfer?: ArrayBuffer[] }} */ data) => { - const pending = getPendingFrame(); - if (!pending) { - const target = getActiveFrame(); - if (target) { - assertIsDefined(target.contentWindow).postMessage(data.message, window.origin, data.transfer); - return; - } - } - pendingMessages.push(data); - }); - - hostMessaging.onMessage('initial-scroll-position', (_event, progress) => { - initData.initialScrollProgress = progress; - }); - - hostMessaging.onMessage('execCommand', (_event, data) => { - const target = getActiveFrame(); - if (!target) { - return; - } - assertIsDefined(target.contentDocument).execCommand(data); - }); - - /** @type {string | undefined} */ - let lastFindValue = undefined; - - hostMessaging.onMessage('find', (_event, data) => { - const target = getActiveFrame(); - if (!target) { - return; - } - - if (!data.previous && lastFindValue !== data.value) { - // Reset selection so we start search at the head of the last search - const selection = target.contentWindow.getSelection(); - selection.collapse(selection.anchorNode); - } - lastFindValue = data.value; - - const didFind = (/** @type {any} */ (target.contentWindow)).find( - data.value, - /* caseSensitive*/ false, - /* backwards*/ data.previous, - /* wrapAround*/ true, - /* wholeWord */ false, - /* searchInFrames*/ false, - false); - hostMessaging.postMessage('did-find', didFind); - }); - - hostMessaging.onMessage('find-stop', (_event, data) => { - const target = getActiveFrame(); - if (!target) { - return; - } - - lastFindValue = undefined; - - if (!data.clearSelection) { - const selection = target.contentWindow.getSelection(); - for (let i = 0; i < selection.rangeCount; i++) { - selection.removeRange(selection.getRangeAt(i)); - } - } - }); - - trackFocus({ - onFocus: () => hostMessaging.postMessage('did-focus'), - onBlur: () => hostMessaging.postMessage('did-blur') - }); - - (/** @type {any} */ (window))[vscodePostMessageFuncName] = (/** @type {string} */ command, /** @type {any} */ data) => { - switch (command) { - case 'onmessage': - case 'do-update-state': - hostMessaging.postMessage(command, data); - break; - } - }; - - // Also forward events before the contents of the webview have loaded - window.addEventListener('keydown', handleInnerKeydown); - window.addEventListener('dragenter', handleInnerDragStartEvent); - window.addEventListener('dragover', handleInnerDragStartEvent); - - hostMessaging.signalReady(); -});