@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.
- package/lib/browser/workspace-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/workspace-frontend-contribution.js +3 -0
- package/lib/browser/workspace-frontend-contribution.js.map +1 -1
- package/lib/browser/workspace-frontend-module.d.ts +1 -0
- package/lib/browser/workspace-frontend-module.d.ts.map +1 -1
- package/lib/browser/workspace-frontend-module.js +2 -0
- package/lib/browser/workspace-frontend-module.js.map +1 -1
- package/lib/browser/workspace-trust-dialog.d.ts +13 -0
- package/lib/browser/workspace-trust-dialog.d.ts.map +1 -0
- package/lib/browser/workspace-trust-dialog.js +66 -0
- package/lib/browser/workspace-trust-dialog.js.map +1 -0
- package/lib/browser/workspace-trust-service.d.ts +58 -2
- package/lib/browser/workspace-trust-service.d.ts.map +1 -1
- package/lib/browser/workspace-trust-service.js +177 -37
- package/lib/browser/workspace-trust-service.js.map +1 -1
- package/lib/browser/workspace-trust-service.spec.js +192 -39
- package/lib/browser/workspace-trust-service.spec.js.map +1 -1
- package/lib/common/untitled-workspace-service.d.ts +8 -1
- package/lib/common/untitled-workspace-service.d.ts.map +1 -1
- package/lib/common/untitled-workspace-service.js +21 -2
- package/lib/common/untitled-workspace-service.js.map +1 -1
- package/package.json +5 -5
- package/src/browser/style/index.css +75 -0
- package/src/browser/workspace-frontend-contribution.ts +2 -0
- package/src/browser/workspace-frontend-module.ts +4 -1
- package/src/browser/workspace-trust-dialog.tsx +90 -0
- package/src/browser/workspace-trust-service.spec.ts +246 -43
- package/src/browser/workspace-trust-service.ts +213 -40
- 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
|
-
|
|
155
|
-
|
|
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.
|
|
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
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
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
|
|
213
|
-
if (
|
|
312
|
+
const uris = this.getWorkspaceUris();
|
|
313
|
+
if (uris.length === 0) {
|
|
214
314
|
return;
|
|
215
315
|
}
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
331
|
+
newFolders,
|
|
221
332
|
PreferenceScope.User
|
|
222
333
|
);
|
|
223
334
|
}
|
|
224
335
|
}
|
|
225
336
|
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
229
|
-
return
|
|
337
|
+
async removeFromTrustedFolders(): Promise<void> {
|
|
338
|
+
const uris = this.getWorkspaceUris();
|
|
339
|
+
if (uris.length === 0) {
|
|
340
|
+
return;
|
|
230
341
|
}
|
|
231
|
-
|
|
342
|
+
|
|
343
|
+
const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
|
|
232
344
|
const caseSensitive = !OS.backend.isWindows;
|
|
233
|
-
|
|
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
|
-
|
|
350
|
+
// Remove folder if it exactly matches any workspace URI
|
|
351
|
+
return !normalizedUris.some(wsUri => wsUri.isEqual(folderUri, caseSensitive));
|
|
237
352
|
} catch {
|
|
238
|
-
return
|
|
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.
|
|
382
|
+
if (await this.isEmptyWorkspace() && this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW]) {
|
|
260
383
|
return;
|
|
261
384
|
}
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
this.setWorkspaceTrust(
|
|
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 &&
|
|
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 &&
|
|
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 = !!
|
|
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:
|
|
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
|
-
|
|
29
|
-
|
|
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> {
|