Path: blob/main/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts
5240 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 { IBuiltinExtensionsScannerService, ExtensionType, IExtensionIdentifier, IExtension, IExtensionManifest, TargetPlatform, IRelaxedExtensionManifest, parseEnabledApiProposalNames } from '../../../../platform/extensions/common/extensions.js';6import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';7import { IScannedExtension, IWebExtensionsScannerService, ScanOptions } from '../common/extensionManagement.js';8import { isWeb, Language } from '../../../../base/common/platform.js';9import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';10import { joinPath } from '../../../../base/common/resources.js';11import { URI, UriComponents } from '../../../../base/common/uri.js';12import { FileOperationError, FileOperationResult, IFileService } from '../../../../platform/files/common/files.js';13import { Queue } from '../../../../base/common/async.js';14import { VSBuffer } from '../../../../base/common/buffer.js';15import { ILogService } from '../../../../platform/log/common/log.js';16import { CancellationToken } from '../../../../base/common/cancellation.js';17import { IExtensionGalleryService, IExtensionInfo, IGalleryExtension, IGalleryMetadata, Metadata } from '../../../../platform/extensionManagement/common/extensionManagement.js';18import { areSameExtensions, getGalleryExtensionId, getExtensionId, isMalicious } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';19import { Disposable } from '../../../../base/common/lifecycle.js';20import { ITranslations, localizeManifest } from '../../../../platform/extensionManagement/common/extensionNls.js';21import { localize, localize2 } from '../../../../nls.js';22import * as semver from '../../../../base/common/semver/semver.js';23import { isString, isUndefined } from '../../../../base/common/types.js';24import { getErrorMessage } from '../../../../base/common/errors.js';25import { ResourceMap } from '../../../../base/common/map.js';26import { IExtensionManifestPropertiesService } from '../../extensions/common/extensionManifestPropertiesService.js';27import { IExtensionResourceLoaderService, migratePlatformSpecificExtensionGalleryResourceURL } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js';28import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';29import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';30import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js';31import { IEditorService } from '../../editor/common/editorService.js';32import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';33import { basename } from '../../../../base/common/path.js';34import { IExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js';35import { isNonEmptyArray } from '../../../../base/common/arrays.js';36import { ILifecycleService, LifecyclePhase } from '../../lifecycle/common/lifecycle.js';37import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';38import { IProductService } from '../../../../platform/product/common/productService.js';39import { validateExtensionManifest } from '../../../../platform/extensions/common/extensionValidator.js';40import Severity from '../../../../base/common/severity.js';41import { IStringDictionary } from '../../../../base/common/collections.js';42import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';43import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';44import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';4546type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string };47type ExtensionInfo = { readonly id: string; preRelease: boolean };4849function isGalleryExtensionInfo(obj: unknown): obj is GalleryExtensionInfo {50const galleryExtensionInfo = obj as GalleryExtensionInfo | undefined;51return typeof galleryExtensionInfo?.id === 'string'52&& (galleryExtensionInfo.preRelease === undefined || typeof galleryExtensionInfo.preRelease === 'boolean')53&& (galleryExtensionInfo.migrateStorageFrom === undefined || typeof galleryExtensionInfo.migrateStorageFrom === 'string');54}5556function isUriComponents(obj: unknown): obj is UriComponents {57if (!obj) {58return false;59}60const thing = obj as UriComponents | undefined;61return typeof thing?.path === 'string' &&62typeof thing?.scheme === 'string';63}6465interface IStoredWebExtension {66readonly identifier: IExtensionIdentifier;67readonly version: string;68readonly location: UriComponents;69readonly manifest?: IExtensionManifest;70readonly readmeUri?: UriComponents;71readonly changelogUri?: UriComponents;72// deprecated in favor of packageNLSUris & fallbackPackageNLSUri73readonly packageNLSUri?: UriComponents;74readonly packageNLSUris?: IStringDictionary<UriComponents>;75readonly fallbackPackageNLSUri?: UriComponents;76readonly defaultManifestTranslations?: ITranslations | null;77readonly metadata?: Metadata;78}7980interface IWebExtension {81identifier: IExtensionIdentifier;82version: string;83location: URI;84manifest?: IExtensionManifest;85readmeUri?: URI;86changelogUri?: URI;87// deprecated in favor of packageNLSUris & fallbackPackageNLSUri88packageNLSUri?: URI;89packageNLSUris?: Map<string, URI>;90fallbackPackageNLSUri?: URI;91defaultManifestTranslations?: ITranslations | null;92metadata?: Metadata;93}9495export class WebExtensionsScannerService extends Disposable implements IWebExtensionsScannerService {9697declare readonly _serviceBrand: undefined;9899private readonly systemExtensionsCacheResource: URI | undefined = undefined;100private readonly customBuiltinExtensionsCacheResource: URI | undefined = undefined;101private readonly resourcesAccessQueueMap = new ResourceMap<Queue<IWebExtension[]>>();102private readonly extensionsEnabledWithApiProposalVersion: string[];103104constructor(105@IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService,106@IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService,107@IFileService private readonly fileService: IFileService,108@ILogService private readonly logService: ILogService,109@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,110@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,111@IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService,112@IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService,113@IStorageService private readonly storageService: IStorageService,114@IProductService private readonly productService: IProductService,115@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,116@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,117@ILifecycleService lifecycleService: ILifecycleService,118) {119super();120if (isWeb) {121this.systemExtensionsCacheResource = joinPath(environmentService.userRoamingDataHome, 'systemExtensionsCache.json');122this.customBuiltinExtensionsCacheResource = joinPath(environmentService.userRoamingDataHome, 'customBuiltinExtensionsCache.json');123124// Eventually update caches125lifecycleService.when(LifecyclePhase.Eventually).then(() => this.updateCaches());126}127this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? [];128}129130private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> | undefined;131private readCustomBuiltinExtensionsInfoFromEnv(): Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> {132if (!this._customBuiltinExtensionsInfoPromise) {133this._customBuiltinExtensionsInfoPromise = (async () => {134let extensions: ExtensionInfo[] = [];135const extensionLocations: URI[] = [];136const extensionGalleryResources: URI[] = [];137const extensionsToMigrate: [string, string][] = [];138const customBuiltinExtensionsInfo = this.environmentService.options && Array.isArray(this.environmentService.options.additionalBuiltinExtensions)139? this.environmentService.options.additionalBuiltinExtensions.map(additionalBuiltinExtension => isString(additionalBuiltinExtension) ? { id: additionalBuiltinExtension } : additionalBuiltinExtension)140: [];141for (const e of customBuiltinExtensionsInfo) {142if (isGalleryExtensionInfo(e)) {143extensions.push({ id: e.id, preRelease: !!e.preRelease });144if (e.migrateStorageFrom) {145extensionsToMigrate.push([e.migrateStorageFrom, e.id]);146}147} else if (isUriComponents(e)) {148const extensionLocation = URI.revive(e);149if (await this.extensionResourceLoaderService.isExtensionGalleryResource(extensionLocation)) {150extensionGalleryResources.push(extensionLocation);151} else {152extensionLocations.push(extensionLocation);153}154}155}156if (extensions.length) {157extensions = await this.checkAdditionalBuiltinExtensions(extensions);158}159if (extensions.length) {160this.logService.info('Found additional builtin gallery extensions in env', extensions);161}162if (extensionLocations.length) {163this.logService.info('Found additional builtin location extensions in env', extensionLocations.map(e => e.toString()));164}165if (extensionGalleryResources.length) {166this.logService.info('Found additional builtin extension gallery resources in env', extensionGalleryResources.map(e => e.toString()));167}168return { extensions, extensionsToMigrate, extensionLocations, extensionGalleryResources };169})();170}171return this._customBuiltinExtensionsInfoPromise;172}173174private async checkAdditionalBuiltinExtensions(extensions: ExtensionInfo[]): Promise<ExtensionInfo[]> {175const extensionsControlManifest = await this.galleryService.getExtensionsControlManifest();176const result: ExtensionInfo[] = [];177for (const extension of extensions) {178if (isMalicious({ id: extension.id }, extensionsControlManifest.malicious)) {179this.logService.info(`Checking additional builtin extensions: Ignoring '${extension.id}' because it is reported to be malicious.`);180continue;181}182const deprecationInfo = extensionsControlManifest.deprecated[extension.id.toLowerCase()];183if (deprecationInfo?.extension?.autoMigrate) {184const preReleaseExtensionId = deprecationInfo.extension.id;185this.logService.info(`Checking additional builtin extensions: '${extension.id}' is deprecated, instead using '${preReleaseExtensionId}'`);186result.push({ id: preReleaseExtensionId, preRelease: !!extension.preRelease });187} else {188result.push(extension);189}190}191return result;192}193194/**195* All system extensions bundled with the product196*/197private async readSystemExtensions(): Promise<IExtension[]> {198const systemExtensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions();199const cachedSystemExtensions = await Promise.all((await this.readSystemExtensionsCache()).map(e => this.toScannedExtension(e, true, ExtensionType.System)));200201const result = new Map<string, IExtension>();202for (const extension of [...systemExtensions, ...cachedSystemExtensions]) {203const existing = result.get(extension.identifier.id.toLowerCase());204if (existing) {205// Incase there are duplicates always take the latest version206if (semver.gt(existing.manifest.version, extension.manifest.version)) {207continue;208}209}210result.set(extension.identifier.id.toLowerCase(), extension);211}212return [...result.values()];213}214215/**216* All extensions defined via `additionalBuiltinExtensions` API217*/218private async readCustomBuiltinExtensions(scanOptions?: ScanOptions): Promise<IScannedExtension[]> {219const [customBuiltinExtensionsFromLocations, customBuiltinExtensionsFromGallery] = await Promise.all([220this.getCustomBuiltinExtensionsFromLocations(scanOptions),221this.getCustomBuiltinExtensionsFromGallery(scanOptions),222]);223const customBuiltinExtensions: IScannedExtension[] = [...customBuiltinExtensionsFromLocations, ...customBuiltinExtensionsFromGallery];224await this.migrateExtensionsStorage(customBuiltinExtensions);225return customBuiltinExtensions;226}227228private async getCustomBuiltinExtensionsFromLocations(scanOptions?: ScanOptions): Promise<IScannedExtension[]> {229const { extensionLocations } = await this.readCustomBuiltinExtensionsInfoFromEnv();230if (!extensionLocations.length) {231return [];232}233const result: IScannedExtension[] = [];234await Promise.allSettled(extensionLocations.map(async extensionLocation => {235try {236const webExtension = await this.toWebExtension(extensionLocation);237const extension = await this.toScannedExtension(webExtension, true);238if (extension.isValid || !scanOptions?.skipInvalidExtensions) {239result.push(extension);240} else {241this.logService.info(`Skipping invalid additional builtin extension ${webExtension.identifier.id}`);242}243} catch (error) {244this.logService.info(`Error while fetching the additional builtin extension ${extensionLocation.toString()}.`, getErrorMessage(error));245}246}));247return result;248}249250private async getCustomBuiltinExtensionsFromGallery(scanOptions?: ScanOptions): Promise<IScannedExtension[]> {251if (!this.galleryService.isEnabled()) {252this.logService.info('Ignoring fetching additional builtin extensions from gallery as it is disabled.');253return [];254}255const result: IScannedExtension[] = [];256const { extensions, extensionGalleryResources } = await this.readCustomBuiltinExtensionsInfoFromEnv();257try {258const cacheValue = JSON.stringify({259extensions: extensions.sort((a, b) => a.id.localeCompare(b.id)),260extensionGalleryResources: extensionGalleryResources.map(e => e.toString()).sort()261});262const useCache = this.storageService.get('additionalBuiltinExtensions', StorageScope.APPLICATION, '{}') === cacheValue;263const webExtensions = await (useCache ? this.getCustomBuiltinExtensionsFromCache() : this.updateCustomBuiltinExtensionsCache());264if (webExtensions.length) {265await Promise.all(webExtensions.map(async webExtension => {266try {267const extension = await this.toScannedExtension(webExtension, true);268if (extension.isValid || !scanOptions?.skipInvalidExtensions) {269result.push(extension);270} else {271this.logService.info(`Skipping invalid additional builtin gallery extension ${webExtension.identifier.id}`);272}273} catch (error) {274this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because there is an error while converting it into scanned extension`, getErrorMessage(error));275}276}));277}278this.storageService.store('additionalBuiltinExtensions', cacheValue, StorageScope.APPLICATION, StorageTarget.MACHINE);279} catch (error) {280this.logService.info('Ignoring following additional builtin extensions as there is an error while fetching them from gallery', extensions.map(({ id }) => id), getErrorMessage(error));281}282return result;283}284285private async getCustomBuiltinExtensionsFromCache(): Promise<IWebExtension[]> {286const cachedCustomBuiltinExtensions = await this.readCustomBuiltinExtensionsCache();287const webExtensionsMap = new Map<string, IWebExtension>();288for (const webExtension of cachedCustomBuiltinExtensions) {289const existing = webExtensionsMap.get(webExtension.identifier.id.toLowerCase());290if (existing) {291// Incase there are duplicates always take the latest version292if (semver.gt(existing.version, webExtension.version)) {293continue;294}295}296/* Update preRelease flag in the cache - https://github.com/microsoft/vscode/issues/142831 */297if (webExtension.metadata?.isPreReleaseVersion && !webExtension.metadata?.preRelease) {298webExtension.metadata.preRelease = true;299}300webExtensionsMap.set(webExtension.identifier.id.toLowerCase(), webExtension);301}302return [...webExtensionsMap.values()];303}304305private _migrateExtensionsStoragePromise: Promise<void> | undefined;306private async migrateExtensionsStorage(customBuiltinExtensions: IExtension[]): Promise<void> {307if (!this._migrateExtensionsStoragePromise) {308this._migrateExtensionsStoragePromise = (async () => {309const { extensionsToMigrate } = await this.readCustomBuiltinExtensionsInfoFromEnv();310if (!extensionsToMigrate.length) {311return;312}313const fromExtensions = await this.galleryService.getExtensions(extensionsToMigrate.map(([id]) => ({ id })), CancellationToken.None);314try {315await Promise.allSettled(extensionsToMigrate.map(async ([from, to]) => {316const toExtension = customBuiltinExtensions.find(extension => areSameExtensions(extension.identifier, { id: to }));317if (toExtension) {318const fromExtension = fromExtensions.find(extension => areSameExtensions(extension.identifier, { id: from }));319const fromExtensionManifest = fromExtension ? await this.galleryService.getManifest(fromExtension, CancellationToken.None) : null;320const fromExtensionId = fromExtensionManifest ? getExtensionId(fromExtensionManifest.publisher, fromExtensionManifest.name) : from;321const toExtensionId = getExtensionId(toExtension.manifest.publisher, toExtension.manifest.name);322this.extensionStorageService.addToMigrationList(fromExtensionId, toExtensionId);323} else {324this.logService.info(`Skipped migrating extension storage from '${from}' to '${to}', because the '${to}' extension is not found.`);325}326}));327} catch (error) {328this.logService.error(error);329}330})();331}332return this._migrateExtensionsStoragePromise;333}334335private async updateCaches(): Promise<void> {336await this.updateSystemExtensionsCache();337await this.updateCustomBuiltinExtensionsCache();338}339340private async updateSystemExtensionsCache(): Promise<void> {341const systemExtensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions();342const cachedSystemExtensions = (await this.readSystemExtensionsCache())343.filter(cached => {344const systemExtension = systemExtensions.find(e => areSameExtensions(e.identifier, cached.identifier));345return systemExtension && semver.gt(cached.version, systemExtension.manifest.version);346});347await this.writeSystemExtensionsCache(() => cachedSystemExtensions);348}349350private _updateCustomBuiltinExtensionsCachePromise: Promise<IWebExtension[]> | undefined;351private async updateCustomBuiltinExtensionsCache(): Promise<IWebExtension[]> {352if (!this._updateCustomBuiltinExtensionsCachePromise) {353this._updateCustomBuiltinExtensionsCachePromise = (async () => {354this.logService.info('Updating additional builtin extensions cache');355const { extensions, extensionGalleryResources } = await this.readCustomBuiltinExtensionsInfoFromEnv();356const [galleryWebExtensions, extensionGalleryResourceWebExtensions] = await Promise.all([357this.resolveBuiltinGalleryExtensions(extensions),358this.resolveBuiltinExtensionGalleryResources(extensionGalleryResources)359]);360const webExtensionsMap = new Map<string, IWebExtension>();361for (const webExtension of [...galleryWebExtensions, ...extensionGalleryResourceWebExtensions]) {362webExtensionsMap.set(webExtension.identifier.id.toLowerCase(), webExtension);363}364await this.resolveDependenciesAndPackedExtensions(extensionGalleryResourceWebExtensions, webExtensionsMap);365const webExtensions = [...webExtensionsMap.values()];366await this.writeCustomBuiltinExtensionsCache(() => webExtensions);367return webExtensions;368})();369}370return this._updateCustomBuiltinExtensionsCachePromise;371}372373private async resolveBuiltinExtensionGalleryResources(extensionGalleryResources: URI[]): Promise<IWebExtension[]> {374if (extensionGalleryResources.length === 0) {375return [];376}377const result = new Map<string, IWebExtension>();378const extensionInfos: IExtensionInfo[] = [];379await Promise.all(extensionGalleryResources.map(async extensionGalleryResource => {380try {381const webExtension = await this.toWebExtensionFromExtensionGalleryResource(extensionGalleryResource);382result.set(webExtension.identifier.id.toLowerCase(), webExtension);383extensionInfos.push({ id: webExtension.identifier.id, version: webExtension.version });384} catch (error) {385this.logService.info(`Ignoring additional builtin extension from gallery resource ${extensionGalleryResource.toString()} because there is an error while converting it into web extension`, getErrorMessage(error));386}387}));388const galleryExtensions = await this.galleryService.getExtensions(extensionInfos, CancellationToken.None);389for (const galleryExtension of galleryExtensions) {390const webExtension = result.get(galleryExtension.identifier.id.toLowerCase());391if (webExtension) {392result.set(galleryExtension.identifier.id.toLowerCase(), {393...webExtension,394identifier: { id: webExtension.identifier.id, uuid: galleryExtension.identifier.uuid },395readmeUri: galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,396changelogUri: galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,397metadata: { isPreReleaseVersion: galleryExtension.properties.isPreReleaseVersion, preRelease: galleryExtension.properties.isPreReleaseVersion, isBuiltin: true, pinned: true }398});399}400}401return [...result.values()];402}403404private async resolveBuiltinGalleryExtensions(extensions: IExtensionInfo[]): Promise<IWebExtension[]> {405if (extensions.length === 0) {406return [];407}408const webExtensions: IWebExtension[] = [];409const galleryExtensionsMap = await this.getExtensionsWithDependenciesAndPackedExtensions(extensions);410const missingExtensions = extensions.filter(({ id }) => !galleryExtensionsMap.has(id.toLowerCase()));411if (missingExtensions.length) {412this.logService.info('Skipping the additional builtin extensions because their compatible versions are not found.', missingExtensions);413}414await Promise.all([...galleryExtensionsMap.values()].map(async gallery => {415try {416const webExtension = await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true });417webExtensions.push(webExtension);418} catch (error) {419this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error));420}421}));422return webExtensions;423}424425private async resolveDependenciesAndPackedExtensions(webExtensions: IWebExtension[], result: Map<string, IWebExtension>): Promise<void> {426const extensionInfos: IExtensionInfo[] = [];427for (const webExtension of webExtensions) {428for (const e of [...(webExtension.manifest?.extensionDependencies ?? []), ...(webExtension.manifest?.extensionPack ?? [])]) {429if (!result.has(e.toLowerCase())) {430extensionInfos.push({ id: e, version: webExtension.version });431}432}433}434if (extensionInfos.length === 0) {435return;436}437const galleryExtensions = await this.getExtensionsWithDependenciesAndPackedExtensions(extensionInfos, new Set<string>([...result.keys()]));438await Promise.all([...galleryExtensions.values()].map(async gallery => {439try {440const webExtension = await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true });441result.set(webExtension.identifier.id.toLowerCase(), webExtension);442} catch (error) {443this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error));444}445}));446}447448private async getExtensionsWithDependenciesAndPackedExtensions(toGet: IExtensionInfo[], seen: Set<string> = new Set<string>(), result: Map<string, IGalleryExtension> = new Map<string, IGalleryExtension>()): Promise<Map<string, IGalleryExtension>> {449if (toGet.length === 0) {450return result;451}452const extensions = await this.galleryService.getExtensions(toGet, { compatible: true, targetPlatform: TargetPlatform.WEB }, CancellationToken.None);453const packsAndDependencies = new Map<string, IExtensionInfo>();454for (const extension of extensions) {455result.set(extension.identifier.id.toLowerCase(), extension);456for (const id of [...(isNonEmptyArray(extension.properties.dependencies) ? extension.properties.dependencies : []), ...(isNonEmptyArray(extension.properties.extensionPack) ? extension.properties.extensionPack : [])]) {457if (!result.has(id.toLowerCase()) && !packsAndDependencies.has(id.toLowerCase()) && !seen.has(id.toLowerCase())) {458const extensionInfo = toGet.find(e => areSameExtensions(e, extension.identifier));459packsAndDependencies.set(id.toLowerCase(), { id, preRelease: extensionInfo?.preRelease });460}461}462}463return this.getExtensionsWithDependenciesAndPackedExtensions([...packsAndDependencies.values()].filter(({ id }) => !result.has(id.toLowerCase())), seen, result);464}465466async scanSystemExtensions(): Promise<IExtension[]> {467return this.readSystemExtensions();468}469470async scanUserExtensions(profileLocation: URI, scanOptions?: ScanOptions): Promise<IScannedExtension[]> {471const extensions = new Map<string, IScannedExtension>();472473// Custom builtin extensions defined through `additionalBuiltinExtensions` API474const customBuiltinExtensions = await this.readCustomBuiltinExtensions(scanOptions);475for (const extension of customBuiltinExtensions) {476extensions.set(extension.identifier.id.toLowerCase(), extension);477}478479// User Installed extensions480const installedExtensions = await this.scanInstalledExtensions(profileLocation, scanOptions);481for (const extension of installedExtensions) {482extensions.set(extension.identifier.id.toLowerCase(), extension);483}484485return [...extensions.values()];486}487488async scanExtensionsUnderDevelopment(): Promise<IExtension[]> {489const devExtensions = this.environmentService.options?.developmentOptions?.extensions;490const result: IExtension[] = [];491if (Array.isArray(devExtensions)) {492await Promise.allSettled(devExtensions.map(async devExtension => {493try {494const location = URI.revive(devExtension);495if (URI.isUri(location)) {496const webExtension = await this.toWebExtension(location);497result.push(await this.toScannedExtension(webExtension, false));498} else {499this.logService.info(`Skipping the extension under development ${devExtension} as it is not URI type.`);500}501} catch (error) {502this.logService.info(`Error while fetching the extension under development ${devExtension.toString()}.`, getErrorMessage(error));503}504}));505}506return result;507}508509async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, profileLocation: URI): Promise<IScannedExtension | null> {510if (extensionType === ExtensionType.System) {511const systemExtensions = await this.scanSystemExtensions();512return systemExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null;513}514const userExtensions = await this.scanUserExtensions(profileLocation);515return userExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null;516}517518async scanExtensionManifest(extensionLocation: URI): Promise<IExtensionManifest | null> {519try {520return await this.getExtensionManifest(extensionLocation);521} catch (error) {522this.logService.warn(`Error while fetching manifest from ${extensionLocation.toString()}`, getErrorMessage(error));523return null;524}525}526527async addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata: Metadata, profileLocation: URI): Promise<IScannedExtension> {528const webExtension = await this.toWebExtensionFromGallery(galleryExtension, metadata);529return this.addWebExtension(webExtension, profileLocation);530}531532async addExtension(location: URI, metadata: Metadata, profileLocation: URI): Promise<IScannedExtension> {533const webExtension = await this.toWebExtension(location, undefined, undefined, undefined, undefined, undefined, undefined, metadata);534const extension = await this.toScannedExtension(webExtension, false);535await this.addToInstalledExtensions([webExtension], profileLocation);536return extension;537}538539async removeExtension(extension: IScannedExtension, profileLocation: URI): Promise<void> {540await this.writeInstalledExtensions(profileLocation, installedExtensions => installedExtensions.filter(installedExtension => !areSameExtensions(installedExtension.identifier, extension.identifier)));541}542543async updateMetadata(extension: IScannedExtension, metadata: Partial<Metadata>, profileLocation: URI): Promise<IScannedExtension> {544let updatedExtension: IWebExtension | undefined = undefined;545await this.writeInstalledExtensions(profileLocation, installedExtensions => {546const result: IWebExtension[] = [];547for (const installedExtension of installedExtensions) {548if (areSameExtensions(extension.identifier, installedExtension.identifier)) {549installedExtension.metadata = { ...installedExtension.metadata, ...metadata };550updatedExtension = installedExtension;551result.push(installedExtension);552} else {553result.push(installedExtension);554}555}556return result;557});558if (!updatedExtension) {559throw new Error('Extension not found');560}561return this.toScannedExtension(updatedExtension, extension.isBuiltin);562}563564async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, filter: (extension: IScannedExtension) => boolean): Promise<void> {565const extensionsToCopy: IWebExtension[] = [];566const fromWebExtensions = await this.readInstalledExtensions(fromProfileLocation);567await Promise.all(fromWebExtensions.map(async webExtension => {568const scannedExtension = await this.toScannedExtension(webExtension, false);569if (filter(scannedExtension)) {570extensionsToCopy.push(webExtension);571}572}));573if (extensionsToCopy.length) {574await this.addToInstalledExtensions(extensionsToCopy, toProfileLocation);575}576}577578private async addWebExtension(webExtension: IWebExtension, profileLocation: URI): Promise<IScannedExtension> {579const isSystem = !!(await this.scanSystemExtensions()).find(e => areSameExtensions(e.identifier, webExtension.identifier));580const isBuiltin = !!webExtension.metadata?.isBuiltin;581const extension = await this.toScannedExtension(webExtension, isBuiltin);582583if (isSystem) {584await this.writeSystemExtensionsCache(systemExtensions => {585// Remove the existing extension to avoid duplicates586systemExtensions = systemExtensions.filter(extension => !areSameExtensions(extension.identifier, webExtension.identifier));587systemExtensions.push(webExtension);588return systemExtensions;589});590return extension;591}592593// Update custom builtin extensions to custom builtin extensions cache594if (isBuiltin) {595await this.writeCustomBuiltinExtensionsCache(customBuiltinExtensions => {596// Remove the existing extension to avoid duplicates597customBuiltinExtensions = customBuiltinExtensions.filter(extension => !areSameExtensions(extension.identifier, webExtension.identifier));598customBuiltinExtensions.push(webExtension);599return customBuiltinExtensions;600});601602const installedExtensions = await this.readInstalledExtensions(profileLocation);603// Also add to installed extensions if it is installed to update its version604if (installedExtensions.some(e => areSameExtensions(e.identifier, webExtension.identifier))) {605await this.addToInstalledExtensions([webExtension], profileLocation);606}607return extension;608}609610// Add to installed extensions611await this.addToInstalledExtensions([webExtension], profileLocation);612return extension;613}614615private async addToInstalledExtensions(webExtensions: IWebExtension[], profileLocation: URI): Promise<void> {616await this.writeInstalledExtensions(profileLocation, installedExtensions => {617// Remove the existing extension to avoid duplicates618installedExtensions = installedExtensions.filter(installedExtension => webExtensions.some(extension => !areSameExtensions(installedExtension.identifier, extension.identifier)));619installedExtensions.push(...webExtensions);620return installedExtensions;621});622}623624private async scanInstalledExtensions(profileLocation: URI, scanOptions?: ScanOptions): Promise<IScannedExtension[]> {625let installedExtensions = await this.readInstalledExtensions(profileLocation);626627// If current profile is not a default profile, then add the application extensions to the list628if (!this.uriIdentityService.extUri.isEqual(profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) {629// Remove application extensions from the non default profile630installedExtensions = installedExtensions.filter(i => !i.metadata?.isApplicationScoped);631// Add application extensions from the default profile to the list632const defaultProfileExtensions = await this.readInstalledExtensions(this.userDataProfilesService.defaultProfile.extensionsResource);633installedExtensions.push(...defaultProfileExtensions.filter(i => i.metadata?.isApplicationScoped));634}635636installedExtensions.sort((a, b) => a.identifier.id < b.identifier.id ? -1 : a.identifier.id > b.identifier.id ? 1 : semver.rcompare(a.version, b.version));637const result = new Map<string, IScannedExtension>();638for (const webExtension of installedExtensions) {639const existing = result.get(webExtension.identifier.id.toLowerCase());640if (existing && semver.gt(existing.manifest.version, webExtension.version)) {641continue;642}643const extension = await this.toScannedExtension(webExtension, false);644if (extension.isValid || !scanOptions?.skipInvalidExtensions) {645result.set(extension.identifier.id.toLowerCase(), extension);646} else {647this.logService.info(`Skipping invalid installed extension ${webExtension.identifier.id}`);648}649}650return [...result.values()];651}652653private async toWebExtensionFromGallery(galleryExtension: IGalleryExtension, metadata?: Metadata): Promise<IWebExtension> {654const extensionLocation = await this.extensionResourceLoaderService.getExtensionGalleryResourceURL({655publisher: galleryExtension.publisher,656name: galleryExtension.name,657version: galleryExtension.version,658targetPlatform: galleryExtension.properties.targetPlatform === TargetPlatform.WEB ? TargetPlatform.WEB : undefined659}, 'extension');660661if (!extensionLocation) {662throw new Error('No extension gallery service configured.');663}664665return this.toWebExtensionFromExtensionGalleryResource(extensionLocation,666galleryExtension.identifier,667galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,668galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,669metadata);670}671672private async toWebExtensionFromExtensionGalleryResource(extensionLocation: URI, identifier?: IExtensionIdentifier, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> {673const extensionResources = await this.listExtensionResources(extensionLocation);674const packageNLSResources = this.getPackageNLSResourceMapFromResources(extensionResources);675676// The fallback, in English, will fill in any gaps missing in the localized file.677const fallbackPackageNLSResource = extensionResources.find(e => basename(e) === 'package.nls.json');678return this.toWebExtension(679extensionLocation,680identifier,681undefined,682packageNLSResources,683fallbackPackageNLSResource ? URI.parse(fallbackPackageNLSResource) : null,684readmeUri,685changelogUri,686metadata);687}688689private getPackageNLSResourceMapFromResources(extensionResources: string[]): Map<string, URI> {690const packageNLSResources = new Map<string, URI>();691extensionResources.forEach(e => {692// Grab all package.nls.{language}.json files693const regexResult = /package\.nls\.([\w-]+)\.json/.exec(basename(e));694if (regexResult?.[1]) {695packageNLSResources.set(regexResult[1], URI.parse(e));696}697});698return packageNLSResources;699}700701private async toWebExtension(extensionLocation: URI, identifier?: IExtensionIdentifier, manifest?: IExtensionManifest, packageNLSUris?: Map<string, URI>, fallbackPackageNLSUri?: URI | ITranslations | null, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> {702if (!manifest) {703try {704manifest = await this.getExtensionManifest(extensionLocation);705} catch (error) {706throw new Error(`Error while fetching manifest from the location '${extensionLocation.toString()}'. ${getErrorMessage(error)}`);707}708}709710if (!this.extensionManifestPropertiesService.canExecuteOnWeb(manifest)) {711throw new Error(localize('not a web extension', "Cannot add '{0}' because this extension is not a web extension.", manifest.displayName || manifest.name));712}713714if (fallbackPackageNLSUri === undefined) {715try {716fallbackPackageNLSUri = joinPath(extensionLocation, 'package.nls.json');717await this.extensionResourceLoaderService.readExtensionResource(fallbackPackageNLSUri);718} catch (error) {719fallbackPackageNLSUri = undefined;720}721}722const defaultManifestTranslations: ITranslations | null | undefined = fallbackPackageNLSUri ? URI.isUri(fallbackPackageNLSUri) ? await this.getTranslations(fallbackPackageNLSUri) : fallbackPackageNLSUri : null;723724return {725identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: identifier?.uuid },726version: manifest.version,727location: extensionLocation,728manifest,729readmeUri,730changelogUri,731packageNLSUris,732fallbackPackageNLSUri: URI.isUri(fallbackPackageNLSUri) ? fallbackPackageNLSUri : undefined,733defaultManifestTranslations,734metadata,735};736}737738private async toScannedExtension(webExtension: IWebExtension, isBuiltin: boolean, type: ExtensionType = ExtensionType.User): Promise<IScannedExtension> {739const validations: [Severity, string][] = [];740let manifest: IRelaxedExtensionManifest | undefined = webExtension.manifest;741742if (!manifest) {743try {744manifest = await this.getExtensionManifest(webExtension.location);745} catch (error) {746validations.push([Severity.Error, `Error while fetching manifest from the location '${webExtension.location}'. ${getErrorMessage(error)}`]);747}748}749750if (!manifest) {751const [publisher, name] = webExtension.identifier.id.split('.');752manifest = {753name,754publisher,755version: webExtension.version,756engines: { vscode: '*' },757};758}759760const packageNLSUri = webExtension.packageNLSUris?.get(Language.value().toLowerCase());761const fallbackPackageNLS = webExtension.defaultManifestTranslations ?? webExtension.fallbackPackageNLSUri;762763if (packageNLSUri) {764manifest = await this.translateManifest(manifest, packageNLSUri, fallbackPackageNLS);765} else if (fallbackPackageNLS) {766manifest = await this.translateManifest(manifest, fallbackPackageNLS);767}768769const uuid = (<IGalleryMetadata | undefined>webExtension.metadata)?.id;770771const validateApiVersion = this.extensionsEnabledWithApiProposalVersion.includes(webExtension.identifier.id.toLowerCase());772validations.push(...validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false, validateApiVersion));773let isValid = true;774for (const [severity, message] of validations) {775if (severity === Severity.Error) {776isValid = false;777this.logService.error(message);778}779}780781if (manifest.enabledApiProposals && validateApiVersion) {782manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]);783}784785return {786identifier: { id: webExtension.identifier.id, uuid: webExtension.identifier.uuid || uuid },787location: webExtension.location,788manifest,789type,790isBuiltin,791readmeUrl: webExtension.readmeUri,792changelogUrl: webExtension.changelogUri,793metadata: webExtension.metadata,794targetPlatform: TargetPlatform.WEB,795validations,796isValid,797preRelease: !!webExtension.metadata?.preRelease,798};799}800801private async listExtensionResources(extensionLocation: URI): Promise<string[]> {802try {803const result = await this.extensionResourceLoaderService.readExtensionResource(extensionLocation);804return JSON.parse(result);805} catch (error) {806this.logService.warn('Error while fetching extension resources list', getErrorMessage(error));807}808return [];809}810811private async translateManifest(manifest: IExtensionManifest, nlsURL: ITranslations | URI, fallbackNLS?: ITranslations | URI): Promise<IRelaxedExtensionManifest> {812try {813const translations = URI.isUri(nlsURL) ? await this.getTranslations(nlsURL) : nlsURL;814const fallbackTranslations = URI.isUri(fallbackNLS) ? await this.getTranslations(fallbackNLS) : fallbackNLS;815if (translations) {816manifest = localizeManifest(this.logService, manifest, translations, fallbackTranslations);817}818} catch (error) { /* ignore */ }819return manifest;820}821822private async getExtensionManifest(location: URI): Promise<IExtensionManifest> {823const url = joinPath(location, 'package.json');824const content = await this.extensionResourceLoaderService.readExtensionResource(url);825return JSON.parse(content);826}827828private async getTranslations(nlsUrl: URI): Promise<ITranslations | undefined> {829try {830const content = await this.extensionResourceLoaderService.readExtensionResource(nlsUrl);831return JSON.parse(content);832} catch (error) {833this.logService.error(`Error while fetching translations of an extension`, nlsUrl.toString(), getErrorMessage(error));834}835return undefined;836}837838private async readInstalledExtensions(profileLocation: URI): Promise<IWebExtension[]> {839return this.withWebExtensions(profileLocation);840}841842private writeInstalledExtensions(profileLocation: URI, updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {843return this.withWebExtensions(profileLocation, updateFn);844}845846private readCustomBuiltinExtensionsCache(): Promise<IWebExtension[]> {847return this.withWebExtensions(this.customBuiltinExtensionsCacheResource);848}849850private writeCustomBuiltinExtensionsCache(updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {851return this.withWebExtensions(this.customBuiltinExtensionsCacheResource, updateFn);852}853854private readSystemExtensionsCache(): Promise<IWebExtension[]> {855return this.withWebExtensions(this.systemExtensionsCacheResource);856}857858private writeSystemExtensionsCache(updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {859return this.withWebExtensions(this.systemExtensionsCacheResource, updateFn);860}861862private async withWebExtensions(file: URI | undefined, updateFn?: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {863if (!file) {864return [];865}866return this.getResourceAccessQueue(file).queue(async () => {867let webExtensions: IWebExtension[] = [];868869// Read870try {871const content = await this.fileService.readFile(file);872const storedWebExtensions: IStoredWebExtension[] = JSON.parse(content.value.toString());873for (const e of storedWebExtensions) {874if (!e.location || !e.identifier || !e.version) {875this.logService.info('Ignoring invalid extension while scanning', storedWebExtensions);876continue;877}878let packageNLSUris: Map<string, URI> | undefined;879if (e.packageNLSUris) {880packageNLSUris = new Map<string, URI>();881Object.entries(e.packageNLSUris).forEach(([key, value]) => packageNLSUris!.set(key, URI.revive(value)));882}883884webExtensions.push({885identifier: e.identifier,886version: e.version,887location: URI.revive(e.location),888manifest: e.manifest,889readmeUri: URI.revive(e.readmeUri),890changelogUri: URI.revive(e.changelogUri),891packageNLSUris,892fallbackPackageNLSUri: URI.revive(e.fallbackPackageNLSUri),893defaultManifestTranslations: e.defaultManifestTranslations,894packageNLSUri: URI.revive(e.packageNLSUri),895metadata: e.metadata,896});897}898899try {900webExtensions = await this.migrateWebExtensions(webExtensions, file);901} catch (error) {902this.logService.error(`Error while migrating scanned extensions in ${file.toString()}`, getErrorMessage(error));903}904905} catch (error) {906/* Ignore */907if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {908this.logService.error(error);909}910}911912// Update913if (updateFn) {914await this.storeWebExtensions(webExtensions = updateFn(webExtensions), file);915}916917return webExtensions;918});919}920921private async migrateWebExtensions(webExtensions: IWebExtension[], file: URI): Promise<IWebExtension[]> {922let update = false;923webExtensions = await Promise.all(webExtensions.map(async webExtension => {924if (!webExtension.manifest) {925try {926webExtension.manifest = await this.getExtensionManifest(webExtension.location);927update = true;928} catch (error) {929this.logService.error(`Error while updating manifest of an extension in ${file.toString()}`, webExtension.identifier.id, getErrorMessage(error));930}931}932if (isUndefined(webExtension.defaultManifestTranslations)) {933if (webExtension.fallbackPackageNLSUri) {934try {935const content = await this.extensionResourceLoaderService.readExtensionResource(webExtension.fallbackPackageNLSUri);936webExtension.defaultManifestTranslations = JSON.parse(content);937update = true;938} catch (error) {939this.logService.error(`Error while fetching default manifest translations of an extension`, webExtension.identifier.id, getErrorMessage(error));940}941} else {942update = true;943webExtension.defaultManifestTranslations = null;944}945}946const migratedLocation = migratePlatformSpecificExtensionGalleryResourceURL(webExtension.location, TargetPlatform.WEB);947if (migratedLocation) {948update = true;949webExtension.location = migratedLocation;950}951if (isUndefined(webExtension.metadata?.hasPreReleaseVersion) && webExtension.metadata?.preRelease) {952update = true;953webExtension.metadata.hasPreReleaseVersion = true;954}955return webExtension;956}));957if (update) {958await this.storeWebExtensions(webExtensions, file);959}960return webExtensions;961}962963private async storeWebExtensions(webExtensions: IWebExtension[], file: URI): Promise<void> {964function toStringDictionary(dictionary: Map<string, URI> | undefined): IStringDictionary<UriComponents> | undefined {965if (!dictionary) {966return undefined;967}968const result: IStringDictionary<UriComponents> = Object.create(null);969dictionary.forEach((value, key) => result[key] = value.toJSON());970return result;971}972const storedWebExtensions: IStoredWebExtension[] = webExtensions.map(e => ({973identifier: e.identifier,974version: e.version,975manifest: e.manifest,976location: e.location.toJSON(),977readmeUri: e.readmeUri?.toJSON(),978changelogUri: e.changelogUri?.toJSON(),979packageNLSUris: toStringDictionary(e.packageNLSUris),980defaultManifestTranslations: e.defaultManifestTranslations,981fallbackPackageNLSUri: e.fallbackPackageNLSUri?.toJSON(),982metadata: e.metadata983}));984await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedWebExtensions)));985}986987private getResourceAccessQueue(file: URI): Queue<IWebExtension[]> {988let resourceQueue = this.resourcesAccessQueueMap.get(file);989if (!resourceQueue) {990this.resourcesAccessQueueMap.set(file, resourceQueue = new Queue<IWebExtension[]>());991}992return resourceQueue;993}994995}996997if (isWeb) {998registerAction2(class extends Action2 {999constructor() {1000super({1001id: 'workbench.extensions.action.openInstalledWebExtensionsResource',1002title: localize2('openInstalledWebExtensionsResource', 'Open Installed Web Extensions Resource'),1003category: Categories.Developer,1004f1: true,1005precondition: IsWebContext1006});1007}1008run(serviceAccessor: ServicesAccessor): void {1009const editorService = serviceAccessor.get(IEditorService);1010const userDataProfileService = serviceAccessor.get(IUserDataProfileService);1011editorService.openEditor({ resource: userDataProfileService.currentProfile.extensionsResource });1012}1013});1014}10151016registerSingleton(IWebExtensionsScannerService, WebExtensionsScannerService, InstantiationType.Delayed);101710181019