Path: blob/main/src/vs/workbench/services/extensionManagement/common/extensionFeaturesManagemetService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter } from '../../../../base/common/event.js';6import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';7import { Disposable } from '../../../../base/common/lifecycle.js';8import Severity from '../../../../base/common/severity.js';9import { Extensions, IExtensionFeatureAccessData, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from './extensionFeatures.js';10import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';11import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';12import { Registry } from '../../../../platform/registry/common/platform.js';13import { IStringDictionary } from '../../../../base/common/collections.js';14import { Mutable, isBoolean } from '../../../../base/common/types.js';15import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';16import { localize } from '../../../../nls.js';17import { IExtensionService } from '../../extensions/common/extensions.js';18import { IStorageChangeEvent } from '../../../../base/parts/storage/common/storage.js';19import { distinct } from '../../../../base/common/arrays.js';20import { equals } from '../../../../base/common/objects.js';2122interface IExtensionFeatureState {23disabled?: boolean;24accessData: Mutable<IExtensionFeatureAccessData>;25}2627const FEATURES_STATE_KEY = 'extension.features.state';2829class ExtensionFeaturesManagementService extends Disposable implements IExtensionFeaturesManagementService {30declare readonly _serviceBrand: undefined;3132private readonly _onDidChangeEnablement = this._register(new Emitter<{ extension: ExtensionIdentifier; featureId: string; enabled: boolean }>());33readonly onDidChangeEnablement = this._onDidChangeEnablement.event;3435private readonly _onDidChangeAccessData = this._register(new Emitter<{ extension: ExtensionIdentifier; featureId: string; accessData: IExtensionFeatureAccessData }>());36readonly onDidChangeAccessData = this._onDidChangeAccessData.event;3738private readonly registry: IExtensionFeaturesRegistry;39private extensionFeaturesState = new Map<string, Map<string, IExtensionFeatureState>>();4041constructor(42@IStorageService private readonly storageService: IStorageService,43@IDialogService private readonly dialogService: IDialogService,44@IExtensionService private readonly extensionService: IExtensionService,45) {46super();47this.registry = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry);48this.extensionFeaturesState = this.loadState();49this.garbageCollectOldRequests();50this._register(storageService.onDidChangeValue(StorageScope.PROFILE, FEATURES_STATE_KEY, this._store)(e => this.onDidStorageChange(e)));51}5253isEnabled(extension: ExtensionIdentifier, featureId: string): boolean {54const feature = this.registry.getExtensionFeature(featureId);55if (!feature) {56return false;57}58const isDisabled = this.getExtensionFeatureState(extension, featureId)?.disabled;59if (isBoolean(isDisabled)) {60return !isDisabled;61}62const defaultExtensionAccess = feature.access.extensionsList?.[extension._lower];63if (isBoolean(defaultExtensionAccess)) {64return defaultExtensionAccess;65}66return !feature.access.requireUserConsent;67}6869setEnablement(extension: ExtensionIdentifier, featureId: string, enabled: boolean): void {70const feature = this.registry.getExtensionFeature(featureId);71if (!feature) {72throw new Error(`No feature with id '${featureId}'`);73}74const featureState = this.getAndSetIfNotExistsExtensionFeatureState(extension, featureId);75if (featureState.disabled !== !enabled) {76featureState.disabled = !enabled;77this._onDidChangeEnablement.fire({ extension, featureId, enabled });78this.saveState();79}80}8182getEnablementData(featureId: string): { readonly extension: ExtensionIdentifier; readonly enabled: boolean }[] {83const result: { readonly extension: ExtensionIdentifier; readonly enabled: boolean }[] = [];84const feature = this.registry.getExtensionFeature(featureId);85if (feature) {86for (const [extension, featuresStateMap] of this.extensionFeaturesState) {87const featureState = featuresStateMap.get(featureId);88if (featureState?.disabled !== undefined) {89result.push({ extension: new ExtensionIdentifier(extension), enabled: !featureState.disabled });90}91}92}93return result;94}9596async getAccess(extension: ExtensionIdentifier, featureId: string, justification?: string): Promise<boolean> {97const feature = this.registry.getExtensionFeature(featureId);98if (!feature) {99return false;100}101const featureState = this.getAndSetIfNotExistsExtensionFeatureState(extension, featureId);102if (featureState.disabled) {103return false;104}105106if (featureState.disabled === undefined) {107let enabled = true;108if (feature.access.requireUserConsent) {109const extensionDescription = this.extensionService.extensions.find(e => ExtensionIdentifier.equals(e.identifier, extension));110const confirmationResult = await this.dialogService.confirm({111title: localize('accessExtensionFeature', "Access '{0}' Feature", feature.label),112message: localize('accessExtensionFeatureMessage', "'{0}' extension would like to access the '{1}' feature.", extensionDescription?.displayName ?? extension._lower, feature.label),113detail: justification ?? feature.description,114custom: true,115primaryButton: localize('allow', "Allow"),116cancelButton: localize('disallow', "Don't Allow"),117});118enabled = confirmationResult.confirmed;119}120this.setEnablement(extension, featureId, enabled);121if (!enabled) {122return false;123}124}125126const accessTime = new Date();127featureState.accessData.current = {128accessTimes: [accessTime].concat(featureState.accessData.current?.accessTimes ?? []),129lastAccessed: accessTime,130status: featureState.accessData.current?.status131};132featureState.accessData.accessTimes = (featureState.accessData.accessTimes ?? []).concat(accessTime);133this.saveState();134this._onDidChangeAccessData.fire({ extension, featureId, accessData: featureState.accessData });135return true;136}137138getAllAccessDataForExtension(extension: ExtensionIdentifier): Map<string, IExtensionFeatureAccessData> {139const result = new Map<string, IExtensionFeatureAccessData>();140const extensionState = this.extensionFeaturesState.get(extension._lower);141if (extensionState) {142for (const [featureId, featureState] of extensionState) {143result.set(featureId, featureState.accessData);144}145}146return result;147}148149getAccessData(extension: ExtensionIdentifier, featureId: string): IExtensionFeatureAccessData | undefined {150const feature = this.registry.getExtensionFeature(featureId);151if (!feature) {152return;153}154return this.getExtensionFeatureState(extension, featureId)?.accessData;155}156157setStatus(extension: ExtensionIdentifier, featureId: string, status: { readonly severity: Severity; readonly message: string } | undefined): void {158const feature = this.registry.getExtensionFeature(featureId);159if (!feature) {160throw new Error(`No feature with id '${featureId}'`);161}162const featureState = this.getAndSetIfNotExistsExtensionFeatureState(extension, featureId);163featureState.accessData.current = {164accessTimes: featureState.accessData.current?.accessTimes ?? [],165lastAccessed: featureState.accessData.current?.lastAccessed ?? new Date(),166status167};168this._onDidChangeAccessData.fire({ extension, featureId, accessData: this.getAccessData(extension, featureId)! });169}170171private getExtensionFeatureState(extension: ExtensionIdentifier, featureId: string): IExtensionFeatureState | undefined {172return this.extensionFeaturesState.get(extension._lower)?.get(featureId);173}174175private getAndSetIfNotExistsExtensionFeatureState(extension: ExtensionIdentifier, featureId: string): Mutable<IExtensionFeatureState> {176let extensionState = this.extensionFeaturesState.get(extension._lower);177if (!extensionState) {178extensionState = new Map<string, IExtensionFeatureState>();179this.extensionFeaturesState.set(extension._lower, extensionState);180}181let featureState = extensionState.get(featureId);182if (!featureState) {183featureState = { accessData: { accessTimes: [] } };184extensionState.set(featureId, featureState);185}186return featureState;187}188189private onDidStorageChange(e: IStorageChangeEvent): void {190if (e.external) {191const oldState = this.extensionFeaturesState;192this.extensionFeaturesState = this.loadState();193for (const extensionId of distinct([...oldState.keys(), ...this.extensionFeaturesState.keys()])) {194const extension = new ExtensionIdentifier(extensionId);195const oldExtensionFeaturesState = oldState.get(extensionId);196const newExtensionFeaturesState = this.extensionFeaturesState.get(extensionId);197for (const featureId of distinct([...oldExtensionFeaturesState?.keys() ?? [], ...newExtensionFeaturesState?.keys() ?? []])) {198const isEnabled = this.isEnabled(extension, featureId);199const wasEnabled = !oldExtensionFeaturesState?.get(featureId)?.disabled;200if (isEnabled !== wasEnabled) {201this._onDidChangeEnablement.fire({ extension, featureId, enabled: isEnabled });202}203const newAccessData = this.getAccessData(extension, featureId);204const oldAccessData = oldExtensionFeaturesState?.get(featureId)?.accessData;205if (!equals(newAccessData, oldAccessData)) {206this._onDidChangeAccessData.fire({ extension, featureId, accessData: newAccessData ?? { accessTimes: [] } });207}208}209}210}211}212213private loadState(): Map<string, Map<string, IExtensionFeatureState>> {214let data: IStringDictionary<IStringDictionary<{ disabled?: boolean; accessTimes?: number[] }>> = {};215const raw = this.storageService.get(FEATURES_STATE_KEY, StorageScope.PROFILE, '{}');216try {217data = JSON.parse(raw);218} catch (e) {219// ignore220}221const result = new Map<string, Map<string, IExtensionFeatureState>>();222for (const extensionId in data) {223const extensionFeatureState = new Map<string, IExtensionFeatureState>();224const extensionFeatures = data[extensionId];225for (const featureId in extensionFeatures) {226const extensionFeature = extensionFeatures[featureId];227extensionFeatureState.set(featureId, {228disabled: extensionFeature.disabled,229accessData: {230accessTimes: (extensionFeature.accessTimes ?? []).map(time => new Date(time)),231}232});233}234result.set(extensionId.toLowerCase(), extensionFeatureState);235}236return result;237}238239private saveState(): void {240const data: IStringDictionary<IStringDictionary<{ disabled?: boolean; accessTimes: number[] }>> = {};241this.extensionFeaturesState.forEach((extensionState, extensionId) => {242const extensionFeatures: IStringDictionary<{ disabled?: boolean; accessTimes: number[] }> = {};243extensionState.forEach((featureState, featureId) => {244extensionFeatures[featureId] = {245disabled: featureState.disabled,246accessTimes: featureState.accessData.accessTimes.map(time => time.getTime()),247};248});249data[extensionId] = extensionFeatures;250});251this.storageService.store(FEATURES_STATE_KEY, JSON.stringify(data), StorageScope.PROFILE, StorageTarget.USER);252}253254private garbageCollectOldRequests(): void {255const now = new Date();256const thirtyDaysAgo = new Date(now.setDate(now.getDate() - 30));257let modified = false;258259for (const [, featuresStateMap] of this.extensionFeaturesState) {260for (const [, featureState] of featuresStateMap) {261const originalLength = featureState.accessData.accessTimes.length;262featureState.accessData.accessTimes = featureState.accessData.accessTimes.filter(accessTime => accessTime > thirtyDaysAgo);263if (featureState.accessData.accessTimes.length !== originalLength) {264modified = true;265}266}267}268269if (modified) {270this.saveState();271}272}273}274275registerSingleton(IExtensionFeaturesManagementService, ExtensionFeaturesManagementService, InstantiationType.Delayed);276277278