src/vs/platform/extensionManagement/common/extensionGalleryService.ts
2045 LOC · 489 covered · 1556 uncovered · 86 ranges · 30 concepts · 11 introducers · 29 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
extensionGalleryService.ts ×69
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { distinct } from '../../../base/common/arrays.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import * as semver from '../../../base/common/semver/semver.js';
import { IStringDictionary } from '../../../base/common/collections.js';
import { CancellationError, getErrorMessage, isCancellationError } from '../../../base/common/errors.js';
import { IPager } from '../../../base/common/paging.js';
import { isWeb, platform } from '../../../base/common/platform.js';
import { arch } from '../../../base/common/process.js';
import { isBoolean, isNumber, isString } from '../../../base/common/types.js';
import { URI } from '../../../base/common/uri.js';
import { IHeaders, IRequestContext, IRequestOptions, isOfflineError } from '../../../base/parts/request/common/request.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IEnvironmentService } from '../../environment/common/environment.js';
import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion, IAllowedExtensionsService, EXTENSION_IDENTIFIER_REGEX, SortBy, FilterType, MaliciousExtensionInfo, ExtensionRequestsTimeoutConfigKey } from './extensionManagement.js';
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from './extensionManagementUtil.js';
import { IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js';
import { isEngineValid } from '../../extensions/common/extensionValidator.js';
import { IFileService } from '../../files/common/files.js';
import { ILogService } from '../../log/common/log.js';
import { IProductService } from '../../product/common/productService.js';
import { asJson, asTextOrError, IRequestService, isClientError, isServerError, isSuccess } from '../../request/common/request.js';
import { resolveMarketplaceHeaders } from '../../externalServices/common/marketplace.js';
import { IStorageService } from '../../storage/common/storage.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { StopWatch } from '../../../base/common/stopwatch.js';
import { format2 } from '../../../base/common/strings.js';
import { ExtensionGalleryResourceType, Flag, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService, ExtensionGalleryManifestStatus } from './extensionGalleryManifest.js';
import { TelemetryTrustedValue } from '../../telemetry/common/telemetryUtils.js';
const CURRENT_TARGET_PLATFORM = isWeb ? TargetPlatform.WEB : getTargetPlatform(platform, arch);
const SEARCH_ACTIVITY_HEADER_NAME = 'X-Market-Search-Activity-Id';
const ACTIVITY_HEADER_NAME = 'Activityid';
const SERVER_HEADER_NAME = 'Server';
const END_END_ID_HEADER_NAME = 'X-Vss-E2eid';
interface IRawGalleryExtensionFile {
readonly assetType: string;
readonly source: string;
}
interface IRawGalleryExtensionProperty {
readonly key: string;
readonly value: string;
}
export interface IRawGalleryExtensionVersion {
readonly version: string;
readonly lastUpdated: string;
readonly assetUri: string;
readonly fallbackAssetUri: string;
readonly files: IRawGalleryExtensionFile[];
properties?: IRawGalleryExtensionProperty[];
readonly targetPlatform?: string;
}
interface IRawGalleryExtensionStatistics {
readonly statisticName: string;
readonly value: number;
}
interface IRawGalleryExtensionPublisher {
readonly displayName: string;
readonly publisherId: string;
readonly publisherName: string;
readonly domain?: string | null;
readonly isDomainVerified?: boolean;
readonly linkType?: string;
}
interface IRawGalleryExtension {
readonly extensionId: string;
readonly extensionName: string;
readonly displayName: string;
readonly shortDescription?: string;
readonly publisher: IRawGalleryExtensionPublisher;
readonly versions: IRawGalleryExtensionVersion[];
readonly statistics: IRawGalleryExtensionStatistics[];
readonly tags: string[] | undefined;
readonly releaseDate: string;
readonly publishedDate: string;
readonly lastUpdated: string;
readonly categories: string[] | undefined;
readonly flags: string;
readonly linkType?: string;
readonly ratingLinkType?: string;
}
interface IRawGalleryExtensionsResult {
readonly galleryExtensions: IRawGalleryExtension[];
readonly total: number;
readonly context?: IStringDictionary<string>;
}
interface IRawGalleryQueryResult {
readonly results: {
readonly extensions: IRawGalleryExtension[];
readonly resultMetadata: {
readonly metadataType: string;
readonly metadataItems: {
readonly name: string;
readonly count: number;
}[];
}[];
}[];
}
const AssetType = {
Icon: 'Microsoft.VisualStudio.Services.Icons.Default',
Details: 'Microsoft.VisualStudio.Services.Content.Details',
Changelog: 'Microsoft.VisualStudio.Services.Content.Changelog',
Manifest: 'Microsoft.VisualStudio.Code.Manifest',
VSIX: 'Microsoft.VisualStudio.Services.VSIXPackage',
License: 'Microsoft.VisualStudio.Services.Content.License',
Repository: 'Microsoft.VisualStudio.Services.Links.Source',
Signature: 'Microsoft.VisualStudio.Services.VsixSignature'
};
const PropertyType = {
Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies',
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
Engine: 'Microsoft.VisualStudio.Code.Engine',
PreRelease: 'Microsoft.VisualStudio.Code.PreRelease',
EnabledApiProposals: 'Microsoft.VisualStudio.Code.EnabledApiProposals',
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
WebExtension: 'Microsoft.VisualStudio.Code.WebExtension',
SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink',
SupportLink: 'Microsoft.VisualStudio.Services.Links.Support',
ExecutesCode: 'Microsoft.VisualStudio.Code.ExecutesCode',
Private: 'PrivateMarketplace',
};
interface ICriterium {
readonly filterType: FilterType;
readonly value?: string;
}
const DefaultPageSize = 10;
interface IQueryState {
readonly pageNumber: number;
readonly pageSize: number;
readonly sortBy: SortBy;
readonly sortOrder: SortOrder;
readonly flags: Flag[];
readonly criteria: ICriterium[];
readonly assetTypes: string[];
readonly source?: string;
}
const DefaultQueryState: IQueryState = {
pageNumber: 1,
pageSize: DefaultPageSize,
sortBy: SortBy.NoneOrRelevance,
sortOrder: SortOrder.Default,
flags: [],
criteria: [],
assetTypes: []
};
type GalleryServiceQueryClassification = {
owner: 'sandy081';
comment: 'Information about Marketplace query and its response';
readonly filterTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Filter types used in the query.' };
readonly flags: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flags passed in the query.' };
readonly sortBy: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'sorted by option passed in the query' };
readonly sortOrder: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'sort order option passed in the query' };
readonly pageNumber: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'requested page number in the query' };
readonly duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'amount of time taken by the query request' };
readonly success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'whether the query request is success or not' };
readonly requestBodySize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'size of the request body' };
readonly responseBodySize?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'size of the response body' };
readonly statusCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'status code of the response' };
readonly errorCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code of the response' };
readonly count?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'total number of extensions matching the query' };
readonly source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'source that requested this query, eg., recommendations, viewlet' };
readonly searchTextLength?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'length of the search text in the query' };
readonly server?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'server that handled the query' };
readonly endToEndId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'end to end operation id' };
readonly activityId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'activity id' };
};
type QueryTelemetryData = {
readonly filterTypes: string[];
readonly flags: string[];
readonly sortBy: string;
readonly sortOrder: string;
readonly pageNumber: string;
readonly source?: string;
readonly searchTextLength?: number;
};
type GalleryServiceQueryEvent = QueryTelemetryData & {
readonly duration: number;
readonly success: boolean;
readonly requestBodySize: string;
readonly responseBodySize?: string;
readonly statusCode?: string;
readonly errorCode?: string;
readonly count?: string;
readonly server?: TelemetryTrustedValue<string>;
readonly endToEndId?: TelemetryTrustedValue<string>;
readonly activityId?: TelemetryTrustedValue<string>;
};
type GalleryServiceAdditionalQueryClassification = {
owner: 'sandy081';
comment: 'Response information about the additional query to the Marketplace for fetching all versions to get release version';
readonly duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'Amount of time taken by the additional query' };
readonly count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Total number of extensions returned by this additional query' };
};
type GalleryServiceAdditionalQueryEvent = {
readonly duration: number;
readonly count: number;
};
type ExtensionsCriteria = {
readonly productVersion: IProductVersion;
readonly targetPlatform: TargetPlatform;
readonly compatible: boolean;
readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[];
readonly versions?: (IExtensionIdentifier & { version: string })[];
readonly isQueryForReleaseVersionFromPreReleaseVersion?: boolean;
};
const enum VersionKind {
Release,
Prerelease,
Latest
}
type ExtensionVersionCriteria = {
readonly productVersion: IProductVersion;
readonly targetPlatform: TargetPlatform;
readonly compatible: boolean;
readonly version: VersionKind | string;
};
class Query {
constructor(private state = DefaultQueryState) { }
get pageNumber(): number { return this.state.pageNumber; }
get pageSize(): number { return this.state.pageSize; }
get sortBy(): SortBy { return this.state.sortBy; }
get sortOrder(): number { return this.state.sortOrder; }
get flags(): Flag[] { return this.state.flags; }
get criteria(): ICriterium[] { return this.state.criteria; }
get assetTypes(): string[] { return this.state.assetTypes; }
get source(): string | undefined { return this.state.source; }
get searchText(): string {
const criterium = this.state.criteria.filter(criterium => criterium.filterType === FilterType.SearchText)[0];
return criterium && criterium.value ? criterium.value : '';
}
withPage(pageNumber: number, pageSize: number = this.state.pageSize): Query {
return new Query({ ...this.state, pageNumber, pageSize });
}
withFilter(filterType: FilterType, ...values: string[]): Query {
const criteria = [
...this.state.criteria,
...values.length ? values.map(value => ({ filterType, value })) : [{ filterType }]
];
return new Query({ ...this.state, criteria });
}
withSortBy(sortBy: SortBy): Query {
return new Query({ ...this.state, sortBy });
}
withSortOrder(sortOrder: SortOrder): Query {
return new Query({ ...this.state, sortOrder });
}
withFlags(...flags: Flag[]): Query {
return new Query({ ...this.state, flags: distinct(flags) });
}
withAssetTypes(...assetTypes: string[]): Query {
return new Query({ ...this.state, assetTypes });
}
withSource(source: string): Query {
return new Query({ ...this.state, source });
}
function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string): number {
const result = (statistics || []).filter(s => s.statisticName === name)[0];
return result ? result.value : 0;
}
function getCoreTranslationAssets(version: IRawGalleryExtensionVersion): [string, IGalleryExtensionAsset][] {
const coreTranslationAssetPrefix = 'Microsoft.VisualStudio.Code.Translation.';
const result = version.files.filter(f => f.assetType.indexOf(coreTranslationAssetPrefix) === 0);
return result.reduce<[string, IGalleryExtensionAsset][]>((result, file) => {
const asset = getVersionAsset(version, file.assetType);
if (asset) {
result.push([file.assetType.substring(coreTranslationAssetPrefix.length), asset]);
}
return result;
}, []);
}
function getRepositoryAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset | null {
if (version.properties) {
const results = version.properties.filter(p => p.key === AssetType.Repository);
const gitRegExp = new RegExp('((git|ssh|http(s)?)|(git@[\\w.]+))(:(//)?)([\\w.@:/\\-~]+)(.git)(/)?');
const uri = results.filter(r => gitRegExp.test(r.value))[0];
return uri ? { uri: uri.value, fallbackUri: uri.value } : null;
}
return getVersionAsset(version, AssetType.Repository);
}
function getDownloadAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset {
return {
// always use fallbackAssetUri for download asset to hit the Marketplace API so that downloads are counted
uri: `${version.fallbackAssetUri}/${AssetType.VSIX}?redirect=true${version.targetPlatform ? `&targetPlatform=${version.targetPlatform}` : ''}`,
fallbackUri: `${version.fallbackAssetUri}/${AssetType.VSIX}${version.targetPlatform ? `?targetPlatform=${version.targetPlatform}` : ''}`
};
}
function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IGalleryExtensionAsset | null {
const result = version.files.filter(f => f.assetType === type)[0];
return result ? {
uri: `${version.assetUri}/${type}${version.targetPlatform ? `?targetPlatform=${version.targetPlatform}` : ''}`,
fallbackUri: `${version.fallbackAssetUri}/${type}${version.targetPlatform ? `?targetPlatform=${version.targetPlatform}` : ''}`
} : null;
}
function getExtensions(version: IRawGalleryExtensionVersion, property: string): string[] {
const values = version.properties ? version.properties.filter(p => p.key === property) : [];
const value = values.length > 0 && values[0].value;
return value ? value.split(',').map(v => adoptToGalleryExtensionId(v)) : [];
}
function getEngine(version: IRawGalleryExtensionVersion): string {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Engine) : [];
return (values.length > 0 && values[0].value) || '';
}
function setEngine(version: IRawGalleryExtensionVersion, engine: string): void {
version.properties = version.properties ?? [];
version.properties.push({ key: PropertyType.Engine, value: engine });
}
function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean {
extensionGalleryService.ts ×5
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.PreRelease) : [];
return values.length > 0 && values[0].value === 'true';
}
function hasPreReleaseForExtension(id: string, productService: IProductService): boolean | undefined {
return productService.extensionProperties?.[id.toLowerCase()]?.hasPrereleaseVersion;
}
function getExcludeVersionRangeForExtension(id: string, productService: IProductService): string | undefined {
return productService.extensionProperties?.[id.toLowerCase()]?.excludeVersionRange;
}
function isPrivateExtension(version: IRawGalleryExtensionVersion): boolean {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Private) : [];
return values.length > 0 && values[0].value === 'true';
}
function executesCode(version: IRawGalleryExtensionVersion): boolean | undefined {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.ExecutesCode) : [];
return values.length > 0 ? values[0].value === 'true' : undefined;
}
function getEnabledApiProposals(version: IRawGalleryExtensionVersion): string[] {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.EnabledApiProposals) : [];
const value = (values.length > 0 && values[0].value) || '';
return value ? value.split(',') : [];
}
function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : [];
const value = (values.length > 0 && values[0].value) || '';
return value ? value.split(',') : [];
}
function getSponsorLink(version: IRawGalleryExtensionVersion): string | undefined {
return version.properties?.find(p => p.key === PropertyType.SponsorLink)?.value;
}
function getSupportLink(version: IRawGalleryExtensionVersion): string | undefined {
return version.properties?.find(p => p.key === PropertyType.SupportLink)?.value;
}
function getIsPreview(flags: string): boolean {
return flags.indexOf('preview') !== -1;
}
function getTargetPlatformForExtensionVersion(version: IRawGalleryExtensionVersion): TargetPlatform {
extensionGalleryService.ts ×1
return version.targetPlatform ? toTargetPlatform(version.targetPlatform) : TargetPlatform.UNDEFINED;
}
function getAllTargetPlatforms(rawGalleryExtension: IRawGalleryExtension): TargetPlatform[] {
const allTargetPlatforms = distinct(rawGalleryExtension.versions.map(getTargetPlatformForExtensionVersion));
// Is a web extension only if it has WEB_EXTENSION_TAG
const isWebExtension = !!rawGalleryExtension.tags?.includes(WEB_EXTENSION_TAG);
// Include Web Target Platform only if it is a web extension
const webTargetPlatformIndex = allTargetPlatforms.indexOf(TargetPlatform.WEB);
if (isWebExtension) {
if (webTargetPlatformIndex === -1) {
// Web extension but does not has web target platform -> add it
allTargetPlatforms.push(TargetPlatform.WEB);
}
} else {
if (webTargetPlatformIndex !== -1) {
// Not a web extension but has web target platform -> remove it
allTargetPlatforms.splice(webTargetPlatformIndex, 1);
}
}
return allTargetPlatforms;
}
export function sortExtensionVersions(versions: IRawGalleryExtensionVersion[], preferredTargetPlatform: TargetPlatform): IRawGalleryExtensionVersion[] {
/* It is expected that versions from Marketplace are sorted by version. So we are just sorting by preferred targetPlatform */
extensionGalleryService.ts ×2
for (let index = 0; index < versions.length; index++) {
const version = versions[index];
if (version.version === versions[index - 1]?.version) {
const versionTargetPlatform = getTargetPlatformForExtensionVersion(version);
/* put it at the beginning */
if (versionTargetPlatform === preferredTargetPlatform) {
while (insertionIndex > 0 && versions[insertionIndex - 1].version === version.version) { insertionIndex--; }
}
if (insertionIndex !== index) {
versions.splice(index, 1);
versions.splice(insertionIndex, 0, version);
}
}
return versions;
}
/**
* Filters extension versions to return only the relevant versions for a given target platform.
*
* This function processes a list of extension versions (expected to be sorted by version descending)
* and returns a filtered list containing:
* 1. All versions that are NOT compatible with the target platform (for other platforms)
* 2. At most one compatible release version (the first/latest one encountered)
* 3. At most one compatible pre-release version (the first/latest one encountered)
*
* When a platform-specific version (exactly matching targetPlatform) is encountered with the same
* version number as a previously stored universal/undefined version, it replaces that version.
* This ensures platform-specific builds are preferred over universal builds for the same version.
*
* @param versions - Array of extension versions, expected to be sorted by version number descending
* @param targetPlatform - The target platform to filter for (e.g., LINUX_X64, WIN32_X64)
* @param allTargetPlatforms - All target platforms the extension supports
* @returns Filtered array of versions relevant for the target platform
*/
export function filterLatestExtensionVersionsForTargetPlatform(versions: IRawGalleryExtensionVersion[], targetPlatform: TargetPlatform, allTargetPlatforms: TargetPlatform[]): IRawGalleryExtensionVersion[] {
let preReleaseVersionIndex: number = -1;
let releaseVersionIndex: number = -1;
for (const version of versions) {
const versionTargetPlatform = getTargetPlatformForExtensionVersion(version);
extensionGalleryService.ts ×5
const isCompatibleWithTargetPlatform = isTargetPlatformCompatible(versionTargetPlatform, allTargetPlatforms, targetPlatform);
// Always include versions that are NOT compatible with the target platform
if (!isCompatibleWithTargetPlatform) {
continue;
}
// For compatible versions, only include the first (latest) of each type
// Prefer specific target platform matches over undefined/universal platforms only when version numbers are the same
if (isPreReleaseVersion(version)) {
preReleaseVersionIndex = latestVersions.length;
latestVersions.push(version);
} else if (versionTargetPlatform === targetPlatform && latestVersions[preReleaseVersionIndex].version === version.version) {
}
releaseVersionIndex = latestVersions.length;
latestVersions.push(version);
} else if (versionTargetPlatform === targetPlatform && latestVersions[releaseVersionIndex].version === version.version) {
}
return latestVersions;
}
function setTelemetry(extension: IGalleryExtension, index: number, querySource?: string): void {
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData2" : {
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"queryActivityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
extension.telemetryData = { index, querySource, queryActivityId: extension.queryContext?.[SEARCH_ACTIVITY_HEADER_NAME] };
}
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], extensionGalleryManifest: IExtensionGalleryManifest, productService: IProductService, queryContext?: IStringDictionary<unknown>): IGalleryExtension {
const latestVersion = galleryExtension.versions[0];
const assets: IGalleryExtensionAssets = {
manifest: getVersionAsset(version, AssetType.Manifest),
readme: getVersionAsset(version, AssetType.Details),
changelog: getVersionAsset(version, AssetType.Changelog),
license: getVersionAsset(version, AssetType.License),
repository: getRepositoryAsset(version),
download: getDownloadAsset(version),
icon: getVersionAsset(version, AssetType.Icon),
signature: getVersionAsset(version, AssetType.Signature),
coreTranslations: getCoreTranslationAssets(version)
};
const detailsViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.linkType ?? ExtensionGalleryResourceType.ExtensionDetailsViewUri);
const publisherViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.publisher.linkType ?? ExtensionGalleryResourceType.PublisherViewUri);
const ratingViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.ratingLinkType ?? ExtensionGalleryResourceType.ExtensionRatingViewUri);
const id = getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName);
return {
type: 'gallery',
identifier: {
id,
uuid: galleryExtension.extensionId
},
name: galleryExtension.extensionName,
version: version.version,
displayName: galleryExtension.displayName,
publisherId: galleryExtension.publisher.publisherId,
publisher: galleryExtension.publisher.publisherName,
publisherDisplayName: galleryExtension.publisher.displayName,
publisherDomain: galleryExtension.publisher.domain ? { link: galleryExtension.publisher.domain, verified: !!galleryExtension.publisher.isDomainVerified } : undefined,
publisherSponsorLink: getSponsorLink(latestVersion),
description: galleryExtension.shortDescription ?? '',
installCount: getStatistic(galleryExtension.statistics, 'install'),
rating: getStatistic(galleryExtension.statistics, 'averagerating'),
ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'),
categories: galleryExtension.categories || [],
tags: galleryExtension.tags || [],
releaseDate: Date.parse(galleryExtension.releaseDate),
lastUpdated: Date.parse(galleryExtension.lastUpdated),
allTargetPlatforms,
assets,
properties: {
dependencies: getExtensions(version, PropertyType.Dependency),
extensionPack: getExtensions(version, PropertyType.ExtensionPack),
engine: getEngine(version),
enabledApiProposals: getEnabledApiProposals(version),
localizedLanguages: getLocalizedLanguages(version),
targetPlatform: getTargetPlatformForExtensionVersion(version),
isPreReleaseVersion: isPreReleaseVersion(version),
executesCode: executesCode(version)
},
hasPreReleaseVersion: hasPreReleaseForExtension(id, productService) ?? isPreReleaseVersion(latestVersion),
hasReleaseVersion: true,
private: isPrivateExtension(latestVersion),
preview: getIsPreview(galleryExtension.flags),
isSigned: !!assets.signature,
queryContext,
supportLink: getSupportLink(latestVersion),
detailsLink: detailsViewUri ? format2(detailsViewUri, { publisher: galleryExtension.publisher.publisherName, name: galleryExtension.extensionName }) : undefined,
publisherLink: publisherViewUri ? format2(publisherViewUri, { publisher: galleryExtension.publisher.publisherName }) : undefined,
ratingLink: ratingViewUri ? format2(ratingViewUri, { publisher: galleryExtension.publisher.publisherName, name: galleryExtension.extensionName }) : undefined,
};
}
interface IRawExtensionsControlManifest {
malicious: string[];
learnMoreLinks?: IStringDictionary<string>;
migrateToPreRelease?: IStringDictionary<{
id: string;
displayName: string;
migrateStorage?: boolean;
engine?: string;
}>;
deprecated?: IStringDictionary<boolean | {
disallowInstall?: boolean;
extension?: {
id: string;
displayName: string;
};
settings?: string[];
additionalInfo?: string;
}>;
search?: ISearchPrefferedResults[];
autoUpdate?: IStringDictionary<string>;
}
export abstract class AbstractExtensionGalleryService implements IExtensionGalleryService {
declare readonly _serviceBrand: undefined;
private readonly extensionsControlUrl: string | undefined;
private readonly unpkgResourceApi: string | undefined;
private readonly commonHeadersPromise: Promise<IHeaders>;
constructor(
storageService: IStorageService | undefined,
@IRequestService private readonly requestService: IRequestService,
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IFileService private readonly fileService: IFileService,
@IProductService private readonly productService: IProductService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService,
@IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService,
) {
this.extensionsControlUrl = productService.extensionsGallery?.controlUrl;
this.unpkgResourceApi = productService.extensionsGallery?.extensionUrlTemplate;
this.commonHeadersPromise = resolveMarketplaceHeaders(
productService.version,
productService,
this.environmentService,
this.configurationService,
this.fileService,
storageService,
this.telemetryService);
}
isEnabled(): boolean {
return this.extensionGalleryManifestService.extensionGalleryManifestStatus === ExtensionGalleryManifestStatus.Available;
}
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, token: CancellationToken): Promise<IGalleryExtension[]>;
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, token: CancellationToken): Promise<IGalleryExtension[]>;
async getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, arg1: CancellationToken | IExtensionQueryOptions, arg2?: CancellationToken): Promise<IGalleryExtension[]> {
const extensionGalleryManifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest();
if (!extensionGalleryManifest) {
throw new Error('No extension gallery service configured.');
}
const options = CancellationToken.isCancellationToken(arg1) ? {} : arg1 as IExtensionQueryOptions;
const token = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2 as CancellationToken;
const resourceApi = this.getResourceApi(extensionGalleryManifest);
const result = resourceApi
? await this.getExtensionsUsingResourceApi(extensionInfos, options, resourceApi, extensionGalleryManifest, token)
: await this.getExtensionsUsingQueryApi(extensionInfos, options, extensionGalleryManifest, token);
const uuids = result.map(r => r.identifier.uuid);
const extensionInfosByName: IExtensionInfo[] = [];
for (const e of extensionInfos) {
if (e.uuid && !uuids.includes(e.uuid)) {
extensionInfosByName.push({ ...e, uuid: undefined });
}
}
if (extensionInfosByName.length) {
// report telemetry data for additional query
this.telemetryService.publicLog2<
{ count: number },
{
owner: 'sandy081';
comment: 'Report the query to the Marketplace for fetching extensions by name';
readonly count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extensions to fetch' };
}>('galleryService:additionalQueryByName', {
count: extensionInfosByName.length
});
const extensions = await this.getExtensionsUsingQueryApi(extensionInfosByName, options, extensionGalleryManifest, token);
result.push(...extensions);
}
return result;
}
private getResourceApi(extensionGalleryManifest: IExtensionGalleryManifest): { uri: string; fallback?: string } | undefined {
const latestVersionResource = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionLatestVersionUri);
if (latestVersionResource) {
return {
uri: latestVersionResource,
fallback: this.unpkgResourceApi
};
}
return undefined;
}
private async getExtensionsUsingQueryApi(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<IGalleryExtension[]> {
const names: string[] = [],
ids: string[] = [],
includePreRelease: (IExtensionIdentifier & { includePreRelease: boolean })[] = [],
versions: (IExtensionIdentifier & { version: string })[] = [];
let isQueryForReleaseVersionFromPreReleaseVersion = true;
for (const extensionInfo of extensionInfos) {
if (extensionInfo.uuid) {
ids.push(extensionInfo.uuid);
} else {
names.push(extensionInfo.id);
}
if (extensionInfo.version) {
versions.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, version: extensionInfo.version });
} else {
includePreRelease.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, includePreRelease: !!extensionInfo.preRelease });
}
isQueryForReleaseVersionFromPreReleaseVersion = isQueryForReleaseVersionFromPreReleaseVersion && (!!extensionInfo.hasPreRelease && !extensionInfo.preRelease);
}
if (!ids.length && !names.length) {
return [];
}
let query = new Query().withPage(1, extensionInfos.length);
if (ids.length) {
query = query.withFilter(FilterType.ExtensionId, ...ids);
}
if (names.length) {
query = query.withFilter(FilterType.ExtensionName, ...names);
}
if (options.queryAllVersions) {
query = query.withFlags(...query.flags, Flag.IncludeVersions);
}
if (options.source) {
query = query.withSource(options.source);
}
const { extensions } = await this.queryGalleryExtensions(
query,
{
targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM,
includePreRelease,
versions,
compatible: !!options.compatible,
productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date },
isQueryForReleaseVersionFromPreReleaseVersion
},
extensionGalleryManifest,
token);
if (options.source) {
extensions.forEach((e, index) => setTelemetry(e, index, options.source));
}
return extensions;
}
private async getExtensionsUsingResourceApi(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, resourceApi: { uri: string; fallback?: string }, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<IGalleryExtension[]> {
const result: IGalleryExtension[] = [];
const toQuery: IExtensionInfo[] = [];
const toFetchLatest: IExtensionInfo[] = [];
for (const extensionInfo of extensionInfos) {
if (!EXTENSION_IDENTIFIER_REGEX.test(extensionInfo.id)) {
continue;
}
if (extensionInfo.version) {
toQuery.push(extensionInfo);
} else {
toFetchLatest.push(extensionInfo);
}
}
await Promise.all(toFetchLatest.map(async extensionInfo => {
let galleryExtension: IGalleryExtension | string;
try {
galleryExtension = await this.getLatestGalleryExtension(extensionInfo, options, resourceApi, extensionGalleryManifest, token);
if (isString(galleryExtension)) {
if (galleryExtension === 'LATEST_IS_OUTDATED') {
this.logService.debug(`Skipping query API fallback for extension ${extensionInfo.id} because the latest gallery version is older than the current version`);
} else {
// fallback to query
this.telemetryService.publicLog2<
{
extension: string;
preRelease: boolean;
compatible: boolean;
errorCode: string;
},
{
owner: 'sandy081';
comment: 'Report the fallback to the Marketplace query for fetching extensions';
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension id' };
preRelease: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Get pre-release version' };
compatible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Get compatible version' };
errorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Error code or reason' };
}>('galleryService:fallbacktoquery', {
extension: extensionInfo.id,
preRelease: !!extensionInfo.preRelease,
compatible: !!options.compatible,
errorCode: galleryExtension
});
toQuery.push(extensionInfo);
}
} else {
result.push(galleryExtension);
}
} catch (error) {
if (error instanceof ExtensionGalleryError) {
switch (error.code) {
case ExtensionGalleryErrorCode.Offline:
case ExtensionGalleryErrorCode.Cancelled:
case ExtensionGalleryErrorCode.Timeout:
throw error;
}
}
// fallback to query
this.logService.error(`Error while getting the latest version for the extension ${extensionInfo.id}.`, getErrorMessage(error));
this.telemetryService.publicLog2<
{
extension: string;
preRelease: boolean;
compatible: boolean;
errorCode: string;
},
{
owner: 'sandy081';
comment: 'Report the fallback to the Marketplace query for fetching extensions';
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension id' };
preRelease: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Get pre-release version' };
compatible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Get compatible version' };
errorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Error code or reason' };
}>('galleryService:fallbacktoquery', {
extension: extensionInfo.id,
preRelease: !!extensionInfo.preRelease,
compatible: !!options.compatible,
errorCode: error instanceof ExtensionGalleryError ? error.code : 'Unknown'
});
toQuery.push(extensionInfo);
}
}));
if (toQuery.length) {
const extensions = await this.getExtensionsUsingQueryApi(toQuery, options, extensionGalleryManifest, token);
result.push(...extensions);
}
return result;
}
private async getLatestGalleryExtension(extensionInfo: IExtensionInfo, options: IExtensionQueryOptions, resourceApi: { uri: string; fallback?: string }, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<IGalleryExtension | string> {
const rawGalleryExtension = await this.getLatestRawGalleryExtensionWithFallback(extensionInfo, resourceApi, token);
if (!rawGalleryExtension) {
return 'NOT_FOUND';
}
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
const rawGalleryExtensionVersion = await this.getValidRawGalleryExtensionVersionFromLatestVersions(rawGalleryExtension, rawGalleryExtension.versions, extensionInfo, options, allTargetPlatforms);
if (!rawGalleryExtensionVersion) {
if (extensionInfo.currentVersion) {
const latestVersion = rawGalleryExtension.versions.length > 0 ? rawGalleryExtension.versions[0].version : undefined;
if (latestVersion && semver.lt(latestVersion, extensionInfo.currentVersion)) {
return 'LATEST_IS_OUTDATED';
}
}
return 'NOT_COMPATIBLE';
}
return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService);
}
private async getValidRawGalleryExtensionVersionFromLatestVersions(rawGalleryExtension: IRawGalleryExtension, latestVersions: IRawGalleryExtensionVersion[], extensionInfo: IExtensionInfo, options: IExtensionQueryOptions, allTargetPlatforms: TargetPlatform[]): Promise<IRawGalleryExtensionVersion | null> {
const targetPlatform = options.targetPlatform ?? CURRENT_TARGET_PLATFORM;
const latestExtensionVersionsForTargetPlatform = filterLatestExtensionVersionsForTargetPlatform(latestVersions, targetPlatform, allTargetPlatforms);
// First, find a valid version matching the requested type (pre-release or release)
const result = await this.getValidRawGalleryExtensionVersion(
rawGalleryExtension,
latestExtensionVersionsForTargetPlatform,
{
targetPlatform,
compatible: !!options.compatible,
productVersion: options.productVersion ?? {
version: this.productService.version,
date: this.productService.date
},
version: extensionInfo.preRelease ? VersionKind.Prerelease : VersionKind.Release
}, allTargetPlatforms);
// For release version requests, simply return the found release version
if (!extensionInfo.preRelease) {
return result;
}
// For pre-release version requests, we need to consider both pre-release and release versions
const prereleaseVersion = result;
const releaseVersion = await this.getValidRawGalleryExtensionVersion(
rawGalleryExtension,
latestExtensionVersionsForTargetPlatform,
{
targetPlatform,
compatible: !!options.compatible,
productVersion: options.productVersion ?? {
version: this.productService.version,
date: this.productService.date
},
version: VersionKind.Release
}, allTargetPlatforms);
// When both versions exist, return whichever has the higher version number
if (prereleaseVersion && releaseVersion) {
return semver.gt(releaseVersion.version, prereleaseVersion.version) ? releaseVersion : prereleaseVersion;
}
// Special handling for compatible version requests
if (options.compatible) {
// If we have a compatible release version, check if it's better than any pre-release
if (releaseVersion) {
// Check if there exists any pre-release version (ignoring compatibility)
const anyPrereleaseVersion = await this.getValidRawGalleryExtensionVersion(
rawGalleryExtension,
latestExtensionVersionsForTargetPlatform,
{
targetPlatform,
compatible: false,
productVersion: options.productVersion ?? {
version: this.productService.version,
date: this.productService.date
},
version: VersionKind.Prerelease
}, allTargetPlatforms);
// If no pre-release exists or the release version is greater, prefer the compatible release
// This ensures users get a stable compatible version when pre-releases aren't newer or compatible
if (!anyPrereleaseVersion || semver.gt(releaseVersion.version, anyPrereleaseVersion.version)) {
return releaseVersion;
}
}
return prereleaseVersion;
}
// Return pre-release if available, otherwise release, otherwise null
return prereleaseVersion ?? releaseVersion ?? null;
}
async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise<IGalleryExtension | null> {
if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) {
return null;
}
if (await this.isExtensionCompatible(extension, includePreRelease, targetPlatform)) {
return extension;
}
if (this.allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName }) !== true) {
return null;
}
const result = await this.getExtensions([{
...extension.identifier,
preRelease: includePreRelease,
hasPreRelease: extension.hasPreReleaseVersion,
}], {
compatible: true,
productVersion,
queryAllVersions: true,
targetPlatform,
}, CancellationToken.None);
return result[0] ?? null;
}
async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise<boolean> {
return this.isValidVersion(
{
id: extension.identifier.id,
version: extension.version,
isPreReleaseVersion: extension.properties.isPreReleaseVersion,
targetPlatform: extension.properties.targetPlatform,
manifestAsset: extension.assets.manifest,
engine: extension.properties.engine,
enabledApiProposals: extension.properties.enabledApiProposals
},
{
targetPlatform,
compatible: true,
productVersion,
version: includePreRelease ? VersionKind.Latest : VersionKind.Release
},
extension.publisherDisplayName,
extension.allTargetPlatforms
);
}
private async isValidVersion(
extension: { id: string; version: string; isPreReleaseVersion: boolean; targetPlatform: TargetPlatform; manifestAsset: IGalleryExtensionAsset | null; engine: string | undefined; enabledApiProposals: string[] | undefined },
{ targetPlatform, compatible, productVersion, version }: Omit<ExtensionVersionCriteria, 'targetPlatform'> & { targetPlatform: TargetPlatform | undefined },
publisherDisplayName: string,
allTargetPlatforms: TargetPlatform[]
): Promise<boolean> {
const hasPreRelease = hasPreReleaseForExtension(extension.id, this.productService);
const excludeVersionRange = getExcludeVersionRangeForExtension(extension.id, this.productService);
if (extension.isPreReleaseVersion && hasPreRelease === false /* Skip if hasPreRelease is not defined for this extension */) {
return false;
}
if (excludeVersionRange && semver.satisfies(extension.version, excludeVersionRange)) {
return false;
}
// Specific version
if (isString(version)) {
if (extension.version !== version) {
return false;
}
}
// Prerelease or release version kind
else if (version === VersionKind.Release || version === VersionKind.Prerelease) {
if (extension.isPreReleaseVersion !== (version === VersionKind.Prerelease)) {
return false;
}
}
if (targetPlatform && !isTargetPlatformCompatible(extension.targetPlatform, allTargetPlatforms, targetPlatform)) {
return false;
}
if (compatible) {
if (this.allowedExtensionsService.isAllowed({ id: extension.id, publisherDisplayName, version: extension.version, prerelease: extension.isPreReleaseVersion, targetPlatform: extension.targetPlatform }) !== true) {
return false;
}
if (!(await this.isEngineValid(extension.id, extension.version, extension.engine, extension.manifestAsset, productVersion))) {
return false;
}
}
return true;
}
private async isEngineValid(extensionId: string, version: string, engine: string | undefined, manifestAsset: IGalleryExtensionAsset | null, productVersion: IProductVersion): Promise<boolean> {
if (!engine) {
try {
engine = await this.getEngine(extensionId, version, manifestAsset);
} catch (error) {
this.logService.error(`Error while getting the engine for the version ${version}.`, getErrorMessage(error));
return false;
}
}
if (!engine) {
this.logService.error(`Missing engine for the extension ${extensionId} with version ${version}`);
return false;
}
return isEngineValid(engine, productVersion.version, productVersion.date);
}
private async getEngine(extensionId: string, version: string, manifestAsset: IGalleryExtensionAsset | null): Promise<string | undefined> {
if (!manifestAsset) {
this.logService.error(`Missing engine and manifest asset for the extension ${extensionId} with version ${version}`);
return undefined;
}
try {
type GalleryServiceEngineFallbackClassification = {
owner: 'sandy081';
comment: 'Fallback request when engine is not found in properties of an extension version';
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' };
extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' };
};
type GalleryServiceEngineFallbackEvent = {
extension: string;
extensionVersion: string;
};
this.telemetryService.publicLog2<GalleryServiceEngineFallbackEvent, GalleryServiceEngineFallbackClassification>('galleryService:engineFallback', { extension: extensionId, extensionVersion: version });
const headers = { 'Accept-Encoding': 'gzip' };
const context = await this.getAsset(extensionId, manifestAsset, AssetType.Manifest, version, 'extensionGalleryService.engineVersion', { headers });
const manifest = await asJson<IExtensionManifest>(context);
if (!manifest) {
this.logService.error(`Manifest was not found for the extension ${extensionId} with version ${version}`);
return undefined;
}
return manifest.engines.vscode;
} catch (error) {
this.logService.error(`Error while getting the engine for the version ${version}.`, getErrorMessage(error));
return undefined;
}
}
async query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>> {
const extensionGalleryManifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest();
if (!extensionGalleryManifest) {
throw new Error('No extension gallery service configured.');
}
let text = options.text || '';
const pageSize = options.pageSize ?? 50;
let query = new Query()
.withPage(1, pageSize);
if (text) {
// Use category filter instead of "category:themes"
text = text.replace(/\bcategory:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedCategory, category) => {
query = query.withFilter(FilterType.Category, category || quotedCategory);
return '';
});
// Use tag filter instead of "tag:debuggers"
text = text.replace(/\btag:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedTag, tag) => {
query = query.withFilter(FilterType.Tag, tag || quotedTag);
return '';
});
// Use featured filter
text = text.replace(/\bfeatured(\s+|\b|$)/g, () => {
query = query.withFilter(FilterType.Featured);
return '';
});
text = text.trim();
if (text) {
text = text.length < 200 ? text : text.substring(0, 200);
query = query.withFilter(FilterType.SearchText, text);
}
if (extensionGalleryManifest.capabilities.extensionQuery.sorting?.some(c => c.name === SortBy.NoneOrRelevance)) {
query = query.withSortBy(SortBy.NoneOrRelevance);
}
} else {
if (extensionGalleryManifest.capabilities.extensionQuery.sorting?.some(c => c.name === SortBy.InstallCount)) {
query = query.withSortBy(SortBy.InstallCount);
}
}
if (options.sortBy && extensionGalleryManifest.capabilities.extensionQuery.sorting?.some(c => c.name === options.sortBy)) {
query = query.withSortBy(options.sortBy);
}
if (typeof options.sortOrder === 'number') {
query = query.withSortOrder(options.sortOrder);
}
if (options.source) {
query = query.withSource(options.source);
}
const runQuery = async (query: Query, token: CancellationToken) => {
const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, extensionGalleryManifest, token);
const result: IGalleryExtension[] = [];
let defaultChatAgentExtension: IGalleryExtension | undefined;
for (let index = 0; index < extensions.length; index++) {
const extension = extensions[index];
setTelemetry(extension, ((query.pageNumber - 1) * query.pageSize) + index, options.source);
if (areSameExtensions(extension.identifier, { id: this.productService.defaultChatAgent.extensionId, })) {
defaultChatAgentExtension = extension;
} else {
result.push(extension);
}
}
if (defaultChatAgentExtension) {
result.push(defaultChatAgentExtension);
}
return { extensions: result, total };
};
const { extensions, total } = await runQuery(query, token);
const getPage = async (pageIndex: number, ct: CancellationToken) => {
if (ct.isCancellationRequested) {
throw new CancellationError();
}
const { extensions } = await runQuery(query.withPage(pageIndex + 1), ct);
return extensions;
};
return { firstPage: extensions, total, pageSize: query.pageSize, getPage };
}
private async queryGalleryExtensions(query: Query, criteria: ExtensionsCriteria, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> {
const flags = query.flags;
/**
* If both version flags (IncludeLatestVersionOnly and IncludeVersions) are included, then only include latest versions (IncludeLatestVersionOnly) flag.
*/
if (query.flags.includes(Flag.IncludeLatestVersionOnly) && query.flags.includes(Flag.IncludeVersions)) {
query = query.withFlags(...query.flags.filter(flag => flag !== Flag.IncludeVersions));
}
/**
* If version flags (IncludeLatestVersionOnly and IncludeVersions) are not included, default is to query for latest versions (IncludeLatestVersionOnly).
*/
if (!query.flags.includes(Flag.IncludeLatestVersionOnly) && !query.flags.includes(Flag.IncludeVersions)) {
query = query.withFlags(...query.flags, Flag.IncludeLatestVersionOnly);
}
/**
* If versions criteria exist or every requested extension is for release version and has a pre-release version, then remove latest flags and add all versions flag.
*/
if (criteria.versions?.length || criteria.isQueryForReleaseVersionFromPreReleaseVersion) {
query = query.withFlags(...query.flags.filter(flag => flag !== Flag.IncludeLatestVersionOnly), Flag.IncludeVersions);
}
/**
* Add necessary extension flags
*/
query = query.withFlags(...query.flags, Flag.IncludeAssetUri, Flag.IncludeCategoryAndTags, Flag.IncludeFiles, Flag.IncludeStatistics, Flag.IncludeVersionProperties);
const { galleryExtensions: rawGalleryExtensions, total, context } = await this.queryRawGalleryExtensions(query, extensionGalleryManifest, token);
const hasAllVersions: boolean = !query.flags.includes(Flag.IncludeLatestVersionOnly);
if (hasAllVersions) {
const extensions: IGalleryExtension[] = [];
for (const rawGalleryExtension of rawGalleryExtensions) {
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease;
const rawGalleryExtensionVersion = await this.getValidRawGalleryExtensionVersion(
rawGalleryExtension,
rawGalleryExtension.versions,
{
compatible: criteria.compatible,
targetPlatform: criteria.targetPlatform,
productVersion: criteria.productVersion,
version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version
?? (includePreRelease ? VersionKind.Latest : VersionKind.Release)
},
allTargetPlatforms
);
if (rawGalleryExtensionVersion) {
extensions.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService, context));
}
}
return { extensions, total };
}
const result: [number, IGalleryExtension][] = [];
const needAllVersions = new Map<string, number>();
for (let index = 0; index < rawGalleryExtensions.length; index++) {
const rawGalleryExtension = rawGalleryExtensions[index];
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease;
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
if (criteria.compatible) {
// Skip looking for all versions if requested for a web-compatible extension and it is not a web extension.
if (isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) {
continue;
}
// Skip looking for all versions if the extension is not allowed.
if (this.allowedExtensionsService.isAllowed({ id: extensionIdentifier.id, publisherDisplayName: rawGalleryExtension.publisher.displayName }) !== true) {
continue;
}
}
const rawGalleryExtensionVersion = await this.getValidRawGalleryExtensionVersion(
rawGalleryExtension,
rawGalleryExtension.versions,
{
compatible: criteria.compatible,
targetPlatform: criteria.targetPlatform,
productVersion: criteria.productVersion,
version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version
?? (includePreRelease ? VersionKind.Latest : VersionKind.Release)
},
allTargetPlatforms
);
const extension = rawGalleryExtensionVersion ? toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService, context) : null;
if (!extension
/** Need all versions if the extension is a pre-release version but
* - the query is to look for a release version or
* - the extension has no release version
* Get all versions to get or check the release version
*/
|| (extension.properties.isPreReleaseVersion && (!includePreRelease || !extension.hasReleaseVersion))
/**
* Need all versions if the extension is a release version with a different target platform than requested and also has a pre-release version
* Because, this is a platform specific extension and can have a newer release version supporting this platform.
* See https://github.com/microsoft/vscode/issues/139628
*/
|| (!extension.properties.isPreReleaseVersion && extension.properties.targetPlatform !== criteria.targetPlatform && extension.hasPreReleaseVersion)
) {
needAllVersions.set(rawGalleryExtension.extensionId, index);
} else {
result.push([index, extension]);
}
}
if (needAllVersions.size) {
const stopWatch = new StopWatch();
const query = new Query()
.withFlags(...flags.filter(flag => flag !== Flag.IncludeLatestVersionOnly), Flag.IncludeVersions)
.withPage(1, needAllVersions.size)
.withFilter(FilterType.ExtensionId, ...needAllVersions.keys());
const { extensions } = await this.queryGalleryExtensions(query, criteria, extensionGalleryManifest, token);
this.telemetryService.publicLog2<GalleryServiceAdditionalQueryEvent, GalleryServiceAdditionalQueryClassification>('galleryService:additionalQuery', {
duration: stopWatch.elapsed(),
count: needAllVersions.size
});
for (const extension of extensions) {
const index = needAllVersions.get(extension.identifier.uuid)!;
result.push([index, extension]);
}
}
return { extensions: result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension), total };
}
private async getValidRawGalleryExtensionVersion(rawGalleryExtension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[], criteria: ExtensionVersionCriteria, allTargetPlatforms: TargetPlatform[]): Promise<IRawGalleryExtensionVersion | null> {
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
const rawGalleryExtensionVersions = sortExtensionVersions(versions, criteria.targetPlatform);
if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) {
return null;
}
const version = isString(criteria.version) ? criteria.version : undefined;
for (let index = 0; index < rawGalleryExtensionVersions.length; index++) {
const rawGalleryExtensionVersion = rawGalleryExtensionVersions[index];
if (criteria.compatible) {
await this.setEngineIfNotExists(extensionIdentifier.id, rawGalleryExtensionVersion);
}
if (await this.isValidVersion(
{
id: extensionIdentifier.id,
version: rawGalleryExtensionVersion.version,
isPreReleaseVersion: isPreReleaseVersion(rawGalleryExtensionVersion),
targetPlatform: getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion),
engine: getEngine(rawGalleryExtensionVersion),
manifestAsset: getVersionAsset(rawGalleryExtensionVersion, AssetType.Manifest),
enabledApiProposals: getEnabledApiProposals(rawGalleryExtensionVersion)
},
criteria,
rawGalleryExtension.publisher.displayName,
allTargetPlatforms)
) {
return rawGalleryExtensionVersion;
}
if (version && rawGalleryExtensionVersion.version === version) {
return null;
}
}
if (version || criteria.compatible) {
return null;
}
/**
* Fallback: Return the latest version
* This can happen when the extension does not have a release version or does not have a version compatible with the given target platform.
*/
return rawGalleryExtension.versions[0];
}
private async setEngineIfNotExists(extensionId: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion): Promise<void> {
if (getEngine(rawGalleryExtensionVersion)) {
return;
}
try {
const engine = await this.getEngine(extensionId, rawGalleryExtensionVersion.version, getVersionAsset(rawGalleryExtensionVersion, AssetType.Manifest));
if (engine) {
setEngine(rawGalleryExtensionVersion, engine);
}
} catch (error) {
this.logService.error(`Error while getting the engine for the version ${rawGalleryExtensionVersion.version}.`, getErrorMessage(error));
}
}
private async queryRawGalleryExtensions(query: Query, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<IRawGalleryExtensionsResult> {
const extensionsQueryApi = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionQueryService);
if (!extensionsQueryApi) {
throw new Error('No extension gallery query service configured.');
}
query = query
/* Always exclude non validated extensions */
.withFlags(...query.flags, Flag.ExcludeNonValidated)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
const unpublishedFlag = extensionGalleryManifest.capabilities.extensionQuery.flags?.find(f => f.name === Flag.Unpublished);
/* Always exclude unpublished extensions */
if (unpublishedFlag) {
query = query.withFilter(FilterType.ExcludeWithFlags, String(unpublishedFlag.value));
}
const data = JSON.stringify({
filters: [
{
criteria: query.criteria.reduce<{ filterType: number; value?: string }[]>((criteria, c) => {
const criterium = extensionGalleryManifest.capabilities.extensionQuery.filtering?.find(f => f.name === c.filterType);
if (criterium) {
criteria.push({
filterType: criterium.value,
value: c.value,
});
}
return criteria;
}, []),
pageNumber: query.pageNumber,
pageSize: query.pageSize,
sortBy: extensionGalleryManifest.capabilities.extensionQuery.sorting?.find(s => s.name === query.sortBy)?.value,
sortOrder: query.sortOrder,
}
],
assetTypes: query.assetTypes,
flags: query.flags.reduce<number>((flags, flag) => {
const flagValue = extensionGalleryManifest.capabilities.extensionQuery.flags?.find(f => f.name === flag);
if (flagValue) {
flags |= flagValue.value;
}
return flags;
}, 0)
});
const commonHeaders = await this.commonHeadersPromise;
const headers = {
...commonHeaders,
'Content-Type': 'application/json',
'Accept': 'application/json;api-version=3.0-preview.1',
'Accept-Encoding': 'gzip',
'Content-Length': String(data.length),
};
const stopWatch = new StopWatch();
let context: IRequestContext | undefined, errorCode: ExtensionGalleryErrorCode | undefined, total: number = 0;
try {
context = await this.requestService.request({
type: 'POST',
url: extensionsQueryApi,
data,
headers,
callSite: 'extensionGalleryService.queryRawGalleryExtensions'
}, token);
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) {
return { galleryExtensions: [], total };
}
const result = await asJson<IRawGalleryQueryResult>(context);
if (result) {
const r = result.results[0];
const galleryExtensions = r.extensions;
const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0];
total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0;
return {
galleryExtensions,
total,
context: context.res.headers['activityid'] ? {
[SEARCH_ACTIVITY_HEADER_NAME]: context.res.headers['activityid']
} : {}
};
}
return { galleryExtensions: [], total };
} catch (e) {
if (isCancellationError(e)) {
errorCode = ExtensionGalleryErrorCode.Cancelled;
throw e;
} else {
const errorMessage = getErrorMessage(e);
errorCode = isOfflineError(e)
? ExtensionGalleryErrorCode.Offline
: errorMessage.startsWith('XHR timeout')
? ExtensionGalleryErrorCode.Timeout
: ExtensionGalleryErrorCode.Failed;
throw new ExtensionGalleryError(errorMessage, errorCode);
}
} finally {
this.telemetryService.publicLog2<GalleryServiceQueryEvent, GalleryServiceQueryClassification>('galleryService:query', {
filterTypes: query.criteria.map(criterium => criterium.filterType),
flags: query.flags,
sortBy: query.sortBy,
sortOrder: String(query.sortOrder),
pageNumber: String(query.pageNumber),
source: query.source,
searchTextLength: query.searchText.length,
requestBodySize: String(data.length),
duration: stopWatch.elapsed(),
success: !!context && isSuccess(context),
responseBodySize: context?.res.headers['Content-Length'],
statusCode: context ? String(context.res.statusCode) : undefined,
errorCode,
count: String(total),
server: this.getHeaderValue(context?.res.headers, SERVER_HEADER_NAME),
activityId: this.getHeaderValue(context?.res.headers, ACTIVITY_HEADER_NAME),
endToEndId: this.getHeaderValue(context?.res.headers, END_END_ID_HEADER_NAME),
});
}
}
private getHeaderValue(headers: IHeaders | undefined, name: string): TelemetryTrustedValue<string> | undefined {
const headerValue = headers?.[name.toLowerCase()];
const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
return value ? new TelemetryTrustedValue(value) : undefined;
}
private async getLatestRawGalleryExtensionWithFallback(extensionInfo: IExtensionInfo, resourceApi: { uri: string; fallback?: string }, token: CancellationToken): Promise<IRawGalleryExtension | null> {
const [publisher, name] = extensionInfo.id.split('.');
let errorCode: string | undefined;
try {
const uri = URI.parse(format2(resourceApi.uri, { publisher, name }));
return await this.getLatestRawGalleryExtension(extensionInfo.id, uri, token);
} catch (error) {
if (error instanceof ExtensionGalleryError) {
errorCode = error.code;
switch (error.code) {
case ExtensionGalleryErrorCode.Offline:
case ExtensionGalleryErrorCode.Cancelled:
case ExtensionGalleryErrorCode.Timeout:
case ExtensionGalleryErrorCode.ClientError:
throw error;
}
} else {
errorCode = 'Unknown';
}
if (!resourceApi.fallback) {
throw error;
}
} finally {
this.telemetryService.publicLog2<
{
extension: string;
errorCode?: string;
},
{
owner: 'sandy081';
comment: 'Report fetching latest version of an extension';
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the extension' };
errorCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error code in case of error' };
}
>('galleryService:getmarketplacelatest', {
extension: extensionInfo.id,
errorCode,
});
}
this.logService.error(`Error while getting the latest version for the extension ${extensionInfo.id} from ${resourceApi.uri}. Trying the fallback ${resourceApi.fallback}`, errorCode);
try {
const uri = URI.parse(format2(resourceApi.fallback, { publisher, name }));
return await this.getLatestRawGalleryExtension(extensionInfo.id, uri, token);
} catch (error) {
errorCode = error instanceof ExtensionGalleryError ? error.code : 'Unknown';
throw error;
} finally {
this.telemetryService.publicLog2<
{
extension: string;
errorCode?: string;
},
{
owner: 'sandy081';
comment: 'Report the fallback to the unpkg service for getting latest extension';
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension id' };
errorCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error code in case of error' };
}>('galleryService:fallbacktounpkg', {
extension: extensionInfo.id,
errorCode,
});
}
}
private async getLatestRawGalleryExtension(extension: string, uri: URI, token: CancellationToken): Promise<IRawGalleryExtension | null> {
let context;
let errorCode: string | undefined;
const stopWatch = new StopWatch();
try {
const commonHeaders = await this.commonHeadersPromise;
const headers = {
...commonHeaders,
'Content-Type': 'application/json',
'Accept': 'application/json;api-version=7.2-preview',
'Accept-Encoding': 'gzip',
};
context = await this.requestService.request({
type: 'GET',
url: uri.toString(true),
headers,
timeout: this.getRequestTimeout(),
callSite: 'extensionGalleryService.getLatestRawGalleryExtension'
}, token);
if (context.res.statusCode === 404) {
errorCode = 'NotFound';
return null;
}
if (context.res.statusCode && context.res.statusCode !== 200) {
throw new Error('Unexpected HTTP response: ' + context.res.statusCode);
}
const result = await asJson<IRawGalleryExtension>(context);
if (!result) {
errorCode = 'NoData';
}
return result;
}
catch (error) {
let galleryErrorCode: ExtensionGalleryErrorCode;
if (isCancellationError(error)) {
galleryErrorCode = ExtensionGalleryErrorCode.Cancelled;
} else if (isOfflineError(error)) {
galleryErrorCode = ExtensionGalleryErrorCode.Offline;
} else if (getErrorMessage(error).startsWith('XHR timeout')) {
galleryErrorCode = ExtensionGalleryErrorCode.Timeout;
} else if (context && isClientError(context)) {
galleryErrorCode = ExtensionGalleryErrorCode.ClientError;
} else if (context && isServerError(context)) {
galleryErrorCode = ExtensionGalleryErrorCode.ServerError;
} else {
galleryErrorCode = ExtensionGalleryErrorCode.Failed;
}
errorCode = galleryErrorCode;
throw new ExtensionGalleryError(error, galleryErrorCode);
}
finally {
type GalleryServiceGetLatestEventClassification = {
owner: 'sandy081';
comment: 'Report the query to the Marketplace for fetching latest version of an extension';
host: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The host of the end point' };
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the extension' };
duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Duration in ms for the query' };
errorCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error code in case of error' };
statusCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The status code in case of error' };
server?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The server of the end point' };
activityId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The activity ID of the request' };
endToEndId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The end-to-end ID of the request' };
};
type GalleryServiceGetLatestEvent = {
extension: string;
host: string;
duration: number;
errorCode?: string;
statusCode?: string;
server?: TelemetryTrustedValue<string>;
activityId?: TelemetryTrustedValue<string>;
endToEndId?: TelemetryTrustedValue<string>;
};
this.telemetryService.publicLog2<GalleryServiceGetLatestEvent, GalleryServiceGetLatestEventClassification>('galleryService:getLatest', {
extension,
host: uri.authority,
duration: stopWatch.elapsed(),
errorCode,
statusCode: context?.res.statusCode && context?.res.statusCode !== 200 ? `${context.res.statusCode}` : undefined,
server: this.getHeaderValue(context?.res.headers, SERVER_HEADER_NAME),
activityId: this.getHeaderValue(context?.res.headers, ACTIVITY_HEADER_NAME),
endToEndId: this.getHeaderValue(context?.res.headers, END_END_ID_HEADER_NAME),
});
}
}
async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void> {
if (isWeb) {
this.logService.info('ExtensionGalleryService#reportStatistic: Skipped in web');
return undefined;
}
const manifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest();
if (!manifest) {
return undefined;
}
const resource = getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.ExtensionStatisticsUri);
if (!resource) {
return;
}
const url = format2(resource, { publisher, name, version, statTypeName: type });
const Accept = '*/*;api-version=4.0-preview.1';
const commonHeaders = await this.commonHeadersPromise;
const headers = { ...commonHeaders, Accept };
try {
await this.requestService.request({
type: 'POST',
url,
headers,
callSite: 'extensionGalleryService.reportStatistic'
}, CancellationToken.None);
} catch (error) { /* Ignore */ }
}
async download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void> {
this.logService.trace('ExtensionGalleryService#download', extension.identifier.id);
const data = getGalleryExtensionTelemetryData(extension);
const startTime = new Date().getTime();
const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : '';
const downloadAsset = operationParam ? {
uri: `${extension.assets.download.uri}${URI.parse(extension.assets.download.uri).query ? '&' : '?'}${operationParam}=true`,
fallbackUri: `${extension.assets.download.fallbackUri}${URI.parse(extension.assets.download.fallbackUri).query ? '&' : '?'}${operationParam}=true`
} : extension.assets.download;
const activityId = extension.queryContext?.[SEARCH_ACTIVITY_HEADER_NAME];
const headers: IHeaders | undefined = activityId && typeof activityId === 'string' ? { [SEARCH_ACTIVITY_HEADER_NAME]: activityId } : undefined;
const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, extension.version, 'extensionGalleryService.download', headers ? { headers } : undefined);
try {
await this.fileService.writeFile(location, context.stream);
} catch (error) {
try {
await this.fileService.del(location);
} catch (e) {
/* ignore */
this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e));
}
throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting);
}
/* __GDPR__
"galleryService:downloadVSIX" : {
"owner": "sandy081",
"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration: new Date().getTime() - startTime });
}
async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise<void> {
if (!extension.assets.signature) {
throw new Error('No signature asset found');
}
this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id);
const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature, extension.version, 'extensionGalleryService.signature');
try {
await this.fileService.writeFile(location, context.stream);
} catch (error) {
try {
await this.fileService.del(location);
} catch (e) {
/* ignore */
this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e));
}
throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting);
}
}
async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
if (extension.assets.readme) {
const context = await this.getAsset(extension.identifier.id, extension.assets.readme, AssetType.Details, extension.version, 'extensionGalleryService.readme', {}, token);
const content = await asTextOrError(context);
return content || '';
}
return '';
}
async getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null> {
if (extension.assets.manifest) {
const context = await this.getAsset(extension.identifier.id, extension.assets.manifest, AssetType.Manifest, extension.version, 'extensionGalleryService.manifest', {}, token);
const text = await asTextOrError(context);
return text ? JSON.parse(text) : null;
}
return null;
}
async getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise<ITranslation | null> {
const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0];
if (asset) {
const context = await this.getAsset(extension.identifier.id, asset[1], asset[0], extension.version, 'extensionGalleryService.coreTranslation');
const text = await asTextOrError(context);
return text ? JSON.parse(text) : null;
}
return null;
}
async getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
if (extension.assets.changelog) {
const context = await this.getAsset(extension.identifier.id, extension.assets.changelog, AssetType.Changelog, extension.version, 'extensionGalleryService.changelog', {}, token);
const content = await asTextOrError(context);
return content || '';
}
return '';
}
async getAllVersions(extensionIdentifier: IExtensionIdentifier): Promise<IGalleryExtensionVersion[]> {
return this.getVersions(extensionIdentifier);
}
async getAllCompatibleVersions(extensionIdentifier: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
return this.getVersions(extensionIdentifier, { version: includePreRelease ? VersionKind.Latest : VersionKind.Release, targetPlatform });
}
private async getVersions(extensionIdentifier: IExtensionIdentifier, onlyCompatible?: { version: VersionKind; targetPlatform: TargetPlatform }): Promise<IGalleryExtensionVersion[]> {
const extensionGalleryManifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest();
if (!extensionGalleryManifest) {
throw new Error('No extension gallery service configured.');
}
let query = new Query()
.withFlags(Flag.IncludeVersions, Flag.IncludeCategoryAndTags, Flag.IncludeFiles, Flag.IncludeVersionProperties)
.withPage(1, 1);
if (extensionIdentifier.uuid) {
query = query.withFilter(FilterType.ExtensionId, extensionIdentifier.uuid);
} else {
query = query.withFilter(FilterType.ExtensionName, extensionIdentifier.id);
}
const { galleryExtensions } = await this.queryRawGalleryExtensions(query, extensionGalleryManifest, CancellationToken.None);
if (!galleryExtensions.length) {
return [];
}
const allTargetPlatforms = getAllTargetPlatforms(galleryExtensions[0]);
if (onlyCompatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, onlyCompatible.targetPlatform)) {
return [];
}
const versions: IRawGalleryExtensionVersion[] = [];
const productVersion = { version: this.productService.version, date: this.productService.date };
await Promise.all(galleryExtensions[0].versions.map(async (version) => {
try {
if (
(await this.isValidVersion(
{
id: extensionIdentifier.id,
version: version.version,
isPreReleaseVersion: isPreReleaseVersion(version),
targetPlatform: getTargetPlatformForExtensionVersion(version),
engine: getEngine(version),
manifestAsset: getVersionAsset(version, AssetType.Manifest),
enabledApiProposals: getEnabledApiProposals(version)
},
{
compatible: !!onlyCompatible,
productVersion,
targetPlatform: onlyCompatible?.targetPlatform,
version: onlyCompatible?.version ?? version.version
},
galleryExtensions[0].publisher.displayName,
allTargetPlatforms))
) {
versions.push(version);
}
} catch (error) { /* Ignore error and skip version */ }
}));
const result: IGalleryExtensionVersion[] = [];
const seen = new Map<string, number>();
for (const version of sortExtensionVersions(versions, onlyCompatible?.targetPlatform ?? CURRENT_TARGET_PLATFORM)) {
const index = seen.get(version.version);
const existing = index !== undefined ? result[index] : undefined;
const targetPlatform = getTargetPlatformForExtensionVersion(version);
if (!existing) {
seen.set(version.version, result.length);
result.push({ version: version.version, date: version.lastUpdated, isPreReleaseVersion: isPreReleaseVersion(version), targetPlatforms: [targetPlatform] });
} else {
existing.targetPlatforms.push(targetPlatform);
}
}
return result;
}
private async getAsset(extension: string, asset: IGalleryExtensionAsset, assetType: string, extensionVersion: string, callSite: string, options: Omit<IRequestOptions, 'callSite'> = {}, token: CancellationToken = CancellationToken.None): Promise<IRequestContext> {
const commonHeaders = await this.commonHeadersPromise;
const baseOptions = { type: 'GET' };
const headers = { ...commonHeaders, ...(options.headers || {}) };
options = { ...options, ...baseOptions, headers };
const url = asset.uri;
const fallbackUrl = asset.fallbackUri;
const firstOptions = { ...options, url, timeout: this.getRequestTimeout(), callSite };
let context;
try {
context = await this.requestService.request(firstOptions, token);
if (context.res.statusCode === 200) {
return context;
}
const message = await asTextOrError(context);
throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`);
} catch (err) {
if (isCancellationError(err)) {
throw err;
}
const message = getErrorMessage(err);
type GalleryServiceCDNFallbackClassification = {
owner: 'sandy081';
comment: 'Fallback request information when the primary asset request to CDN fails';
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' };
assetType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'asset that failed' };
message: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error message' };
extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' };
readonly server?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'server that handled the query' };
readonly endToEndId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'end to end operation id' };
readonly activityId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'activity id' };
};
type GalleryServiceCDNFallbackEvent = {
extension: string;
assetType: string;
message: string;
extensionVersion: string;
server?: TelemetryTrustedValue<string>;
endToEndId?: TelemetryTrustedValue<string>;
activityId?: TelemetryTrustedValue<string>;
};
this.telemetryService.publicLog2<GalleryServiceCDNFallbackEvent, GalleryServiceCDNFallbackClassification>('galleryService:cdnFallback', {
extension,
assetType,
message,
extensionVersion,
server: this.getHeaderValue(context?.res.headers, SERVER_HEADER_NAME),
activityId: this.getHeaderValue(context?.res.headers, ACTIVITY_HEADER_NAME),
endToEndId: this.getHeaderValue(context?.res.headers, END_END_ID_HEADER_NAME),
});
const fallbackOptions = { ...options, url: fallbackUrl, timeout: this.getRequestTimeout(), callSite: `${callSite}.fallback` };
return this.requestService.request(fallbackOptions, token);
}
}
async getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
const manifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest();
if (!manifest) {
throw new Error('No extension gallery service configured.');
}
if (!this.extensionsControlUrl) {
return { malicious: [], deprecated: {}, search: [], autoUpdate: {} };
}
const context = await this.requestService.request({
type: 'GET',
url: this.extensionsControlUrl,
timeout: this.getRequestTimeout(),
callSite: 'extensionGalleryService.getExtensionsControlManifest'
}, CancellationToken.None);
if (context.res.statusCode !== 200) {
throw new Error('Could not get extensions report.');
}
const result = await asJson<IRawExtensionsControlManifest>(context);
const malicious: Array<MaliciousExtensionInfo> = [];
const deprecated: IStringDictionary<IDeprecationInfo> = {};
const search: ISearchPrefferedResults[] = [];
const autoUpdate: IStringDictionary<string> = result?.autoUpdate ?? {};
if (result) {
for (const id of result.malicious) {
if (!isString(id)) {
continue;
}
const publisherOrExtension = EXTENSION_IDENTIFIER_REGEX.test(id) ? { id } : id;
malicious.push({ extensionOrPublisher: publisherOrExtension, learnMoreLink: result.learnMoreLinks?.[id] });
}
if (result.migrateToPreRelease) {
for (const [unsupportedPreReleaseExtensionId, preReleaseExtensionInfo] of Object.entries(result.migrateToPreRelease)) {
if (!preReleaseExtensionInfo.engine || isEngineValid(preReleaseExtensionInfo.engine, this.productService.version, this.productService.date)) {
deprecated[unsupportedPreReleaseExtensionId.toLowerCase()] = {
disallowInstall: true,
extension: {
id: preReleaseExtensionInfo.id,
displayName: preReleaseExtensionInfo.displayName,
autoMigrate: { storage: !!preReleaseExtensionInfo.migrateStorage },
preRelease: true
}
};
}
}
}
if (result.deprecated) {
for (const [deprecatedExtensionId, deprecationInfo] of Object.entries(result.deprecated)) {
if (deprecationInfo) {
deprecated[deprecatedExtensionId.toLowerCase()] = isBoolean(deprecationInfo) ? {} : deprecationInfo;
}
}
}
if (result.search) {
for (const s of result.search) {
search.push(s);
}
}
}
deprecated[this.productService.defaultChatAgent.extensionId.toLowerCase()] = {
disallowInstall: true,
extension: {
id: this.productService.defaultChatAgent.chatExtensionId,
displayName: 'GitHub Copilot Chat',
autoMigrate: { storage: false, donotDisable: true },
preRelease: this.productService.quality !== 'stable'
}
};
return { malicious, deprecated, search, autoUpdate };
}
private getRequestTimeout(): number {
const configuredTimeout = this.configurationService.getValue<number>(ExtensionRequestsTimeoutConfigKey);
return isNumber(configuredTimeout) && configuredTimeout >= 0 ? configuredTimeout : 60_000;
}
}
export class ExtensionGalleryService extends AbstractExtensionGalleryService {
constructor(
@IStorageService storageService: IStorageService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService,
@IEnvironmentService environmentService: IEnvironmentService,
@ITelemetryService telemetryService: ITelemetryService,
@IFileService fileService: IFileService,
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
@IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService,
) {
super(storageService, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService, allowedExtensionsService, extensionGalleryManifestService);
}
export class ExtensionGalleryServiceWithNoStorageService extends AbstractExtensionGalleryService {
constructor(
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService,
@IEnvironmentService environmentService: IEnvironmentService,
@ITelemetryService telemetryService: ITelemetryService,
@IFileService fileService: IFileService,
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
@IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService,
) {
super(undefined, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService, allowedExtensionsService, extensionGalleryManifestService);
}