@theia/workspace
Version:
Theia - Workspace Extension
504 lines • 24.4 kB
JavaScript
"use strict";
// *****************************************************************************
// Copyright (C) 2021 EclipseSource and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkspaceTrustService = exports.WorkspaceRestrictionContribution = exports.WORKSPACE_TRUST_STATUS_BAR_ID = void 0;
const tslib_1 = require("tslib");
const browser_1 = require("@theia/core/lib/browser");
const markdown_string_1 = require("@theia/core/lib/common/markdown-rendering/markdown-string");
const status_bar_1 = require("@theia/core/lib/browser/status-bar/status-bar");
const core_1 = require("@theia/core");
const common_1 = require("@theia/core/lib/common");
const uri_1 = require("@theia/core/lib/common/uri");
const preferences_1 = require("@theia/core/lib/common/preferences");
const message_service_1 = require("@theia/core/lib/common/message-service");
const nls_1 = require("@theia/core/lib/common/nls");
const promise_util_1 = require("@theia/core/lib/common/promise-util");
const inversify_1 = require("@theia/core/shared/inversify");
const window_service_1 = require("@theia/core/lib/browser/window/window-service");
const workspace_trust_preferences_1 = require("../common/workspace-trust-preferences");
const frontend_application_config_provider_1 = require("@theia/core/lib/browser/frontend-application-config-provider");
const workspace_service_1 = require("./workspace-service");
const workspace_commands_1 = require("./workspace-commands");
const context_key_service_1 = require("@theia/core/lib/browser/context-key-service");
const workspace_trust_dialog_1 = require("./workspace-trust-dialog");
const untitled_workspace_service_1 = require("../common/untitled-workspace-service");
const env_variables_1 = require("@theia/core/lib/common/env-variables");
const STORAGE_TRUSTED = 'trusted';
exports.WORKSPACE_TRUST_STATUS_BAR_ID = 'workspace-trust-status';
/**
* Contribution interface for features that are restricted in untrusted workspaces.
* Implementations can provide information about what is being restricted.
*/
exports.WorkspaceRestrictionContribution = Symbol('WorkspaceRestrictionContribution');
let WorkspaceTrustService = class WorkspaceTrustService {
constructor() {
this.workspaceTrust = new promise_util_1.Deferred();
this.onDidChangeWorkspaceTrustEmitter = new common_1.Emitter();
this.onDidChangeWorkspaceTrust = this.onDidChangeWorkspaceTrustEmitter.event;
this.toDispose = new core_1.DisposableCollection(this.onDidChangeWorkspaceTrustEmitter);
}
init() {
this.doInit();
}
async doInit() {
await this.workspaceService.ready;
await this.workspaceTrustPref.ready;
await this.preferenceSchemaService.ready;
await this.resolveWorkspaceTrust();
this.toDispose.push(this.preferences.onPreferenceChanged(change => this.handlePreferenceChange(change)));
this.toDispose.push(this.workspaceService.onWorkspaceChanged(() => this.handleWorkspaceChanged()));
// Show status bar item if starting in restricted mode
const initialTrust = await this.getWorkspaceTrust();
this.updateRestrictedModeIndicator(initialTrust);
// React to trust changes
this.toDispose.push(this.onDidChangeWorkspaceTrust(trust => {
this.updateRestrictedModeIndicator(trust);
}));
}
onStop() {
this.toDispose.dispose();
}
getWorkspaceTrust() {
// Return current trust if already resolved, otherwise wait for initial resolution
if (this.currentTrust !== undefined) {
return Promise.resolve(this.currentTrust);
}
return this.workspaceTrust.promise;
}
async resolveWorkspaceTrust(givenTrust) {
if (!this.isWorkspaceTrustResolved()) {
const trust = givenTrust ?? await this.calculateWorkspaceTrust();
if (trust !== undefined) {
await this.storeWorkspaceTrust(trust);
this.contextKeyService.setContext('isWorkspaceTrusted', trust);
this.currentTrust = trust;
this.workspaceTrust.resolve(trust);
this.onDidChangeWorkspaceTrustEmitter.fire(trust);
if (trust && this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_ENABLED]) {
await this.addToTrustedFolders();
}
}
}
}
async setWorkspaceTrust(trusted, reload = true) {
if (this.currentTrust === trusted) {
return;
}
const needsReload = reload && this.shouldReloadForTrustChange(trusted);
if (needsReload && !await this.confirmRestart()) {
return;
}
// Must be set before add/removeFromTrustedFolders to prevent handlePreferenceChange from recursing.
this.currentTrust = trusted;
this.contextKeyService.setContext('isWorkspaceTrusted', trusted);
this.onDidChangeWorkspaceTrustEmitter.fire(trusted);
await this.storeWorkspaceTrust(trusted);
await (trusted ? this.addToTrustedFolders() : this.removeFromTrustedFolders());
if (needsReload) {
this.windowService.reload();
}
}
shouldReloadForTrustChange(newTrust) {
return this.restrictionContributions.getContributions()
.some(c => c.requiresReloadOnTrustChange?.(newTrust) ?? false);
}
isWorkspaceTrustResolved() {
return this.workspaceTrust.state !== 'unresolved';
}
async calculateWorkspaceTrust() {
const trustEnabled = this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_ENABLED];
if (!trustEnabled) {
return true;
}
// Empty workspace - no folders open
if (await this.isEmptyWorkspace()) {
return !!this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_EMPTY_WINDOW];
}
if (await this.areAllWorkspaceUrisTrusted()) {
return true;
}
if (this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_STARTUP_PROMPT] === workspace_trust_preferences_1.WorkspaceTrustPrompt.NEVER) {
return false;
}
// For ONCE mode, check stored trust first
if (this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_STARTUP_PROMPT] === workspace_trust_preferences_1.WorkspaceTrustPrompt.ONCE) {
const storedTrust = await this.loadWorkspaceTrust();
if (storedTrust !== undefined) {
return storedTrust;
}
}
// For ALWAYS mode or ONCE mode with no stored decision, show dialog
return this.showTrustPromptDialog();
}
/**
* Check if the workspace is empty (no workspace or folder opened, or
* an untitled workspace with no folders).
* A saved workspace file with 0 folders is NOT empty - it still needs trust
* evaluation because it could have tasks defined.
*/
async isEmptyWorkspace() {
const workspace = this.workspaceService.workspace;
if (!workspace) {
return true;
}
const roots = this.workspaceService.tryGetRoots();
// Only consider it empty if it's an untitled workspace with no folders
// Use secure check with configDirUri for trust-related decisions
if (roots.length === 0) {
const configDirUri = new uri_1.default(await this.envVariablesServer.getConfigDirUri());
if (this.untitledWorkspaceService.isUntitledWorkspace(workspace.resource, configDirUri)) {
return true;
}
}
return false;
}
/**
* Get the URIs that need to be trusted for the current workspace.
* This includes all workspace folder URIs, plus the workspace file URI
* for saved workspaces (since workspace files can contain tasks/settings).
*/
getWorkspaceUris() {
const uris = this.workspaceService.tryGetRoots().map(root => root.resource);
const workspace = this.workspaceService.workspace;
// For saved workspaces, include the workspace file itself
if (workspace && this.workspaceService.saved) {
uris.push(workspace.resource);
}
return uris;
}
/**
* Check if all workspace URIs are trusted.
* A workspace is trusted only if ALL of its folders (and the workspace
* file for saved workspaces) are trusted.
*/
async areAllWorkspaceUrisTrusted() {
const uris = this.getWorkspaceUris();
if (uris.length === 0) {
return false;
}
return uris.every(uri => this.isUriTrusted(uri));
}
/**
* Check if a URI is trusted. A URI is trusted if it or any of its
* parent folders is in the trusted folders list.
*/
isUriTrusted(uri) {
const trustedFolders = this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
const caseSensitive = !core_1.OS.backend.isWindows;
const normalizedUri = uri.normalizePath();
return trustedFolders.some(folder => {
try {
const folderUri = new uri_1.default(folder).normalizePath();
// Check if the trusted folder is equal to or a parent of the URI
return folderUri.isEqualOrParent(normalizedUri, caseSensitive);
}
catch {
return false; // Invalid URI in preferences
}
});
}
async showTrustPromptDialog() {
// If dialog is already open, wait for its result
if (this.pendingTrustDialog) {
return this.pendingTrustDialog.promise;
}
this.pendingTrustDialog = new promise_util_1.Deferred();
try {
// Show the workspace folders in the dialog
const folderUris = this.workspaceService.tryGetRoots().map(root => root.resource);
const dialog = new workspace_trust_dialog_1.WorkspaceTrustDialog(folderUris);
const result = await dialog.open();
const trusted = result === true;
this.pendingTrustDialog.resolve(trusted);
return trusted;
}
catch (e) {
this.pendingTrustDialog.resolve(false);
throw e;
}
finally {
this.pendingTrustDialog = undefined;
}
}
async addToTrustedFolders() {
const uris = this.getWorkspaceUris();
if (uris.length === 0) {
return;
}
const currentFolders = this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
const newFolders = [...currentFolders];
let changed = false;
for (const uri of uris) {
if (!this.isUriTrusted(uri)) {
newFolders.push(uri.toString());
changed = true;
}
}
if (changed) {
await this.preferences.set(workspace_trust_preferences_1.WORKSPACE_TRUST_TRUSTED_FOLDERS, newFolders, preferences_1.PreferenceScope.User);
}
}
async removeFromTrustedFolders() {
const uris = this.getWorkspaceUris();
if (uris.length === 0) {
return;
}
const currentFolders = this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
const caseSensitive = !core_1.OS.backend.isWindows;
const normalizedUris = uris.map(uri => uri.normalizePath());
const updatedFolders = currentFolders.filter(folder => {
try {
const folderUri = new uri_1.default(folder).normalizePath();
// Remove folder if it exactly matches any workspace URI
return !normalizedUris.some(wsUri => wsUri.isEqual(folderUri, caseSensitive));
}
catch {
return true; // Keep invalid URIs
}
});
if (updatedFolders.length !== currentFolders.length) {
await this.preferences.set(workspace_trust_preferences_1.WORKSPACE_TRUST_TRUSTED_FOLDERS, updatedFolders, preferences_1.PreferenceScope.User);
}
}
async loadWorkspaceTrust() {
if (this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_STARTUP_PROMPT] === workspace_trust_preferences_1.WorkspaceTrustPrompt.ONCE) {
return this.storage.getData(STORAGE_TRUSTED);
}
}
async storeWorkspaceTrust(trust) {
if (this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_STARTUP_PROMPT] === workspace_trust_preferences_1.WorkspaceTrustPrompt.ONCE) {
return this.storage.setData(STORAGE_TRUSTED, trust);
}
}
async handlePreferenceChange(change) {
// Handle trustedFolders changes regardless of scope
if (change.preferenceName === workspace_trust_preferences_1.WORKSPACE_TRUST_TRUSTED_FOLDERS) {
// For empty windows with emptyWindow setting enabled, trust should remain true
if (await this.isEmptyWorkspace() && this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_EMPTY_WINDOW]) {
return;
}
const areAllUrisTrusted = await this.areAllWorkspaceUrisTrusted();
if (areAllUrisTrusted !== this.currentTrust) {
await this.setWorkspaceTrust(areAllUrisTrusted);
}
return;
}
if (change.scope === preferences_1.PreferenceScope.User) {
if (change.preferenceName === workspace_trust_preferences_1.WORKSPACE_TRUST_STARTUP_PROMPT && this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_STARTUP_PROMPT] !== workspace_trust_preferences_1.WorkspaceTrustPrompt.ONCE) {
this.storage.setData(STORAGE_TRUSTED, undefined);
}
if (change.preferenceName === workspace_trust_preferences_1.WORKSPACE_TRUST_ENABLED) {
if (!await this.isEmptyWorkspace() && this.isWorkspaceTrustResolved() && await this.confirmRestart()) {
this.windowService.reload();
}
this.resolveWorkspaceTrust();
}
// Handle emptyWindow setting change for empty windows
if (change.preferenceName === workspace_trust_preferences_1.WORKSPACE_TRUST_EMPTY_WINDOW && await this.isEmptyWorkspace()) {
// For empty windows, directly update trust based on the new setting value
const shouldTrust = !!this.workspaceTrustPref[workspace_trust_preferences_1.WORKSPACE_TRUST_EMPTY_WINDOW];
if (this.currentTrust !== shouldTrust) {
// No reload needed: in an empty window there are no workspace folders,
// so no extensions are blocked by trust and no extension host restart is required.
await this.setWorkspaceTrust(shouldTrust, false);
}
}
}
}
async handleWorkspaceChanged() {
// Reset trust state for the new workspace
this.workspaceTrust = new promise_util_1.Deferred();
this.currentTrust = undefined;
// Re-evaluate trust for the new workspace
await this.resolveWorkspaceTrust();
// Update status bar indicator
const trust = await this.getWorkspaceTrust();
this.updateRestrictedModeIndicator(trust);
}
async confirmRestart() {
const shouldRestart = await new browser_1.ConfirmDialog({
title: nls_1.nls.localizeByDefault('A setting has changed that requires a restart to take effect.'),
msg: nls_1.nls.localizeByDefault('Press the restart button to restart {0} and enable the setting.', frontend_application_config_provider_1.FrontendApplicationConfigProvider.get().applicationName),
ok: nls_1.nls.localizeByDefault('Restart'),
cancel: browser_1.Dialog.CANCEL,
}).open();
return shouldRestart === true;
}
updateRestrictedModeIndicator(trusted) {
if (trusted) {
this.hideRestrictedModeStatusBarItem();
}
else {
this.showRestrictedModeStatusBarItem();
}
}
showRestrictedModeStatusBarItem() {
this.statusBar.setElement(exports.WORKSPACE_TRUST_STATUS_BAR_ID, {
text: '$(shield) ' + nls_1.nls.localizeByDefault('Restricted Mode'),
alignment: status_bar_1.StatusBarAlignment.LEFT,
backgroundColor: 'var(--theia-statusBarItem-prominentBackground)',
color: 'var(--theia-statusBarItem-prominentForeground)',
priority: 5000,
tooltip: this.createRestrictedModeTooltip(),
command: workspace_commands_1.WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id
});
}
createRestrictedModeTooltip() {
const md = new markdown_string_1.MarkdownStringImpl('', { supportThemeIcons: true });
md.appendMarkdown(`**${nls_1.nls.localizeByDefault('Restricted Mode')}**\n\n`);
md.appendMarkdown(nls_1.nls.localize('theia/workspace/restrictedModeDescription', 'Some features are disabled because this workspace is not trusted.'));
md.appendMarkdown('\n\n');
md.appendMarkdown(nls_1.nls.localize('theia/workspace/restrictedModeNote', '*Please note: The workspace trust feature is currently under development in Theia; not all features are integrated with workspace trust yet*'));
const restrictions = this.collectRestrictions();
if (restrictions.length > 0) {
md.appendMarkdown('\n\n---\n\n');
for (const restriction of restrictions) {
md.appendMarkdown(`**${restriction.label}**\n\n`);
if (restriction.details && restriction.details.length > 0) {
for (const detail of restriction.details) {
md.appendMarkdown(`- ${detail}\n`);
}
md.appendMarkdown('\n');
}
}
}
md.appendMarkdown('\n\n---\n\n');
md.appendMarkdown(nls_1.nls.localize('theia/workspace/clickToManageTrust', 'Click to manage trust settings.'));
return md;
}
collectRestrictions() {
const restrictions = [];
for (const contribution of this.restrictionContributions.getContributions()) {
restrictions.push(...contribution.getRestrictions());
}
return restrictions;
}
hideRestrictedModeStatusBarItem() {
this.statusBar.removeElement(exports.WORKSPACE_TRUST_STATUS_BAR_ID);
}
/**
* Refreshes the restricted mode status bar item.
* Call this when restriction contributions change.
*/
refreshRestrictedModeIndicator() {
if (this.currentTrust === false) {
this.showRestrictedModeStatusBarItem();
}
}
/**
* Request workspace trust from the user. This method follows VS Code's pattern:
* - If already trusted, returns true immediately
* - If there's already a pending trust request, returns the same promise (avoiding duplicate dialogs)
* - Otherwise, shows a dialog and waits for the user's response
*
* Unlike the initial trust resolution, this can be called multiple times and will
* prompt the user each time (unless a dialog is already open).
*/
async requestWorkspaceTrust() {
// If already trusted, return true immediately
if (this.currentTrust === true) {
return true;
}
// If there's already a pending request, return the same promise to avoid duplicate dialogs
if (this.pendingTrustRequest) {
return this.pendingTrustRequest.promise;
}
// Create a new pending request
this.pendingTrustRequest = new promise_util_1.Deferred();
try {
const grantedTrust = await this.showTrustPromptDialog();
if (grantedTrust) {
await this.setWorkspaceTrust(true);
}
this.pendingTrustRequest.resolve(grantedTrust);
return grantedTrust;
}
catch (e) {
this.pendingTrustRequest.resolve(undefined);
throw e;
}
finally {
this.pendingTrustRequest = undefined;
}
}
};
exports.WorkspaceTrustService = WorkspaceTrustService;
tslib_1.__decorate([
(0, inversify_1.inject)(workspace_service_1.WorkspaceService),
tslib_1.__metadata("design:type", workspace_service_1.WorkspaceService)
], WorkspaceTrustService.prototype, "workspaceService", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(preferences_1.PreferenceService),
tslib_1.__metadata("design:type", Object)
], WorkspaceTrustService.prototype, "preferences", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(browser_1.StorageService),
tslib_1.__metadata("design:type", Object)
], WorkspaceTrustService.prototype, "storage", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(message_service_1.MessageService),
tslib_1.__metadata("design:type", message_service_1.MessageService)
], WorkspaceTrustService.prototype, "messageService", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(workspace_trust_preferences_1.WorkspaceTrustPreferences),
tslib_1.__metadata("design:type", Object)
], WorkspaceTrustService.prototype, "workspaceTrustPref", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(preferences_1.PreferenceSchemaService),
tslib_1.__metadata("design:type", Object)
], WorkspaceTrustService.prototype, "preferenceSchemaService", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(window_service_1.WindowService),
tslib_1.__metadata("design:type", Object)
], WorkspaceTrustService.prototype, "windowService", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(context_key_service_1.ContextKeyService),
tslib_1.__metadata("design:type", Object)
], WorkspaceTrustService.prototype, "contextKeyService", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(status_bar_1.StatusBar),
tslib_1.__metadata("design:type", Object)
], WorkspaceTrustService.prototype, "statusBar", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(core_1.ContributionProvider),
(0, inversify_1.named)(exports.WorkspaceRestrictionContribution),
tslib_1.__metadata("design:type", Object)
], WorkspaceTrustService.prototype, "restrictionContributions", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(untitled_workspace_service_1.UntitledWorkspaceService),
tslib_1.__metadata("design:type", untitled_workspace_service_1.UntitledWorkspaceService)
], WorkspaceTrustService.prototype, "untitledWorkspaceService", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(env_variables_1.EnvVariablesServer),
tslib_1.__metadata("design:type", Object)
], WorkspaceTrustService.prototype, "envVariablesServer", void 0);
tslib_1.__decorate([
(0, inversify_1.postConstruct)(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], WorkspaceTrustService.prototype, "init", null);
tslib_1.__decorate([
(0, inversify_1.preDestroy)(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], WorkspaceTrustService.prototype, "onStop", null);
exports.WorkspaceTrustService = WorkspaceTrustService = tslib_1.__decorate([
(0, inversify_1.injectable)()
], WorkspaceTrustService);
//# sourceMappingURL=workspace-trust-service.js.map