@theia/plugin-ext 1.70.0-next.34 → 1.70.0-next.43
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/common/plugin-api-rpc-model.d.ts +6 -1
- package/lib/common/plugin-api-rpc-model.d.ts.map +1 -1
- package/lib/common/plugin-api-rpc-model.js +2 -1
- package/lib/common/plugin-api-rpc-model.js.map +1 -1
- package/lib/common/plugin-protocol.d.ts +8 -0
- package/lib/common/plugin-protocol.d.ts.map +1 -1
- package/lib/common/plugin-protocol.js.map +1 -1
- package/lib/hosted/browser/hosted-plugin.d.ts +2 -0
- package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
- package/lib/hosted/browser/hosted-plugin.js +7 -0
- package/lib/hosted/browser/hosted-plugin.js.map +1 -1
- package/lib/hosted/common/hosted-plugin.d.ts +3 -0
- package/lib/hosted/common/hosted-plugin.d.ts.map +1 -1
- package/lib/hosted/common/hosted-plugin.js +13 -0
- package/lib/hosted/common/hosted-plugin.js.map +1 -1
- package/lib/hosted/common/hosted-plugin.spec.d.ts +2 -0
- package/lib/hosted/common/hosted-plugin.spec.d.ts.map +1 -0
- package/lib/hosted/common/hosted-plugin.spec.js +308 -0
- package/lib/hosted/common/hosted-plugin.spec.js.map +1 -0
- package/lib/hosted/node/hosted-plugin-process.d.ts +2 -0
- package/lib/hosted/node/hosted-plugin-process.d.ts.map +1 -1
- package/lib/hosted/node/hosted-plugin-process.js +8 -0
- package/lib/hosted/node/hosted-plugin-process.js.map +1 -1
- package/lib/hosted/node/plugin-host-navigator-override.d.ts +12 -0
- package/lib/hosted/node/plugin-host-navigator-override.d.ts.map +1 -0
- package/lib/hosted/node/plugin-host-navigator-override.js +37 -0
- package/lib/hosted/node/plugin-host-navigator-override.js.map +1 -0
- package/lib/hosted/node/plugin-host-navigator-override.spec.d.ts +2 -0
- package/lib/hosted/node/plugin-host-navigator-override.spec.d.ts.map +1 -0
- package/lib/hosted/node/plugin-host-navigator-override.spec.js +49 -0
- package/lib/hosted/node/plugin-host-navigator-override.spec.js.map +1 -0
- package/lib/hosted/node/plugin-host.js +3 -0
- package/lib/hosted/node/plugin-host.js.map +1 -1
- package/lib/hosted/node/scanners/scanner-theia.d.ts +1 -0
- package/lib/hosted/node/scanners/scanner-theia.d.ts.map +1 -1
- package/lib/hosted/node/scanners/scanner-theia.js +7 -0
- package/lib/hosted/node/scanners/scanner-theia.js.map +1 -1
- package/lib/hosted/node/scanners/scanner-theia.spec.d.ts +2 -0
- package/lib/hosted/node/scanners/scanner-theia.spec.d.ts.map +1 -0
- package/lib/hosted/node/scanners/scanner-theia.spec.js +93 -0
- package/lib/hosted/node/scanners/scanner-theia.spec.js.map +1 -0
- package/lib/main/browser/languages-main.d.ts +2 -2
- package/lib/main/browser/languages-main.d.ts.map +1 -1
- package/lib/main/browser/languages-main.js +8 -9
- package/lib/main/browser/languages-main.js.map +1 -1
- package/lib/main/browser/plugin-ext-frontend-module.d.ts.map +1 -1
- package/lib/main/browser/plugin-ext-frontend-module.js +26 -0
- package/lib/main/browser/plugin-ext-frontend-module.js.map +1 -1
- package/lib/main/browser/plugin-ext-widget.d.ts +1 -1
- package/lib/main/browser/plugin-ext-widget.d.ts.map +1 -1
- package/lib/main/browser/plugin-ext-widget.js +10 -3
- package/lib/main/browser/plugin-ext-widget.js.map +1 -1
- package/lib/main/browser/text-editor-main.js +2 -2
- package/lib/main/common/plugin-host-environment-preferences.d.ts +14 -0
- package/lib/main/common/plugin-host-environment-preferences.d.ts.map +1 -0
- package/lib/main/common/plugin-host-environment-preferences.js +43 -0
- package/lib/main/common/plugin-host-environment-preferences.js.map +1 -0
- package/lib/main/node/plugin-ext-backend-module.d.ts.map +1 -1
- package/lib/main/node/plugin-ext-backend-module.js +5 -0
- package/lib/main/node/plugin-ext-backend-module.js.map +1 -1
- package/lib/main/node/plugin-host-navigator-state-initializer.d.ts +17 -0
- package/lib/main/node/plugin-host-navigator-state-initializer.d.ts.map +1 -0
- package/lib/main/node/plugin-host-navigator-state-initializer.js +53 -0
- package/lib/main/node/plugin-host-navigator-state-initializer.js.map +1 -0
- package/lib/plugin/file-system-event-service-ext-impl.d.ts +1 -1
- package/lib/plugin/file-system-event-service-ext-impl.d.ts.map +1 -1
- package/lib/plugin/file-system-ext-impl.d.ts +1 -1
- package/lib/plugin/file-system-ext-impl.d.ts.map +1 -1
- package/lib/plugin/file-system-ext-impl.js +5 -1
- package/lib/plugin/file-system-ext-impl.js.map +1 -1
- package/lib/plugin/types-impl.d.ts +2 -1
- package/lib/plugin/types-impl.d.ts.map +1 -1
- package/lib/plugin/types-impl.js.map +1 -1
- package/package.json +30 -30
- package/src/common/plugin-api-rpc-model.ts +10 -1
- package/src/common/plugin-protocol.ts +9 -1
- package/src/hosted/browser/hosted-plugin.ts +6 -0
- package/src/hosted/common/hosted-plugin.spec.ts +377 -0
- package/src/hosted/common/hosted-plugin.ts +18 -1
- package/src/hosted/node/hosted-plugin-process.ts +7 -0
- package/src/hosted/node/plugin-host-navigator-override.spec.ts +53 -0
- package/src/hosted/node/plugin-host-navigator-override.ts +34 -0
- package/src/hosted/node/plugin-host.ts +4 -0
- package/src/hosted/node/scanners/scanner-theia.spec.ts +112 -0
- package/src/hosted/node/scanners/scanner-theia.ts +8 -0
- package/src/main/browser/languages-main.ts +32 -51
- package/src/main/browser/plugin-ext-frontend-module.ts +31 -1
- package/src/main/browser/plugin-ext-widget.tsx +13 -3
- package/src/main/browser/style/plugin-sidebar.css +13 -0
- package/src/main/common/plugin-host-environment-preferences.ts +48 -0
- package/src/main/node/plugin-ext-backend-module.ts +5 -1
- package/src/main/node/plugin-host-navigator-state-initializer.ts +47 -0
- package/src/plugin/file-system-event-service-ext-impl.ts +1 -1
- package/src/plugin/file-system-ext-impl.ts +1 -2
- package/src/plugin/types-impl.ts +2 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
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 { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
|
19
|
+
import { DeployedPlugin, PluginIdentifiers } from '../../common/plugin-protocol';
|
|
20
|
+
import { AbstractHostedPluginSupport, PluginContributions } from './hosted-plugin';
|
|
21
|
+
import { Measurement } from '@theia/core/lib/common/performance/measurement';
|
|
22
|
+
|
|
23
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
24
|
+
|
|
25
|
+
function createMockDeployedPlugin(id: string, untrustedWorkspacesSupport?: boolean | 'limited'): DeployedPlugin {
|
|
26
|
+
return {
|
|
27
|
+
metadata: {
|
|
28
|
+
host: 'main',
|
|
29
|
+
model: {
|
|
30
|
+
id,
|
|
31
|
+
name: id.split('.')[1] || id,
|
|
32
|
+
publisher: id.split('.')[0] || 'test',
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
displayName: id,
|
|
35
|
+
description: '',
|
|
36
|
+
engine: { type: 'theiaPlugin' as any, version: '1.0.0' },
|
|
37
|
+
entryPoint: { backend: 'main.js' },
|
|
38
|
+
packageUri: '',
|
|
39
|
+
packagePath: '',
|
|
40
|
+
untrustedWorkspacesSupport
|
|
41
|
+
},
|
|
42
|
+
lifecycle: { startMethod: 'activate', stopMethod: 'deactivate' } as any,
|
|
43
|
+
outOfSync: false
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createNoopMeasurement(): Measurement {
|
|
49
|
+
return {
|
|
50
|
+
stop: () => 0,
|
|
51
|
+
name: 'test',
|
|
52
|
+
log: () => { },
|
|
53
|
+
debug: () => { },
|
|
54
|
+
info: () => { },
|
|
55
|
+
warn: () => { },
|
|
56
|
+
error: () => { }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Minimal concrete subclass of AbstractHostedPluginSupport for testing.
|
|
62
|
+
* Overrides all abstract methods with no-ops/stubs.
|
|
63
|
+
*/
|
|
64
|
+
class TestHostedPluginSupport extends AbstractHostedPluginSupport<any, any> {
|
|
65
|
+
constructor() {
|
|
66
|
+
super('test-client');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected createTheiaReadyPromise(): Promise<unknown> {
|
|
70
|
+
return Promise.resolve();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected acceptPlugin(_plugin: DeployedPlugin): boolean | DeployedPlugin {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected handleContributions(_plugin: DeployedPlugin): Disposable {
|
|
78
|
+
return Disposable.NULL;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
protected async obtainManager(
|
|
82
|
+
_host: string, _hostContributions: PluginContributions[],
|
|
83
|
+
_toDisconnect: DisposableCollection
|
|
84
|
+
): Promise<undefined> {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected async getStoragePath(): Promise<string | undefined> {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected async getHostGlobalStoragePath(): Promise<string> {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
protected override measure(_name: string): Measurement {
|
|
97
|
+
return createNoopMeasurement();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Populate the contributions map with mock deployed plugins in the given state.
|
|
102
|
+
*/
|
|
103
|
+
addPlugin(plugin: DeployedPlugin, state = PluginContributions.State.INITIALIZING): void {
|
|
104
|
+
const pluginId = PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model);
|
|
105
|
+
const contributions = new PluginContributions(plugin);
|
|
106
|
+
contributions.state = state;
|
|
107
|
+
this.contributions.set(pluginId, contributions);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Expose the protected loadContributions for testing.
|
|
112
|
+
*/
|
|
113
|
+
testLoadContributions(): Map<string, PluginContributions[]> {
|
|
114
|
+
const toDisconnect = new DisposableCollection(Disposable.create(() => { }));
|
|
115
|
+
return this.loadContributions(toDisconnect);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setWorkspaceTrusted(trusted: boolean): void {
|
|
119
|
+
this.workspaceTrusted = trusted;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
clearDisabledByTrust(): void {
|
|
123
|
+
this._disabledByTrust.clear();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
simulateLoadCycle(): Map<string, PluginContributions[]> {
|
|
127
|
+
this._disabledByTrust.clear();
|
|
128
|
+
const result = this.testLoadContributions();
|
|
129
|
+
if (this._disabledByTrust.size > 0) {
|
|
130
|
+
this.onDidChangePluginsEmitter.fire(undefined);
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe('AbstractHostedPluginSupport - workspace trust filtering', () => {
|
|
137
|
+
let support: TestHostedPluginSupport;
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
support = new TestHostedPluginSupport();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should skip plugin with untrustedWorkspacesSupport: false when workspace is untrusted', () => {
|
|
144
|
+
support.setWorkspaceTrusted(false);
|
|
145
|
+
const plugin = createMockDeployedPlugin('test.untrusted-false', false);
|
|
146
|
+
support.addPlugin(plugin);
|
|
147
|
+
|
|
148
|
+
const result = support.testLoadContributions();
|
|
149
|
+
|
|
150
|
+
// Plugin should not be in the returned host contributions
|
|
151
|
+
let pluginFound = false;
|
|
152
|
+
for (const contributions of result.values()) {
|
|
153
|
+
for (const c of contributions) {
|
|
154
|
+
if (c.plugin.metadata.model.id === 'test.untrusted-false') {
|
|
155
|
+
pluginFound = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
expect(pluginFound).to.equal(false);
|
|
160
|
+
expect(support.disabledByTrust.has('test.untrusted-false')).to.equal(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should load plugin with untrustedWorkspacesSupport: true when workspace is untrusted', () => {
|
|
164
|
+
support.setWorkspaceTrusted(false);
|
|
165
|
+
const plugin = createMockDeployedPlugin('test.untrusted-true', true);
|
|
166
|
+
support.addPlugin(plugin);
|
|
167
|
+
|
|
168
|
+
const result = support.testLoadContributions();
|
|
169
|
+
|
|
170
|
+
let pluginFound = false;
|
|
171
|
+
for (const contributions of result.values()) {
|
|
172
|
+
for (const c of contributions) {
|
|
173
|
+
if (c.plugin.metadata.model.id === 'test.untrusted-true') {
|
|
174
|
+
pluginFound = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
expect(pluginFound).to.equal(true);
|
|
179
|
+
expect(support.disabledByTrust.has('test.untrusted-true')).to.equal(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should load plugin with untrustedWorkspacesSupport: "limited" when workspace is untrusted', () => {
|
|
183
|
+
support.setWorkspaceTrusted(false);
|
|
184
|
+
const plugin = createMockDeployedPlugin('test.untrusted-limited', 'limited');
|
|
185
|
+
support.addPlugin(plugin);
|
|
186
|
+
|
|
187
|
+
const result = support.testLoadContributions();
|
|
188
|
+
|
|
189
|
+
let pluginFound = false;
|
|
190
|
+
for (const contributions of result.values()) {
|
|
191
|
+
for (const c of contributions) {
|
|
192
|
+
if (c.plugin.metadata.model.id === 'test.untrusted-limited') {
|
|
193
|
+
pluginFound = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
expect(pluginFound).to.equal(true);
|
|
198
|
+
expect(support.disabledByTrust.has('test.untrusted-limited')).to.equal(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should load plugin with untrustedWorkspacesSupport: undefined when workspace is untrusted', () => {
|
|
202
|
+
support.setWorkspaceTrusted(false);
|
|
203
|
+
const plugin = createMockDeployedPlugin('test.untrusted-undefined', undefined);
|
|
204
|
+
support.addPlugin(plugin);
|
|
205
|
+
|
|
206
|
+
const result = support.testLoadContributions();
|
|
207
|
+
|
|
208
|
+
let pluginFound = false;
|
|
209
|
+
for (const contributions of result.values()) {
|
|
210
|
+
for (const c of contributions) {
|
|
211
|
+
if (c.plugin.metadata.model.id === 'test.untrusted-undefined') {
|
|
212
|
+
pluginFound = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
expect(pluginFound).to.equal(true);
|
|
217
|
+
expect(support.disabledByTrust.has('test.untrusted-undefined')).to.equal(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should load plugin with untrustedWorkspacesSupport: false when workspace is trusted', () => {
|
|
221
|
+
support.setWorkspaceTrusted(true);
|
|
222
|
+
const plugin = createMockDeployedPlugin('test.trusted-false', false);
|
|
223
|
+
support.addPlugin(plugin);
|
|
224
|
+
|
|
225
|
+
const result = support.testLoadContributions();
|
|
226
|
+
|
|
227
|
+
let pluginFound = false;
|
|
228
|
+
for (const contributions of result.values()) {
|
|
229
|
+
for (const c of contributions) {
|
|
230
|
+
if (c.plugin.metadata.model.id === 'test.trusted-false') {
|
|
231
|
+
pluginFound = true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
expect(pluginFound).to.equal(true);
|
|
236
|
+
expect(support.disabledByTrust.has('test.trusted-false')).to.equal(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should clear disabledByTrust when workspace becomes trusted on re-load', () => {
|
|
240
|
+
// First load: untrusted workspace, plugin should be disabled
|
|
241
|
+
support.setWorkspaceTrusted(false);
|
|
242
|
+
const plugin = createMockDeployedPlugin('test.reload-trust', false);
|
|
243
|
+
support.addPlugin(plugin);
|
|
244
|
+
|
|
245
|
+
support.testLoadContributions();
|
|
246
|
+
expect(support.disabledByTrust.has('test.reload-trust')).to.equal(true);
|
|
247
|
+
expect(support.disabledByTrust.size).to.equal(1);
|
|
248
|
+
|
|
249
|
+
// Second load: simulate doLoad() which clears disabledByTrust before loadContributions
|
|
250
|
+
support.setWorkspaceTrusted(true);
|
|
251
|
+
support.clearDisabledByTrust();
|
|
252
|
+
support.testLoadContributions();
|
|
253
|
+
expect(support.disabledByTrust.size).to.equal(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should correctly populate disabledByTrust with only filtered plugins', () => {
|
|
257
|
+
support.setWorkspaceTrusted(false);
|
|
258
|
+
const blockedPlugin = createMockDeployedPlugin('test.blocked', false);
|
|
259
|
+
const allowedPlugin = createMockDeployedPlugin('test.allowed', true);
|
|
260
|
+
support.addPlugin(blockedPlugin);
|
|
261
|
+
support.addPlugin(allowedPlugin);
|
|
262
|
+
|
|
263
|
+
support.testLoadContributions();
|
|
264
|
+
|
|
265
|
+
expect(support.disabledByTrust.size).to.equal(1);
|
|
266
|
+
expect(support.disabledByTrust.has('test.blocked')).to.equal(true);
|
|
267
|
+
expect(support.disabledByTrust.has('test.allowed')).to.equal(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('plugins deployed after initial load (re-load cycle)', () => {
|
|
271
|
+
it('should disable a newly deployed plugin with untrustedWorkspacesSupport: false on re-load', () => {
|
|
272
|
+
support.setWorkspaceTrusted(false);
|
|
273
|
+
|
|
274
|
+
const initialPlugin = createMockDeployedPlugin('test.initial', false);
|
|
275
|
+
support.addPlugin(initialPlugin);
|
|
276
|
+
support.simulateLoadCycle();
|
|
277
|
+
expect(support.disabledByTrust.has('test.initial')).to.equal(true);
|
|
278
|
+
|
|
279
|
+
const newPlugin = createMockDeployedPlugin('test.new-deploy', false);
|
|
280
|
+
support.addPlugin(newPlugin);
|
|
281
|
+
support.simulateLoadCycle();
|
|
282
|
+
|
|
283
|
+
expect(support.disabledByTrust.has('test.initial')).to.equal(true);
|
|
284
|
+
expect(support.disabledByTrust.has('test.new-deploy')).to.equal(true);
|
|
285
|
+
expect(support.disabledByTrust.size).to.equal(2);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should re-add previously disabled plugins to disabledByTrust on each re-load cycle', () => {
|
|
289
|
+
support.setWorkspaceTrusted(false);
|
|
290
|
+
const plugin = createMockDeployedPlugin('test.persistent', false);
|
|
291
|
+
support.addPlugin(plugin);
|
|
292
|
+
|
|
293
|
+
support.simulateLoadCycle();
|
|
294
|
+
expect(support.disabledByTrust.has('test.persistent')).to.equal(true);
|
|
295
|
+
|
|
296
|
+
support.simulateLoadCycle();
|
|
297
|
+
expect(support.disabledByTrust.has('test.persistent')).to.equal(true);
|
|
298
|
+
|
|
299
|
+
support.simulateLoadCycle();
|
|
300
|
+
expect(support.disabledByTrust.has('test.persistent')).to.equal(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should not add already-started allowed plugins to disabledByTrust on re-load', () => {
|
|
304
|
+
support.setWorkspaceTrusted(false);
|
|
305
|
+
const startedAllowed = createMockDeployedPlugin('test.started-allowed', true);
|
|
306
|
+
support.addPlugin(startedAllowed, PluginContributions.State.STARTED);
|
|
307
|
+
const startedUndefined = createMockDeployedPlugin('test.started-undefined', undefined);
|
|
308
|
+
support.addPlugin(startedUndefined, PluginContributions.State.STARTED);
|
|
309
|
+
|
|
310
|
+
support.simulateLoadCycle();
|
|
311
|
+
|
|
312
|
+
expect(support.disabledByTrust.has('test.started-allowed')).to.equal(false);
|
|
313
|
+
expect(support.disabledByTrust.has('test.started-undefined')).to.equal(false);
|
|
314
|
+
expect(support.disabledByTrust.size).to.equal(0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should add already-started plugins with untrustedWorkspacesSupport: false to disabledByTrust on re-load', () => {
|
|
318
|
+
support.setWorkspaceTrusted(false);
|
|
319
|
+
const startedBlocked = createMockDeployedPlugin('test.started-blocked', false);
|
|
320
|
+
support.addPlugin(startedBlocked, PluginContributions.State.STARTED);
|
|
321
|
+
|
|
322
|
+
support.simulateLoadCycle();
|
|
323
|
+
|
|
324
|
+
expect(support.disabledByTrust.has('test.started-blocked')).to.equal(true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should fire onDidChangePlugins after loadContributions when a newly deployed plugin is disabled by trust', () => {
|
|
328
|
+
support.setWorkspaceTrusted(false);
|
|
329
|
+
const existingPlugin = createMockDeployedPlugin('test.existing', false);
|
|
330
|
+
support.addPlugin(existingPlugin);
|
|
331
|
+
support.simulateLoadCycle();
|
|
332
|
+
|
|
333
|
+
const newPlugin = createMockDeployedPlugin('test.late-deploy', false);
|
|
334
|
+
support.addPlugin(newPlugin);
|
|
335
|
+
|
|
336
|
+
let eventFiredCount = 0;
|
|
337
|
+
support.onDidChangePlugins(() => { eventFiredCount++; });
|
|
338
|
+
|
|
339
|
+
support.simulateLoadCycle();
|
|
340
|
+
|
|
341
|
+
expect(support.disabledByTrust.has('test.late-deploy')).to.equal(true);
|
|
342
|
+
expect(eventFiredCount).to.equal(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should not fire onDidChangePlugins when no plugins are disabled by trust', () => {
|
|
346
|
+
support.setWorkspaceTrusted(false);
|
|
347
|
+
const allowedPlugin = createMockDeployedPlugin('test.allowed-nodeploy', true);
|
|
348
|
+
support.addPlugin(allowedPlugin);
|
|
349
|
+
|
|
350
|
+
let eventFiredCount = 0;
|
|
351
|
+
support.onDidChangePlugins(() => { eventFiredCount++; });
|
|
352
|
+
|
|
353
|
+
support.simulateLoadCycle();
|
|
354
|
+
|
|
355
|
+
expect(support.disabledByTrust.size).to.equal(0);
|
|
356
|
+
expect(eventFiredCount).to.equal(0);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should include both initial and newly deployed disabled plugins in disabledByTrust when workspace is untrusted', () => {
|
|
360
|
+
support.setWorkspaceTrusted(false);
|
|
361
|
+
|
|
362
|
+
const builtin = createMockDeployedPlugin('test.builtin', false);
|
|
363
|
+
support.addPlugin(builtin);
|
|
364
|
+
support.simulateLoadCycle();
|
|
365
|
+
expect(support.disabledByTrust.size).to.equal(1);
|
|
366
|
+
|
|
367
|
+
const userInstalled = createMockDeployedPlugin('test.user-installed', false);
|
|
368
|
+
support.addPlugin(userInstalled);
|
|
369
|
+
support.simulateLoadCycle();
|
|
370
|
+
|
|
371
|
+
expect(support.disabledByTrust.size).to.equal(2);
|
|
372
|
+
expect(support.disabledByTrust.has('test.builtin')).to.equal(true);
|
|
373
|
+
expect(support.disabledByTrust.has('test.user-installed')).to.equal(true);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
});
|
|
@@ -84,6 +84,14 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
|
|
|
84
84
|
|
|
85
85
|
protected readonly activationEvents = new Set<string>();
|
|
86
86
|
|
|
87
|
+
protected workspaceTrusted: boolean = false;
|
|
88
|
+
|
|
89
|
+
protected readonly _disabledByTrust = new Set<string>();
|
|
90
|
+
|
|
91
|
+
get disabledByTrust(): ReadonlySet<string> {
|
|
92
|
+
return this._disabledByTrust;
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
protected readonly onDidChangePluginsEmitter = new Emitter<void>();
|
|
88
96
|
readonly onDidChangePlugins = this.onDidChangePluginsEmitter.event;
|
|
89
97
|
|
|
@@ -150,6 +158,8 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
|
|
|
150
158
|
protected async doLoad(): Promise<void> {
|
|
151
159
|
const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ }));
|
|
152
160
|
|
|
161
|
+
this._disabledByTrust.clear();
|
|
162
|
+
|
|
153
163
|
await this.beforeSyncPlugins(toDisconnect);
|
|
154
164
|
|
|
155
165
|
// process empty plugins as well in order to properly remove stale plugin widgets
|
|
@@ -166,6 +176,9 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
|
|
|
166
176
|
return;
|
|
167
177
|
}
|
|
168
178
|
const contributionsByHost = this.loadContributions(toDisconnect);
|
|
179
|
+
if (this._disabledByTrust.size > 0) {
|
|
180
|
+
this.onDidChangePluginsEmitter.fire(undefined);
|
|
181
|
+
}
|
|
169
182
|
|
|
170
183
|
await this.afterLoadContributions(toDisconnect);
|
|
171
184
|
|
|
@@ -301,9 +314,13 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
|
|
|
301
314
|
const hostContributions = new Map<PluginHost, PluginContributions[]>();
|
|
302
315
|
console.log(`[${this.clientId}] Loading plugin contributions`);
|
|
303
316
|
for (const contributions of this.contributions.values()) {
|
|
317
|
+
if (!this.workspaceTrusted && contributions.plugin.metadata.model.untrustedWorkspacesSupport === false) {
|
|
318
|
+
this._disabledByTrust.add(PluginIdentifiers.componentsToUnversionedId(contributions.plugin.metadata.model));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
304
322
|
const plugin = contributions.plugin.metadata;
|
|
305
323
|
const pluginId = plugin.model.id;
|
|
306
|
-
|
|
307
324
|
if (contributions.state === PluginContributions.State.INITIALIZING) {
|
|
308
325
|
contributions.state = PluginContributions.State.LOADING;
|
|
309
326
|
contributions.push(Disposable.create(() => console.log(`[${pluginId}]: Unloaded plugin.`)));
|
|
@@ -22,6 +22,7 @@ import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
|
22
22
|
import * as cp from 'child_process';
|
|
23
23
|
import { Duplex } from 'stream';
|
|
24
24
|
import { HostedPluginClient, PLUGIN_HOST_BACKEND, PluginHostEnvironmentVariable, ServerPluginRunner } from '../../common/plugin-protocol';
|
|
25
|
+
import { PluginHostNavigatorState } from '../../main/common/plugin-host-environment-preferences';
|
|
25
26
|
import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution';
|
|
26
27
|
import { HostedPluginLocalizationService } from './hosted-plugin-localization-service';
|
|
27
28
|
import { ProcessTerminateMessage, ProcessTerminatedMessage } from './hosted-plugin-protocol';
|
|
@@ -64,6 +65,9 @@ export class HostedPluginProcess implements ServerPluginRunner {
|
|
|
64
65
|
@inject(ProcessUtils)
|
|
65
66
|
protected readonly processUtils: ProcessUtils;
|
|
66
67
|
|
|
68
|
+
@inject(PluginHostNavigatorState)
|
|
69
|
+
protected readonly navigatorState: PluginHostNavigatorState;
|
|
70
|
+
|
|
67
71
|
private childProcess: cp.ChildProcess | undefined;
|
|
68
72
|
private messagePipe?: BinaryMessagePipe;
|
|
69
73
|
private client: HostedPluginClient;
|
|
@@ -179,6 +183,9 @@ export class HostedPluginProcess implements ServerPluginRunner {
|
|
|
179
183
|
env['VSCODE_NLS_CONFIG'] = JSON.stringify(this.localizationService.getNlsConfig());
|
|
180
184
|
// apply external env variables
|
|
181
185
|
this.pluginHostEnvironmentVariables.getContributions().forEach(envVar => envVar.process(env));
|
|
186
|
+
if (this.navigatorState.supportNodeGlobalNavigator) {
|
|
187
|
+
env['THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR'] = 'true';
|
|
188
|
+
}
|
|
182
189
|
if (this.cli.extensionTestsPath) {
|
|
183
190
|
env.extensionTestsPath = this.cli.extensionTestsPath;
|
|
184
191
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 STMicroelectronics and others.
|
|
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 { suppressNodeNavigator } from './plugin-host-navigator-override';
|
|
19
|
+
|
|
20
|
+
describe('suppressNodeNavigator', () => {
|
|
21
|
+
|
|
22
|
+
it('should override navigator to return undefined when env var is not set', () => {
|
|
23
|
+
const target = { navigator: { userAgent: 'test' } } as typeof globalThis;
|
|
24
|
+
suppressNodeNavigator({}, target);
|
|
25
|
+
expect(target.navigator).to.be.undefined;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should override navigator to return undefined when env var is set to a non-true value', () => {
|
|
29
|
+
const target = { navigator: { userAgent: 'test' } } as typeof globalThis;
|
|
30
|
+
suppressNodeNavigator({ 'THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR': 'false' }, target);
|
|
31
|
+
expect(target.navigator).to.be.undefined;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should preserve navigator when env var is set to true', () => {
|
|
35
|
+
const original = { userAgent: 'test' };
|
|
36
|
+
const target = { navigator: original } as typeof globalThis;
|
|
37
|
+
suppressNodeNavigator({ 'THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR': 'true' }, target);
|
|
38
|
+
expect(target.navigator).to.equal(original);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should make the override configurable so it can be restored', () => {
|
|
42
|
+
const target = { navigator: { userAgent: 'test' } } as typeof globalThis;
|
|
43
|
+
suppressNodeNavigator({}, target);
|
|
44
|
+
expect(target.navigator).to.be.undefined;
|
|
45
|
+
|
|
46
|
+
const restored = { userAgent: 'restored' };
|
|
47
|
+
Object.defineProperty(target, 'navigator', {
|
|
48
|
+
value: restored,
|
|
49
|
+
configurable: true,
|
|
50
|
+
});
|
|
51
|
+
expect(target.navigator).to.equal(restored);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 STMicroelectronics and others.
|
|
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
|
+
/**
|
|
18
|
+
* Workaround: Node 21+ defines `globalThis.navigator`, which breaks VS Code
|
|
19
|
+
* extensions that use its absence to detect a Node.js environment.
|
|
20
|
+
* Unless explicitly opted in via `THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR=true`,
|
|
21
|
+
* override it with a getter that returns `undefined` to match VS Code behavior.
|
|
22
|
+
* See https://github.com/eclipse-theia/theia/issues/16233
|
|
23
|
+
*
|
|
24
|
+
* @param env the environment variables to read the opt-in flag from
|
|
25
|
+
* @param target the object on which to override the `navigator` property (defaults to `globalThis`)
|
|
26
|
+
*/
|
|
27
|
+
export function suppressNodeNavigator(env: Record<string, string | undefined> = process.env, target: typeof globalThis = globalThis): void {
|
|
28
|
+
if (env['THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR'] !== 'true') {
|
|
29
|
+
Object.defineProperty(target, 'navigator', {
|
|
30
|
+
get: () => undefined,
|
|
31
|
+
configurable: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
import '@theia/core/shared/reflect-metadata';
|
|
17
|
+
import { suppressNodeNavigator } from './plugin-host-navigator-override';
|
|
17
18
|
import { Container } from '@theia/core/shared/inversify';
|
|
18
19
|
import { URI as VSCodeURI } from '@theia/core/shared/vscode-uri';
|
|
19
20
|
import { MsgPackExtensionManager } from '@theia/core/lib/common/message-rpc/msg-pack-extension-manager';
|
|
@@ -23,6 +24,9 @@ import { PluginHostRPC } from './plugin-host-rpc';
|
|
|
23
24
|
import pluginHostModule from './plugin-host-module';
|
|
24
25
|
import { URI } from '../../plugin/types-impl';
|
|
25
26
|
|
|
27
|
+
// Undefine globalThis.navigator unless opted in, see https://github.com/eclipse-theia/theia/issues/16233
|
|
28
|
+
suppressNodeNavigator();
|
|
29
|
+
|
|
26
30
|
console.log('PLUGIN_HOST(' + process.pid + ') starting instance');
|
|
27
31
|
|
|
28
32
|
// override exit() function, to do not allow plugin kill this node
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
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
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
18
|
+
|
|
19
|
+
import { expect } from 'chai';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import { AbstractPluginScanner } from './scanner-theia';
|
|
24
|
+
import { PluginPackage, PluginEntryPoint } from '../../../common/plugin-protocol';
|
|
25
|
+
import URI from '@theia/core/lib/common/uri';
|
|
26
|
+
|
|
27
|
+
class TestPluginScanner extends AbstractPluginScanner {
|
|
28
|
+
constructor() {
|
|
29
|
+
// Use a dummy api type; no backend init path needed
|
|
30
|
+
super('theiaPlugin' as any);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected getEntryPoint(_plugin: PluginPackage): PluginEntryPoint {
|
|
34
|
+
return { backend: 'main.js' };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createMinimalPluginPackage(
|
|
39
|
+
capabilities?: PluginPackage['capabilities'],
|
|
40
|
+
packagePath?: string
|
|
41
|
+
): PluginPackage {
|
|
42
|
+
return {
|
|
43
|
+
name: 'test-plugin',
|
|
44
|
+
publisher: 'test-publisher',
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
engines: { theiaPlugin: '1.0.0' },
|
|
47
|
+
displayName: 'Test Plugin',
|
|
48
|
+
description: 'A test plugin',
|
|
49
|
+
packagePath: packagePath ?? os.tmpdir(),
|
|
50
|
+
capabilities
|
|
51
|
+
} as PluginPackage;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('AbstractPluginScanner', () => {
|
|
55
|
+
let scanner: TestPluginScanner;
|
|
56
|
+
let tmpDir: string;
|
|
57
|
+
|
|
58
|
+
before(() => {
|
|
59
|
+
scanner = new TestPluginScanner();
|
|
60
|
+
// Inject a mock pluginUriFactory
|
|
61
|
+
(scanner as any).pluginUriFactory = {
|
|
62
|
+
createUri: (_pkg: PluginPackage, _relativePath?: string) => new URI('file:///dummy')
|
|
63
|
+
};
|
|
64
|
+
// Create a real temp directory so readdirSync in getLicenseUrl/getReadmeUrl works
|
|
65
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scanner-test-'));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
after(() => {
|
|
69
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should set untrustedWorkspacesSupport to false when capabilities.untrustedWorkspaces.supported is false', () => {
|
|
73
|
+
const pkg = createMinimalPluginPackage({
|
|
74
|
+
untrustedWorkspaces: { supported: false }
|
|
75
|
+
}, tmpDir);
|
|
76
|
+
|
|
77
|
+
const model = scanner.getModel(pkg);
|
|
78
|
+
expect(model.untrustedWorkspacesSupport).to.equal(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should set untrustedWorkspacesSupport to true when capabilities.untrustedWorkspaces.supported is true', () => {
|
|
82
|
+
const pkg = createMinimalPluginPackage({
|
|
83
|
+
untrustedWorkspaces: { supported: true }
|
|
84
|
+
}, tmpDir);
|
|
85
|
+
|
|
86
|
+
const model = scanner.getModel(pkg);
|
|
87
|
+
expect(model.untrustedWorkspacesSupport).to.equal(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should set untrustedWorkspacesSupport to "limited" when capabilities.untrustedWorkspaces.supported is "limited"', () => {
|
|
91
|
+
const pkg = createMinimalPluginPackage({
|
|
92
|
+
untrustedWorkspaces: { supported: 'limited' }
|
|
93
|
+
}, tmpDir);
|
|
94
|
+
|
|
95
|
+
const model = scanner.getModel(pkg);
|
|
96
|
+
expect(model.untrustedWorkspacesSupport).to.equal('limited');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should leave untrustedWorkspacesSupport undefined when no capabilities field is present', () => {
|
|
100
|
+
const pkg = createMinimalPluginPackage(undefined, tmpDir);
|
|
101
|
+
|
|
102
|
+
const model = scanner.getModel(pkg);
|
|
103
|
+
expect(model.untrustedWorkspacesSupport).to.equal(undefined);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should leave untrustedWorkspacesSupport undefined when capabilities has no untrustedWorkspaces', () => {
|
|
107
|
+
const pkg = createMinimalPluginPackage({} as any, tmpDir);
|
|
108
|
+
|
|
109
|
+
const model = scanner.getModel(pkg);
|
|
110
|
+
expect(model.untrustedWorkspacesSupport).to.equal(undefined);
|
|
111
|
+
});
|
|
112
|
+
});
|