diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index ac06cab08a9..de0524c037e 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1182,7 +1182,7 @@ export function computeScreenAwareSize(cssPx: number): number { /** * Open safely a new window. This is the best way to do so, but you cannot tell * if the window was opened or if it was blocked by the browser's popup blocker. - * If you want to tell if the browser blocked the new window, use `windowOpenNoOpenerWithSuccess`. + * If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}. * * See https://github.com/microsoft/monaco-editor/issues/601 * To protect against malicious code in the linked site, particularly phishing attempts, @@ -1201,19 +1201,49 @@ export function windowOpenNoOpener(url: string): void { } /** - * Open safely a new window. This technique is not appropriate in certain contexts, - * like for example when the JS context is executing inside a sandboxed iframe. - * If it is not necessary to know if the browser blocked the new window, use - * `windowOpenNoOpener`. + * Open a new window in a popup. This is the best way to do so, but you cannot tell + * if the window was opened or if it was blocked by the browser's popup blocker. + * If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}. + * + * Note: this does not set {@link window.opener} to null. This is to allow the opened popup to + * be able to use {@link window.close} to close itself. Because of this, you should only use + * this function on urls that you trust. + * + * In otherwords, you should almost always use {@link windowOpenNoOpener} instead of this function. + */ +const popupWidth = 780, popupHeight = 640; +export function windowOpenPopup(url: string): void { + const left = Math.floor(window.screenLeft + window.innerWidth / 2 - popupWidth / 2); + const top = Math.floor(window.screenTop + window.innerHeight / 2 - popupHeight / 2); + window.open( + url, + '_blank', + `width=${popupWidth},height=${popupHeight},top=${top},left=${left}` + ); +} + +/** + * Attempts to open a window and returns whether it succeeded. This technique is + * not appropriate in certain contexts, like for example when the JS context is + * executing inside a sandboxed iframe. If it is not necessary to know if the + * browser blocked the new window, use {@link windowOpenNoOpener}. * * See https://github.com/microsoft/monaco-editor/issues/601 * See https://github.com/microsoft/monaco-editor/issues/2474 * See https://mathiasbynens.github.io/rel-noopener/ + * + * @param url the url to open + * @param noOpener whether or not to set the {@link window.opener} to null. You should leave the default + * (true) unless you trust the url that is being opened. + * @returns boolean indicating if the {@link window.open} call succeeded */ -export function windowOpenNoOpenerWithSuccess(url: string): boolean { +export function windowOpenWithSuccess(url: string, noOpener = true): boolean { const newTab = window.open(); if (newTab) { - (newTab as any).opener = null; + if (noOpener) { + // see `windowOpenNoOpener` for details on why this is important + (newTab as any).opener = null; + } newTab.location.href = url; return true; } diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index cdfe8e4855f..29481aceb2a 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, addDisposableThrottledListener, detectFullscreen, EventHelper, EventType, windowOpenNoOpenerWithSuccess, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { addDisposableListener, addDisposableThrottledListener, detectFullscreen, EventHelper, EventType, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; @@ -141,10 +141,20 @@ export class BrowserWindow extends Disposable { } } + let isAllowedOpener = false; + if (this.environmentService.options?.openerAllowedExternalUrlPrefixes) { + for (const trustedPopupPrefix of this.environmentService.options.openerAllowedExternalUrlPrefixes) { + if (href.startsWith(trustedPopupPrefix)) { + isAllowedOpener = true; + break; + } + } + } + // HTTP(s): open in new window and deal with potential popup blockers if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) { if (isSafari) { - const opened = windowOpenNoOpenerWithSuccess(href); + const opened = windowOpenWithSuccess(href, !isAllowedOpener); if (!opened) { const showResult = await this.dialogService.show( Severity.Warning, @@ -161,7 +171,9 @@ export class BrowserWindow extends Disposable { ); if (showResult.choice === 0) { - windowOpenNoOpener(href); + isAllowedOpener + ? windowOpenPopup(href) + : windowOpenNoOpener(href); } if (showResult.choice === 1) { diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 0f8c28093d7..6f570f2f74c 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -448,6 +448,13 @@ interface IWorkbenchConstructionOptions { */ readonly additionalTrustedDomains?: string[]; + /** + * Urls that will be opened externally that are allowed access + * to the opener window. This is primarily used to allow + * `window.close()` to be called from the newly opened window. + */ + readonly openerAllowedExternalUrlPrefixes?: string[]; + /** * Support for URL callbacks. */