Path: blob/main/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.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 { ExtensionIdentifier, ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from '../../../../platform/extensions/common/extensions.js';6import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions, IProductVersion, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js';7import { URI } from '../../../../base/common/uri.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import { areSameExtensions, getGalleryExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';10import { IProfileAwareExtensionManagementService, IScannedExtension, IWebExtensionsScannerService } from './extensionManagement.js';11import { ILogService } from '../../../../platform/log/common/log.js';12import { CancellationToken } from '../../../../base/common/cancellation.js';13import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, toExtensionManagementError, UninstallExtensionTaskOptions } from '../../../../platform/extensionManagement/common/abstractExtensionManagementService.js';14import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';15import { IExtensionManifestPropertiesService } from '../../extensions/common/extensionManifestPropertiesService.js';16import { IProductService } from '../../../../platform/product/common/productService.js';17import { isBoolean, isUndefined } from '../../../../base/common/types.js';18import { DidChangeUserDataProfileEvent, IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';19import { delta } from '../../../../base/common/arrays.js';20import { compare } from '../../../../base/common/strings.js';21import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';22import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';23import { DisposableStore } from '../../../../base/common/lifecycle.js';2425export class WebExtensionManagementService extends AbstractExtensionManagementService implements IProfileAwareExtensionManagementService {2627declare readonly _serviceBrand: undefined;2829private readonly disposables = this._register(new DisposableStore());3031get onProfileAwareInstallExtension() { return super.onInstallExtension; }32override get onInstallExtension() { return Event.filter(this.onProfileAwareInstallExtension, e => this.filterEvent(e), this.disposables); }3334get onProfileAwareDidInstallExtensions() { return super.onDidInstallExtensions; }35override get onDidInstallExtensions() {36return Event.filter(37Event.map(this.onProfileAwareDidInstallExtensions, results => results.filter(e => this.filterEvent(e)), this.disposables),38results => results.length > 0, this.disposables);39}4041get onProfileAwareUninstallExtension() { return super.onUninstallExtension; }42override get onUninstallExtension() { return Event.filter(this.onProfileAwareUninstallExtension, e => this.filterEvent(e), this.disposables); }4344get onProfileAwareDidUninstallExtension() { return super.onDidUninstallExtension; }45override get onDidUninstallExtension() { return Event.filter(this.onProfileAwareDidUninstallExtension, e => this.filterEvent(e), this.disposables); }4647private readonly _onDidChangeProfile = this._register(new Emitter<{ readonly added: ILocalExtension[]; readonly removed: ILocalExtension[] }>());48readonly onDidChangeProfile = this._onDidChangeProfile.event;4950get onProfileAwareDidUpdateExtensionMetadata() { return super.onDidUpdateExtensionMetadata; }5152constructor(53@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,54@ITelemetryService telemetryService: ITelemetryService,55@ILogService logService: ILogService,56@IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService,57@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,58@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,59@IProductService productService: IProductService,60@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,61@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService,62@IUriIdentityService uriIdentityService: IUriIdentityService,63) {64super(extensionGalleryService, telemetryService, uriIdentityService, logService, productService, allowedExtensionsService, userDataProfilesService);65this._register(userDataProfileService.onDidChangeCurrentProfile(e => {66if (!this.uriIdentityService.extUri.isEqual(e.previous.extensionsResource, e.profile.extensionsResource)) {67e.join(this.whenProfileChanged(e));68}69}));70}7172private filterEvent({ profileLocation, applicationScoped }: { profileLocation?: URI; applicationScoped?: boolean }): boolean {73profileLocation = profileLocation ?? this.userDataProfileService.currentProfile.extensionsResource;74return applicationScoped || this.uriIdentityService.extUri.isEqual(this.userDataProfileService.currentProfile.extensionsResource, profileLocation);75}7677async getTargetPlatform(): Promise<TargetPlatform> {78return TargetPlatform.WEB;79}8081protected override async isExtensionPlatformCompatible(extension: IGalleryExtension): Promise<boolean> {82if (this.isConfiguredToExecuteOnWeb(extension)) {83return true;84}85return super.isExtensionPlatformCompatible(extension);86}8788async getInstalled(type?: ExtensionType, profileLocation?: URI): Promise<ILocalExtension[]> {89const extensions = [];90if (type === undefined || type === ExtensionType.System) {91const systemExtensions = await this.webExtensionsScannerService.scanSystemExtensions();92extensions.push(...systemExtensions);93}94if (type === undefined || type === ExtensionType.User) {95const userExtensions = await this.webExtensionsScannerService.scanUserExtensions(profileLocation ?? this.userDataProfileService.currentProfile.extensionsResource);96extensions.push(...userExtensions);97}98return extensions.map(e => toLocalExtension(e));99}100101async install(location: URI, options: InstallOptions = {}): Promise<ILocalExtension> {102this.logService.trace('ExtensionManagementService#install', location.toString());103const manifest = await this.webExtensionsScannerService.scanExtensionManifest(location);104if (!manifest || !manifest.name || !manifest.version) {105throw new Error(`Cannot find a valid extension from the location ${location.toString()}`);106}107const result = await this.installExtensions([{ manifest, extension: location, options }]);108if (result[0]?.local) {109return result[0]?.local;110}111if (result[0]?.error) {112throw result[0].error;113}114throw toExtensionManagementError(new Error(`Unknown error while installing extension ${getGalleryExtensionId(manifest.publisher, manifest.name)}`));115}116117installFromLocation(location: URI, profileLocation: URI): Promise<ILocalExtension> {118return this.install(location, { profileLocation });119}120121protected async deleteExtension(extension: ILocalExtension): Promise<void> {122// do nothing123}124125protected async copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {126const target = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, toProfileLocation);127const source = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, fromProfileLocation);128metadata = { ...source?.metadata, ...metadata };129130let scanned;131if (target) {132scanned = await this.webExtensionsScannerService.updateMetadata(extension, { ...target.metadata, ...metadata }, toProfileLocation);133} else {134scanned = await this.webExtensionsScannerService.addExtension(extension.location, metadata, toProfileLocation);135}136return toLocalExtension(scanned);137}138139protected async moveExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {140const target = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, toProfileLocation);141const source = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, fromProfileLocation);142metadata = { ...source?.metadata, ...metadata };143144let scanned;145if (target) {146scanned = await this.webExtensionsScannerService.updateMetadata(extension, { ...target.metadata, ...metadata }, toProfileLocation);147} else {148scanned = await this.webExtensionsScannerService.addExtension(extension.location, metadata, toProfileLocation);149if (source) {150await this.webExtensionsScannerService.removeExtension(source, fromProfileLocation);151}152}153return toLocalExtension(scanned);154}155156protected async removeExtension(extension: ILocalExtension, fromProfileLocation: URI): Promise<void> {157const source = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, fromProfileLocation);158if (source) {159await this.webExtensionsScannerService.removeExtension(source, fromProfileLocation);160}161}162163async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise<ILocalExtension[]> {164const result: ILocalExtension[] = [];165const extensionsToInstall = (await this.webExtensionsScannerService.scanUserExtensions(fromProfileLocation))166.filter(e => extensions.some(id => areSameExtensions(id, e.identifier)));167if (extensionsToInstall.length) {168await Promise.allSettled(extensionsToInstall.map(async e => {169let local = await this.installFromLocation(e.location, toProfileLocation);170if (e.metadata) {171local = await this.updateMetadata(local, e.metadata, fromProfileLocation);172}173result.push(local);174}));175}176return result;177}178179async updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>, profileLocation: URI): Promise<ILocalExtension> {180// unset if false181if (metadata.isMachineScoped === false) {182metadata.isMachineScoped = undefined;183}184if (metadata.isBuiltin === false) {185metadata.isBuiltin = undefined;186}187if (metadata.pinned === false) {188metadata.pinned = undefined;189}190const updatedExtension = await this.webExtensionsScannerService.updateMetadata(local, metadata, profileLocation);191const updatedLocalExtension = toLocalExtension(updatedExtension);192this._onDidUpdateExtensionMetadata.fire({ local: updatedLocalExtension, profileLocation });193return updatedLocalExtension;194}195196override async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void> {197await this.webExtensionsScannerService.copyExtensions(fromProfileLocation, toProfileLocation, e => !e.metadata?.isApplicationScoped);198}199200protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise<IGalleryExtension | null> {201const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease, productVersion);202if (compatibleExtension) {203return compatibleExtension;204}205if (this.isConfiguredToExecuteOnWeb(extension)) {206return extension;207}208return null;209}210211private isConfiguredToExecuteOnWeb(gallery: IGalleryExtension): boolean {212const configuredExtensionKind = this.extensionManifestPropertiesService.getUserConfiguredExtensionKind(gallery.identifier);213return !!configuredExtensionKind && configuredExtensionKind.includes('web');214}215216protected getCurrentExtensionsManifestLocation(): URI {217return this.userDataProfileService.currentProfile.extensionsResource;218}219220protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask {221return new InstallExtensionTask(manifest, extension, options, this.webExtensionsScannerService, this.userDataProfilesService);222}223224protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask {225return new UninstallExtensionTask(extension, options, this.webExtensionsScannerService);226}227228zip(extension: ILocalExtension): Promise<URI> { throw new Error('unsupported'); }229getManifest(vsix: URI): Promise<IExtensionManifest> { throw new Error('unsupported'); }230download(): Promise<URI> { throw new Error('unsupported'); }231232async cleanUp(): Promise<void> { }233234private async whenProfileChanged(e: DidChangeUserDataProfileEvent): Promise<void> {235const previousProfileLocation = e.previous.extensionsResource;236const currentProfileLocation = e.profile.extensionsResource;237if (!previousProfileLocation || !currentProfileLocation) {238throw new Error('This should not happen');239}240const oldExtensions = await this.webExtensionsScannerService.scanUserExtensions(previousProfileLocation);241const newExtensions = await this.webExtensionsScannerService.scanUserExtensions(currentProfileLocation);242const { added, removed } = delta(oldExtensions, newExtensions, (a, b) => compare(`${ExtensionIdentifier.toKey(a.identifier.id)}@${a.manifest.version}`, `${ExtensionIdentifier.toKey(b.identifier.id)}@${b.manifest.version}`));243this._onDidChangeProfile.fire({ added: added.map(e => toLocalExtension(e)), removed: removed.map(e => toLocalExtension(e)) });244}245}246247function toLocalExtension(extension: IExtension): ILocalExtension {248const metadata = getMetadata(undefined, extension);249return {250...extension,251identifier: { id: extension.identifier.id, uuid: metadata.id ?? extension.identifier.uuid },252isMachineScoped: !!metadata.isMachineScoped,253isApplicationScoped: !!metadata.isApplicationScoped,254publisherId: metadata.publisherId || null,255publisherDisplayName: metadata.publisherDisplayName,256installedTimestamp: metadata.installedTimestamp,257isPreReleaseVersion: !!metadata.isPreReleaseVersion,258hasPreReleaseVersion: !!metadata.hasPreReleaseVersion,259preRelease: extension.preRelease,260targetPlatform: TargetPlatform.WEB,261updated: !!metadata.updated,262pinned: !!metadata?.pinned,263private: !!metadata.private,264isWorkspaceScoped: false,265source: metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'resource'),266size: metadata.size ?? 0,267};268}269270function getMetadata(options?: InstallOptions, existingExtension?: IExtension): Metadata {271const metadata: Metadata = { ...((<IScannedExtension>existingExtension)?.metadata || {}) };272metadata.isMachineScoped = options?.isMachineScoped || metadata.isMachineScoped;273return metadata;274}275276class InstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implements IInstallExtensionTask {277278readonly identifier: IExtensionIdentifier;279readonly source: URI | IGalleryExtension;280281private _profileLocation: URI;282get profileLocation() { return this._profileLocation; }283284private _operation = InstallOperation.Install;285get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; }286287constructor(288readonly manifest: IExtensionManifest,289private readonly extension: URI | IGalleryExtension,290readonly options: InstallExtensionTaskOptions,291private readonly webExtensionsScannerService: IWebExtensionsScannerService,292private readonly userDataProfilesService: IUserDataProfilesService,293) {294super();295this._profileLocation = options.profileLocation;296this.identifier = URI.isUri(extension) ? { id: getGalleryExtensionId(manifest.publisher, manifest.name) } : extension.identifier;297this.source = extension;298}299300protected async doRun(token: CancellationToken): Promise<ILocalExtension> {301const userExtensions = await this.webExtensionsScannerService.scanUserExtensions(this.options.profileLocation);302const existingExtension = userExtensions.find(e => areSameExtensions(e.identifier, this.identifier));303if (existingExtension) {304this._operation = InstallOperation.Update;305}306307const metadata = getMetadata(this.options, existingExtension);308if (!URI.isUri(this.extension)) {309metadata.id = this.extension.identifier.uuid;310metadata.publisherDisplayName = this.extension.publisherDisplayName;311metadata.publisherId = this.extension.publisherId;312metadata.installedTimestamp = Date.now();313metadata.isPreReleaseVersion = this.extension.properties.isPreReleaseVersion;314metadata.hasPreReleaseVersion = metadata.hasPreReleaseVersion || this.extension.properties.isPreReleaseVersion;315metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin;316metadata.isSystem = existingExtension?.type === ExtensionType.System ? true : undefined;317metadata.updated = !!existingExtension;318metadata.isApplicationScoped = this.options.isApplicationScoped || metadata.isApplicationScoped;319metadata.private = this.extension.private;320metadata.preRelease = isBoolean(this.options.preRelease)321? this.options.preRelease322: this.options.installPreReleaseVersion || this.extension.properties.isPreReleaseVersion || metadata.preRelease;323metadata.source = URI.isUri(this.extension) ? 'resource' : 'gallery';324}325metadata.pinned = this.options.installGivenVersion ? true : (this.options.pinned ?? metadata.pinned);326327this._profileLocation = metadata.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : this.options.profileLocation;328const scannedExtension = URI.isUri(this.extension) ? await this.webExtensionsScannerService.addExtension(this.extension, metadata, this.profileLocation)329: await this.webExtensionsScannerService.addExtensionFromGallery(this.extension, metadata, this.profileLocation);330return toLocalExtension(scannedExtension);331}332}333334class UninstallExtensionTask extends AbstractExtensionTask<void> implements IUninstallExtensionTask {335336constructor(337readonly extension: ILocalExtension,338readonly options: UninstallExtensionTaskOptions,339private readonly webExtensionsScannerService: IWebExtensionsScannerService,340) {341super();342}343344protected doRun(token: CancellationToken): Promise<void> {345return this.webExtensionsScannerService.removeExtension(this.extension, this.options.profileLocation);346}347}348349350