@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
1,219 lines (1,218 loc) • 45.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
d;
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param =
(this && this.__param) ||
function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex);
};
};
import { distinct } from '@sussudio/base/common/arrays.mjs';
import { CancellationToken } from '@sussudio/base/common/cancellation.mjs';
import { CancellationError, getErrorMessage, isCancellationError } from '@sussudio/base/common/errors.mjs';
import { isWeb, platform } from '@sussudio/base/common/platform.mjs';
import { arch } from '@sussudio/base/common/process.mjs';
import { isBoolean } from '@sussudio/base/common/types.mjs';
import { URI } from '@sussudio/base/common/uri.mjs';
import { IConfigurationService } from '../../configuration/common/configuration.mjs';
import { IEnvironmentService } from '../../environment/common/environment.mjs';
import {
getFallbackTargetPlarforms,
getTargetPlatform,
isNotWebExtensionInWebTargetPlatform,
isTargetPlatformCompatible,
toTargetPlatform,
WEB_EXTENSION_TAG,
} from './extensionManagement.mjs';
import {
adoptToGalleryExtensionId,
areSameExtensions,
getGalleryExtensionId,
getGalleryExtensionTelemetryData,
} from './extensionManagementUtil.mjs';
import { isEngineValid } from '../../extensions/common/extensionValidator.mjs';
import { IFileService } from '../../files/common/files.mjs';
import { ILogService } from '../../log/common/log.mjs';
import { IProductService } from '../../product/common/productService.mjs';
import { asJson, asTextOrError, IRequestService, isSuccess } from '../../request/common/request.mjs';
import { resolveMarketplaceHeaders } from '../../externalServices/common/marketplace.mjs';
import { IStorageService } from '../../storage/common/storage.mjs';
import { ITelemetryService } from '../../telemetry/common/telemetry.mjs';
const CURRENT_TARGET_PLATFORM = isWeb ? 'web' /* TargetPlatform.WEB */ : getTargetPlatform(platform, arch);
var Flags;
(function (Flags) {
/**
* None is used to retrieve only the basic extension details.
*/
Flags[(Flags['None'] = 0)] = 'None';
/**
* IncludeVersions will return version information for extensions returned
*/
Flags[(Flags['IncludeVersions'] = 1)] = 'IncludeVersions';
/**
* IncludeFiles will return information about which files were found
* within the extension that were stored independent of the manifest.
* When asking for files, versions will be included as well since files
* are returned as a property of the versions.
* These files can be retrieved using the path to the file without
* requiring the entire manifest be downloaded.
*/
Flags[(Flags['IncludeFiles'] = 2)] = 'IncludeFiles';
/**
* Include the Categories and Tags that were added to the extension definition.
*/
Flags[(Flags['IncludeCategoryAndTags'] = 4)] = 'IncludeCategoryAndTags';
/**
* Include the details about which accounts the extension has been shared
* with if the extension is a private extension.
*/
Flags[(Flags['IncludeSharedAccounts'] = 8)] = 'IncludeSharedAccounts';
/**
* Include properties associated with versions of the extension
*/
Flags[(Flags['IncludeVersionProperties'] = 16)] = 'IncludeVersionProperties';
/**
* Excluding non-validated extensions will remove any extension versions that
* either are in the process of being validated or have failed validation.
*/
Flags[(Flags['ExcludeNonValidated'] = 32)] = 'ExcludeNonValidated';
/**
* Include the set of installation targets the extension has requested.
*/
Flags[(Flags['IncludeInstallationTargets'] = 64)] = 'IncludeInstallationTargets';
/**
* Include the base uri for assets of this extension
*/
Flags[(Flags['IncludeAssetUri'] = 128)] = 'IncludeAssetUri';
/**
* Include the statistics associated with this extension
*/
Flags[(Flags['IncludeStatistics'] = 256)] = 'IncludeStatistics';
/**
* When retrieving versions from a query, only include the latest
* version of the extensions that matched. This is useful when the
* caller doesn't need all the published versions. It will save a
* significant size in the returned payload.
*/
Flags[(Flags['IncludeLatestVersionOnly'] = 512)] = 'IncludeLatestVersionOnly';
/**
* This flag switches the asset uri to use GetAssetByName instead of CDN
* When this is used, values of base asset uri and base asset uri fallback are switched
* When this is used, source of asset files are pointed to Gallery service always even if CDN is available
*/
Flags[(Flags['Unpublished'] = 4096)] = 'Unpublished';
/**
* Include the details if an extension is in conflict list or not
*/
Flags[(Flags['IncludeNameConflictInfo'] = 32768)] = 'IncludeNameConflictInfo';
})(Flags || (Flags = {}));
function flagsToString(...flags) {
return String(flags.reduce((r, f) => r | f, 0));
}
var FilterType;
(function (FilterType) {
FilterType[(FilterType['Tag'] = 1)] = 'Tag';
FilterType[(FilterType['ExtensionId'] = 4)] = 'ExtensionId';
FilterType[(FilterType['Category'] = 5)] = 'Category';
FilterType[(FilterType['ExtensionName'] = 7)] = 'ExtensionName';
FilterType[(FilterType['Target'] = 8)] = 'Target';
FilterType[(FilterType['Featured'] = 9)] = 'Featured';
FilterType[(FilterType['SearchText'] = 10)] = 'SearchText';
FilterType[(FilterType['ExcludeWithFlags'] = 12)] = 'ExcludeWithFlags';
})(FilterType || (FilterType = {}));
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',
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
WebExtension: 'Microsoft.VisualStudio.Code.WebExtension',
SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink',
};
const DefaultPageSize = 10;
const DefaultQueryState = {
pageNumber: 1,
pageSize: DefaultPageSize,
sortBy: 0 /* SortBy.NoneOrRelevance */,
sortOrder: 0 /* SortOrder.Default */,
flags: Flags.None,
criteria: [],
assetTypes: [],
};
class Query {
state;
constructor(state = DefaultQueryState) {
this.state = state;
}
get pageNumber() {
return this.state.pageNumber;
}
get pageSize() {
return this.state.pageSize;
}
get sortBy() {
return this.state.sortBy;
}
get sortOrder() {
return this.state.sortOrder;
}
get flags() {
return this.state.flags;
}
get criteria() {
return this.state.criteria;
}
withPage(pageNumber, pageSize = this.state.pageSize) {
return new Query({ ...this.state, pageNumber, pageSize });
}
withFilter(filterType, ...values) {
const criteria = [
...this.state.criteria,
...(values.length ? values.map((value) => ({ filterType, value })) : [{ filterType }]),
];
return new Query({ ...this.state, criteria });
}
withSortBy(sortBy) {
return new Query({ ...this.state, sortBy });
}
withSortOrder(sortOrder) {
return new Query({ ...this.state, sortOrder });
}
withFlags(...flags) {
return new Query({ ...this.state, flags: flags.reduce((r, f) => r | f, 0) });
}
withAssetTypes(...assetTypes) {
return new Query({ ...this.state, assetTypes });
}
withSource(source) {
return new Query({ ...this.state, source });
}
get raw() {
const { criteria, pageNumber, pageSize, sortBy, sortOrder, flags, assetTypes } = this.state;
const filters = [{ criteria, pageNumber, pageSize, sortBy, sortOrder }];
return { filters, assetTypes, flags };
}
get searchText() {
const criterium = this.state.criteria.filter((criterium) => criterium.filterType === FilterType.SearchText)[0];
return criterium && criterium.value ? criterium.value : '';
}
get telemetryData() {
return {
filterTypes: this.state.criteria.map((criterium) => String(criterium.filterType)),
flags: this.state.flags,
sortBy: String(this.sortBy),
sortOrder: String(this.sortOrder),
pageNumber: String(this.pageNumber),
source: this.state.source,
searchTextLength: this.searchText.length,
};
}
}
function getStatistic(statistics, name) {
const result = (statistics || []).filter((s) => s.statisticName === name)[0];
return result ? result.value : 0;
}
function getCoreTranslationAssets(version) {
const coreTranslationAssetPrefix = 'Microsoft.VisualStudio.Code.Translation.';
const result = version.files.filter((f) => f.assetType.indexOf(coreTranslationAssetPrefix) === 0);
return result.reduce((result, file) => {
const asset = getVersionAsset(version, file.assetType);
if (asset) {
result.push([file.assetType.substring(coreTranslationAssetPrefix.length), asset]);
}
return result;
}, []);
}
function getRepositoryAsset(version) {
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) {
return {
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, type) {
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, property) {
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) {
const values = version.properties ? version.properties.filter((p) => p.key === PropertyType.Engine) : [];
return (values.length > 0 && values[0].value) || '';
}
function isPreReleaseVersion(version) {
const values = version.properties ? version.properties.filter((p) => p.key === PropertyType.PreRelease) : [];
return values.length > 0 && values[0].value === 'true';
}
function getLocalizedLanguages(version) {
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) {
return version.properties?.find((p) => p.key === PropertyType.SponsorLink)?.value;
}
function getIsPreview(flags) {
return flags.indexOf('preview') !== -1;
}
function getTargetPlatformForExtensionVersion(version) {
return version.targetPlatform ? toTargetPlatform(version.targetPlatform) : 'undefined' /* TargetPlatform.UNDEFINED */;
}
function getAllTargetPlatforms(rawGalleryExtension) {
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('web' /* TargetPlatform.WEB */);
if (isWebExtension) {
if (webTargetPlatformIndex === -1) {
// Web extension but does not has web target platform -> add it
allTargetPlatforms.push('web' /* 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, preferredTargetPlatform) {
/* It is expected that versions from Marketplace are sorted by version. So we are just sorting by preferred targetPlatform */
const fallbackTargetPlatforms = getFallbackTargetPlarforms(preferredTargetPlatform);
for (let index = 0; index < versions.length; index++) {
const version = versions[index];
if (version.version === versions[index - 1]?.version) {
let insertionIndex = index;
const versionTargetPlatform = getTargetPlatformForExtensionVersion(version);
/* put it at the beginning */
if (versionTargetPlatform === preferredTargetPlatform) {
while (insertionIndex > 0 && versions[insertionIndex - 1].version === version.version) {
insertionIndex--;
}
} else if (fallbackTargetPlatforms.includes(versionTargetPlatform)) {
/* put it after version with preferred targetPlatform or at the beginning */
while (
insertionIndex > 0 &&
versions[insertionIndex - 1].version === version.version &&
getTargetPlatformForExtensionVersion(versions[insertionIndex - 1]) !== preferredTargetPlatform
) {
insertionIndex--;
}
}
if (insertionIndex !== index) {
versions.splice(index, 1);
versions.splice(insertionIndex, 0, version);
}
}
}
return versions;
}
function setTelemetry(extension, index, querySource) {
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData2" : {
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
extension.telemetryData = { index, querySource };
}
function toExtension(galleryExtension, version, allTargetPlatforms) {
const latestVersion = galleryExtension.versions[0];
const assets = {
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),
};
return {
identifier: {
id: getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName),
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),
localizedLanguages: getLocalizedLanguages(version),
targetPlatform: getTargetPlatformForExtensionVersion(version),
isPreReleaseVersion: isPreReleaseVersion(version),
},
hasPreReleaseVersion: isPreReleaseVersion(latestVersion),
hasReleaseVersion: true,
preview: getIsPreview(galleryExtension.flags),
isSigned: !!assets.signature,
};
}
let AbstractExtensionGalleryService = class AbstractExtensionGalleryService {
requestService;
logService;
environmentService;
telemetryService;
fileService;
productService;
configurationService;
extensionsGalleryUrl;
extensionsGallerySearchUrl;
extensionsControlUrl;
commonHeadersPromise;
constructor(
storageService,
requestService,
logService,
environmentService,
telemetryService,
fileService,
productService,
configurationService,
) {
this.requestService = requestService;
this.logService = logService;
this.environmentService = environmentService;
this.telemetryService = telemetryService;
this.fileService = fileService;
this.productService = productService;
this.configurationService = configurationService;
const config = productService.extensionsGallery;
const isPPEEnabled = config?.servicePPEUrl && configurationService.getValue('_extensionsGallery.enablePPE');
this.extensionsGalleryUrl = isPPEEnabled ? config.servicePPEUrl : config?.serviceUrl;
this.extensionsGallerySearchUrl = isPPEEnabled ? undefined : config?.searchUrl;
this.extensionsControlUrl = config?.controlUrl;
this.commonHeadersPromise = resolveMarketplaceHeaders(
productService.version,
productService,
this.environmentService,
this.configurationService,
this.fileService,
storageService,
this.telemetryService,
);
}
api(path = '') {
return `${this.extensionsGalleryUrl}${path}`;
}
isEnabled() {
return !!this.extensionsGalleryUrl;
}
async getExtensions(extensionInfos, arg1, arg2) {
const options = CancellationToken.isCancellationToken(arg1) ? {} : arg1;
const token = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2;
const names = [];
const ids = [],
includePreReleases = [],
versions = [];
let isQueryForReleaseVersionFromPreReleaseVersion = true;
for (const extensionInfo of extensionInfos) {
if (extensionInfo.uuid) {
ids.push(extensionInfo.uuid);
} else {
names.push(extensionInfo.id);
}
// Set includePreRelease to true if version is set, because the version can be a pre-release version
const includePreRelease = !!(extensionInfo.version || extensionInfo.preRelease);
includePreReleases.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, includePreRelease });
if (extensionInfo.version) {
versions.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, version: extensionInfo.version });
}
isQueryForReleaseVersionFromPreReleaseVersion =
isQueryForReleaseVersionFromPreReleaseVersion && !!extensionInfo.hasPreRelease && !includePreRelease;
}
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 ||
isQueryForReleaseVersionFromPreReleaseVersion /* Inlcude all versions if every requested extension is for release version and has pre-release version */
) {
query = query.withFlags(query.flags, Flags.IncludeVersions);
}
if (options.source) {
query = query.withSource(options.source);
}
const { extensions } = await this.queryGalleryExtensions(
query,
{
targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM,
includePreRelease: includePreReleases,
versions,
compatible: !!options.compatible,
},
token,
);
if (options.source) {
extensions.forEach((e, index) => setTelemetry(e, index, options.source));
}
return extensions;
}
async getCompatibleExtension(extension, includePreRelease, targetPlatform) {
if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) {
return null;
}
if (await this.isExtensionCompatible(extension, includePreRelease, targetPlatform)) {
return extension;
}
const query = new Query()
.withFlags(Flags.IncludeVersions)
.withPage(1, 1)
.withFilter(FilterType.ExtensionId, extension.identifier.uuid);
const { extensions } = await this.queryGalleryExtensions(
query,
{ targetPlatform, compatible: true, includePreRelease },
CancellationToken.None,
);
return extensions[0] || null;
}
async isExtensionCompatible(extension, includePreRelease, targetPlatform) {
if (
!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)
) {
return false;
}
if (!includePreRelease && extension.properties.isPreReleaseVersion) {
// Pre-releases are not allowed when include pre-release flag is not set
return false;
}
let engine = extension.properties.engine;
if (!engine) {
const manifest = await this.getManifest(extension, CancellationToken.None);
if (!manifest) {
throw new Error('Manifest was not found');
}
engine = manifest.engines.vscode;
}
return isEngineValid(engine, this.productService.version, this.productService.date);
}
async isValidVersion(rawGalleryExtensionVersion, versionType, compatible, allTargetPlatforms, targetPlatform) {
if (
!isTargetPlatformCompatible(
getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion),
allTargetPlatforms,
targetPlatform,
)
) {
return false;
}
if (versionType !== 'any' && isPreReleaseVersion(rawGalleryExtensionVersion) !== (versionType === 'prerelease')) {
return false;
}
if (compatible) {
try {
const engine = await this.getEngine(rawGalleryExtensionVersion);
if (!isEngineValid(engine, this.productService.version, this.productService.date)) {
return false;
}
} catch (error) {
this.logService.error(
`Error while getting the engine for the version ${rawGalleryExtensionVersion.version}.`,
getErrorMessage(error),
);
return false;
}
}
return true;
}
async query(options, token) {
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);
}
query = query.withSortBy(0 /* SortBy.NoneOrRelevance */);
} else if (options.ids) {
query = query.withFilter(FilterType.ExtensionId, ...options.ids);
} else if (options.names) {
query = query.withFilter(FilterType.ExtensionName, ...options.names);
} else {
query = query.withSortBy(4 /* SortBy.InstallCount */);
}
if (typeof options.sortBy === 'number') {
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, token) => {
const { extensions, total } = await this.queryGalleryExtensions(
query,
{ targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease },
token,
);
extensions.forEach((e, index) =>
setTelemetry(e, (query.pageNumber - 1) * query.pageSize + index, options.source),
);
return { extensions, total };
};
const { extensions, total } = await runQuery(query, token);
const getPage = async (pageIndex, ct) => {
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 };
}
async queryGalleryExtensions(query, criteria, token) {
const flags = query.flags;
/**
* If both version flags (IncludeLatestVersionOnly and IncludeVersions) are included, then only include latest versions (IncludeLatestVersionOnly) flag.
*/
if (!!(query.flags & Flags.IncludeLatestVersionOnly) && !!(query.flags & Flags.IncludeVersions)) {
query = query.withFlags(query.flags & ~Flags.IncludeVersions, Flags.IncludeLatestVersionOnly);
}
/**
* If version flags (IncludeLatestVersionOnly and IncludeVersions) are not included, default is to query for latest versions (IncludeLatestVersionOnly).
*/
if (!(query.flags & Flags.IncludeLatestVersionOnly) && !(query.flags & Flags.IncludeVersions)) {
query = query.withFlags(query.flags, Flags.IncludeLatestVersionOnly);
}
/**
* If versions criteria exist, then remove IncludeLatestVersionOnly flag and add IncludeVersions flag.
*/
if (criteria.versions?.length) {
query = query.withFlags(query.flags & ~Flags.IncludeLatestVersionOnly, Flags.IncludeVersions);
}
/**
* Add necessary extension flags
*/
query = query.withFlags(
query.flags,
Flags.IncludeAssetUri,
Flags.IncludeCategoryAndTags,
Flags.IncludeFiles,
Flags.IncludeStatistics,
Flags.IncludeVersionProperties,
);
const { galleryExtensions: rawGalleryExtensions, total } = await this.queryRawGalleryExtensions(query, token);
const hasAllVersions = !(query.flags & Flags.IncludeLatestVersionOnly);
if (hasAllVersions) {
const extensions = [];
for (const rawGalleryExtension of rawGalleryExtensions) {
const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria);
if (extension) {
extensions.push(extension);
}
}
return { extensions, total };
}
const result = [];
const needAllVersions = new Map();
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;
if (
criteria.compatible &&
isNotWebExtensionInWebTargetPlatform(getAllTargetPlatforms(rawGalleryExtension), criteria.targetPlatform)
) {
/** Skip if requested for a web-compatible extension and it is not a web extension.
* All versions are not needed in this case
*/
continue;
}
const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria);
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 startTime = new Date().getTime();
const query = new Query()
.withFlags(flags & ~Flags.IncludeLatestVersionOnly, Flags.IncludeVersions)
.withPage(1, needAllVersions.size)
.withFilter(FilterType.ExtensionId, ...needAllVersions.keys());
const { extensions } = await this.queryGalleryExtensions(query, criteria, token);
this.telemetryService.publicLog2('galleryService:additionalQuery', {
duration: new Date().getTime() - startTime,
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 };
}
async toGalleryExtensionWithCriteria(rawGalleryExtension, criteria) {
const extensionIdentifier = {
id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName),
uuid: rawGalleryExtension.extensionId,
};
const version = criteria.versions?.find((extensionIdentifierWithVersion) =>
areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier),
)?.version;
const includePreRelease = isBoolean(criteria.includePreRelease)
? criteria.includePreRelease
: !!criteria.includePreRelease.find((extensionIdentifierWithPreRelease) =>
areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier),
)?.includePreRelease;
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
const rawGalleryExtensionVersions = sortExtensionVersions(rawGalleryExtension.versions, criteria.targetPlatform);
if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) {
return null;
}
for (let index = 0; index < rawGalleryExtensionVersions.length; index++) {
const rawGalleryExtensionVersion = rawGalleryExtensionVersions[index];
if (version && rawGalleryExtensionVersion.version !== version) {
continue;
}
// Allow any version if includePreRelease flag is set otherwise only release versions are allowed
if (
await this.isValidVersion(
rawGalleryExtensionVersion,
includePreRelease ? 'any' : 'release',
criteria.compatible,
allTargetPlatforms,
criteria.targetPlatform,
)
) {
return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms);
}
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 toExtension(rawGalleryExtension, rawGalleryExtension.versions[0], allTargetPlatforms);
}
async queryRawGalleryExtensions(query, token) {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
query = query
/* Always exclude non validated extensions */
.withFlags(query.flags, Flags.ExcludeNonValidated)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
/* Always exclude unpublished extensions */
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished));
const commonHeaders = await this.commonHeadersPromise;
const data = JSON.stringify(query.raw);
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 startTime = new Date().getTime();
let context,
error,
total = 0;
try {
context = await this.requestService.request(
{
type: 'POST',
url:
this.extensionsGallerySearchUrl && query.criteria.some((c) => c.filterType === FilterType.SearchText)
? this.extensionsGallerySearchUrl
: this.api('/extensionquery'),
data,
headers,
},
token,
);
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) {
return { galleryExtensions: [], total };
}
const result = await asJson(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 };
}
return { galleryExtensions: [], total };
} catch (e) {
error = e;
throw e;
} finally {
this.telemetryService.publicLog2('galleryService:query', {
...query.telemetryData,
requestBodySize: String(data.length),
duration: new Date().getTime() - startTime,
success: !!context && isSuccess(context),
responseBodySize: context?.res.headers['Content-Length'],
statusCode: context ? String(context.res.statusCode) : undefined,
errorCode: error
? isCancellationError(error)
? 'canceled'
: getErrorMessage(error).startsWith('XHR timeout')
? 'timeout'
: 'failed'
: undefined,
count: String(total),
});
}
}
async reportStatistic(publisher, name, version, type) {
if (!this.isEnabled()) {
return undefined;
}
const url = isWeb
? this.api(
`/itemName/${publisher}.${name}/version/${version}/statType/${
type === 'install' /* StatisticType.Install */ ? '1' : '3'
}/vscodewebextension`,
)
: this.api(`/publishers/${publisher}/extensions/${name}/${version}/stats?statType=${type}`);
const Accept = isWeb ? 'api-version=6.1-preview.1' : '*/*;api-version=4.0-preview.1';
const commonHeaders = await this.commonHeadersPromise;
const headers = { ...commonHeaders, Accept };
try {
await this.requestService.request(
{
type: 'POST',
url,
headers,
},
CancellationToken.None,
);
} catch (error) {
/* Ignore */
}
}
async download(extension, location, operation) {
this.logService.trace('ExtensionGalleryService#download', extension.identifier.id);
const data = getGalleryExtensionTelemetryData(extension);
const startTime = new Date().getTime();
/* __GDPR__
"galleryService:downloadVSIX" : {
"owner": "sandy081",
"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
const log = (duration) => this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration });
const operationParam =
operation === 2 /* InstallOperation.Install */
? 'install'
: operation === 3 /* 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 context = await this.getAsset(downloadAsset);
await this.fileService.writeFile(location, context.stream);
log(new Date().getTime() - startTime);
}
async downloadSignatureArchive(extension, location) {
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.assets.signature);
await this.fileService.writeFile(location, context.stream);
}
async getReadme(extension, token) {
if (extension.assets.readme) {
const context = await this.getAsset(extension.assets.readme, {}, token);
const content = await asTextOrError(context);
return content || '';
}
return '';
}
async getManifest(extension, token) {
if (extension.assets.manifest) {
const context = await this.getAsset(extension.assets.manifest, {}, token);
const text = await asTextOrError(context);
return text ? JSON.parse(text) : null;
}
return null;
}
async getManifestFromRawExtensionVersion(rawExtensionVersion, token) {
const manifestAsset = getVersionAsset(rawExtensionVersion, AssetType.Manifest);
if (!manifestAsset) {
throw new Error('Manifest was not found');
}
const headers = { 'Accept-Encoding': 'gzip' };
const context = await this.getAsset(manifestAsset, { headers });
return await asJson(context);
}
async getCoreTranslation(extension, languageId) {
const asset = extension.assets.coreTranslations.filter((t) => t[0] === languageId.toUpperCase())[0];
if (asset) {
const context = await this.getAsset(asset[1]);
const text = await asTextOrError(context);
return text ? JSON.parse(text) : null;
}
return null;
}
async getChangelog(extension, token) {
if (extension.assets.changelog) {
const context = await this.getAsset(extension.assets.changelog, {}, token);
const content = await asTextOrError(context);
return content || '';
}
return '';
}
async getAllCompatibleVersions(extension, includePreRelease, targetPlatform) {
let query = new Query()
.withFlags(
Flags.IncludeVersions,
Flags.IncludeCategoryAndTags,
Flags.IncludeFiles,
Flags.IncludeVersionProperties,
)
.withPage(1, 1);
if (extension.identifier.uuid) {
query = query.withFilter(FilterType.ExtensionId, extension.identifier.uuid);
} else {
query = query.withFilter(FilterType.ExtensionName, extension.identifier.id);
}
const { galleryExtensions } = await this.queryRawGalleryExtensions(query, CancellationToken.None);
if (!galleryExtensions.length) {
return [];
}
const allTargetPlatforms = getAllTargetPlatforms(galleryExtensions[0]);
if (isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, targetPlatform)) {
return [];
}
const validVersions = [];
await Promise.all(
galleryExtensions[0].versions.map(async (version) => {
try {
if (
await this.isValidVersion(
version,
includePreRelease ? 'any' : 'release',
true,
allTargetPlatforms,
targetPlatform,
)
) {
validVersions.push(version);
}
} catch (error) {
/* Ignore error and skip version */
}
}),
);
const result = [];
const seen = new Set();
for (const version of sortExtensionVersions(validVersions, targetPlatform)) {
if (!seen.has(version.version)) {
seen.add(version.version);
result.push({
version: version.version,
date: version.lastUpdated,
isPreReleaseVersion: isPreReleaseVersion(version),
});
}
}
return result;
}
async getAsset(asset, options = {}, token = CancellationToken.None) {
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 };
try {
const 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);
this.telemetryService.publicLog2('galleryService:cdnFallback', { url, message });
const fallbackOptions = { ...options, url: fallbackUrl };
return this.requestService.request(fallbackOptions, token);
}
}
async getEngine(rawExtensionVersion) {
let engine = getEngine(rawExtensionVersion);
if (!engine) {
const manifest = await this.getManifestFromRawExtensionVersion(rawExtensionVersion, CancellationToken.None);
if (!manifest) {
throw new Error('Manifest was not found');
}
engine = manifest.engines.vscode;
}
return engine;
}
async getExtensionsControlManifest() {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
if (!this.extensionsControlUrl) {
return { malicious: [], deprecated: {} };
}
const context = await this.requestService.request(
{ type: 'GET', url: this.extensionsControlUrl },
CancellationToken.None,
);
if (context.res.statusCode !== 200) {
throw new Error('Could not get extensions report.');
}
const result = await asJson(context);
const malicious = [];
const deprecated = {};
if (result) {
for (const id of result.malicious) {
malicious.push({ 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;
}
}
}
}
return { malicious, deprecated };
}
};
AbstractExtensionGalleryService = __decorate(
[
__param(1, IRequestService),
__param(2, ILogService),
__param(3, IEnvironmentService),
__param(4, ITelemetryService),
__param(5, IFileService),
__param(6, IProductService),
__param(7, IConfigurationService),
],
AbstractExtensionGalleryService,
);
let ExtensionGalleryService = class ExtensionGalleryService extends AbstractExtensionGalleryService {
constructor(
storageService,
requestService,
logService,
environmentService,
telemetryService,
fileService,
productService,
configurationService,
) {
super(
storageService,
requestService,
logService,
environmentService,
telemetryService,
fileService,
productService,
configurationService,
);
}
};
ExtensionGalleryService = __decorate(
[
__param(0, IStorageService),
__param(1, IRequestService),
__param(2, ILogService),
__param(3, IEnvironmentService),
__param(4, ITelemetryService),
__param(5, IFileService),
__param(6, IProductService),
__param(7, IConfigurationService),
],
ExtensionGalleryService,
);
export { ExtensionGalleryService };
let ExtensionGalleryServiceWithNoStorageService = class ExtensionGalleryServiceWithNoStorageService extends AbstractExtensionGalleryService {
constructor(
requestService,
logService,
environmentService,
telemetryService,
fileService,
productService,
configurationService,
) {
super(
undefined,
requestService,
logService,
environmentService,
telemetryService,
fileService,
productService,
configurationService,
);
}
};
ExtensionGalleryServiceWithNoStorageService = __decorate(
[
__param(0, IRequestService),
__param(1, ILogService),
__param(2, IEnvironmentService),
__param(3, ITelemetryService),
__param(4, IFileService),
__param(5, IProductService),
__param(6, IConfigurationService),
],
ExtensionGalleryServiceWithNoStorageService,
);
export { ExtensionGalleryServiceWithNoStorageService };