@@ -8,6 +8,7 @@ import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar
88import { IAction , toAction } from '../../../../base/common/actions.js' ;
99import { timeout } from '../../../../base/common/async.js' ;
1010import { CancellationToken } from '../../../../base/common/cancellation.js' ;
11+ import { isWeb } from '../../../../base/common/platform.js' ;
1112import { Emitter , Event } from '../../../../base/common/event.js' ;
1213import { DisposableStore , MutableDisposable } from '../../../../base/common/lifecycle.js' ;
1314import { extname , isEqual } from '../../../../base/common/resources.js' ;
@@ -16,10 +17,11 @@ import { generateUuid } from '../../../../base/common/uuid.js';
1617import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js' ;
1718import { localize } from '../../../../nls.js' ;
1819import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js' ;
20+ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js' ;
1921import { IEditorOptions } from '../../../../platform/editor/common/editor.js' ;
2022import { ByteSize , FileOperationError , FileOperationResult , IFileService , TooLargeFileOperationError } from '../../../../platform/files/common/files.js' ;
2123import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js' ;
22- import { IStorageService } from '../../../../platform/storage/common/storage.js' ;
24+ import { IStorageService , StorageScope , StorageTarget } from '../../../../platform/storage/common/storage.js' ;
2325import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js' ;
2426import { IThemeService } from '../../../../platform/theme/common/themeService.js' ;
2527import { Selection } from '../../../../editor/common/core/selection.js' ;
@@ -37,6 +39,7 @@ import { NotebookEditorInput } from '../common/notebookEditorInput.js';
3739import { NotebookPerfMarks } from '../common/notebookPerformance.js' ;
3840import { GroupsOrder , IEditorGroup , IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js' ;
3941import { IEditorService } from '../../../services/editor/common/editorService.js' ;
42+ import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js' ;
4043import { IEditorProgressService } from '../../../../platform/progress/common/progress.js' ;
4144import { InstallRecommendedExtensionAction } from '../../extensions/browser/extensionsActions.js' ;
4245import { INotebookService } from '../common/notebookService.js' ;
@@ -51,6 +54,15 @@ import { StopWatch } from '../../../../base/common/stopwatch.js';
5154import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js' ;
5255
5356const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState' ;
57+ const NOTEBOOK_WEB_HOST_OPEN_CONFIRMED_KEY = 'notebook.webHost.openConfirmed' ;
58+
59+ /**
60+ * Notebook resources that have already been confirmed for opening in a serverless web
61+ * session. This prevents re-prompting when the user switches back to an already-open
62+ * notebook, while still gating the first open of each notebook.
63+ */
64+ const confirmedWebHostNotebooks = new Set < string > ( ) ;
65+
5466
5567export class NotebookEditor extends EditorPane implements INotebookEditorPane , IEditorPaneWithScrolling {
5668 static readonly ID : string = NOTEBOOK_EDITOR_ID ;
@@ -84,7 +96,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane, I
8496 @ITelemetryService telemetryService : ITelemetryService ,
8597 @IThemeService themeService : IThemeService ,
8698 @IInstantiationService private readonly _instantiationService : IInstantiationService ,
87- @IStorageService storageService : IStorageService ,
99+ @IStorageService private readonly _storageService : IStorageService ,
88100 @IEditorService private readonly _editorService : IEditorService ,
89101 @IEditorGroupsService private readonly _editorGroupService : IEditorGroupsService ,
90102 @INotebookEditorService private readonly _notebookWidgetService : INotebookEditorService ,
@@ -96,9 +108,11 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane, I
96108 @IExtensionsWorkbenchService private readonly _extensionsWorkbenchService : IExtensionsWorkbenchService ,
97109 @IWorkingCopyBackupService private readonly _workingCopyBackupService : IWorkingCopyBackupService ,
98110 @ILogService private readonly logService : ILogService ,
99- @IPreferencesService private readonly _preferencesService : IPreferencesService
111+ @IPreferencesService private readonly _preferencesService : IPreferencesService ,
112+ @IDialogService private readonly _dialogService : IDialogService ,
113+ @IWorkbenchEnvironmentService private readonly _environmentService : IWorkbenchEnvironmentService
100114 ) {
101- super ( NotebookEditor . ID , group , telemetryService , themeService , storageService ) ;
115+ super ( NotebookEditor . ID , group , telemetryService , themeService , _storageService ) ;
102116 this . _editorMemento = this . getEditorMemento < INotebookEditorViewState > ( _editorGroupService , configurationService , NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY ) ;
103117
104118 this . _register ( this . _fileService . onDidChangeFileSystemProviderCapabilities ( e => this . _onDidChangeFileSystemProvider ( e . scheme ) ) ) ;
@@ -194,7 +208,56 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane, I
194208 return ! ! value && ( DOM . isAncestorOfActiveElement ( value . getDomNode ( ) || DOM . isAncestorOfActiveElement ( value . getOverflowContainerDomNode ( ) ) ) ) ;
195209 }
196210
211+ /**
212+ * When running serverless on the web (i.e. in the browser with no remote server
213+ * connected), prompt the user to confirm that they really want to open the notebook.
214+ * The confirmation is only shown the first time a given notebook is opened in the
215+ * session (so switching back to an already-open notebook does not re-prompt), and the
216+ * choice can be remembered for the whole workspace via a "Don't ask again" checkbox.
217+ */
218+ private async _confirmOpenOnWebHost ( input : NotebookEditorInput ) : Promise < void > {
219+ const isServerlessWeb = isWeb && ! this . _environmentService . remoteAuthority ;
220+ if ( ! isServerlessWeb ) {
221+ return ;
222+ }
223+
224+ if ( this . _storageService . getBoolean ( NOTEBOOK_WEB_HOST_OPEN_CONFIRMED_KEY , StorageScope . WORKSPACE , false ) ) {
225+ return ;
226+ }
227+
228+ const resourceKey = input . resource . toString ( ) ;
229+ if ( confirmedWebHostNotebooks . has ( resourceKey ) ) {
230+ return ;
231+ }
232+
233+ const { confirmed, checkboxChecked } = await this . _dialogService . confirm ( {
234+ type : 'warning' ,
235+ message : localize ( 'notebook.webHost.confirm' , "Do you trust the authors of this notebook?" ) ,
236+ detail : localize ( 'notebook.webHost.detail' , "Notebooks can run code that has access to your browser session, including any signed-in accounts. Only open notebooks from authors you trust." ) ,
237+ primaryButton : localize ( 'notebook.webHost.open' , "Open Notebook" ) ,
238+ checkbox : { label : localize ( 'notebook.webHost.remember' , "Don't ask me again" ) }
239+ } ) ;
240+
241+ if ( ! confirmed ) {
242+ throw createEditorOpenError ( localize ( 'notebook.webHost.declined' , "The notebook was not opened because its authors are not trusted." ) , [
243+ toAction ( {
244+ id : 'workbench.notebook.action.openAsText' , label : localize ( 'notebookOpenAsText' , "Open As Text" ) , run : async ( ) => {
245+ this . _editorService . openEditor ( { resource : input . resource , options : { override : DEFAULT_EDITOR_ASSOCIATION . id , pinned : true } } ) ;
246+ }
247+ } )
248+ ] , { forceMessage : true } ) ;
249+ }
250+
251+ confirmedWebHostNotebooks . add ( resourceKey ) ;
252+
253+ if ( checkboxChecked ) {
254+ this . _storageService . store ( NOTEBOOK_WEB_HOST_OPEN_CONFIRMED_KEY , true , StorageScope . WORKSPACE , StorageTarget . MACHINE ) ;
255+ }
256+ }
257+
197258 override async setInput ( input : NotebookEditorInput , options : INotebookEditorOptions | undefined , context : IEditorOpenContext , token : CancellationToken , noRetry ?: boolean ) : Promise < void > {
259+ await this . _confirmOpenOnWebHost ( input ) ;
260+
198261 try {
199262 let perfMarksCaptured = false ;
200263 const fileOpenMonitor = timeout ( 10000 ) ;
0 commit comments