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