@theia/workspace 1.68.0-next.48 → 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 (29) hide show
  1. package/lib/browser/workspace-frontend-contribution.d.ts.map +1 -1
  2. package/lib/browser/workspace-frontend-contribution.js +3 -0
  3. package/lib/browser/workspace-frontend-contribution.js.map +1 -1
  4. package/lib/browser/workspace-frontend-module.d.ts +1 -0
  5. package/lib/browser/workspace-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/workspace-frontend-module.js +2 -0
  7. package/lib/browser/workspace-frontend-module.js.map +1 -1
  8. package/lib/browser/workspace-trust-dialog.d.ts +13 -0
  9. package/lib/browser/workspace-trust-dialog.d.ts.map +1 -0
  10. package/lib/browser/workspace-trust-dialog.js +66 -0
  11. package/lib/browser/workspace-trust-dialog.js.map +1 -0
  12. package/lib/browser/workspace-trust-service.d.ts +58 -2
  13. package/lib/browser/workspace-trust-service.d.ts.map +1 -1
  14. package/lib/browser/workspace-trust-service.js +177 -37
  15. package/lib/browser/workspace-trust-service.js.map +1 -1
  16. package/lib/browser/workspace-trust-service.spec.js +192 -39
  17. package/lib/browser/workspace-trust-service.spec.js.map +1 -1
  18. package/lib/common/untitled-workspace-service.d.ts +8 -1
  19. package/lib/common/untitled-workspace-service.d.ts.map +1 -1
  20. package/lib/common/untitled-workspace-service.js +21 -2
  21. package/lib/common/untitled-workspace-service.js.map +1 -1
  22. package/package.json +5 -5
  23. package/src/browser/style/index.css +75 -0
  24. package/src/browser/workspace-frontend-contribution.ts +2 -0
  25. package/src/browser/workspace-frontend-module.ts +4 -1
  26. package/src/browser/workspace-trust-dialog.tsx +90 -0
  27. package/src/browser/workspace-trust-service.spec.ts +246 -43
  28. package/src/browser/workspace-trust-service.ts +213 -40
  29. package/src/common/untitled-workspace-service.ts +21 -2
@@ -15,16 +15,16 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { ConfirmDialog, Dialog, StorageService } from '@theia/core/lib/browser';
18
+ import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
18
19
  import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
19
- import { OS } from '@theia/core';
20
- import { DisposableCollection } from '@theia/core/lib/common/disposable';
20
+ import { OS, ContributionProvider, DisposableCollection } from '@theia/core';
21
21
  import { Emitter, Event } from '@theia/core/lib/common';
22
22
  import URI from '@theia/core/lib/common/uri';
23
23
  import { PreferenceChange, PreferenceSchemaService, PreferenceScope, PreferenceService } from '@theia/core/lib/common/preferences';
24
24
  import { MessageService } from '@theia/core/lib/common/message-service';
25
25
  import { nls } from '@theia/core/lib/common/nls';
26
26
  import { Deferred } from '@theia/core/lib/common/promise-util';
27
- import { inject, injectable, postConstruct, preDestroy } from '@theia/core/shared/inversify';
27
+ import { inject, injectable, named, postConstruct, preDestroy } from '@theia/core/shared/inversify';
28
28
  import { WindowService } from '@theia/core/lib/browser/window/window-service';
29
29
  import {
30
30
  WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WORKSPACE_TRUST_TRUSTED_FOLDERS, WorkspaceTrustPrompt
@@ -33,10 +33,33 @@ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/front
33
33
  import { WorkspaceService } from './workspace-service';
34
34
  import { WorkspaceCommands } from './workspace-commands';
35
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';
36
39
 
37
40
  const STORAGE_TRUSTED = 'trusted';
38
41
  export const WORKSPACE_TRUST_STATUS_BAR_ID = 'workspace-trust-status';
39
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
+ }
62
+
40
63
  @injectable()
41
64
  export class WorkspaceTrustService {
42
65
  @inject(WorkspaceService)
@@ -66,6 +89,15 @@ export class WorkspaceTrustService {
66
89
  @inject(StatusBar)
67
90
  protected readonly statusBar: StatusBar;
68
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
+
69
101
  protected workspaceTrust = new Deferred<boolean>();
70
102
  protected currentTrust: boolean | undefined;
71
103
  protected pendingTrustDialog: Deferred<boolean> | undefined;
@@ -110,6 +142,10 @@ export class WorkspaceTrustService {
110
142
  }
111
143
 
112
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
+ }
113
149
  return this.workspaceTrust.promise;
114
150
  }
115
151
 
@@ -151,11 +187,12 @@ export class WorkspaceTrustService {
151
187
  return true;
152
188
  }
153
189
 
154
- if (this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW] && !this.workspaceService.workspace) {
155
- return true;
190
+ // Empty workspace - no folders open
191
+ if (await this.isEmptyWorkspace()) {
192
+ return !!this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW];
156
193
  }
157
194
 
158
- if (this.isWorkspaceInTrustedFolders()) {
195
+ if (await this.areAllWorkspaceUrisTrusted()) {
159
196
  return true;
160
197
  }
161
198
 
@@ -175,6 +212,77 @@ export class WorkspaceTrustService {
175
212
  return this.showTrustPromptDialog();
176
213
  }
177
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
+
178
286
  protected async showTrustPromptDialog(): Promise<boolean> {
179
287
  // If dialog is already open, wait for its result
180
288
  if (this.pendingTrustDialog) {
@@ -183,18 +291,10 @@ export class WorkspaceTrustService {
183
291
 
184
292
  this.pendingTrustDialog = new Deferred<boolean>();
185
293
  try {
186
- const trust = nls.localizeByDefault('Yes, I trust the authors');
187
- const dontTrust = nls.localizeByDefault("No, I don't trust the authors");
188
- const folderPath = this.workspaceService.workspace?.resource?.path?.toString() ?? '';
189
-
190
- const dialog = new ConfirmDialog({
191
- title: nls.localizeByDefault('Do you trust the authors of the files in this folder?'),
192
- msg: nls.localize('theia/workspace/trustDialogMessage',
193
- 'If you trust the authors of this folder, code inside may be executed. Only trust folders that you trust the contents of.') +
194
- (folderPath ? `\n\n"${folderPath}"` : ''),
195
- ok: trust,
196
- cancel: dontTrust,
197
- });
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);
198
298
 
199
299
  const result = await dialog.open();
200
300
  const trusted = result === true;
@@ -209,35 +309,58 @@ export class WorkspaceTrustService {
209
309
  }
210
310
 
211
311
  async addToTrustedFolders(): Promise<void> {
212
- const workspaceUri = this.workspaceService.workspace?.resource;
213
- if (!workspaceUri) {
312
+ const uris = this.getWorkspaceUris();
313
+ if (uris.length === 0) {
214
314
  return;
215
315
  }
216
- if (!this.isWorkspaceInTrustedFolders()) {
217
- const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
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) {
218
329
  await this.preferences.set(
219
330
  WORKSPACE_TRUST_TRUSTED_FOLDERS,
220
- [...currentFolders, workspaceUri.toString()],
331
+ newFolders,
221
332
  PreferenceScope.User
222
333
  );
223
334
  }
224
335
  }
225
336
 
226
- protected isWorkspaceInTrustedFolders(): boolean {
227
- const workspaceUri = this.workspaceService.workspace?.resource;
228
- if (!workspaceUri) {
229
- return false;
337
+ async removeFromTrustedFolders(): Promise<void> {
338
+ const uris = this.getWorkspaceUris();
339
+ if (uris.length === 0) {
340
+ return;
230
341
  }
231
- const trustedFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
342
+
343
+ const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
232
344
  const caseSensitive = !OS.backend.isWindows;
233
- return trustedFolders.some(folder => {
345
+ const normalizedUris = uris.map(uri => uri.normalizePath());
346
+
347
+ const updatedFolders = currentFolders.filter(folder => {
234
348
  try {
235
349
  const folderUri = new URI(folder).normalizePath();
236
- return workspaceUri.normalizePath().isEqual(folderUri, caseSensitive);
350
+ // Remove folder if it exactly matches any workspace URI
351
+ return !normalizedUris.some(wsUri => wsUri.isEqual(folderUri, caseSensitive));
237
352
  } catch {
238
- return false; // Invalid URI in preferences
353
+ return true; // Keep invalid URIs
239
354
  }
240
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
+ }
241
364
  }
242
365
 
243
366
  protected async loadWorkspaceTrust(): Promise<boolean | undefined> {
@@ -256,18 +379,18 @@ export class WorkspaceTrustService {
256
379
  // Handle trustedFolders changes regardless of scope
257
380
  if (change.preferenceName === WORKSPACE_TRUST_TRUSTED_FOLDERS) {
258
381
  // For empty windows with emptyWindow setting enabled, trust should remain true
259
- if (this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW] && !this.workspaceService.workspace) {
382
+ if (await this.isEmptyWorkspace() && this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW]) {
260
383
  return;
261
384
  }
262
- const isNowInTrustedFolders = this.isWorkspaceInTrustedFolders();
263
- if (isNowInTrustedFolders !== this.currentTrust) {
264
- this.setWorkspaceTrust(isNowInTrustedFolders);
385
+ const areAllUrisTrusted = await this.areAllWorkspaceUrisTrusted();
386
+ if (areAllUrisTrusted !== this.currentTrust) {
387
+ this.setWorkspaceTrust(areAllUrisTrusted);
265
388
  }
266
389
  return;
267
390
  }
268
391
 
269
392
  if (change.scope === PreferenceScope.User) {
270
- 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) {
271
394
  this.storage.setData(STORAGE_TRUSTED, undefined);
272
395
  }
273
396
 
@@ -281,9 +404,9 @@ export class WorkspaceTrustService {
281
404
  }
282
405
 
283
406
  // Handle emptyWindow setting change for empty windows
284
- if (change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW && !this.workspaceService.workspace) {
407
+ if (change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW && await this.isEmptyWorkspace()) {
285
408
  // For empty windows, directly update trust based on the new setting value
286
- const shouldTrust = !!change.newValue;
409
+ const shouldTrust = !!this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW];
287
410
  if (this.currentTrust !== shouldTrust) {
288
411
  this.setWorkspaceTrust(shouldTrust);
289
412
  }
@@ -326,17 +449,67 @@ export class WorkspaceTrustService {
326
449
  this.statusBar.setElement(WORKSPACE_TRUST_STATUS_BAR_ID, {
327
450
  text: '$(shield) ' + nls.localizeByDefault('Restricted Mode'),
328
451
  alignment: StatusBarAlignment.LEFT,
452
+ backgroundColor: 'var(--theia-statusBarItem-prominentBackground)',
453
+ color: 'var(--theia-statusBarItem-prominentForeground)',
329
454
  priority: 5000,
330
- tooltip: nls.localize('theia/workspace/restrictedModeTooltip',
331
- 'Running in Restricted Mode. Some features are disabled because this folder is not trusted. Click to manage trust settings.'),
455
+ tooltip: this.createRestrictedModeTooltip(),
332
456
  command: WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id
333
457
  });
334
458
  }
335
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
+
336
499
  protected hideRestrictedModeStatusBarItem(): void {
337
500
  this.statusBar.removeElement(WORKSPACE_TRUST_STATUS_BAR_ID);
338
501
  }
339
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
+
340
513
  async requestWorkspaceTrust(): Promise<boolean | undefined> {
341
514
  if (!this.isWorkspaceTrustResolved()) {
342
515
  const trusted = await this.showTrustPromptDialog();
@@ -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> {