Path: blob/main/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.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 { 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(thing: unknown): thing is UriComponents {57if (!thing) {58return false;59}60return isString((<any>thing).path) &&61isString((<any>thing).scheme);62}6364interface IStoredWebExtension {65readonly identifier: IExtensionIdentifier;66readonly version: string;67readonly location: UriComponents;68readonly manifest?: IExtensionManifest;69readonly readmeUri?: UriComponents;70readonly changelogUri?: UriComponents;71// deprecated in favor of packageNLSUris & fallbackPackageNLSUri72readonly packageNLSUri?: UriComponents;73readonly packageNLSUris?: IStringDictionary<UriComponents>;74readonly fallbackPackageNLSUri?: UriComponents;75readonly defaultManifestTranslations?: ITranslations | null;76readonly metadata?: Metadata;77}7879interface IWebExtension {80identifier: IExtensionIdentifier;81version: string;82location: URI;83manifest?: IExtensionManifest;84readmeUri?: URI;85changelogUri?: URI;86// deprecated in favor of packageNLSUris & fallbackPackageNLSUri87packageNLSUri?: URI;88packageNLSUris?: Map<string, URI>;89fallbackPackageNLSUri?: URI;90defaultManifestTranslations?: ITranslations | null;91metadata?: Metadata;92}9394export class WebExtensionsScannerService extends Disposable implements IWebExtensionsScannerService {9596declare readonly _serviceBrand: undefined;9798private readonly systemExtensionsCacheResource: URI | undefined = undefined;99private readonly customBuiltinExtensionsCacheResource: URI | undefined = undefined;100private readonly resourcesAccessQueueMap = new ResourceMap<Queue<IWebExtension[]>>();101private readonly extensionsEnabledWithApiProposalVersion: string[];102103constructor(104@IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService,105@IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService,106@IFileService private readonly fileService: IFileService,107@ILogService private readonly logService: ILogService,108@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,109@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,110@IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService,111@IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService,112@IStorageService private readonly storageService: IStorageService,113@IProductService private readonly productService: IProductService,114@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,115@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,116@ILifecycleService lifecycleService: ILifecycleService,117) {118super();119if (isWeb) {120this.systemExtensionsCacheResource = joinPath(environmentService.userRoamingDataHome, 'systemExtensionsCache.json');121this.customBuiltinExtensionsCacheResource = joinPath(environmentService.userRoamingDataHome, 'customBuiltinExtensionsCache.json');122123// Eventually update caches124lifecycleService.when(LifecyclePhase.Eventually).then(() => this.updateCaches());125}126this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? [];127}128129private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> | undefined;130private readCustomBuiltinExtensionsInfoFromEnv(): Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> {131if (!this._customBuiltinExtensionsInfoPromise) {132this._customBuiltinExtensionsInfoPromise = (async () => {133let extensions: ExtensionInfo[] = [];134const extensionLocations: URI[] = [];135const extensionGalleryResources: URI[] = [];136const extensionsToMigrate: [string, string][] = [];137const customBuiltinExtensionsInfo = this.environmentService.options && Array.isArray(this.environmentService.options.additionalBuiltinExtensions)138? this.environmentService.options.additionalBuiltinExtensions.map(additionalBuiltinExtension => isString(additionalBuiltinExtension) ? { id: additionalBuiltinExtension } : additionalBuiltinExtension)139: [];140for (const e of customBuiltinExtensionsInfo) {141if (isGalleryExtensionInfo(e)) {142extensions.push({ id: e.id, preRelease: !!e.preRelease });143if (e.migrateStorageFrom) {144extensionsToMigrate.push([e.migrateStorageFrom, e.id]);145}146} else if (isUriComponents(e)) {147const extensionLocation = URI.revive(e);148if (await this.extensionResourceLoaderService.isExtensionGalleryResource(extensionLocation)) {149extensionGalleryResources.push(extensionLocation);150} else {151extensionLocations.push(extensionLocation);152}153}154}155if (extensions.length) {156extensions = await this.checkAdditionalBuiltinExtensions(extensions);157}158if (extensions.length) {159this.logService.info('Found additional builtin gallery extensions in env', extensions);160}161if (extensionLocations.length) {162this.logService.info('Found additional builtin location extensions in env', extensionLocations.map(e => e.toString()));163}164if (extensionGalleryResources.length) {165this.logService.info('Found additional builtin extension gallery resources in env', extensionGalleryResources.map(e => e.toString()));166}167return { extensions, extensionsToMigrate, extensionLocations, extensionGalleryResources };168})();169}170return this._customBuiltinExtensionsInfoPromise;171}172173private async checkAdditionalBuiltinExtensions(extensions: ExtensionInfo[]): Promise<ExtensionInfo[]> {174const extensionsControlManifest = await this.galleryService.getExtensionsControlManifest();175const result: ExtensionInfo[] = [];176for (const extension of extensions) {177if (isMalicious({ id: extension.id }, extensionsControlManifest.malicious)) {178this.logService.info(`Checking additional builtin extensions: Ignoring '${extension.id}' because it is reported to be malicious.`);179continue;180}181const deprecationInfo = extensionsControlManifest.deprecated[extension.id.toLowerCase()];182if (deprecationInfo?.extension?.autoMigrate) {183const preReleaseExtensionId = deprecationInfo.extension.id;184this.logService.info(`Checking additional builtin extensions: '${extension.id}' is deprecated, instead using '${preReleaseExtensionId}'`);185result.push({ id: preReleaseExtensionId, preRelease: !!extension.preRelease });186} else {187result.push(extension);188}189}190return result;191}192193/**194* All system extensions bundled with the product195*/196private async readSystemExtensions(): Promise<IExtension[]> {197const systemExtensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions();198const cachedSystemExtensions = await Promise.all((await this.readSystemExtensionsCache()).map(e => this.toScannedExtension(e, true, ExtensionType.System)));199200const result = new Map<string, IExtension>();201for (const extension of [...systemExtensions, ...cachedSystemExtensions]) {202const existing = result.get(extension.identifier.id.toLowerCase());203if (existing) {204// Incase there are duplicates always take the latest version205if (semver.gt(existing.manifest.version, extension.manifest.version)) {206continue;207}208}209result.set(extension.identifier.id.toLowerCase(), extension);210}211return [...result.values()];212}213214/**215* All extensions defined via `additionalBuiltinExtensions` API216*/217private async readCustomBuiltinExtensions(scanOptions?: ScanOptions): Promise<IScannedExtension[]> {218const [customBuiltinExtensionsFromLocations, customBuiltinExtensionsFromGallery] = await Promise.all([219this.getCustomBuiltinExtensionsFromLocations(scanOptions),220this.getCustomBuiltinExtensionsFromGallery(scanOptions),221]);222const customBuiltinExtensions: IScannedExtension[] = [...customBuiltinExtensionsFromLocations, ...customBuiltinExtensionsFromGallery];223await this.migrateExtensionsStorage(customBuiltinExtensions);224return customBuiltinExtensions;225}226227private async getCustomBuiltinExtensionsFromLocations(scanOptions?: ScanOptions): Promise<IScannedExtension[]> {228const { extensionLocations } = await this.readCustomBuiltinExtensionsInfoFromEnv();229if (!extensionLocations.length) {230return [];231}232const result: IScannedExtension[] = [];233await Promise.allSettled(extensionLocations.map(async extensionLocation => {234try {235const webExtension = await this.toWebExtension(extensionLocation);236const extension = await this.toScannedExtension(webExtension, true);237if (extension.isValid || !scanOptions?.skipInvalidExtensions) {238result.push(extension);239} else {240this.logService.info(`Skipping invalid additional builtin extension ${webExtension.identifier.id}`);241}242} catch (error) {243this.logService.info(`Error while fetching the additional builtin extension ${extensionLocation.toString()}.`, getErrorMessage(error));244}245}));246return result;247}248249private async getCustomBuiltinExtensionsFromGallery(scanOptions?: ScanOptions): Promise<IScannedExtension[]> {250if (!this.galleryService.isEnabled()) {251this.logService.info('Ignoring fetching additional builtin extensions from gallery as it is disabled.');252return [];253}254const result: IScannedExtension[] = [];255const { extensions, extensionGalleryResources } = await this.readCustomBuiltinExtensionsInfoFromEnv();256try {257const cacheValue = JSON.stringify({258extensions: extensions.sort((a, b) => a.id.localeCompare(b.id)),259extensionGalleryResources: extensionGalleryResources.map(e => e.toString()).sort()260});261const useCache = this.storageService.get('additionalBuiltinExtensions', StorageScope.APPLICATION, '{}') === cacheValue;262const webExtensions = await (useCache ? this.getCustomBuiltinExtensionsFromCache() : this.updateCustomBuiltinExtensionsCache());263if (webExtensions.length) {264await Promise.all(webExtensions.map(async webExtension => {265try {266const extension = await this.toScannedExtension(webExtension, true);267if (extension.isValid || !scanOptions?.skipInvalidExtensions) {268result.push(extension);269} else {270this.logService.info(`Skipping invalid additional builtin gallery extension ${webExtension.identifier.id}`);271}272} catch (error) {273this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because there is an error while converting it into scanned extension`, getErrorMessage(error));274}275}));276}277this.storageService.store('additionalBuiltinExtensions', cacheValue, StorageScope.APPLICATION, StorageTarget.MACHINE);278} catch (error) {279this.logService.info('Ignoring following additional builtin extensions as there is an error while fetching them from gallery', extensions.map(({ id }) => id), getErrorMessage(error));280}281return result;282}283284private async getCustomBuiltinExtensionsFromCache(): Promise<IWebExtension[]> {285const cachedCustomBuiltinExtensions = await this.readCustomBuiltinExtensionsCache();286const webExtensionsMap = new Map<string, IWebExtension>();287for (const webExtension of cachedCustomBuiltinExtensions) {288const existing = webExtensionsMap.get(webExtension.identifier.id.toLowerCase());289if (existing) {290// Incase there are duplicates always take the latest version291if (semver.gt(existing.version, webExtension.version)) {292continue;293}294}295/* Update preRelease flag in the cache - https://github.com/microsoft/vscode/issues/142831 */296if (webExtension.metadata?.isPreReleaseVersion && !webExtension.metadata?.preRelease) {297webExtension.metadata.preRelease = true;298}299webExtensionsMap.set(webExtension.identifier.id.toLowerCase(), webExtension);300}301return [...webExtensionsMap.values()];302}303304private _migrateExtensionsStoragePromise: Promise<void> | undefined;305private async migrateExtensionsStorage(customBuiltinExtensions: IExtension[]): Promise<void> {306if (!this._migrateExtensionsStoragePromise) {307this._migrateExtensionsStoragePromise = (async () => {308const { extensionsToMigrate } = await this.readCustomBuiltinExtensionsInfoFromEnv();309if (!extensionsToMigrate.length) {310return;311}312const fromExtensions = await this.galleryService.getExtensions(extensionsToMigrate.map(([id]) => ({ id })), CancellationToken.None);313try {314await Promise.allSettled(extensionsToMigrate.map(async ([from, to]) => {315const toExtension = customBuiltinExtensions.find(extension => areSameExtensions(extension.identifier, { id: to }));316if (toExtension) {317const fromExtension = fromExtensions.find(extension => areSameExtensions(extension.identifier, { id: from }));318const fromExtensionManifest = fromExtension ? await this.galleryService.getManifest(fromExtension, CancellationToken.None) : null;319const fromExtensionId = fromExtensionManifest ? getExtensionId(fromExtensionManifest.publisher, fromExtensionManifest.name) : from;320const toExtensionId = getExtensionId(toExtension.manifest.publisher, toExtension.manifest.name);321this.extensionStorageService.addToMigrationList(fromExtensionId, toExtensionId);322} else {323this.logService.info(`Skipped migrating extension storage from '${from}' to '${to}', because the '${to}' extension is not found.`);324}325}));326} catch (error) {327this.logService.error(error);328}329})();330}331return this._migrateExtensionsStoragePromise;332}333334private async updateCaches(): Promise<void> {335await this.updateSystemExtensionsCache();336await this.updateCustomBuiltinExtensionsCache();337}338339private async updateSystemExtensionsCache(): Promise<void> {340const systemExtensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions();341const cachedSystemExtensions = (await this.readSystemExtensionsCache())342.filter(cached => {343const systemExtension = systemExtensions.find(e => areSameExtensions(e.identifier, cached.identifier));344return systemExtension && semver.gt(cached.version, systemExtension.manifest.version);345});346await this.writeSystemExtensionsCache(() => cachedSystemExtensions);347}348349private _updateCustomBuiltinExtensionsCachePromise: Promise<IWebExtension[]> | undefined;350private async updateCustomBuiltinExtensionsCache(): Promise<IWebExtension[]> {351if (!this._updateCustomBuiltinExtensionsCachePromise) {352this._updateCustomBuiltinExtensionsCachePromise = (async () => {353this.logService.info('Updating additional builtin extensions cache');354const { extensions, extensionGalleryResources } = await this.readCustomBuiltinExtensionsInfoFromEnv();355const [galleryWebExtensions, extensionGalleryResourceWebExtensions] = await Promise.all([356this.resolveBuiltinGalleryExtensions(extensions),357this.resolveBuiltinExtensionGalleryResources(extensionGalleryResources)358]);359const webExtensionsMap = new Map<string, IWebExtension>();360for (const webExtension of [...galleryWebExtensions, ...extensionGalleryResourceWebExtensions]) {361webExtensionsMap.set(webExtension.identifier.id.toLowerCase(), webExtension);362}363await this.resolveDependenciesAndPackedExtensions(extensionGalleryResourceWebExtensions, webExtensionsMap);364const webExtensions = [...webExtensionsMap.values()];365await this.writeCustomBuiltinExtensionsCache(() => webExtensions);366return webExtensions;367})();368}369return this._updateCustomBuiltinExtensionsCachePromise;370}371372private async resolveBuiltinExtensionGalleryResources(extensionGalleryResources: URI[]): Promise<IWebExtension[]> {373if (extensionGalleryResources.length === 0) {374return [];375}376const result = new Map<string, IWebExtension>();377const extensionInfos: IExtensionInfo[] = [];378await Promise.all(extensionGalleryResources.map(async extensionGalleryResource => {379try {380const webExtension = await this.toWebExtensionFromExtensionGalleryResource(extensionGalleryResource);381result.set(webExtension.identifier.id.toLowerCase(), webExtension);382extensionInfos.push({ id: webExtension.identifier.id, version: webExtension.version });383} catch (error) {384this.logService.info(`Ignoring additional builtin extension from gallery resource ${extensionGalleryResource.toString()} because there is an error while converting it into web extension`, getErrorMessage(error));385}386}));387const galleryExtensions = await this.galleryService.getExtensions(extensionInfos, CancellationToken.None);388for (const galleryExtension of galleryExtensions) {389const webExtension = result.get(galleryExtension.identifier.id.toLowerCase());390if (webExtension) {391result.set(galleryExtension.identifier.id.toLowerCase(), {392...webExtension,393identifier: { id: webExtension.identifier.id, uuid: galleryExtension.identifier.uuid },394readmeUri: galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,395changelogUri: galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,396metadata: { isPreReleaseVersion: galleryExtension.properties.isPreReleaseVersion, preRelease: galleryExtension.properties.isPreReleaseVersion, isBuiltin: true, pinned: true }397});398}399}400return [...result.values()];401}402403private async resolveBuiltinGalleryExtensions(extensions: IExtensionInfo[]): Promise<IWebExtension[]> {404if (extensions.length === 0) {405return [];406}407const webExtensions: IWebExtension[] = [];408const galleryExtensionsMap = await this.getExtensionsWithDependenciesAndPackedExtensions(extensions);409const missingExtensions = extensions.filter(({ id }) => !galleryExtensionsMap.has(id.toLowerCase()));410if (missingExtensions.length) {411this.logService.info('Skipping the additional builtin extensions because their compatible versions are not found.', missingExtensions);412}413await Promise.all([...galleryExtensionsMap.values()].map(async gallery => {414try {415const webExtension = await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true });416webExtensions.push(webExtension);417} catch (error) {418this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error));419}420}));421return webExtensions;422}423424private async resolveDependenciesAndPackedExtensions(webExtensions: IWebExtension[], result: Map<string, IWebExtension>): Promise<void> {425const extensionInfos: IExtensionInfo[] = [];426for (const webExtension of webExtensions) {427for (const e of [...(webExtension.manifest?.extensionDependencies ?? []), ...(webExtension.manifest?.extensionPack ?? [])]) {428if (!result.has(e.toLowerCase())) {429extensionInfos.push({ id: e, version: webExtension.version });430}431}432}433if (extensionInfos.length === 0) {434return;435}436const galleryExtensions = await this.getExtensionsWithDependenciesAndPackedExtensions(extensionInfos, new Set<string>([...result.keys()]));437await Promise.all([...galleryExtensions.values()].map(async gallery => {438try {439const webExtension = await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true });440result.set(webExtension.identifier.id.toLowerCase(), webExtension);441} catch (error) {442this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error));443}444}));445}446447private async getExtensionsWithDependenciesAndPackedExtensions(toGet: IExtensionInfo[], seen: Set<string> = new Set<string>(), result: Map<string, IGalleryExtension> = new Map<string, IGalleryExtension>()): Promise<Map<string, IGalleryExtension>> {448if (toGet.length === 0) {449return result;450}451const extensions = await this.galleryService.getExtensions(toGet, { compatible: true, targetPlatform: TargetPlatform.WEB }, CancellationToken.None);452const packsAndDependencies = new Map<string, IExtensionInfo>();453for (const extension of extensions) {454result.set(extension.identifier.id.toLowerCase(), extension);455for (const id of [...(isNonEmptyArray(extension.properties.dependencies) ? extension.properties.dependencies : []), ...(isNonEmptyArray(extension.properties.extensionPack) ? extension.properties.extensionPack : [])]) {456if (!result.has(id.toLowerCase()) && !packsAndDependencies.has(id.toLowerCase()) && !seen.has(id.toLowerCase())) {457const extensionInfo = toGet.find(e => areSameExtensions(e, extension.identifier));458packsAndDependencies.set(id.toLowerCase(), { id, preRelease: extensionInfo?.preRelease });459}460}461}462return this.getExtensionsWithDependenciesAndPackedExtensions([...packsAndDependencies.values()].filter(({ id }) => !result.has(id.toLowerCase())), seen, result);463}464465async scanSystemExtensions(): Promise<IExtension[]> {466return this.readSystemExtensions();467}468469async scanUserExtensions(profileLocation: URI, scanOptions?: ScanOptions): Promise<IScannedExtension[]> {470const extensions = new Map<string, IScannedExtension>();471472// Custom builtin extensions defined through `additionalBuiltinExtensions` API473const customBuiltinExtensions = await this.readCustomBuiltinExtensions(scanOptions);474for (const extension of customBuiltinExtensions) {475extensions.set(extension.identifier.id.toLowerCase(), extension);476}477478// User Installed extensions479const installedExtensions = await this.scanInstalledExtensions(profileLocation, scanOptions);480for (const extension of installedExtensions) {481extensions.set(extension.identifier.id.toLowerCase(), extension);482}483484return [...extensions.values()];485}486487async scanExtensionsUnderDevelopment(): Promise<IExtension[]> {488const devExtensions = this.environmentService.options?.developmentOptions?.extensions;489const result: IExtension[] = [];490if (Array.isArray(devExtensions)) {491await Promise.allSettled(devExtensions.map(async devExtension => {492try {493const location = URI.revive(devExtension);494if (URI.isUri(location)) {495const webExtension = await this.toWebExtension(location);496result.push(await this.toScannedExtension(webExtension, false));497} else {498this.logService.info(`Skipping the extension under development ${devExtension} as it is not URI type.`);499}500} catch (error) {501this.logService.info(`Error while fetching the extension under development ${devExtension.toString()}.`, getErrorMessage(error));502}503}));504}505return result;506}507508async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, profileLocation: URI): Promise<IScannedExtension | null> {509if (extensionType === ExtensionType.System) {510const systemExtensions = await this.scanSystemExtensions();511return systemExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null;512}513const userExtensions = await this.scanUserExtensions(profileLocation);514return userExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null;515}516517async scanExtensionManifest(extensionLocation: URI): Promise<IExtensionManifest | null> {518try {519return await this.getExtensionManifest(extensionLocation);520} catch (error) {521this.logService.warn(`Error while fetching manifest from ${extensionLocation.toString()}`, getErrorMessage(error));522return null;523}524}525526async addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata: Metadata, profileLocation: URI): Promise<IScannedExtension> {527const webExtension = await this.toWebExtensionFromGallery(galleryExtension, metadata);528return this.addWebExtension(webExtension, profileLocation);529}530531async addExtension(location: URI, metadata: Metadata, profileLocation: URI): Promise<IScannedExtension> {532const webExtension = await this.toWebExtension(location, undefined, undefined, undefined, undefined, undefined, undefined, metadata);533const extension = await this.toScannedExtension(webExtension, false);534await this.addToInstalledExtensions([webExtension], profileLocation);535return extension;536}537538async removeExtension(extension: IScannedExtension, profileLocation: URI): Promise<void> {539await this.writeInstalledExtensions(profileLocation, installedExtensions => installedExtensions.filter(installedExtension => !areSameExtensions(installedExtension.identifier, extension.identifier)));540}541542async updateMetadata(extension: IScannedExtension, metadata: Partial<Metadata>, profileLocation: URI): Promise<IScannedExtension> {543let updatedExtension: IWebExtension | undefined = undefined;544await this.writeInstalledExtensions(profileLocation, installedExtensions => {545const result: IWebExtension[] = [];546for (const installedExtension of installedExtensions) {547if (areSameExtensions(extension.identifier, installedExtension.identifier)) {548installedExtension.metadata = { ...installedExtension.metadata, ...metadata };549updatedExtension = installedExtension;550result.push(installedExtension);551} else {552result.push(installedExtension);553}554}555return result;556});557if (!updatedExtension) {558throw new Error('Extension not found');559}560return this.toScannedExtension(updatedExtension, extension.isBuiltin);561}562563async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, filter: (extension: IScannedExtension) => boolean): Promise<void> {564const extensionsToCopy: IWebExtension[] = [];565const fromWebExtensions = await this.readInstalledExtensions(fromProfileLocation);566await Promise.all(fromWebExtensions.map(async webExtension => {567const scannedExtension = await this.toScannedExtension(webExtension, false);568if (filter(scannedExtension)) {569extensionsToCopy.push(webExtension);570}571}));572if (extensionsToCopy.length) {573await this.addToInstalledExtensions(extensionsToCopy, toProfileLocation);574}575}576577private async addWebExtension(webExtension: IWebExtension, profileLocation: URI): Promise<IScannedExtension> {578const isSystem = !!(await this.scanSystemExtensions()).find(e => areSameExtensions(e.identifier, webExtension.identifier));579const isBuiltin = !!webExtension.metadata?.isBuiltin;580const extension = await this.toScannedExtension(webExtension, isBuiltin);581582if (isSystem) {583await this.writeSystemExtensionsCache(systemExtensions => {584// Remove the existing extension to avoid duplicates585systemExtensions = systemExtensions.filter(extension => !areSameExtensions(extension.identifier, webExtension.identifier));586systemExtensions.push(webExtension);587return systemExtensions;588});589return extension;590}591592// Update custom builtin extensions to custom builtin extensions cache593if (isBuiltin) {594await this.writeCustomBuiltinExtensionsCache(customBuiltinExtensions => {595// Remove the existing extension to avoid duplicates596customBuiltinExtensions = customBuiltinExtensions.filter(extension => !areSameExtensions(extension.identifier, webExtension.identifier));597customBuiltinExtensions.push(webExtension);598return customBuiltinExtensions;599});600601const installedExtensions = await this.readInstalledExtensions(profileLocation);602// Also add to installed extensions if it is installed to update its version603if (installedExtensions.some(e => areSameExtensions(e.identifier, webExtension.identifier))) {604await this.addToInstalledExtensions([webExtension], profileLocation);605}606return extension;607}608609// Add to installed extensions610await this.addToInstalledExtensions([webExtension], profileLocation);611return extension;612}613614private async addToInstalledExtensions(webExtensions: IWebExtension[], profileLocation: URI): Promise<void> {615await this.writeInstalledExtensions(profileLocation, installedExtensions => {616// Remove the existing extension to avoid duplicates617installedExtensions = installedExtensions.filter(installedExtension => webExtensions.some(extension => !areSameExtensions(installedExtension.identifier, extension.identifier)));618installedExtensions.push(...webExtensions);619return installedExtensions;620});621}622623private async scanInstalledExtensions(profileLocation: URI, scanOptions?: ScanOptions): Promise<IScannedExtension[]> {624let installedExtensions = await this.readInstalledExtensions(profileLocation);625626// If current profile is not a default profile, then add the application extensions to the list627if (!this.uriIdentityService.extUri.isEqual(profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) {628// Remove application extensions from the non default profile629installedExtensions = installedExtensions.filter(i => !i.metadata?.isApplicationScoped);630// Add application extensions from the default profile to the list631const defaultProfileExtensions = await this.readInstalledExtensions(this.userDataProfilesService.defaultProfile.extensionsResource);632installedExtensions.push(...defaultProfileExtensions.filter(i => i.metadata?.isApplicationScoped));633}634635installedExtensions.sort((a, b) => a.identifier.id < b.identifier.id ? -1 : a.identifier.id > b.identifier.id ? 1 : semver.rcompare(a.version, b.version));636const result = new Map<string, IScannedExtension>();637for (const webExtension of installedExtensions) {638const existing = result.get(webExtension.identifier.id.toLowerCase());639if (existing && semver.gt(existing.manifest.version, webExtension.version)) {640continue;641}642const extension = await this.toScannedExtension(webExtension, false);643if (extension.isValid || !scanOptions?.skipInvalidExtensions) {644result.set(extension.identifier.id.toLowerCase(), extension);645} else {646this.logService.info(`Skipping invalid installed extension ${webExtension.identifier.id}`);647}648}649return [...result.values()];650}651652private async toWebExtensionFromGallery(galleryExtension: IGalleryExtension, metadata?: Metadata): Promise<IWebExtension> {653const extensionLocation = await this.extensionResourceLoaderService.getExtensionGalleryResourceURL({654publisher: galleryExtension.publisher,655name: galleryExtension.name,656version: galleryExtension.version,657targetPlatform: galleryExtension.properties.targetPlatform === TargetPlatform.WEB ? TargetPlatform.WEB : undefined658}, 'extension');659660if (!extensionLocation) {661throw new Error('No extension gallery service configured.');662}663664return this.toWebExtensionFromExtensionGalleryResource(extensionLocation,665galleryExtension.identifier,666galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,667galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,668metadata);669}670671private async toWebExtensionFromExtensionGalleryResource(extensionLocation: URI, identifier?: IExtensionIdentifier, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> {672const extensionResources = await this.listExtensionResources(extensionLocation);673const packageNLSResources = this.getPackageNLSResourceMapFromResources(extensionResources);674675// The fallback, in English, will fill in any gaps missing in the localized file.676const fallbackPackageNLSResource = extensionResources.find(e => basename(e) === 'package.nls.json');677return this.toWebExtension(678extensionLocation,679identifier,680undefined,681packageNLSResources,682fallbackPackageNLSResource ? URI.parse(fallbackPackageNLSResource) : null,683readmeUri,684changelogUri,685metadata);686}687688private getPackageNLSResourceMapFromResources(extensionResources: string[]): Map<string, URI> {689const packageNLSResources = new Map<string, URI>();690extensionResources.forEach(e => {691// Grab all package.nls.{language}.json files692const regexResult = /package\.nls\.([\w-]+)\.json/.exec(basename(e));693if (regexResult?.[1]) {694packageNLSResources.set(regexResult[1], URI.parse(e));695}696});697return packageNLSResources;698}699700private async toWebExtension(extensionLocation: URI, identifier?: IExtensionIdentifier, manifest?: IExtensionManifest, packageNLSUris?: Map<string, URI>, fallbackPackageNLSUri?: URI | ITranslations | null, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> {701if (!manifest) {702try {703manifest = await this.getExtensionManifest(extensionLocation);704} catch (error) {705throw new Error(`Error while fetching manifest from the location '${extensionLocation.toString()}'. ${getErrorMessage(error)}`);706}707}708709if (!this.extensionManifestPropertiesService.canExecuteOnWeb(manifest)) {710throw new Error(localize('not a web extension', "Cannot add '{0}' because this extension is not a web extension.", manifest.displayName || manifest.name));711}712713if (fallbackPackageNLSUri === undefined) {714try {715fallbackPackageNLSUri = joinPath(extensionLocation, 'package.nls.json');716await this.extensionResourceLoaderService.readExtensionResource(fallbackPackageNLSUri);717} catch (error) {718fallbackPackageNLSUri = undefined;719}720}721const defaultManifestTranslations: ITranslations | null | undefined = fallbackPackageNLSUri ? URI.isUri(fallbackPackageNLSUri) ? await this.getTranslations(fallbackPackageNLSUri) : fallbackPackageNLSUri : null;722723return {724identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: identifier?.uuid },725version: manifest.version,726location: extensionLocation,727manifest,728readmeUri,729changelogUri,730packageNLSUris,731fallbackPackageNLSUri: URI.isUri(fallbackPackageNLSUri) ? fallbackPackageNLSUri : undefined,732defaultManifestTranslations,733metadata,734};735}736737private async toScannedExtension(webExtension: IWebExtension, isBuiltin: boolean, type: ExtensionType = ExtensionType.User): Promise<IScannedExtension> {738const validations: [Severity, string][] = [];739let manifest: IRelaxedExtensionManifest | undefined = webExtension.manifest;740741if (!manifest) {742try {743manifest = await this.getExtensionManifest(webExtension.location);744} catch (error) {745validations.push([Severity.Error, `Error while fetching manifest from the location '${webExtension.location}'. ${getErrorMessage(error)}`]);746}747}748749if (!manifest) {750const [publisher, name] = webExtension.identifier.id.split('.');751manifest = {752name,753publisher,754version: webExtension.version,755engines: { vscode: '*' },756};757}758759const packageNLSUri = webExtension.packageNLSUris?.get(Language.value().toLowerCase());760const fallbackPackageNLS = webExtension.defaultManifestTranslations ?? webExtension.fallbackPackageNLSUri;761762if (packageNLSUri) {763manifest = await this.translateManifest(manifest, packageNLSUri, fallbackPackageNLS);764} else if (fallbackPackageNLS) {765manifest = await this.translateManifest(manifest, fallbackPackageNLS);766}767768const uuid = (<IGalleryMetadata | undefined>webExtension.metadata)?.id;769770const validateApiVersion = this.extensionsEnabledWithApiProposalVersion.includes(webExtension.identifier.id.toLowerCase());771validations.push(...validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false, validateApiVersion));772let isValid = true;773for (const [severity, message] of validations) {774if (severity === Severity.Error) {775isValid = false;776this.logService.error(message);777}778}779780if (manifest.enabledApiProposals && validateApiVersion) {781manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]);782}783784return {785identifier: { id: webExtension.identifier.id, uuid: webExtension.identifier.uuid || uuid },786location: webExtension.location,787manifest,788type,789isBuiltin,790readmeUrl: webExtension.readmeUri,791changelogUrl: webExtension.changelogUri,792metadata: webExtension.metadata,793targetPlatform: TargetPlatform.WEB,794validations,795isValid,796preRelease: !!webExtension.metadata?.preRelease,797};798}799800private async listExtensionResources(extensionLocation: URI): Promise<string[]> {801try {802const result = await this.extensionResourceLoaderService.readExtensionResource(extensionLocation);803return JSON.parse(result);804} catch (error) {805this.logService.warn('Error while fetching extension resources list', getErrorMessage(error));806}807return [];808}809810private async translateManifest(manifest: IExtensionManifest, nlsURL: ITranslations | URI, fallbackNLS?: ITranslations | URI): Promise<IRelaxedExtensionManifest> {811try {812const translations = URI.isUri(nlsURL) ? await this.getTranslations(nlsURL) : nlsURL;813const fallbackTranslations = URI.isUri(fallbackNLS) ? await this.getTranslations(fallbackNLS) : fallbackNLS;814if (translations) {815manifest = localizeManifest(this.logService, manifest, translations, fallbackTranslations);816}817} catch (error) { /* ignore */ }818return manifest;819}820821private async getExtensionManifest(location: URI): Promise<IExtensionManifest> {822const url = joinPath(location, 'package.json');823const content = await this.extensionResourceLoaderService.readExtensionResource(url);824return JSON.parse(content);825}826827private async getTranslations(nlsUrl: URI): Promise<ITranslations | undefined> {828try {829const content = await this.extensionResourceLoaderService.readExtensionResource(nlsUrl);830return JSON.parse(content);831} catch (error) {832this.logService.error(`Error while fetching translations of an extension`, nlsUrl.toString(), getErrorMessage(error));833}834return undefined;835}836837private async readInstalledExtensions(profileLocation: URI): Promise<IWebExtension[]> {838return this.withWebExtensions(profileLocation);839}840841private writeInstalledExtensions(profileLocation: URI, updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {842return this.withWebExtensions(profileLocation, updateFn);843}844845private readCustomBuiltinExtensionsCache(): Promise<IWebExtension[]> {846return this.withWebExtensions(this.customBuiltinExtensionsCacheResource);847}848849private writeCustomBuiltinExtensionsCache(updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {850return this.withWebExtensions(this.customBuiltinExtensionsCacheResource, updateFn);851}852853private readSystemExtensionsCache(): Promise<IWebExtension[]> {854return this.withWebExtensions(this.systemExtensionsCacheResource);855}856857private writeSystemExtensionsCache(updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {858return this.withWebExtensions(this.systemExtensionsCacheResource, updateFn);859}860861private async withWebExtensions(file: URI | undefined, updateFn?: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {862if (!file) {863return [];864}865return this.getResourceAccessQueue(file).queue(async () => {866let webExtensions: IWebExtension[] = [];867868// Read869try {870const content = await this.fileService.readFile(file);871const storedWebExtensions: IStoredWebExtension[] = JSON.parse(content.value.toString());872for (const e of storedWebExtensions) {873if (!e.location || !e.identifier || !e.version) {874this.logService.info('Ignoring invalid extension while scanning', storedWebExtensions);875continue;876}877let packageNLSUris: Map<string, URI> | undefined;878if (e.packageNLSUris) {879packageNLSUris = new Map<string, URI>();880Object.entries(e.packageNLSUris).forEach(([key, value]) => packageNLSUris!.set(key, URI.revive(value)));881}882883webExtensions.push({884identifier: e.identifier,885version: e.version,886location: URI.revive(e.location),887manifest: e.manifest,888readmeUri: URI.revive(e.readmeUri),889changelogUri: URI.revive(e.changelogUri),890packageNLSUris,891fallbackPackageNLSUri: URI.revive(e.fallbackPackageNLSUri),892defaultManifestTranslations: e.defaultManifestTranslations,893packageNLSUri: URI.revive(e.packageNLSUri),894metadata: e.metadata,895});896}897898try {899webExtensions = await this.migrateWebExtensions(webExtensions, file);900} catch (error) {901this.logService.error(`Error while migrating scanned extensions in ${file.toString()}`, getErrorMessage(error));902}903904} catch (error) {905/* Ignore */906if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {907this.logService.error(error);908}909}910911// Update912if (updateFn) {913await this.storeWebExtensions(webExtensions = updateFn(webExtensions), file);914}915916return webExtensions;917});918}919920private async migrateWebExtensions(webExtensions: IWebExtension[], file: URI): Promise<IWebExtension[]> {921let update = false;922webExtensions = await Promise.all(webExtensions.map(async webExtension => {923if (!webExtension.manifest) {924try {925webExtension.manifest = await this.getExtensionManifest(webExtension.location);926update = true;927} catch (error) {928this.logService.error(`Error while updating manifest of an extension in ${file.toString()}`, webExtension.identifier.id, getErrorMessage(error));929}930}931if (isUndefined(webExtension.defaultManifestTranslations)) {932if (webExtension.fallbackPackageNLSUri) {933try {934const content = await this.extensionResourceLoaderService.readExtensionResource(webExtension.fallbackPackageNLSUri);935webExtension.defaultManifestTranslations = JSON.parse(content);936update = true;937} catch (error) {938this.logService.error(`Error while fetching default manifest translations of an extension`, webExtension.identifier.id, getErrorMessage(error));939}940} else {941update = true;942webExtension.defaultManifestTranslations = null;943}944}945const migratedLocation = migratePlatformSpecificExtensionGalleryResourceURL(webExtension.location, TargetPlatform.WEB);946if (migratedLocation) {947update = true;948webExtension.location = migratedLocation;949}950if (isUndefined(webExtension.metadata?.hasPreReleaseVersion) && webExtension.metadata?.preRelease) {951update = true;952webExtension.metadata.hasPreReleaseVersion = true;953}954return webExtension;955}));956if (update) {957await this.storeWebExtensions(webExtensions, file);958}959return webExtensions;960}961962private async storeWebExtensions(webExtensions: IWebExtension[], file: URI): Promise<void> {963function toStringDictionary(dictionary: Map<string, URI> | undefined): IStringDictionary<UriComponents> | undefined {964if (!dictionary) {965return undefined;966}967const result: IStringDictionary<UriComponents> = Object.create(null);968dictionary.forEach((value, key) => result[key] = value.toJSON());969return result;970}971const storedWebExtensions: IStoredWebExtension[] = webExtensions.map(e => ({972identifier: e.identifier,973version: e.version,974manifest: e.manifest,975location: e.location.toJSON(),976readmeUri: e.readmeUri?.toJSON(),977changelogUri: e.changelogUri?.toJSON(),978packageNLSUris: toStringDictionary(e.packageNLSUris),979defaultManifestTranslations: e.defaultManifestTranslations,980fallbackPackageNLSUri: e.fallbackPackageNLSUri?.toJSON(),981metadata: e.metadata982}));983await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedWebExtensions)));984}985986private getResourceAccessQueue(file: URI): Queue<IWebExtension[]> {987let resourceQueue = this.resourcesAccessQueueMap.get(file);988if (!resourceQueue) {989this.resourcesAccessQueueMap.set(file, resourceQueue = new Queue<IWebExtension[]>());990}991return resourceQueue;992}993994}995996if (isWeb) {997registerAction2(class extends Action2 {998constructor() {999super({1000id: 'workbench.extensions.action.openInstalledWebExtensionsResource',1001title: localize2('openInstalledWebExtensionsResource', 'Open Installed Web Extensions Resource'),1002category: Categories.Developer,1003f1: true,1004precondition: IsWebContext1005});1006}1007run(serviceAccessor: ServicesAccessor): void {1008const editorService = serviceAccessor.get(IEditorService);1009const userDataProfileService = serviceAccessor.get(IUserDataProfileService);1010editorService.openEditor({ resource: userDataProfileService.currentProfile.extensionsResource });1011}1012});1013}10141015registerSingleton(IWebExtensionsScannerService, WebExtensionsScannerService, InstantiationType.Delayed);101610171018