@theia/plugin-ext 1.74.0-next.6 → 1.74.0-next.62

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 (60) hide show
  1. package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
  2. package/lib/hosted/browser/hosted-plugin.js +14 -3
  3. package/lib/hosted/browser/hosted-plugin.js.map +1 -1
  4. package/lib/hosted/node/plugin-reader.d.ts +6 -5
  5. package/lib/hosted/node/plugin-reader.d.ts.map +1 -1
  6. package/lib/hosted/node/plugin-reader.js +14 -7
  7. package/lib/hosted/node/plugin-reader.js.map +1 -1
  8. package/lib/hosted/node/plugin-reader.spec.d.ts +2 -0
  9. package/lib/hosted/node/plugin-reader.spec.d.ts.map +1 -0
  10. package/lib/hosted/node/plugin-reader.spec.js +72 -0
  11. package/lib/hosted/node/plugin-reader.spec.js.map +1 -0
  12. package/lib/main/browser/custom-editors/custom-editor-opener.d.ts.map +1 -1
  13. package/lib/main/browser/custom-editors/custom-editor-opener.js +25 -1
  14. package/lib/main/browser/custom-editors/custom-editor-opener.js.map +1 -1
  15. package/lib/main/browser/custom-editors/custom-editor-opener.spec.d.ts +2 -0
  16. package/lib/main/browser/custom-editors/custom-editor-opener.spec.d.ts.map +1 -0
  17. package/lib/main/browser/custom-editors/custom-editor-opener.spec.js +90 -0
  18. package/lib/main/browser/custom-editors/custom-editor-opener.spec.js.map +1 -0
  19. package/lib/main/browser/custom-editors/custom-editor-widget.d.ts +4 -1
  20. package/lib/main/browser/custom-editors/custom-editor-widget.d.ts.map +1 -1
  21. package/lib/main/browser/custom-editors/custom-editor-widget.js +14 -0
  22. package/lib/main/browser/custom-editors/custom-editor-widget.js.map +1 -1
  23. package/lib/main/browser/preference-registry-main.d.ts.map +1 -1
  24. package/lib/main/browser/preference-registry-main.js +8 -0
  25. package/lib/main/browser/preference-registry-main.js.map +1 -1
  26. package/lib/main/browser/tabs/tabs-main.d.ts.map +1 -1
  27. package/lib/main/browser/tabs/tabs-main.js +9 -1
  28. package/lib/main/browser/tabs/tabs-main.js.map +1 -1
  29. package/lib/main/node/plugin-http-resolver.d.ts.map +1 -1
  30. package/lib/main/node/plugin-http-resolver.js +5 -3
  31. package/lib/main/node/plugin-http-resolver.js.map +1 -1
  32. package/lib/main/node/plugin-http-resolver.spec.d.ts +2 -0
  33. package/lib/main/node/plugin-http-resolver.spec.d.ts.map +1 -0
  34. package/lib/main/node/plugin-http-resolver.spec.js +40 -0
  35. package/lib/main/node/plugin-http-resolver.spec.js.map +1 -0
  36. package/lib/main/node/plugin-service.d.ts.map +1 -1
  37. package/lib/main/node/plugin-service.js +8 -3
  38. package/lib/main/node/plugin-service.js.map +1 -1
  39. package/lib/plugin/telemetry-ext.d.ts +4 -1
  40. package/lib/plugin/telemetry-ext.d.ts.map +1 -1
  41. package/lib/plugin/telemetry-ext.js +19 -3
  42. package/lib/plugin/telemetry-ext.js.map +1 -1
  43. package/lib/plugin/telemetry-ext.spec.d.ts +2 -0
  44. package/lib/plugin/telemetry-ext.spec.d.ts.map +1 -0
  45. package/lib/plugin/telemetry-ext.spec.js +51 -0
  46. package/lib/plugin/telemetry-ext.spec.js.map +1 -0
  47. package/package.json +31 -31
  48. package/src/hosted/browser/hosted-plugin.ts +14 -3
  49. package/src/hosted/node/plugin-reader.spec.ts +80 -0
  50. package/src/hosted/node/plugin-reader.ts +16 -7
  51. package/src/main/browser/custom-editors/custom-editor-opener.spec.ts +104 -0
  52. package/src/main/browser/custom-editors/custom-editor-opener.tsx +26 -1
  53. package/src/main/browser/custom-editors/custom-editor-widget.ts +15 -1
  54. package/src/main/browser/preference-registry-main.ts +8 -0
  55. package/src/main/browser/tabs/tabs-main.ts +8 -1
  56. package/src/main/node/plugin-http-resolver.spec.ts +41 -0
  57. package/src/main/node/plugin-http-resolver.ts +4 -3
  58. package/src/main/node/plugin-service.ts +7 -3
  59. package/src/plugin/telemetry-ext.spec.ts +61 -0
  60. package/src/plugin/telemetry-ext.ts +23 -4
@@ -277,9 +277,20 @@ export class HostedPluginSupport extends AbstractHostedPluginSupport<PluginManag
277
277
  }
278
278
 
279
279
  protected override async afterLoadContributions(toDisconnect: DisposableCollection): Promise<void> {
280
- await this.viewRegistry.initWidgets();
281
- // remove restored plugin widgets which were not registered by contributions
282
- this.viewRegistry.removeStaleWidgets();
280
+ // Defer view initialization until the layout has been restored: initWidgets/removeStaleWidgets
281
+ // must not run while the ShellLayoutRestorer still holds restored plugin view widgets, otherwise
282
+ // removeStaleWidgets can dispose a restored view container mid-restore and abort the whole
283
+ // layout restoration (see https://github.com/eclipse-theia/theia/issues/17770).
284
+ // Deliberately not awaited: startPlugins runs after this hook and may register file system
285
+ // providers the layout restoration depends on (see beforeLoadContributions).
286
+ this.appState.reachedState('initialized_layout').then(async () => {
287
+ if (toDisconnect.disposed) {
288
+ return;
289
+ }
290
+ await this.viewRegistry.initWidgets();
291
+ // remove restored plugin widgets which were not registered by contributions
292
+ this.viewRegistry.removeStaleWidgets();
293
+ }).catch(console.error);
283
294
  this.workspaceTrustService.refreshRestrictedModeIndicator();
284
295
  }
285
296
 
@@ -0,0 +1,80 @@
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 * as fs from 'fs';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import { HostedPluginReader } from './plugin-reader';
22
+
23
+ class TestHostedPluginReader extends HostedPluginReader {
24
+ resolve(localPath: string, filePath: string): Promise<string | undefined> {
25
+ return this.resolveFile(localPath, filePath);
26
+ }
27
+ }
28
+
29
+ describe('HostedPluginReader#resolveFile', () => {
30
+
31
+ let pluginDir: string;
32
+ let nearSiblingFile: string;
33
+ let farSiblingFile: string;
34
+ const reader = new TestHostedPluginReader();
35
+
36
+ before(() => {
37
+ // Layout:
38
+ // <root>/far-sibling.txt (reachable via `../../far-sibling.txt`)
39
+ // <root>/wrapper/sibling.txt (reachable via `../sibling.txt`)
40
+ // <root>/wrapper/plugin/ (pluginDir)
41
+ // ├── main.js
42
+ // └── media/icon.png
43
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'theia-plugin-reader-'));
44
+ const wrapper = path.join(root, 'wrapper');
45
+ pluginDir = path.join(wrapper, 'plugin');
46
+ fs.mkdirSync(pluginDir, { recursive: true });
47
+ fs.writeFileSync(path.join(pluginDir, 'main.js'), 'console.log("ok");');
48
+ fs.mkdirSync(path.join(pluginDir, 'media'));
49
+ fs.writeFileSync(path.join(pluginDir, 'media', 'icon.png'), 'png');
50
+ nearSiblingFile = path.join(wrapper, 'sibling.txt');
51
+ fs.writeFileSync(nearSiblingFile, 'near');
52
+ farSiblingFile = path.join(root, 'far-sibling.txt');
53
+ fs.writeFileSync(farSiblingFile, 'far');
54
+ });
55
+
56
+ it('serves a file inside the plugin directory', async () => {
57
+ expect(await reader.resolve(pluginDir, 'main.js')).to.equal(path.join(pluginDir, 'main.js'));
58
+ expect(await reader.resolve(pluginDir, 'media/icon.png')).to.equal(path.join(pluginDir, 'media', 'icon.png'));
59
+ });
60
+
61
+ it('resolves extensionless references with the .js fallback', async () => {
62
+ expect(await reader.resolve(pluginDir, 'main')).to.equal(path.join(pluginDir, 'main.js'));
63
+ });
64
+
65
+ it('returns undefined for a relative path pointing outside the plugin directory', async () => {
66
+ expect(await reader.resolve(pluginDir, '../sibling.txt')).to.be.undefined;
67
+ });
68
+
69
+ it('returns undefined for a relative path composed of parent-directory segments', async () => {
70
+ expect(await reader.resolve(pluginDir, '../../far-sibling.txt')).to.be.undefined;
71
+ });
72
+
73
+ it('returns undefined for an absolute path outside the plugin directory', async () => {
74
+ expect(await reader.resolve(pluginDir, nearSiblingFile)).to.be.undefined;
75
+ });
76
+
77
+ it('returns undefined for a missing file inside the plugin directory', async () => {
78
+ expect(await reader.resolve(pluginDir, 'does-not-exist.js')).to.be.undefined;
79
+ });
80
+ });
@@ -49,8 +49,7 @@ export class HostedPluginReader implements BackendApplicationContribution {
49
49
 
50
50
  const localPath = this.pluginsIdsFiles.get(pluginId);
51
51
  if (localPath) {
52
- const absolutePath = path.resolve(localPath, filePath);
53
- const resolvedFile = await this.resolveFile(absolutePath);
52
+ const resolvedFile = await this.resolveFile(localPath, filePath);
54
53
  if (!resolvedFile) {
55
54
  res.status(404).send(`No such file found in '${escape_html(pluginId)}' plugin.`);
56
55
  return;
@@ -79,13 +78,23 @@ export class HostedPluginReader implements BackendApplicationContribution {
79
78
  }
80
79
 
81
80
  /**
82
- * Resolves a plugin file path with fallback to .js and .cjs extensions.
83
- *
84
- * This handles cases where plugins reference modules without extensions,
85
- * which is common in Node.js/CommonJS environments.
81
+ * Resolves `filePath` under `localPath`, with fallback to `.js`, `.cjs` and `.mjs`
82
+ * extensions. Returns `undefined` if the resolved path is not under `localPath`
83
+ * or if no candidate exists on disk.
86
84
  *
85
+ * The extension fallback handles cases where plugins reference modules without
86
+ * extensions, which is common in Node.js/CommonJS environments.
87
87
  */
88
- protected async resolveFile(absolutePath: string): Promise<string | undefined> {
88
+ protected async resolveFile(localPath: string, filePath: string): Promise<string | undefined> {
89
+ const absolutePath = path.resolve(localPath, filePath);
90
+
91
+ // `path.relative` yields a path starting with '..' (or an absolute path on
92
+ // Windows when the drive differs) when `absolutePath` is not under `localPath`.
93
+ const relativePath = path.relative(localPath, absolutePath);
94
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
95
+ return undefined;
96
+ }
97
+
89
98
  const candidates = [absolutePath];
90
99
  const pathExtension = path.extname(absolutePath).toLowerCase();
91
100
 
@@ -0,0 +1,104 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Safi Seid-Ahmad, K2view 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
18
+ let disableJSDOM = enableJSDOM();
19
+
20
+ import * as chai from 'chai';
21
+ import URI from '@theia/core/lib/common/uri';
22
+ import { CustomEditorOpener } from './custom-editor-opener';
23
+ import { CustomEditor, CustomEditorPriority } from '../../../common';
24
+
25
+ disableJSDOM();
26
+
27
+ const expect = chai.expect;
28
+
29
+ describe('CustomEditorOpener#selectorMatches', () => {
30
+
31
+ before(() => {
32
+ disableJSDOM = enableJSDOM();
33
+ });
34
+
35
+ after(() => {
36
+ disableJSDOM();
37
+ });
38
+
39
+ // Only selector matching is exercised — none of the collaborators are touched.
40
+ function createOpener(filenamePattern: string): CustomEditorOpener {
41
+ const editor: CustomEditor = {
42
+ viewType: 'test.editor',
43
+ displayName: 'Test Editor',
44
+ selector: [{ filenamePattern }],
45
+ priority: CustomEditorPriority.default
46
+ };
47
+ return new CustomEditorOpener(editor, undefined!, undefined!, undefined!, undefined!);
48
+ }
49
+
50
+ function matches(filenamePattern: string, path: string): boolean {
51
+ const opener = createOpener(filenamePattern);
52
+ return opener.matches([{ filenamePattern }], new URI(`file://${path}`));
53
+ }
54
+
55
+ function matchesUri(filenamePattern: string, uri: string): boolean {
56
+ const opener = createOpener(filenamePattern);
57
+ return opener.matches([{ filenamePattern }], new URI(uri));
58
+ }
59
+
60
+ it('matches a plain extension pattern against the basename', () => {
61
+ expect(matches('*.custom', '/project/src/file.custom')).true;
62
+ expect(matches('*.custom', '/project/src/file.other')).false;
63
+ });
64
+
65
+ it('matches a plain filename pattern against the basename anywhere', () => {
66
+ expect(matches('config.json', '/anywhere/at/all/config.json')).true;
67
+ expect(matches('config.json', '/anywhere/at/all/other.json')).false;
68
+ });
69
+
70
+ it('matches case-insensitively', () => {
71
+ expect(matches('*.CUSTOM', '/project/file.custom')).true;
72
+ expect(matches('*.custom', '/project/FILE.CUSTOM')).true;
73
+ });
74
+
75
+ it('matches a pattern containing a path separator against the full path (VS Code parity)', () => {
76
+ const pattern = '**/components/*/config.json';
77
+ expect(matches(pattern, '/project/src/components/button/config.json')).true;
78
+ expect(matches(pattern, '/project/elsewhere/config.json')).false;
79
+ expect(matches(pattern, '/project/src/components/button/nested/config.json')).false;
80
+ });
81
+
82
+ it('matches nested path patterns', () => {
83
+ const pattern = '**/components/*/styles/*.css';
84
+ expect(matches(pattern, '/project/src/components/button/styles/theme.css')).true;
85
+ expect(matches(pattern, '/project/src/components/button/config.json')).false;
86
+ expect(matches(pattern, '/project/src/components/button/styles/dark/theme.css')).false;
87
+ });
88
+
89
+ it('does not let a path pattern match a bare basename', () => {
90
+ expect(matches('**/components/*/config.json', '/config.json')).false;
91
+ });
92
+
93
+ it('never matches the excluded schemes, mirroring VS Code (globMatchesResource)', () => {
94
+ // Patterns that would match the basename or full path on a `file:` resource...
95
+ expect(matchesUri('*.json', 'file:///project/settings.json')).true;
96
+ expect(matchesUri('**/settings.json', 'file:///project/settings.json')).true;
97
+ // ...must not match on VS Code's internal schemes.
98
+ expect(matchesUri('*.json', 'vscode-settings:/settings.json')).false;
99
+ expect(matchesUri('**/settings.json', 'vscode-settings:/project/settings.json')).false;
100
+ expect(matchesUri('*.json', 'webview-panel:/panel/config.json')).false;
101
+ expect(matchesUri('*', 'extension:/some/resource')).false;
102
+ expect(matchesUri('*', 'vscode-workspace-trust:/trust')).false;
103
+ });
104
+ });
@@ -24,6 +24,20 @@ import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry';
24
24
  import { generateUuid } from '@theia/core/lib/common/uuid';
25
25
  import { DisposableCollection, Emitter, PreferenceService } from '@theia/core';
26
26
  import { match } from '@theia/core/lib/common/glob';
27
+ import { Schemes } from '../../../common/uri-components';
28
+
29
+ /**
30
+ * Schemes that never match a custom editor's `filenamePattern`, mirroring the
31
+ * excluded schemes in VS Code's `editorResolverService#globMatchesResource`.
32
+ * Theia has no scheme constants for `extension` and `vscode-workspace-trust`,
33
+ * so those are referenced by their literal values.
34
+ */
35
+ const nonMatchingSchemes = new Set<string>([
36
+ 'extension',
37
+ Schemes.webviewPanel,
38
+ 'vscode-workspace-trust',
39
+ Schemes.vscodeSettings
40
+ ]);
27
41
 
28
42
  export class CustomEditorOpener implements OpenHandler {
29
43
 
@@ -200,8 +214,19 @@ export class CustomEditorOpener implements OpenHandler {
200
214
  }
201
215
 
202
216
  selectorMatches(selector: CustomEditorSelector, resource: URI): boolean {
217
+ if (nonMatchingSchemes.has(resource.scheme)) {
218
+ // These schemes match no glob pattern, mirroring VS Code's `globMatchesResource`.
219
+ return false;
220
+ }
203
221
  if (selector.filenamePattern) {
204
- if (match(selector.filenamePattern.toLowerCase(), resource.path.name.toLowerCase() + resource.path.ext.toLowerCase())) {
222
+ const filenamePattern = selector.filenamePattern.toLowerCase();
223
+ // Mirror VS Code's editor-association matching (editorResolverService#globMatchesResource):
224
+ // a pattern containing a path separator is matched against the full `scheme:path`,
225
+ // a plain pattern is matched against the basename only.
226
+ const target = filenamePattern.includes('/')
227
+ ? `${resource.scheme}:${resource.path.toString()}`.toLowerCase()
228
+ : resource.path.base.toLowerCase();
229
+ if (match(filenamePattern, target)) {
205
230
  return true;
206
231
  }
207
232
  }
@@ -17,8 +17,9 @@
17
17
  import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
18
18
  import URI from '@theia/core/lib/common/uri';
19
19
  import { FileOperation } from '@theia/filesystem/lib/common/files';
20
- import { ApplicationShell, DelegatingSaveable, NavigatableWidget, Saveable, SaveableSource } from '@theia/core/lib/browser';
20
+ import { ApplicationShell, DelegatingSaveable, Message, NavigatableWidget, Saveable, SaveableSource } from '@theia/core/lib/browser';
21
21
  import { SaveableService } from '@theia/core/lib/browser/saveable-service';
22
+ import { Disposable, SelectionService } from '@theia/core/lib/common';
22
23
  import { Reference } from '@theia/core/lib/common/reference';
23
24
  import { WebviewWidget } from '../webview/webview';
24
25
  import { CustomEditorModel } from './custom-editors-main';
@@ -54,6 +55,9 @@ export class CustomEditorWidget extends WebviewWidget implements CustomEditorWid
54
55
  @inject(SaveableService)
55
56
  protected readonly saveService: SaveableService;
56
57
 
58
+ @inject(SelectionService)
59
+ protected readonly selectionService: SelectionService;
60
+
57
61
  @postConstruct()
58
62
  protected override init(): void {
59
63
  super.init();
@@ -63,6 +67,16 @@ export class CustomEditorWidget extends WebviewWidget implements CustomEditorWid
63
67
  this.doMove(e.target.resource);
64
68
  }
65
69
  }));
70
+ this.toDispose.push(Disposable.create(() => {
71
+ if (this.selectionService.selection === this) {
72
+ this.selectionService.selection = undefined;
73
+ }
74
+ }));
75
+ }
76
+
77
+ protected override onActivateRequest(msg: Message): void {
78
+ super.onActivateRequest(msg);
79
+ this.selectionService.selection = this;
66
80
  }
67
81
 
68
82
  updateID(): void {
@@ -36,8 +36,16 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa
36
36
 
37
37
  export function getPreferences(preferenceProviderProvider: PreferenceProviderProvider, rootFolders: FileStat[]): PreferenceData {
38
38
  const folders = rootFolders.map(root => root.resource.toString());
39
+ // Session-scoped values are process-lifetime CLI overrides (see PreferenceScope.Session).
40
+ // The plugin-side `parse()` in `preference-registry.ts` only reads up to Folder, so
41
+ // shipping session data across would leak potentially security-sensitive overrides
42
+ // (e.g. AI tool auto-approval) into the plugin host without any consumer on the other
43
+ // side. Exclude the scope explicitly until plugins can opt in to session values.
39
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
45
  return PreferenceScope.getScopes().reduce((result: { [key: number]: any }, scope: PreferenceScope) => {
46
+ if (scope === PreferenceScope.Session) {
47
+ return result;
48
+ }
41
49
  result[scope] = {};
42
50
  const provider = preferenceProviderProvider(scope);
43
51
  if (provider) {
@@ -27,6 +27,7 @@ import { DisposableCollection } from '@theia/core';
27
27
  import { NotebookEditorWidget } from '@theia/notebook/lib/browser';
28
28
  import { Deferred } from '@theia/core/lib/common/promise-util';
29
29
  import { MergeEditor } from '@theia/scm/lib/browser/merge-editor/merge-editor';
30
+ import { CustomEditorWidget } from '../custom-editors/custom-editor-widget';
30
31
 
31
32
  interface TabInfo {
32
33
  tab: TabDto;
@@ -181,7 +182,7 @@ export class TabsMainImpl implements TabsMain, Disposable {
181
182
  const oldDto = this.tabGroupModel.get(tabBar);
182
183
  const groupId = oldDto?.groupId ?? this.groupIdCounter++;
183
184
  const tabs = tabBar.titles.map(title => this.createTabDto(title, groupId));
184
- const viewColumn = 0; // TODO: Implement correct viewColumn handling
185
+ const viewColumn = Math.max(0, this.applicationShell.mainAreaTabBars.indexOf(tabBar));
185
186
  return {
186
187
  groupId,
187
188
  tabs,
@@ -225,6 +226,12 @@ export class TabsMainImpl implements TabsMain, Disposable {
225
226
  uri: toUriComponents(widget.editor.uri.toString())
226
227
  };
227
228
  }
229
+ } else if (widget instanceof CustomEditorWidget) {
230
+ return {
231
+ kind: TabInputKind.CustomEditorInput,
232
+ viewType: widget.viewType,
233
+ uri: toUriComponents(widget.resource.toString())
234
+ };
228
235
  } else if (widget instanceof ViewContainer) {
229
236
  return {
230
237
  kind: TabInputKind.WebviewEditorInput,
@@ -0,0 +1,41 @@
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 { HttpPluginDeployerResolver } from './plugin-http-resolver';
19
+ import { PluginDeployerResolverContext } from '../../common';
20
+
21
+ describe('HttpPluginDeployerResolver', () => {
22
+
23
+ function createContext(originId: string): PluginDeployerResolverContext {
24
+ return {
25
+ getOriginId: () => originId
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ } as any as PluginDeployerResolverContext;
28
+ }
29
+
30
+ it('should reject a malformed link URI', async () => {
31
+ const resolver = new HttpPluginDeployerResolver();
32
+ let error: Error | undefined;
33
+ try {
34
+ await resolver.resolve(createContext(':::not a url'));
35
+ } catch (e) {
36
+ error = e as Error;
37
+ }
38
+ expect(error).to.be.an.instanceOf(Error);
39
+ expect(error!.message).to.contain('invalid link URI');
40
+ });
41
+ });
@@ -19,7 +19,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
19
19
  import { Deferred } from '@theia/core/lib/common/promise-util';
20
20
  import { promises as fs } from 'fs';
21
21
  import * as path from 'path';
22
- import * as url from 'url';
23
22
  import { PluginDeployerResolver, PluginDeployerResolverContext } from '../../common';
24
23
  import { getTempDirPathAsync } from './temp-dir-util';
25
24
 
@@ -56,8 +55,10 @@ export class HttpPluginDeployerResolver implements PluginDeployerResolver {
56
55
  // download the file
57
56
  // keep filename of the url
58
57
  const urlPath = pluginResolverContext.getOriginId();
59
- const link = url.parse(urlPath);
60
- if (!link.pathname) {
58
+ let link: URL;
59
+ try {
60
+ link = new URL(urlPath);
61
+ } catch {
61
62
  throw new Error('invalid link URI' + urlPath);
62
63
  }
63
64
 
@@ -16,7 +16,6 @@
16
16
 
17
17
  import * as http from 'http';
18
18
  import * as path from 'path';
19
- import * as url from 'url';
20
19
  const vhost = require('vhost');
21
20
  import * as express from '@theia/core/shared/express';
22
21
  import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
@@ -63,8 +62,13 @@ export class PluginApiContribution implements BackendApplicationContribution, Ws
63
62
 
64
63
  allowWsUpgrade(request: http.IncomingMessage): MaybePromise<boolean> {
65
64
  if (request.headers.origin && !this.serveSameOrigin) {
66
- const origin = url.parse(request.headers.origin);
67
- if (origin.host && this.webviewExternalEndpointRegExp.test(origin.host)) {
65
+ let host: string | undefined;
66
+ try {
67
+ host = new URL(request.headers.origin).host;
68
+ } catch {
69
+ host = undefined;
70
+ }
71
+ if (host && this.webviewExternalEndpointRegExp.test(host)) {
68
72
  // If the origin comes from the WebViews, refuse:
69
73
  return false;
70
74
  }
@@ -0,0 +1,61 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 suzunn 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 * as chai from 'chai';
18
+ import { TelemetryExtImpl } from './telemetry-ext';
19
+
20
+ const expect = chai.expect;
21
+
22
+ describe('TelemetryLogger', () => {
23
+ const sender = {
24
+ sendEventData: () => { },
25
+ sendErrorData: () => { }
26
+ };
27
+
28
+ it('reflects the current telemetry enabled state', () => {
29
+ const telemetry = new TelemetryExtImpl();
30
+ const logger = telemetry.createTelemetryLogger(sender);
31
+
32
+ expect(logger.telemetryEnabled).to.equal(false);
33
+ expect(logger.isUsageEnabled).to.equal(false);
34
+ expect(logger.isErrorsEnabled).to.equal(false);
35
+
36
+ telemetry.isTelemetryEnabled = true;
37
+
38
+ expect(logger.telemetryEnabled).to.equal(true);
39
+ expect(logger.isUsageEnabled).to.equal(true);
40
+ expect(logger.isErrorsEnabled).to.equal(true);
41
+
42
+ telemetry.isTelemetryEnabled = false;
43
+
44
+ expect(logger.telemetryEnabled).to.equal(false);
45
+ expect(logger.isUsageEnabled).to.equal(false);
46
+ expect(logger.isErrorsEnabled).to.equal(false);
47
+ });
48
+
49
+ it('emits enable-state changes when telemetry is toggled', () => {
50
+ const telemetry = new TelemetryExtImpl();
51
+ const logger = telemetry.createTelemetryLogger(sender);
52
+ let changes = 0;
53
+
54
+ logger.onDidChangeEnableStates(() => changes++);
55
+
56
+ telemetry.isTelemetryEnabled = true;
57
+ telemetry.isTelemetryEnabled = false;
58
+
59
+ expect(changes).to.equal(2);
60
+ });
61
+ });
@@ -49,7 +49,7 @@ export class TelemetryLogger {
49
49
  readonly options: TelemetryLoggerOptions | undefined;
50
50
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
51
  readonly commonProperties: Record<string, any>;
52
- telemetryEnabled: boolean;
52
+ private _telemetryEnabled: boolean;
53
53
 
54
54
  private readonly onDidChangeEnableStatesEmitter: Emitter<TelemetryLogger> = new Emitter();
55
55
  readonly onDidChangeEnableStates: Event<TelemetryLogger> = this.onDidChangeEnableStatesEmitter.event;
@@ -60,9 +60,20 @@ export class TelemetryLogger {
60
60
  this.sender = sender;
61
61
  this.options = options;
62
62
  this.commonProperties = this.getCommonProperties();
63
- this._isErrorsEnabled = true;
64
- this._isUsageEnabled = true;
65
- this.telemetryEnabled = telemetryEnabled;
63
+ this._telemetryEnabled = telemetryEnabled;
64
+ this._isErrorsEnabled = telemetryEnabled;
65
+ this._isUsageEnabled = telemetryEnabled;
66
+ }
67
+
68
+ get telemetryEnabled(): boolean {
69
+ return this._telemetryEnabled;
70
+ }
71
+
72
+ set telemetryEnabled(telemetryEnabled: boolean) {
73
+ if (this._telemetryEnabled !== telemetryEnabled) {
74
+ this._telemetryEnabled = telemetryEnabled;
75
+ this.updateEnableStates(telemetryEnabled, telemetryEnabled);
76
+ }
66
77
  }
67
78
 
68
79
  get isUsageEnabled(): boolean {
@@ -87,6 +98,14 @@ export class TelemetryLogger {
87
98
  }
88
99
  }
89
100
 
101
+ private updateEnableStates(isUsageEnabled: boolean, isErrorsEnabled: boolean): void {
102
+ if (this._isUsageEnabled !== isUsageEnabled || this._isErrorsEnabled !== isErrorsEnabled) {
103
+ this._isUsageEnabled = isUsageEnabled;
104
+ this._isErrorsEnabled = isErrorsEnabled;
105
+ this.onDidChangeEnableStatesEmitter.fire(this);
106
+ }
107
+ }
108
+
90
109
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
110
  logUsage(eventName: string, data?: Record<string, any | TelemetryTrustedValue<any>>): void {
92
111
  if (!this.telemetryEnabled || !this.isUsageEnabled) {