@theia/workspace 1.68.0-next.7 → 1.68.0-next.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/lib/browser/quick-open-workspace.js +3 -4
  2. package/lib/browser/quick-open-workspace.js.map +1 -1
  3. package/lib/browser/workspace-breadcrumbs-contribution.js +3 -3
  4. package/lib/browser/workspace-breadcrumbs-contribution.js.map +1 -1
  5. package/lib/browser/workspace-commands.d.ts +1 -0
  6. package/lib/browser/workspace-commands.d.ts.map +1 -1
  7. package/lib/browser/workspace-commands.js +6 -2
  8. package/lib/browser/workspace-commands.js.map +1 -1
  9. package/lib/browser/workspace-frontend-contribution.d.ts +5 -1
  10. package/lib/browser/workspace-frontend-contribution.d.ts.map +1 -1
  11. package/lib/browser/workspace-frontend-contribution.js +48 -7
  12. package/lib/browser/workspace-frontend-contribution.js.map +1 -1
  13. package/lib/browser/workspace-frontend-module.d.ts +1 -0
  14. package/lib/browser/workspace-frontend-module.d.ts.map +1 -1
  15. package/lib/browser/workspace-frontend-module.js +2 -0
  16. package/lib/browser/workspace-frontend-module.js.map +1 -1
  17. package/lib/browser/workspace-schema-updater.d.ts.map +1 -1
  18. package/lib/browser/workspace-service.js +3 -5
  19. package/lib/browser/workspace-service.js.map +1 -1
  20. package/lib/browser/workspace-trust-dialog.d.ts +13 -0
  21. package/lib/browser/workspace-trust-dialog.d.ts.map +1 -0
  22. package/lib/browser/workspace-trust-dialog.js +66 -0
  23. package/lib/browser/workspace-trust-dialog.js.map +1 -0
  24. package/lib/browser/workspace-trust-service.d.ts +77 -1
  25. package/lib/browser/workspace-trust-service.d.ts.map +1 -1
  26. package/lib/browser/workspace-trust-service.js +322 -12
  27. package/lib/browser/workspace-trust-service.js.map +1 -1
  28. package/lib/browser/workspace-trust-service.spec.d.ts +2 -0
  29. package/lib/browser/workspace-trust-service.spec.d.ts.map +1 -0
  30. package/lib/browser/workspace-trust-service.spec.js +357 -0
  31. package/lib/browser/workspace-trust-service.spec.js.map +1 -0
  32. package/lib/browser/workspace-uri-contribution.js +1 -2
  33. package/lib/browser/workspace-uri-contribution.js.map +1 -1
  34. package/lib/browser/workspace-user-working-directory-provider.js +6 -5
  35. package/lib/browser/workspace-user-working-directory-provider.js.map +1 -1
  36. package/lib/common/untitled-workspace-service.d.ts +8 -1
  37. package/lib/common/untitled-workspace-service.d.ts.map +1 -1
  38. package/lib/common/untitled-workspace-service.js +22 -3
  39. package/lib/common/untitled-workspace-service.js.map +1 -1
  40. package/lib/common/workspace-preferences.js +3 -3
  41. package/lib/common/workspace-preferences.js.map +1 -1
  42. package/lib/common/workspace-trust-preferences.d.ts +2 -0
  43. package/lib/common/workspace-trust-preferences.d.ts.map +1 -1
  44. package/lib/common/workspace-trust-preferences.js +13 -3
  45. package/lib/common/workspace-trust-preferences.js.map +1 -1
  46. package/lib/node/default-workspace-server.d.ts +0 -1
  47. package/lib/node/default-workspace-server.d.ts.map +1 -1
  48. package/lib/node/default-workspace-server.js +2 -3
  49. package/lib/node/default-workspace-server.js.map +1 -1
  50. package/package.json +5 -5
  51. package/src/browser/style/index.css +75 -0
  52. package/src/browser/workspace-commands.ts +5 -0
  53. package/src/browser/workspace-frontend-contribution.ts +45 -2
  54. package/src/browser/workspace-frontend-module.ts +4 -1
  55. package/src/browser/workspace-trust-dialog.tsx +90 -0
  56. package/src/browser/workspace-trust-service.spec.ts +462 -0
  57. package/src/browser/workspace-trust-service.ts +381 -15
  58. package/src/common/untitled-workspace-service.ts +21 -2
  59. package/src/common/workspace-trust-preferences.ts +11 -0
@@ -15,20 +15,50 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { ConfirmDialog, Dialog, StorageService } from '@theia/core/lib/browser';
18
- import { PreferenceChange, PreferenceScope, PreferenceService } from '@theia/core/lib/common/preferences';
18
+ import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
19
+ import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
20
+ import { OS, ContributionProvider, DisposableCollection } from '@theia/core';
21
+ import { Emitter, Event } from '@theia/core/lib/common';
22
+ import URI from '@theia/core/lib/common/uri';
23
+ import { PreferenceChange, PreferenceSchemaService, PreferenceScope, PreferenceService } from '@theia/core/lib/common/preferences';
19
24
  import { MessageService } from '@theia/core/lib/common/message-service';
20
25
  import { nls } from '@theia/core/lib/common/nls';
21
26
  import { Deferred } from '@theia/core/lib/common/promise-util';
22
- import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
27
+ import { inject, injectable, named, postConstruct, preDestroy } from '@theia/core/shared/inversify';
23
28
  import { WindowService } from '@theia/core/lib/browser/window/window-service';
24
29
  import {
25
- WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WorkspaceTrustPrompt
30
+ WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WORKSPACE_TRUST_TRUSTED_FOLDERS, WorkspaceTrustPrompt
26
31
  } from '../common/workspace-trust-preferences';
27
32
  import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
28
33
  import { WorkspaceService } from './workspace-service';
34
+ import { WorkspaceCommands } from './workspace-commands';
29
35
  import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
36
+ import { WorkspaceTrustDialog } from './workspace-trust-dialog';
37
+ import { UntitledWorkspaceService } from '../common/untitled-workspace-service';
38
+ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
30
39
 
31
40
  const STORAGE_TRUSTED = 'trusted';
41
+ export const WORKSPACE_TRUST_STATUS_BAR_ID = 'workspace-trust-status';
42
+
43
+ /**
44
+ * Contribution interface for features that are restricted in untrusted workspaces.
45
+ * Implementations can provide information about what is being restricted.
46
+ */
47
+ export const WorkspaceRestrictionContribution = Symbol('WorkspaceRestrictionContribution');
48
+ export interface WorkspaceRestrictionContribution {
49
+ /**
50
+ * Returns the restrictions currently active due to workspace trust.
51
+ * Called when building the restricted mode status bar tooltip.
52
+ */
53
+ getRestrictions(): WorkspaceRestriction[];
54
+ }
55
+
56
+ export interface WorkspaceRestriction {
57
+ /** Display name of the feature being restricted */
58
+ label: string;
59
+ /** Optional details (e.g., list of blocked items) */
60
+ details?: string[];
61
+ }
32
62
 
33
63
  @injectable()
34
64
  export class WorkspaceTrustService {
@@ -47,13 +77,35 @@ export class WorkspaceTrustService {
47
77
  @inject(WorkspaceTrustPreferences)
48
78
  protected readonly workspaceTrustPref: WorkspaceTrustPreferences;
49
79
 
80
+ @inject(PreferenceSchemaService)
81
+ protected readonly preferenceSchemaService: PreferenceSchemaService;
82
+
50
83
  @inject(WindowService)
51
84
  protected readonly windowService: WindowService;
52
85
 
53
86
  @inject(ContextKeyService)
54
87
  protected readonly contextKeyService: ContextKeyService;
55
88
 
89
+ @inject(StatusBar)
90
+ protected readonly statusBar: StatusBar;
91
+
92
+ @inject(ContributionProvider) @named(WorkspaceRestrictionContribution)
93
+ protected readonly restrictionContributions: ContributionProvider<WorkspaceRestrictionContribution>;
94
+
95
+ @inject(UntitledWorkspaceService)
96
+ protected readonly untitledWorkspaceService: UntitledWorkspaceService;
97
+
98
+ @inject(EnvVariablesServer)
99
+ protected readonly envVariablesServer: EnvVariablesServer;
100
+
56
101
  protected workspaceTrust = new Deferred<boolean>();
102
+ protected currentTrust: boolean | undefined;
103
+ protected pendingTrustDialog: Deferred<boolean> | undefined;
104
+
105
+ protected readonly onDidChangeWorkspaceTrustEmitter = new Emitter<boolean>();
106
+ readonly onDidChangeWorkspaceTrust: Event<boolean> = this.onDidChangeWorkspaceTrustEmitter.event;
107
+
108
+ protected readonly toDispose = new DisposableCollection(this.onDidChangeWorkspaceTrustEmitter);
57
109
 
58
110
  @postConstruct()
59
111
  protected init(): void {
@@ -62,11 +114,38 @@ export class WorkspaceTrustService {
62
114
 
63
115
  protected async doInit(): Promise<void> {
64
116
  await this.workspaceService.ready;
117
+ await this.workspaceTrustPref.ready;
118
+ await this.preferenceSchemaService.ready;
65
119
  await this.resolveWorkspaceTrust();
66
- this.preferences.onPreferenceChanged(change => this.handlePreferenceChange(change));
120
+ this.toDispose.push(
121
+ this.preferences.onPreferenceChanged(change => this.handlePreferenceChange(change))
122
+ );
123
+ this.toDispose.push(
124
+ this.workspaceService.onWorkspaceChanged(() => this.handleWorkspaceChanged())
125
+ );
126
+
127
+ // Show status bar item if starting in restricted mode
128
+ const initialTrust = await this.getWorkspaceTrust();
129
+ this.updateRestrictedModeIndicator(initialTrust);
130
+
131
+ // React to trust changes
132
+ this.toDispose.push(
133
+ this.onDidChangeWorkspaceTrust(trust => {
134
+ this.updateRestrictedModeIndicator(trust);
135
+ })
136
+ );
137
+ }
138
+
139
+ @preDestroy()
140
+ protected onStop(): void {
141
+ this.toDispose.dispose();
67
142
  }
68
143
 
69
144
  getWorkspaceTrust(): Promise<boolean> {
145
+ // Return current trust if already resolved, otherwise wait for initial resolution
146
+ if (this.currentTrust !== undefined) {
147
+ return Promise.resolve(this.currentTrust);
148
+ }
70
149
  return this.workspaceTrust.promise;
71
150
  }
72
151
 
@@ -76,22 +155,44 @@ export class WorkspaceTrustService {
76
155
  if (trust !== undefined) {
77
156
  await this.storeWorkspaceTrust(trust);
78
157
  this.contextKeyService.setContext('isWorkspaceTrusted', trust);
158
+ this.currentTrust = trust;
79
159
  this.workspaceTrust.resolve(trust);
160
+ this.onDidChangeWorkspaceTrustEmitter.fire(trust);
161
+ if (trust && this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED]) {
162
+ await this.addToTrustedFolders();
163
+ }
80
164
  }
81
165
  }
82
166
  }
83
167
 
168
+ setWorkspaceTrust(trusted: boolean): void {
169
+ if (this.currentTrust === trusted) {
170
+ return;
171
+ }
172
+ this.currentTrust = trusted;
173
+ this.contextKeyService.setContext('isWorkspaceTrusted', trusted);
174
+ if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
175
+ this.storeWorkspaceTrust(trusted);
176
+ }
177
+ this.onDidChangeWorkspaceTrustEmitter.fire(trusted);
178
+ }
179
+
84
180
  protected isWorkspaceTrustResolved(): boolean {
85
181
  return this.workspaceTrust.state !== 'unresolved';
86
182
  }
87
183
 
88
184
  protected async calculateWorkspaceTrust(): Promise<boolean | undefined> {
89
- if (!this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED]) {
90
- // in VS Code if workspace trust is disabled, we implicitly trust the workspace
185
+ const trustEnabled = this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED];
186
+ if (!trustEnabled) {
91
187
  return true;
92
188
  }
93
189
 
94
- if (this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW] && !this.workspaceService.workspace) {
190
+ // Empty workspace - no folders open
191
+ if (await this.isEmptyWorkspace()) {
192
+ return !!this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW];
193
+ }
194
+
195
+ if (await this.areAllWorkspaceUrisTrusted()) {
95
196
  return true;
96
197
  }
97
198
 
@@ -99,7 +200,167 @@ export class WorkspaceTrustService {
99
200
  return false;
100
201
  }
101
202
 
102
- return this.loadWorkspaceTrust();
203
+ // For ONCE mode, check stored trust first
204
+ if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
205
+ const storedTrust = await this.loadWorkspaceTrust();
206
+ if (storedTrust !== undefined) {
207
+ return storedTrust;
208
+ }
209
+ }
210
+
211
+ // For ALWAYS mode or ONCE mode with no stored decision, show dialog
212
+ return this.showTrustPromptDialog();
213
+ }
214
+
215
+ /**
216
+ * Check if the workspace is empty (no workspace or folder opened, or
217
+ * an untitled workspace with no folders).
218
+ * A saved workspace file with 0 folders is NOT empty - it still needs trust
219
+ * evaluation because it could have tasks defined.
220
+ */
221
+ protected async isEmptyWorkspace(): Promise<boolean> {
222
+ const workspace = this.workspaceService.workspace;
223
+ if (!workspace) {
224
+ return true;
225
+ }
226
+ const roots = this.workspaceService.tryGetRoots();
227
+ // Only consider it empty if it's an untitled workspace with no folders
228
+ // Use secure check with configDirUri for trust-related decisions
229
+ if (roots.length === 0) {
230
+ const configDirUri = new URI(await this.envVariablesServer.getConfigDirUri());
231
+ if (this.untitledWorkspaceService.isUntitledWorkspace(workspace.resource, configDirUri)) {
232
+ return true;
233
+ }
234
+ }
235
+ return false;
236
+ }
237
+
238
+ /**
239
+ * Get the URIs that need to be trusted for the current workspace.
240
+ * This includes all workspace folder URIs, plus the workspace file URI
241
+ * for saved workspaces (since workspace files can contain tasks/settings).
242
+ */
243
+ protected getWorkspaceUris(): URI[] {
244
+ const uris = this.workspaceService.tryGetRoots().map(root => root.resource);
245
+ const workspace = this.workspaceService.workspace;
246
+ // For saved workspaces, include the workspace file itself
247
+ if (workspace && this.workspaceService.saved) {
248
+ uris.push(workspace.resource);
249
+ }
250
+ return uris;
251
+ }
252
+
253
+ /**
254
+ * Check if all workspace URIs are trusted.
255
+ * A workspace is trusted only if ALL of its folders (and the workspace
256
+ * file for saved workspaces) are trusted.
257
+ */
258
+ protected async areAllWorkspaceUrisTrusted(): Promise<boolean> {
259
+ const uris = this.getWorkspaceUris();
260
+ if (uris.length === 0) {
261
+ return false;
262
+ }
263
+ return uris.every(uri => this.isUriTrusted(uri));
264
+ }
265
+
266
+ /**
267
+ * Check if a URI is trusted. A URI is trusted if it or any of its
268
+ * parent folders is in the trusted folders list.
269
+ */
270
+ protected isUriTrusted(uri: URI): boolean {
271
+ const trustedFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
272
+ const caseSensitive = !OS.backend.isWindows;
273
+ const normalizedUri = uri.normalizePath();
274
+
275
+ return trustedFolders.some(folder => {
276
+ try {
277
+ const folderUri = new URI(folder).normalizePath();
278
+ // Check if the trusted folder is equal to or a parent of the URI
279
+ return folderUri.isEqualOrParent(normalizedUri, caseSensitive);
280
+ } catch {
281
+ return false; // Invalid URI in preferences
282
+ }
283
+ });
284
+ }
285
+
286
+ protected async showTrustPromptDialog(): Promise<boolean> {
287
+ // If dialog is already open, wait for its result
288
+ if (this.pendingTrustDialog) {
289
+ return this.pendingTrustDialog.promise;
290
+ }
291
+
292
+ this.pendingTrustDialog = new Deferred<boolean>();
293
+ try {
294
+ // Show the workspace folders in the dialog
295
+ const folderUris = this.workspaceService.tryGetRoots().map(root => root.resource);
296
+
297
+ const dialog = new WorkspaceTrustDialog(folderUris);
298
+
299
+ const result = await dialog.open();
300
+ const trusted = result === true;
301
+ this.pendingTrustDialog.resolve(trusted);
302
+ return trusted;
303
+ } catch (e) {
304
+ this.pendingTrustDialog.resolve(false);
305
+ throw e;
306
+ } finally {
307
+ this.pendingTrustDialog = undefined;
308
+ }
309
+ }
310
+
311
+ async addToTrustedFolders(): Promise<void> {
312
+ const uris = this.getWorkspaceUris();
313
+ if (uris.length === 0) {
314
+ return;
315
+ }
316
+
317
+ const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
318
+ const newFolders = [...currentFolders];
319
+ let changed = false;
320
+
321
+ for (const uri of uris) {
322
+ if (!this.isUriTrusted(uri)) {
323
+ newFolders.push(uri.toString());
324
+ changed = true;
325
+ }
326
+ }
327
+
328
+ if (changed) {
329
+ await this.preferences.set(
330
+ WORKSPACE_TRUST_TRUSTED_FOLDERS,
331
+ newFolders,
332
+ PreferenceScope.User
333
+ );
334
+ }
335
+ }
336
+
337
+ async removeFromTrustedFolders(): Promise<void> {
338
+ const uris = this.getWorkspaceUris();
339
+ if (uris.length === 0) {
340
+ return;
341
+ }
342
+
343
+ const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
344
+ const caseSensitive = !OS.backend.isWindows;
345
+ const normalizedUris = uris.map(uri => uri.normalizePath());
346
+
347
+ const updatedFolders = currentFolders.filter(folder => {
348
+ try {
349
+ const folderUri = new URI(folder).normalizePath();
350
+ // Remove folder if it exactly matches any workspace URI
351
+ return !normalizedUris.some(wsUri => wsUri.isEqual(folderUri, caseSensitive));
352
+ } catch {
353
+ return true; // Keep invalid URIs
354
+ }
355
+ });
356
+
357
+ if (updatedFolders.length !== currentFolders.length) {
358
+ await this.preferences.set(
359
+ WORKSPACE_TRUST_TRUSTED_FOLDERS,
360
+ updatedFolders,
361
+ PreferenceScope.User
362
+ );
363
+ }
103
364
  }
104
365
 
105
366
  protected async loadWorkspaceTrust(): Promise<boolean | undefined> {
@@ -115,8 +376,21 @@ export class WorkspaceTrustService {
115
376
  }
116
377
 
117
378
  protected async handlePreferenceChange(change: PreferenceChange): Promise<void> {
379
+ // Handle trustedFolders changes regardless of scope
380
+ if (change.preferenceName === WORKSPACE_TRUST_TRUSTED_FOLDERS) {
381
+ // For empty windows with emptyWindow setting enabled, trust should remain true
382
+ if (await this.isEmptyWorkspace() && this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW]) {
383
+ return;
384
+ }
385
+ const areAllUrisTrusted = await this.areAllWorkspaceUrisTrusted();
386
+ if (areAllUrisTrusted !== this.currentTrust) {
387
+ this.setWorkspaceTrust(areAllUrisTrusted);
388
+ }
389
+ return;
390
+ }
391
+
118
392
  if (change.scope === PreferenceScope.User) {
119
- if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT && change.newValue !== WorkspaceTrustPrompt.ONCE) {
393
+ if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT && this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] !== WorkspaceTrustPrompt.ONCE) {
120
394
  this.storage.setData(STORAGE_TRUSTED, undefined);
121
395
  }
122
396
 
@@ -125,12 +399,34 @@ export class WorkspaceTrustService {
125
399
  this.windowService.reload();
126
400
  }
127
401
 
128
- if (change.preferenceName === WORKSPACE_TRUST_ENABLED || change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW) {
402
+ if (change.preferenceName === WORKSPACE_TRUST_ENABLED) {
129
403
  this.resolveWorkspaceTrust();
130
404
  }
405
+
406
+ // Handle emptyWindow setting change for empty windows
407
+ if (change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW && await this.isEmptyWorkspace()) {
408
+ // For empty windows, directly update trust based on the new setting value
409
+ const shouldTrust = !!this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW];
410
+ if (this.currentTrust !== shouldTrust) {
411
+ this.setWorkspaceTrust(shouldTrust);
412
+ }
413
+ }
131
414
  }
132
415
  }
133
416
 
417
+ protected async handleWorkspaceChanged(): Promise<void> {
418
+ // Reset trust state for the new workspace
419
+ this.workspaceTrust = new Deferred<boolean>();
420
+ this.currentTrust = undefined;
421
+
422
+ // Re-evaluate trust for the new workspace
423
+ await this.resolveWorkspaceTrust();
424
+
425
+ // Update status bar indicator
426
+ const trust = await this.getWorkspaceTrust();
427
+ this.updateRestrictedModeIndicator(trust);
428
+ }
429
+
134
430
  protected async confirmRestart(): Promise<boolean> {
135
431
  const shouldRestart = await new ConfirmDialog({
136
432
  title: nls.localizeByDefault('A setting has changed that requires a restart to take effect.'),
@@ -141,13 +437,83 @@ export class WorkspaceTrustService {
141
437
  return shouldRestart === true;
142
438
  }
143
439
 
440
+ protected updateRestrictedModeIndicator(trusted: boolean): void {
441
+ if (trusted) {
442
+ this.hideRestrictedModeStatusBarItem();
443
+ } else {
444
+ this.showRestrictedModeStatusBarItem();
445
+ }
446
+ }
447
+
448
+ protected showRestrictedModeStatusBarItem(): void {
449
+ this.statusBar.setElement(WORKSPACE_TRUST_STATUS_BAR_ID, {
450
+ text: '$(shield) ' + nls.localizeByDefault('Restricted Mode'),
451
+ alignment: StatusBarAlignment.LEFT,
452
+ backgroundColor: 'var(--theia-statusBarItem-prominentBackground)',
453
+ color: 'var(--theia-statusBarItem-prominentForeground)',
454
+ priority: 5000,
455
+ tooltip: this.createRestrictedModeTooltip(),
456
+ command: WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id
457
+ });
458
+ }
459
+
460
+ protected createRestrictedModeTooltip(): MarkdownString {
461
+ const md = new MarkdownStringImpl('', { supportThemeIcons: true });
462
+
463
+ md.appendMarkdown(`**${nls.localizeByDefault('Restricted Mode')}**\n\n`);
464
+
465
+ md.appendMarkdown(nls.localize('theia/workspace/restrictedModeDescription',
466
+ 'Some features are disabled because this workspace is not trusted.'));
467
+ md.appendMarkdown('\n\n');
468
+ md.appendMarkdown(nls.localize('theia/workspace/restrictedModeNote',
469
+ '*Please note: The workspace trust feature is currently under development in Theia; not all features are integrated with workspace trust yet*'));
470
+
471
+ const restrictions = this.collectRestrictions();
472
+ if (restrictions.length > 0) {
473
+ md.appendMarkdown('\n\n---\n\n');
474
+ for (const restriction of restrictions) {
475
+ md.appendMarkdown(`**${restriction.label}**\n\n`);
476
+ if (restriction.details && restriction.details.length > 0) {
477
+ for (const detail of restriction.details) {
478
+ md.appendMarkdown(`- ${detail}\n`);
479
+ }
480
+ md.appendMarkdown('\n');
481
+ }
482
+ }
483
+ }
484
+
485
+ md.appendMarkdown('\n\n---\n\n');
486
+ md.appendMarkdown(nls.localize('theia/workspace/clickToManageTrust', 'Click to manage trust settings.'));
487
+
488
+ return md;
489
+ }
490
+
491
+ protected collectRestrictions(): WorkspaceRestriction[] {
492
+ const restrictions: WorkspaceRestriction[] = [];
493
+ for (const contribution of this.restrictionContributions.getContributions()) {
494
+ restrictions.push(...contribution.getRestrictions());
495
+ }
496
+ return restrictions;
497
+ }
498
+
499
+ protected hideRestrictedModeStatusBarItem(): void {
500
+ this.statusBar.removeElement(WORKSPACE_TRUST_STATUS_BAR_ID);
501
+ }
502
+
503
+ /**
504
+ * Refreshes the restricted mode status bar item.
505
+ * Call this when restriction contributions change.
506
+ */
507
+ refreshRestrictedModeIndicator(): void {
508
+ if (this.currentTrust === false) {
509
+ this.showRestrictedModeStatusBarItem();
510
+ }
511
+ }
512
+
144
513
  async requestWorkspaceTrust(): Promise<boolean | undefined> {
145
514
  if (!this.isWorkspaceTrustResolved()) {
146
- const isTrusted = await this.messageService.info(nls.localize('theia/workspace/trustRequest',
147
- 'An extension requests workspace trust but the corresponding API is not yet fully supported. Do you want to trust this workspace?'),
148
- Dialog.YES, Dialog.NO);
149
- const trusted = isTrusted === Dialog.YES;
150
- this.resolveWorkspaceTrust(trusted);
515
+ const trusted = await this.showTrustPromptDialog();
516
+ await this.resolveWorkspaceTrust(trusted);
151
517
  }
152
518
  return this.workspaceTrust.promise;
153
519
  }
@@ -25,8 +25,27 @@ export class UntitledWorkspaceService {
25
25
  @inject(WorkspaceFileService)
26
26
  protected readonly workspaceFileService: WorkspaceFileService;
27
27
 
28
- isUntitledWorkspace(candidate?: URI): boolean {
29
- return !!candidate && this.workspaceFileService.isWorkspaceFile(candidate) && candidate.path.base.startsWith('Untitled');
28
+ /**
29
+ * Check if a URI is an untitled workspace.
30
+ * @param candidate The URI to check
31
+ * @param configDirUri Optional config directory URI. If provided, also verifies
32
+ * that the candidate is under the expected workspaces directory.
33
+ * This is the secure check and should be used when possible.
34
+ */
35
+ isUntitledWorkspace(candidate?: URI, configDirUri?: URI): boolean {
36
+ if (!candidate || !this.workspaceFileService.isWorkspaceFile(candidate)) {
37
+ return false;
38
+ }
39
+ if (!candidate.path.base.startsWith('Untitled')) {
40
+ return false;
41
+ }
42
+ // If configDirUri is provided, verify the candidate is in the expected location
43
+ if (configDirUri) {
44
+ const expectedParentDir = configDirUri.resolve('workspaces');
45
+ return expectedParentDir.isEqualOrParent(candidate);
46
+ }
47
+ // Without configDirUri, fall back to name-only check (less secure)
48
+ return true;
30
49
  }
31
50
 
32
51
  async getUntitledWorkspaceUri(configDirUri: URI, isAcceptable: (candidate: URI) => MaybePromise<boolean>, warnOnHits?: () => unknown): Promise<URI> {
@@ -21,6 +21,7 @@ import { interfaces } from '@theia/core/shared/inversify';
21
21
  export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled';
22
22
  export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt';
23
23
  export const WORKSPACE_TRUST_EMPTY_WINDOW = 'security.workspace.trust.emptyWindow';
24
+ export const WORKSPACE_TRUST_TRUSTED_FOLDERS = 'security.workspace.trust.trustedFolders';
24
25
 
25
26
  export enum WorkspaceTrustPrompt {
26
27
  ALWAYS = 'always',
@@ -45,6 +46,15 @@ export const workspaceTrustPreferenceSchema: PreferenceSchema = {
45
46
  description: nls.localize('theia/workspace/trustEmptyWindow', 'Controls whether or not the empty workspace is trusted by default.'),
46
47
  type: 'boolean',
47
48
  default: true
49
+ },
50
+ [WORKSPACE_TRUST_TRUSTED_FOLDERS]: {
51
+ description: nls.localize('theia/workspace/trustTrustedFolders', 'List of folder URIs that are trusted without prompting.'),
52
+ type: 'array',
53
+ items: {
54
+ type: 'string'
55
+ },
56
+ default: [],
57
+ scope: PreferenceScope.User
48
58
  }
49
59
  }
50
60
  };
@@ -53,6 +63,7 @@ export interface WorkspaceTrustConfiguration {
53
63
  [WORKSPACE_TRUST_ENABLED]: boolean,
54
64
  [WORKSPACE_TRUST_STARTUP_PROMPT]: WorkspaceTrustPrompt;
55
65
  [WORKSPACE_TRUST_EMPTY_WINDOW]: boolean;
66
+ [WORKSPACE_TRUST_TRUSTED_FOLDERS]: string[];
56
67
  }
57
68
 
58
69
  export const WorkspaceTrustPreferenceContribution = Symbol('WorkspaceTrustPreferenceContribution');