@theia/workspace 1.68.0-next.34 → 1.68.0-next.48
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-commands.d.ts +1 -0
- package/lib/browser/workspace-commands.d.ts.map +1 -1
- package/lib/browser/workspace-commands.js +5 -0
- package/lib/browser/workspace-commands.js.map +1 -1
- package/lib/browser/workspace-frontend-contribution.d.ts +5 -1
- package/lib/browser/workspace-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/workspace-frontend-contribution.js +41 -0
- package/lib/browser/workspace-frontend-contribution.js.map +1 -1
- package/lib/browser/workspace-trust-service.d.ts +21 -1
- package/lib/browser/workspace-trust-service.d.ts.map +1 -1
- package/lib/browser/workspace-trust-service.js +179 -9
- package/lib/browser/workspace-trust-service.js.map +1 -1
- package/lib/browser/workspace-trust-service.spec.d.ts +2 -0
- package/lib/browser/workspace-trust-service.spec.d.ts.map +1 -0
- package/lib/browser/workspace-trust-service.spec.js +204 -0
- package/lib/browser/workspace-trust-service.spec.js.map +1 -0
- package/lib/common/workspace-trust-preferences.d.ts +2 -0
- package/lib/common/workspace-trust-preferences.d.ts.map +1 -1
- package/lib/common/workspace-trust-preferences.js +11 -1
- package/lib/common/workspace-trust-preferences.js.map +1 -1
- package/package.json +5 -5
- package/src/browser/workspace-commands.ts +5 -0
- package/src/browser/workspace-frontend-contribution.ts +43 -2
- package/src/browser/workspace-trust-service.spec.ts +259 -0
- package/src/browser/workspace-trust-service.ts +206 -13
- package/src/common/workspace-trust-preferences.ts +11 -0
|
@@ -148,6 +148,11 @@ export namespace WorkspaceCommands {
|
|
|
148
148
|
id: 'navigator.copyRelativeFilePath',
|
|
149
149
|
label: 'Copy Relative Path'
|
|
150
150
|
});
|
|
151
|
+
export const MANAGE_WORKSPACE_TRUST = Command.toDefaultLocalizedCommand({
|
|
152
|
+
id: 'workspace:manageTrust',
|
|
153
|
+
category: WORKSPACE_CATEGORY,
|
|
154
|
+
label: 'Manage Workspace Trust'
|
|
155
|
+
});
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
@injectable()
|
|
@@ -19,13 +19,15 @@ import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegist
|
|
|
19
19
|
import { isOSX, environment } from '@theia/core';
|
|
20
20
|
import {
|
|
21
21
|
open, OpenerService, CommonMenus, KeybindingRegistry, KeybindingContribution,
|
|
22
|
-
FrontendApplicationContribution, SHELL_TABBAR_CONTEXT_COPY, OnWillStopAction, Navigatable, SaveableSource, Widget
|
|
22
|
+
FrontendApplicationContribution, SHELL_TABBAR_CONTEXT_COPY, OnWillStopAction, Navigatable, SaveableSource, Widget,
|
|
23
|
+
QuickInputService, QuickPickItem
|
|
23
24
|
} from '@theia/core/lib/browser';
|
|
24
25
|
import { FileDialogService, OpenFileDialogProps, FileDialogTreeFilters } from '@theia/filesystem/lib/browser';
|
|
25
26
|
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
|
26
27
|
import { WorkspaceService } from './workspace-service';
|
|
27
28
|
import { WorkspaceFileService, THEIA_EXT, VSCODE_EXT } from '../common';
|
|
28
29
|
import { WorkspaceCommands } from './workspace-commands';
|
|
30
|
+
import { WorkspaceTrustService } from './workspace-trust-service';
|
|
29
31
|
import { QuickOpenWorkspace } from './quick-open-workspace';
|
|
30
32
|
import URI from '@theia/core/lib/common/uri';
|
|
31
33
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
@@ -74,6 +76,10 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
74
76
|
@inject(PreferenceConfigurations) protected readonly preferenceConfigurations: PreferenceConfigurations;
|
|
75
77
|
@inject(FilesystemSaveableService) protected readonly saveService: FilesystemSaveableService;
|
|
76
78
|
@inject(WorkspaceFileService) protected readonly workspaceFileService: WorkspaceFileService;
|
|
79
|
+
@inject(QuickInputService)
|
|
80
|
+
protected readonly quickInputService: QuickInputService;
|
|
81
|
+
@inject(WorkspaceTrustService)
|
|
82
|
+
protected readonly workspaceTrustService: WorkspaceTrustService;
|
|
77
83
|
|
|
78
84
|
configure(): void {
|
|
79
85
|
const workspaceExtensions = this.workspaceFileService.getWorkspaceFileExtensions();
|
|
@@ -165,7 +171,9 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
165
171
|
open(this.openerService, this.workspaceService.workspace.resource);
|
|
166
172
|
}
|
|
167
173
|
}
|
|
168
|
-
|
|
174
|
+
});
|
|
175
|
+
commands.registerCommand(WorkspaceCommands.MANAGE_WORKSPACE_TRUST, {
|
|
176
|
+
execute: () => this.manageWorkspaceTrust()
|
|
169
177
|
});
|
|
170
178
|
}
|
|
171
179
|
|
|
@@ -495,6 +503,39 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
495
503
|
return this.workspaceService.workspace?.resource;
|
|
496
504
|
}
|
|
497
505
|
|
|
506
|
+
protected async manageWorkspaceTrust(): Promise<void> {
|
|
507
|
+
const currentTrust = await this.workspaceTrustService.getWorkspaceTrust();
|
|
508
|
+
const trust = nls.localizeByDefault('Trust');
|
|
509
|
+
const dontTrust = nls.localizeByDefault("Don't Trust");
|
|
510
|
+
const currentSuffix = `(${nls.localizeByDefault('Current')})`;
|
|
511
|
+
|
|
512
|
+
const items: QuickPickItem[] = [
|
|
513
|
+
{
|
|
514
|
+
label: trust,
|
|
515
|
+
description: currentTrust ? currentSuffix : undefined
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
label: dontTrust,
|
|
519
|
+
description: !currentTrust ? currentSuffix : undefined
|
|
520
|
+
}
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
const selected = await this.quickInputService.showQuickPick(items, {
|
|
524
|
+
title: nls.localizeByDefault('Manage Workspace Trust'),
|
|
525
|
+
placeholder: nls.localize('theia/workspace/manageTrustPlaceholder', 'Select trust state for this workspace')
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (selected) {
|
|
529
|
+
const newTrust = selected.label === trust;
|
|
530
|
+
if (newTrust !== currentTrust) {
|
|
531
|
+
this.workspaceTrustService.setWorkspaceTrust(newTrust);
|
|
532
|
+
if (newTrust) {
|
|
533
|
+
await this.workspaceTrustService.addToTrustedFolders();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
498
539
|
onWillStop(): OnWillStopAction<boolean> | undefined {
|
|
499
540
|
const { workspace } = this.workspaceService;
|
|
500
541
|
if (workspace && this.workspaceService.isUntitledWorkspace(workspace.resource)) {
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import * as sinon from 'sinon';
|
|
19
|
+
import { PreferenceChange, PreferenceScope } from '@theia/core/lib/common/preferences';
|
|
20
|
+
import { WorkspaceTrustService } from './workspace-trust-service';
|
|
21
|
+
import { WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_TRUSTED_FOLDERS } from '../common/workspace-trust-preferences';
|
|
22
|
+
|
|
23
|
+
class TestableWorkspaceTrustService extends WorkspaceTrustService {
|
|
24
|
+
public async testHandlePreferenceChange(change: PreferenceChange): Promise<void> {
|
|
25
|
+
return this.handlePreferenceChange(change);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public async testHandleWorkspaceChanged(): Promise<void> {
|
|
29
|
+
return this.handleWorkspaceChanged();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public setCurrentTrust(trust: boolean): void {
|
|
33
|
+
this.currentTrust = trust;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public getCurrentTrust(): boolean | undefined {
|
|
37
|
+
return this.currentTrust;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public override isWorkspaceTrustResolved(): boolean {
|
|
41
|
+
return super.isWorkspaceTrustResolved();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('WorkspaceTrustService', () => {
|
|
46
|
+
let service: TestableWorkspaceTrustService;
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
service = new TestableWorkspaceTrustService();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('handleWorkspaceChanged', () => {
|
|
53
|
+
let resolveWorkspaceTrustStub: sinon.SinonStub;
|
|
54
|
+
let getWorkspaceTrustStub: sinon.SinonStub;
|
|
55
|
+
let updateRestrictedModeIndicatorStub: sinon.SinonStub;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
resolveWorkspaceTrustStub = sinon.stub(service as unknown as { resolveWorkspaceTrust: () => Promise<void> }, 'resolveWorkspaceTrust').resolves();
|
|
59
|
+
getWorkspaceTrustStub = sinon.stub(service, 'getWorkspaceTrust').resolves(true);
|
|
60
|
+
updateRestrictedModeIndicatorStub = sinon.stub(
|
|
61
|
+
service as unknown as { updateRestrictedModeIndicator: (trust: boolean) => void },
|
|
62
|
+
'updateRestrictedModeIndicator'
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
sinon.restore();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should reset trust state when workspace changes', async () => {
|
|
71
|
+
service.setCurrentTrust(true);
|
|
72
|
+
|
|
73
|
+
await service.testHandleWorkspaceChanged();
|
|
74
|
+
|
|
75
|
+
expect(service.getCurrentTrust()).to.be.undefined;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should re-evaluate trust when workspace changes', async () => {
|
|
79
|
+
service.setCurrentTrust(true);
|
|
80
|
+
|
|
81
|
+
await service.testHandleWorkspaceChanged();
|
|
82
|
+
|
|
83
|
+
expect(resolveWorkspaceTrustStub.calledOnce).to.be.true;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should update restricted mode indicator after workspace change if not trusted', async () => {
|
|
87
|
+
getWorkspaceTrustStub.resolves(false);
|
|
88
|
+
|
|
89
|
+
await service.testHandleWorkspaceChanged();
|
|
90
|
+
|
|
91
|
+
expect(updateRestrictedModeIndicatorStub.calledOnceWith(false)).to.be.true;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should reset workspaceTrust deferred to unresolved state', async () => {
|
|
95
|
+
// First resolve the trust
|
|
96
|
+
service.setCurrentTrust(true);
|
|
97
|
+
|
|
98
|
+
await service.testHandleWorkspaceChanged();
|
|
99
|
+
|
|
100
|
+
// After workspace change, it should be reset and resolved again via resolveWorkspaceTrust
|
|
101
|
+
expect(resolveWorkspaceTrustStub.calledOnce).to.be.true;
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('handlePreferenceChange', () => {
|
|
106
|
+
let isWorkspaceInTrustedFoldersStub: sinon.SinonStub;
|
|
107
|
+
let setWorkspaceTrustStub: sinon.SinonStub;
|
|
108
|
+
let workspaceTrustPrefStub: { [key: string]: unknown };
|
|
109
|
+
let workspaceServiceStub: { workspace: unknown };
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
isWorkspaceInTrustedFoldersStub = sinon.stub(service as unknown as { isWorkspaceInTrustedFolders: () => boolean }, 'isWorkspaceInTrustedFolders');
|
|
113
|
+
setWorkspaceTrustStub = sinon.stub(service, 'setWorkspaceTrust');
|
|
114
|
+
// Mock workspaceTrustPref - default emptyWindow to false so trusted folders logic runs
|
|
115
|
+
workspaceTrustPrefStub = { [WORKSPACE_TRUST_EMPTY_WINDOW]: false };
|
|
116
|
+
(service as unknown as { workspaceTrustPref: { [key: string]: unknown } }).workspaceTrustPref = workspaceTrustPrefStub;
|
|
117
|
+
// Mock workspaceService with a workspace (non-empty)
|
|
118
|
+
workspaceServiceStub = { workspace: { resource: { toString: () => 'file:///some/workspace' } } };
|
|
119
|
+
(service as unknown as { workspaceService: { workspace: unknown } }).workspaceService = workspaceServiceStub;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
sinon.restore();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should update trust to true when folder is added to trustedFolders', async () => {
|
|
127
|
+
service.setCurrentTrust(false);
|
|
128
|
+
isWorkspaceInTrustedFoldersStub.returns(true);
|
|
129
|
+
|
|
130
|
+
const change: PreferenceChange = {
|
|
131
|
+
preferenceName: WORKSPACE_TRUST_TRUSTED_FOLDERS,
|
|
132
|
+
scope: PreferenceScope.User,
|
|
133
|
+
domain: [],
|
|
134
|
+
newValue: ['/some/path'],
|
|
135
|
+
oldValue: [],
|
|
136
|
+
affects: () => true
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await service.testHandlePreferenceChange(change);
|
|
140
|
+
|
|
141
|
+
expect(setWorkspaceTrustStub.calledOnceWith(true)).to.be.true;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should update trust to false when folder is removed from trustedFolders', async () => {
|
|
145
|
+
service.setCurrentTrust(true);
|
|
146
|
+
isWorkspaceInTrustedFoldersStub.returns(false);
|
|
147
|
+
|
|
148
|
+
const change: PreferenceChange = {
|
|
149
|
+
preferenceName: WORKSPACE_TRUST_TRUSTED_FOLDERS,
|
|
150
|
+
scope: PreferenceScope.User,
|
|
151
|
+
domain: [],
|
|
152
|
+
newValue: [],
|
|
153
|
+
oldValue: ['/some/path'],
|
|
154
|
+
affects: () => true
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await service.testHandlePreferenceChange(change);
|
|
158
|
+
|
|
159
|
+
expect(setWorkspaceTrustStub.calledOnceWith(false)).to.be.true;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should not update trust when trustedFolders change does not affect current workspace', async () => {
|
|
163
|
+
service.setCurrentTrust(false);
|
|
164
|
+
isWorkspaceInTrustedFoldersStub.returns(false);
|
|
165
|
+
|
|
166
|
+
const change: PreferenceChange = {
|
|
167
|
+
preferenceName: WORKSPACE_TRUST_TRUSTED_FOLDERS,
|
|
168
|
+
scope: PreferenceScope.User,
|
|
169
|
+
domain: [],
|
|
170
|
+
newValue: ['/other/path'],
|
|
171
|
+
oldValue: [],
|
|
172
|
+
affects: () => true
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await service.testHandlePreferenceChange(change);
|
|
176
|
+
|
|
177
|
+
expect(setWorkspaceTrustStub.called).to.be.false;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('emptyWindow setting changes', () => {
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
// Reset workspace to undefined for empty window tests
|
|
183
|
+
workspaceServiceStub.workspace = undefined;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should update trust to true when emptyWindow setting changes to true for empty window', async () => {
|
|
187
|
+
service.setCurrentTrust(false);
|
|
188
|
+
workspaceServiceStub.workspace = undefined;
|
|
189
|
+
|
|
190
|
+
const change: PreferenceChange = {
|
|
191
|
+
preferenceName: WORKSPACE_TRUST_EMPTY_WINDOW,
|
|
192
|
+
scope: PreferenceScope.User,
|
|
193
|
+
domain: [],
|
|
194
|
+
newValue: true,
|
|
195
|
+
oldValue: false,
|
|
196
|
+
affects: () => true
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await service.testHandlePreferenceChange(change);
|
|
200
|
+
|
|
201
|
+
expect(setWorkspaceTrustStub.calledOnceWith(true)).to.be.true;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should update trust to false when emptyWindow setting changes to false for empty window', async () => {
|
|
205
|
+
service.setCurrentTrust(true);
|
|
206
|
+
workspaceServiceStub.workspace = undefined;
|
|
207
|
+
|
|
208
|
+
const change: PreferenceChange = {
|
|
209
|
+
preferenceName: WORKSPACE_TRUST_EMPTY_WINDOW,
|
|
210
|
+
scope: PreferenceScope.User,
|
|
211
|
+
domain: [],
|
|
212
|
+
newValue: false,
|
|
213
|
+
oldValue: true,
|
|
214
|
+
affects: () => true
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
await service.testHandlePreferenceChange(change);
|
|
218
|
+
|
|
219
|
+
expect(setWorkspaceTrustStub.calledOnceWith(false)).to.be.true;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should not update trust when emptyWindow setting changes but workspace is open', async () => {
|
|
223
|
+
service.setCurrentTrust(false);
|
|
224
|
+
workspaceServiceStub.workspace = { resource: { toString: () => 'file:///some/path' } };
|
|
225
|
+
|
|
226
|
+
const change: PreferenceChange = {
|
|
227
|
+
preferenceName: WORKSPACE_TRUST_EMPTY_WINDOW,
|
|
228
|
+
scope: PreferenceScope.User,
|
|
229
|
+
domain: [],
|
|
230
|
+
newValue: true,
|
|
231
|
+
oldValue: false,
|
|
232
|
+
affects: () => true
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
await service.testHandlePreferenceChange(change);
|
|
236
|
+
|
|
237
|
+
expect(setWorkspaceTrustStub.called).to.be.false;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should not update trust when emptyWindow setting changes but trust already matches', async () => {
|
|
241
|
+
service.setCurrentTrust(true);
|
|
242
|
+
workspaceServiceStub.workspace = undefined;
|
|
243
|
+
|
|
244
|
+
const change: PreferenceChange = {
|
|
245
|
+
preferenceName: WORKSPACE_TRUST_EMPTY_WINDOW,
|
|
246
|
+
scope: PreferenceScope.User,
|
|
247
|
+
domain: [],
|
|
248
|
+
newValue: true,
|
|
249
|
+
oldValue: false,
|
|
250
|
+
affects: () => true
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await service.testHandlePreferenceChange(change);
|
|
254
|
+
|
|
255
|
+
expect(setWorkspaceTrustStub.called).to.be.false;
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -15,20 +15,27 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { ConfirmDialog, Dialog, StorageService } from '@theia/core/lib/browser';
|
|
18
|
-
import {
|
|
18
|
+
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';
|
|
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, 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';
|
|
30
36
|
|
|
31
37
|
const STORAGE_TRUSTED = 'trusted';
|
|
38
|
+
export const WORKSPACE_TRUST_STATUS_BAR_ID = 'workspace-trust-status';
|
|
32
39
|
|
|
33
40
|
@injectable()
|
|
34
41
|
export class WorkspaceTrustService {
|
|
@@ -47,13 +54,26 @@ export class WorkspaceTrustService {
|
|
|
47
54
|
@inject(WorkspaceTrustPreferences)
|
|
48
55
|
protected readonly workspaceTrustPref: WorkspaceTrustPreferences;
|
|
49
56
|
|
|
57
|
+
@inject(PreferenceSchemaService)
|
|
58
|
+
protected readonly preferenceSchemaService: PreferenceSchemaService;
|
|
59
|
+
|
|
50
60
|
@inject(WindowService)
|
|
51
61
|
protected readonly windowService: WindowService;
|
|
52
62
|
|
|
53
63
|
@inject(ContextKeyService)
|
|
54
64
|
protected readonly contextKeyService: ContextKeyService;
|
|
55
65
|
|
|
66
|
+
@inject(StatusBar)
|
|
67
|
+
protected readonly statusBar: StatusBar;
|
|
68
|
+
|
|
56
69
|
protected workspaceTrust = new Deferred<boolean>();
|
|
70
|
+
protected currentTrust: boolean | undefined;
|
|
71
|
+
protected pendingTrustDialog: Deferred<boolean> | undefined;
|
|
72
|
+
|
|
73
|
+
protected readonly onDidChangeWorkspaceTrustEmitter = new Emitter<boolean>();
|
|
74
|
+
readonly onDidChangeWorkspaceTrust: Event<boolean> = this.onDidChangeWorkspaceTrustEmitter.event;
|
|
75
|
+
|
|
76
|
+
protected readonly toDispose = new DisposableCollection(this.onDidChangeWorkspaceTrustEmitter);
|
|
57
77
|
|
|
58
78
|
@postConstruct()
|
|
59
79
|
protected init(): void {
|
|
@@ -62,8 +82,31 @@ export class WorkspaceTrustService {
|
|
|
62
82
|
|
|
63
83
|
protected async doInit(): Promise<void> {
|
|
64
84
|
await this.workspaceService.ready;
|
|
85
|
+
await this.workspaceTrustPref.ready;
|
|
86
|
+
await this.preferenceSchemaService.ready;
|
|
65
87
|
await this.resolveWorkspaceTrust();
|
|
66
|
-
this.
|
|
88
|
+
this.toDispose.push(
|
|
89
|
+
this.preferences.onPreferenceChanged(change => this.handlePreferenceChange(change))
|
|
90
|
+
);
|
|
91
|
+
this.toDispose.push(
|
|
92
|
+
this.workspaceService.onWorkspaceChanged(() => this.handleWorkspaceChanged())
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Show status bar item if starting in restricted mode
|
|
96
|
+
const initialTrust = await this.getWorkspaceTrust();
|
|
97
|
+
this.updateRestrictedModeIndicator(initialTrust);
|
|
98
|
+
|
|
99
|
+
// React to trust changes
|
|
100
|
+
this.toDispose.push(
|
|
101
|
+
this.onDidChangeWorkspaceTrust(trust => {
|
|
102
|
+
this.updateRestrictedModeIndicator(trust);
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@preDestroy()
|
|
108
|
+
protected onStop(): void {
|
|
109
|
+
this.toDispose.dispose();
|
|
67
110
|
}
|
|
68
111
|
|
|
69
112
|
getWorkspaceTrust(): Promise<boolean> {
|
|
@@ -76,18 +119,35 @@ export class WorkspaceTrustService {
|
|
|
76
119
|
if (trust !== undefined) {
|
|
77
120
|
await this.storeWorkspaceTrust(trust);
|
|
78
121
|
this.contextKeyService.setContext('isWorkspaceTrusted', trust);
|
|
122
|
+
this.currentTrust = trust;
|
|
79
123
|
this.workspaceTrust.resolve(trust);
|
|
124
|
+
this.onDidChangeWorkspaceTrustEmitter.fire(trust);
|
|
125
|
+
if (trust && this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED]) {
|
|
126
|
+
await this.addToTrustedFolders();
|
|
127
|
+
}
|
|
80
128
|
}
|
|
81
129
|
}
|
|
82
130
|
}
|
|
83
131
|
|
|
132
|
+
setWorkspaceTrust(trusted: boolean): void {
|
|
133
|
+
if (this.currentTrust === trusted) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.currentTrust = trusted;
|
|
137
|
+
this.contextKeyService.setContext('isWorkspaceTrusted', trusted);
|
|
138
|
+
if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
|
|
139
|
+
this.storeWorkspaceTrust(trusted);
|
|
140
|
+
}
|
|
141
|
+
this.onDidChangeWorkspaceTrustEmitter.fire(trusted);
|
|
142
|
+
}
|
|
143
|
+
|
|
84
144
|
protected isWorkspaceTrustResolved(): boolean {
|
|
85
145
|
return this.workspaceTrust.state !== 'unresolved';
|
|
86
146
|
}
|
|
87
147
|
|
|
88
148
|
protected async calculateWorkspaceTrust(): Promise<boolean | undefined> {
|
|
89
|
-
|
|
90
|
-
|
|
149
|
+
const trustEnabled = this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED];
|
|
150
|
+
if (!trustEnabled) {
|
|
91
151
|
return true;
|
|
92
152
|
}
|
|
93
153
|
|
|
@@ -95,11 +155,89 @@ export class WorkspaceTrustService {
|
|
|
95
155
|
return true;
|
|
96
156
|
}
|
|
97
157
|
|
|
158
|
+
if (this.isWorkspaceInTrustedFolders()) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
98
162
|
if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.NEVER) {
|
|
99
163
|
return false;
|
|
100
164
|
}
|
|
101
165
|
|
|
102
|
-
|
|
166
|
+
// For ONCE mode, check stored trust first
|
|
167
|
+
if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
|
|
168
|
+
const storedTrust = await this.loadWorkspaceTrust();
|
|
169
|
+
if (storedTrust !== undefined) {
|
|
170
|
+
return storedTrust;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// For ALWAYS mode or ONCE mode with no stored decision, show dialog
|
|
175
|
+
return this.showTrustPromptDialog();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
protected async showTrustPromptDialog(): Promise<boolean> {
|
|
179
|
+
// If dialog is already open, wait for its result
|
|
180
|
+
if (this.pendingTrustDialog) {
|
|
181
|
+
return this.pendingTrustDialog.promise;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.pendingTrustDialog = new Deferred<boolean>();
|
|
185
|
+
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
|
+
});
|
|
198
|
+
|
|
199
|
+
const result = await dialog.open();
|
|
200
|
+
const trusted = result === true;
|
|
201
|
+
this.pendingTrustDialog.resolve(trusted);
|
|
202
|
+
return trusted;
|
|
203
|
+
} catch (e) {
|
|
204
|
+
this.pendingTrustDialog.resolve(false);
|
|
205
|
+
throw e;
|
|
206
|
+
} finally {
|
|
207
|
+
this.pendingTrustDialog = undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async addToTrustedFolders(): Promise<void> {
|
|
212
|
+
const workspaceUri = this.workspaceService.workspace?.resource;
|
|
213
|
+
if (!workspaceUri) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!this.isWorkspaceInTrustedFolders()) {
|
|
217
|
+
const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
|
|
218
|
+
await this.preferences.set(
|
|
219
|
+
WORKSPACE_TRUST_TRUSTED_FOLDERS,
|
|
220
|
+
[...currentFolders, workspaceUri.toString()],
|
|
221
|
+
PreferenceScope.User
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
protected isWorkspaceInTrustedFolders(): boolean {
|
|
227
|
+
const workspaceUri = this.workspaceService.workspace?.resource;
|
|
228
|
+
if (!workspaceUri) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const trustedFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
|
|
232
|
+
const caseSensitive = !OS.backend.isWindows;
|
|
233
|
+
return trustedFolders.some(folder => {
|
|
234
|
+
try {
|
|
235
|
+
const folderUri = new URI(folder).normalizePath();
|
|
236
|
+
return workspaceUri.normalizePath().isEqual(folderUri, caseSensitive);
|
|
237
|
+
} catch {
|
|
238
|
+
return false; // Invalid URI in preferences
|
|
239
|
+
}
|
|
240
|
+
});
|
|
103
241
|
}
|
|
104
242
|
|
|
105
243
|
protected async loadWorkspaceTrust(): Promise<boolean | undefined> {
|
|
@@ -115,6 +253,19 @@ export class WorkspaceTrustService {
|
|
|
115
253
|
}
|
|
116
254
|
|
|
117
255
|
protected async handlePreferenceChange(change: PreferenceChange): Promise<void> {
|
|
256
|
+
// Handle trustedFolders changes regardless of scope
|
|
257
|
+
if (change.preferenceName === WORKSPACE_TRUST_TRUSTED_FOLDERS) {
|
|
258
|
+
// For empty windows with emptyWindow setting enabled, trust should remain true
|
|
259
|
+
if (this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW] && !this.workspaceService.workspace) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const isNowInTrustedFolders = this.isWorkspaceInTrustedFolders();
|
|
263
|
+
if (isNowInTrustedFolders !== this.currentTrust) {
|
|
264
|
+
this.setWorkspaceTrust(isNowInTrustedFolders);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
118
269
|
if (change.scope === PreferenceScope.User) {
|
|
119
270
|
if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT && change.newValue !== WorkspaceTrustPrompt.ONCE) {
|
|
120
271
|
this.storage.setData(STORAGE_TRUSTED, undefined);
|
|
@@ -125,12 +276,34 @@ export class WorkspaceTrustService {
|
|
|
125
276
|
this.windowService.reload();
|
|
126
277
|
}
|
|
127
278
|
|
|
128
|
-
if (change.preferenceName === WORKSPACE_TRUST_ENABLED
|
|
279
|
+
if (change.preferenceName === WORKSPACE_TRUST_ENABLED) {
|
|
129
280
|
this.resolveWorkspaceTrust();
|
|
130
281
|
}
|
|
282
|
+
|
|
283
|
+
// Handle emptyWindow setting change for empty windows
|
|
284
|
+
if (change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW && !this.workspaceService.workspace) {
|
|
285
|
+
// For empty windows, directly update trust based on the new setting value
|
|
286
|
+
const shouldTrust = !!change.newValue;
|
|
287
|
+
if (this.currentTrust !== shouldTrust) {
|
|
288
|
+
this.setWorkspaceTrust(shouldTrust);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
131
291
|
}
|
|
132
292
|
}
|
|
133
293
|
|
|
294
|
+
protected async handleWorkspaceChanged(): Promise<void> {
|
|
295
|
+
// Reset trust state for the new workspace
|
|
296
|
+
this.workspaceTrust = new Deferred<boolean>();
|
|
297
|
+
this.currentTrust = undefined;
|
|
298
|
+
|
|
299
|
+
// Re-evaluate trust for the new workspace
|
|
300
|
+
await this.resolveWorkspaceTrust();
|
|
301
|
+
|
|
302
|
+
// Update status bar indicator
|
|
303
|
+
const trust = await this.getWorkspaceTrust();
|
|
304
|
+
this.updateRestrictedModeIndicator(trust);
|
|
305
|
+
}
|
|
306
|
+
|
|
134
307
|
protected async confirmRestart(): Promise<boolean> {
|
|
135
308
|
const shouldRestart = await new ConfirmDialog({
|
|
136
309
|
title: nls.localizeByDefault('A setting has changed that requires a restart to take effect.'),
|
|
@@ -141,13 +314,33 @@ export class WorkspaceTrustService {
|
|
|
141
314
|
return shouldRestart === true;
|
|
142
315
|
}
|
|
143
316
|
|
|
317
|
+
protected updateRestrictedModeIndicator(trusted: boolean): void {
|
|
318
|
+
if (trusted) {
|
|
319
|
+
this.hideRestrictedModeStatusBarItem();
|
|
320
|
+
} else {
|
|
321
|
+
this.showRestrictedModeStatusBarItem();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
protected showRestrictedModeStatusBarItem(): void {
|
|
326
|
+
this.statusBar.setElement(WORKSPACE_TRUST_STATUS_BAR_ID, {
|
|
327
|
+
text: '$(shield) ' + nls.localizeByDefault('Restricted Mode'),
|
|
328
|
+
alignment: StatusBarAlignment.LEFT,
|
|
329
|
+
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.'),
|
|
332
|
+
command: WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
protected hideRestrictedModeStatusBarItem(): void {
|
|
337
|
+
this.statusBar.removeElement(WORKSPACE_TRUST_STATUS_BAR_ID);
|
|
338
|
+
}
|
|
339
|
+
|
|
144
340
|
async requestWorkspaceTrust(): Promise<boolean | undefined> {
|
|
145
341
|
if (!this.isWorkspaceTrustResolved()) {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
Dialog.YES, Dialog.NO);
|
|
149
|
-
const trusted = isTrusted === Dialog.YES;
|
|
150
|
-
this.resolveWorkspaceTrust(trusted);
|
|
342
|
+
const trusted = await this.showTrustPromptDialog();
|
|
343
|
+
await this.resolveWorkspaceTrust(trusted);
|
|
151
344
|
}
|
|
152
345
|
return this.workspaceTrust.promise;
|
|
153
346
|
}
|