Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/extensionManagement/common/extensionGalleryService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { distinct } from '../../../base/common/arrays.js';
7
import { CancellationToken } from '../../../base/common/cancellation.js';
8
import * as semver from '../../../base/common/semver/semver.js';
9
import { IStringDictionary } from '../../../base/common/collections.js';
10
import { CancellationError, getErrorMessage, isCancellationError } from '../../../base/common/errors.js';
11
import { IPager } from '../../../base/common/paging.js';
12
import { isWeb, platform } from '../../../base/common/platform.js';
13
import { arch } from '../../../base/common/process.js';
14
import { isBoolean, isString } from '../../../base/common/types.js';
15
import { URI } from '../../../base/common/uri.js';
16
import { IHeaders, IRequestContext, IRequestOptions, isOfflineError } from '../../../base/parts/request/common/request.js';
17
import { IConfigurationService } from '../../configuration/common/configuration.js';
18
import { IEnvironmentService } from '../../environment/common/environment.js';
19
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 } from './extensionManagement.js';
20
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from './extensionManagementUtil.js';
21
import { IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js';
22
import { areApiProposalsCompatible, isEngineValid } from '../../extensions/common/extensionValidator.js';
23
import { IFileService } from '../../files/common/files.js';
24
import { ILogService } from '../../log/common/log.js';
25
import { IProductService } from '../../product/common/productService.js';
26
import { asJson, asTextOrError, IRequestService, isClientError, isServerError, isSuccess } from '../../request/common/request.js';
27
import { resolveMarketplaceHeaders } from '../../externalServices/common/marketplace.js';
28
import { IStorageService } from '../../storage/common/storage.js';
29
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
30
import { StopWatch } from '../../../base/common/stopwatch.js';
31
import { format2 } from '../../../base/common/strings.js';
32
import { IAssignmentService } from '../../assignment/common/assignment.js';
33
import { ExtensionGalleryResourceType, Flag, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService, ExtensionGalleryManifestStatus } from './extensionGalleryManifest.js';
34
import { TelemetryTrustedValue } from '../../telemetry/common/telemetryUtils.js';
35
36
const CURRENT_TARGET_PLATFORM = isWeb ? TargetPlatform.WEB : getTargetPlatform(platform, arch);
37
const SEARCH_ACTIVITY_HEADER_NAME = 'X-Market-Search-Activity-Id';
38
const ACTIVITY_HEADER_NAME = 'Activityid';
39
const SERVER_HEADER_NAME = 'Server';
40
const END_END_ID_HEADER_NAME = 'X-Vss-E2eid';
41
const REQUEST_TIME_OUT = 10_000;
42
43
interface IRawGalleryExtensionFile {
44
readonly assetType: string;
45
readonly source: string;
46
}
47
48
interface IRawGalleryExtensionProperty {
49
readonly key: string;
50
readonly value: string;
51
}
52
53
export interface IRawGalleryExtensionVersion {
54
readonly version: string;
55
readonly lastUpdated: string;
56
readonly assetUri: string;
57
readonly fallbackAssetUri: string;
58
readonly files: IRawGalleryExtensionFile[];
59
readonly properties?: IRawGalleryExtensionProperty[];
60
readonly targetPlatform?: string;
61
}
62
63
interface IRawGalleryExtensionStatistics {
64
readonly statisticName: string;
65
readonly value: number;
66
}
67
68
interface IRawGalleryExtensionPublisher {
69
readonly displayName: string;
70
readonly publisherId: string;
71
readonly publisherName: string;
72
readonly domain?: string | null;
73
readonly isDomainVerified?: boolean;
74
readonly linkType?: string;
75
}
76
77
interface IRawGalleryExtension {
78
readonly extensionId: string;
79
readonly extensionName: string;
80
readonly displayName: string;
81
readonly shortDescription?: string;
82
readonly publisher: IRawGalleryExtensionPublisher;
83
readonly versions: IRawGalleryExtensionVersion[];
84
readonly statistics: IRawGalleryExtensionStatistics[];
85
readonly tags: string[] | undefined;
86
readonly releaseDate: string;
87
readonly publishedDate: string;
88
readonly lastUpdated: string;
89
readonly categories: string[] | undefined;
90
readonly flags: string;
91
readonly linkType?: string;
92
readonly ratingLinkType?: string;
93
}
94
95
interface IRawGalleryExtensionsResult {
96
readonly galleryExtensions: IRawGalleryExtension[];
97
readonly total: number;
98
readonly context?: IStringDictionary<string>;
99
}
100
101
interface IRawGalleryQueryResult {
102
readonly results: {
103
readonly extensions: IRawGalleryExtension[];
104
readonly resultMetadata: {
105
readonly metadataType: string;
106
readonly metadataItems: {
107
readonly name: string;
108
readonly count: number;
109
}[];
110
}[];
111
}[];
112
}
113
114
const AssetType = {
115
Icon: 'Microsoft.VisualStudio.Services.Icons.Default',
116
Details: 'Microsoft.VisualStudio.Services.Content.Details',
117
Changelog: 'Microsoft.VisualStudio.Services.Content.Changelog',
118
Manifest: 'Microsoft.VisualStudio.Code.Manifest',
119
VSIX: 'Microsoft.VisualStudio.Services.VSIXPackage',
120
License: 'Microsoft.VisualStudio.Services.Content.License',
121
Repository: 'Microsoft.VisualStudio.Services.Links.Source',
122
Signature: 'Microsoft.VisualStudio.Services.VsixSignature'
123
};
124
125
const PropertyType = {
126
Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies',
127
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
128
Engine: 'Microsoft.VisualStudio.Code.Engine',
129
PreRelease: 'Microsoft.VisualStudio.Code.PreRelease',
130
EnabledApiProposals: 'Microsoft.VisualStudio.Code.EnabledApiProposals',
131
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
132
WebExtension: 'Microsoft.VisualStudio.Code.WebExtension',
133
SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink',
134
SupportLink: 'Microsoft.VisualStudio.Services.Links.Support',
135
ExecutesCode: 'Microsoft.VisualStudio.Code.ExecutesCode',
136
Private: 'PrivateMarketplace',
137
};
138
139
interface ICriterium {
140
readonly filterType: FilterType;
141
readonly value?: string;
142
}
143
144
const DefaultPageSize = 10;
145
146
interface IQueryState {
147
readonly pageNumber: number;
148
readonly pageSize: number;
149
readonly sortBy: SortBy;
150
readonly sortOrder: SortOrder;
151
readonly flags: Flag[];
152
readonly criteria: ICriterium[];
153
readonly assetTypes: string[];
154
readonly source?: string;
155
}
156
157
const DefaultQueryState: IQueryState = {
158
pageNumber: 1,
159
pageSize: DefaultPageSize,
160
sortBy: SortBy.NoneOrRelevance,
161
sortOrder: SortOrder.Default,
162
flags: [],
163
criteria: [],
164
assetTypes: []
165
};
166
167
type GalleryServiceQueryClassification = {
168
owner: 'sandy081';
169
comment: 'Information about Marketplace query and its response';
170
readonly filterTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Filter types used in the query.' };
171
readonly flags: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flags passed in the query.' };
172
readonly sortBy: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'sorted by option passed in the query' };
173
readonly sortOrder: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'sort order option passed in the query' };
174
readonly pageNumber: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'requested page number in the query' };
175
readonly duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'amount of time taken by the query request' };
176
readonly success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'whether the query request is success or not' };
177
readonly requestBodySize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'size of the request body' };
178
readonly responseBodySize?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'size of the response body' };
179
readonly statusCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'status code of the response' };
180
readonly errorCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code of the response' };
181
readonly count?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'total number of extensions matching the query' };
182
readonly source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'source that requested this query, eg., recommendations, viewlet' };
183
readonly searchTextLength?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'length of the search text in the query' };
184
readonly server?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'server that handled the query' };
185
readonly endToEndId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'end to end operation id' };
186
readonly activityId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'activity id' };
187
};
188
189
type QueryTelemetryData = {
190
readonly filterTypes: string[];
191
readonly flags: string[];
192
readonly sortBy: string;
193
readonly sortOrder: string;
194
readonly pageNumber: string;
195
readonly source?: string;
196
readonly searchTextLength?: number;
197
};
198
199
type GalleryServiceQueryEvent = QueryTelemetryData & {
200
readonly duration: number;
201
readonly success: boolean;
202
readonly requestBodySize: string;
203
readonly responseBodySize?: string;
204
readonly statusCode?: string;
205
readonly errorCode?: string;
206
readonly count?: string;
207
readonly server?: TelemetryTrustedValue<string>;
208
readonly endToEndId?: TelemetryTrustedValue<string>;
209
readonly activityId?: TelemetryTrustedValue<string>;
210
};
211
212
type GalleryServiceAdditionalQueryClassification = {
213
owner: 'sandy081';
214
comment: 'Response information about the additional query to the Marketplace for fetching all versions to get release version';
215
readonly duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'Amount of time taken by the additional query' };
216
readonly count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Total number of extensions returned by this additional query' };
217
};
218
219
type GalleryServiceAdditionalQueryEvent = {
220
readonly duration: number;
221
readonly count: number;
222
};
223
224
type ExtensionsCriteria = {
225
readonly productVersion: IProductVersion;
226
readonly targetPlatform: TargetPlatform;
227
readonly compatible: boolean;
228
readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[];
229
readonly versions?: (IExtensionIdentifier & { version: string })[];
230
readonly isQueryForReleaseVersionFromPreReleaseVersion?: boolean;
231
};
232
233
const enum VersionKind {
234
Release,
235
Prerelease,
236
Latest
237
}
238
239
type ExtensionVersionCriteria = {
240
readonly productVersion: IProductVersion;
241
readonly targetPlatform: TargetPlatform;
242
readonly compatible: boolean;
243
readonly version: VersionKind | string;
244
};
245
246
class Query {
247
248
constructor(private state = DefaultQueryState) { }
249
250
get pageNumber(): number { return this.state.pageNumber; }
251
get pageSize(): number { return this.state.pageSize; }
252
get sortBy(): SortBy { return this.state.sortBy; }
253
get sortOrder(): number { return this.state.sortOrder; }
254
get flags(): Flag[] { return this.state.flags; }
255
get criteria(): ICriterium[] { return this.state.criteria; }
256
get assetTypes(): string[] { return this.state.assetTypes; }
257
get source(): string | undefined { return this.state.source; }
258
get searchText(): string {
259
const criterium = this.state.criteria.filter(criterium => criterium.filterType === FilterType.SearchText)[0];
260
return criterium && criterium.value ? criterium.value : '';
261
}
262
263
264
withPage(pageNumber: number, pageSize: number = this.state.pageSize): Query {
265
return new Query({ ...this.state, pageNumber, pageSize });
266
}
267
268
withFilter(filterType: FilterType, ...values: string[]): Query {
269
const criteria = [
270
...this.state.criteria,
271
...values.length ? values.map(value => ({ filterType, value })) : [{ filterType }]
272
];
273
274
return new Query({ ...this.state, criteria });
275
}
276
277
withSortBy(sortBy: SortBy): Query {
278
return new Query({ ...this.state, sortBy });
279
}
280
281
withSortOrder(sortOrder: SortOrder): Query {
282
return new Query({ ...this.state, sortOrder });
283
}
284
285
withFlags(...flags: Flag[]): Query {
286
return new Query({ ...this.state, flags: distinct(flags) });
287
}
288
289
withAssetTypes(...assetTypes: string[]): Query {
290
return new Query({ ...this.state, assetTypes });
291
}
292
293
withSource(source: string): Query {
294
return new Query({ ...this.state, source });
295
}
296
}
297
298
function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string): number {
299
const result = (statistics || []).filter(s => s.statisticName === name)[0];
300
return result ? result.value : 0;
301
}
302
303
function getCoreTranslationAssets(version: IRawGalleryExtensionVersion): [string, IGalleryExtensionAsset][] {
304
const coreTranslationAssetPrefix = 'Microsoft.VisualStudio.Code.Translation.';
305
const result = version.files.filter(f => f.assetType.indexOf(coreTranslationAssetPrefix) === 0);
306
return result.reduce<[string, IGalleryExtensionAsset][]>((result, file) => {
307
const asset = getVersionAsset(version, file.assetType);
308
if (asset) {
309
result.push([file.assetType.substring(coreTranslationAssetPrefix.length), asset]);
310
}
311
return result;
312
}, []);
313
}
314
315
function getRepositoryAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset | null {
316
if (version.properties) {
317
const results = version.properties.filter(p => p.key === AssetType.Repository);
318
const gitRegExp = new RegExp('((git|ssh|http(s)?)|(git@[\\w.]+))(:(//)?)([\\w.@:/\\-~]+)(.git)(/)?');
319
320
const uri = results.filter(r => gitRegExp.test(r.value))[0];
321
return uri ? { uri: uri.value, fallbackUri: uri.value } : null;
322
}
323
return getVersionAsset(version, AssetType.Repository);
324
}
325
326
function getDownloadAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset {
327
return {
328
// always use fallbackAssetUri for download asset to hit the Marketplace API so that downloads are counted
329
uri: `${version.fallbackAssetUri}/${AssetType.VSIX}?redirect=true${version.targetPlatform ? `&targetPlatform=${version.targetPlatform}` : ''}`,
330
fallbackUri: `${version.fallbackAssetUri}/${AssetType.VSIX}${version.targetPlatform ? `?targetPlatform=${version.targetPlatform}` : ''}`
331
};
332
}
333
334
function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IGalleryExtensionAsset | null {
335
const result = version.files.filter(f => f.assetType === type)[0];
336
return result ? {
337
uri: `${version.assetUri}/${type}${version.targetPlatform ? `?targetPlatform=${version.targetPlatform}` : ''}`,
338
fallbackUri: `${version.fallbackAssetUri}/${type}${version.targetPlatform ? `?targetPlatform=${version.targetPlatform}` : ''}`
339
} : null;
340
}
341
342
function getExtensions(version: IRawGalleryExtensionVersion, property: string): string[] {
343
const values = version.properties ? version.properties.filter(p => p.key === property) : [];
344
const value = values.length > 0 && values[0].value;
345
return value ? value.split(',').map(v => adoptToGalleryExtensionId(v)) : [];
346
}
347
348
function getEngine(version: IRawGalleryExtensionVersion): string {
349
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Engine) : [];
350
return (values.length > 0 && values[0].value) || '';
351
}
352
353
function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean {
354
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.PreRelease) : [];
355
return values.length > 0 && values[0].value === 'true';
356
}
357
358
function hasPreReleaseForExtension(id: string, productService: IProductService): boolean | undefined {
359
return productService.extensionProperties?.[id.toLowerCase()]?.hasPrereleaseVersion;
360
}
361
362
function getExcludeVersionRangeForExtension(id: string, productService: IProductService): string | undefined {
363
return productService.extensionProperties?.[id.toLowerCase()]?.excludeVersionRange;
364
}
365
366
function isPrivateExtension(version: IRawGalleryExtensionVersion): boolean {
367
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Private) : [];
368
return values.length > 0 && values[0].value === 'true';
369
}
370
371
function executesCode(version: IRawGalleryExtensionVersion): boolean | undefined {
372
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.ExecutesCode) : [];
373
return values.length > 0 ? values[0].value === 'true' : undefined;
374
}
375
376
function getEnabledApiProposals(version: IRawGalleryExtensionVersion): string[] {
377
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.EnabledApiProposals) : [];
378
const value = (values.length > 0 && values[0].value) || '';
379
return value ? value.split(',') : [];
380
}
381
382
function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] {
383
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : [];
384
const value = (values.length > 0 && values[0].value) || '';
385
return value ? value.split(',') : [];
386
}
387
388
function getSponsorLink(version: IRawGalleryExtensionVersion): string | undefined {
389
return version.properties?.find(p => p.key === PropertyType.SponsorLink)?.value;
390
}
391
392
function getSupportLink(version: IRawGalleryExtensionVersion): string | undefined {
393
return version.properties?.find(p => p.key === PropertyType.SupportLink)?.value;
394
}
395
396
function getIsPreview(flags: string): boolean {
397
return flags.indexOf('preview') !== -1;
398
}
399
400
function getTargetPlatformForExtensionVersion(version: IRawGalleryExtensionVersion): TargetPlatform {
401
return version.targetPlatform ? toTargetPlatform(version.targetPlatform) : TargetPlatform.UNDEFINED;
402
}
403
404
function getAllTargetPlatforms(rawGalleryExtension: IRawGalleryExtension): TargetPlatform[] {
405
const allTargetPlatforms = distinct(rawGalleryExtension.versions.map(getTargetPlatformForExtensionVersion));
406
407
// Is a web extension only if it has WEB_EXTENSION_TAG
408
const isWebExtension = !!rawGalleryExtension.tags?.includes(WEB_EXTENSION_TAG);
409
410
// Include Web Target Platform only if it is a web extension
411
const webTargetPlatformIndex = allTargetPlatforms.indexOf(TargetPlatform.WEB);
412
if (isWebExtension) {
413
if (webTargetPlatformIndex === -1) {
414
// Web extension but does not has web target platform -> add it
415
allTargetPlatforms.push(TargetPlatform.WEB);
416
}
417
} else {
418
if (webTargetPlatformIndex !== -1) {
419
// Not a web extension but has web target platform -> remove it
420
allTargetPlatforms.splice(webTargetPlatformIndex, 1);
421
}
422
}
423
424
return allTargetPlatforms;
425
}
426
427
export function sortExtensionVersions(versions: IRawGalleryExtensionVersion[], preferredTargetPlatform: TargetPlatform): IRawGalleryExtensionVersion[] {
428
/* It is expected that versions from Marketplace are sorted by version. So we are just sorting by preferred targetPlatform */
429
for (let index = 0; index < versions.length; index++) {
430
const version = versions[index];
431
if (version.version === versions[index - 1]?.version) {
432
let insertionIndex = index;
433
const versionTargetPlatform = getTargetPlatformForExtensionVersion(version);
434
/* put it at the beginning */
435
if (versionTargetPlatform === preferredTargetPlatform) {
436
while (insertionIndex > 0 && versions[insertionIndex - 1].version === version.version) { insertionIndex--; }
437
}
438
if (insertionIndex !== index) {
439
versions.splice(index, 1);
440
versions.splice(insertionIndex, 0, version);
441
}
442
}
443
}
444
return versions;
445
}
446
447
function setTelemetry(extension: IGalleryExtension, index: number, querySource?: string): void {
448
/* __GDPR__FRAGMENT__
449
"GalleryExtensionTelemetryData2" : {
450
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
451
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
452
"queryActivityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
453
}
454
*/
455
extension.telemetryData = { index, querySource, queryActivityId: extension.queryContext?.[SEARCH_ACTIVITY_HEADER_NAME] };
456
}
457
458
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], extensionGalleryManifest: IExtensionGalleryManifest, productService: IProductService, queryContext?: IStringDictionary<any>): IGalleryExtension {
459
const latestVersion = galleryExtension.versions[0];
460
const assets: IGalleryExtensionAssets = {
461
manifest: getVersionAsset(version, AssetType.Manifest),
462
readme: getVersionAsset(version, AssetType.Details),
463
changelog: getVersionAsset(version, AssetType.Changelog),
464
license: getVersionAsset(version, AssetType.License),
465
repository: getRepositoryAsset(version),
466
download: getDownloadAsset(version),
467
icon: getVersionAsset(version, AssetType.Icon),
468
signature: getVersionAsset(version, AssetType.Signature),
469
coreTranslations: getCoreTranslationAssets(version)
470
};
471
472
const detailsViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.linkType ?? ExtensionGalleryResourceType.ExtensionDetailsViewUri);
473
const publisherViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.publisher.linkType ?? ExtensionGalleryResourceType.PublisherViewUri);
474
const ratingViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.ratingLinkType ?? ExtensionGalleryResourceType.ExtensionRatingViewUri);
475
const id = getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName);
476
477
return {
478
type: 'gallery',
479
identifier: {
480
id,
481
uuid: galleryExtension.extensionId
482
},
483
name: galleryExtension.extensionName,
484
version: version.version,
485
displayName: galleryExtension.displayName,
486
publisherId: galleryExtension.publisher.publisherId,
487
publisher: galleryExtension.publisher.publisherName,
488
publisherDisplayName: galleryExtension.publisher.displayName,
489
publisherDomain: galleryExtension.publisher.domain ? { link: galleryExtension.publisher.domain, verified: !!galleryExtension.publisher.isDomainVerified } : undefined,
490
publisherSponsorLink: getSponsorLink(latestVersion),
491
description: galleryExtension.shortDescription ?? '',
492
installCount: getStatistic(galleryExtension.statistics, 'install'),
493
rating: getStatistic(galleryExtension.statistics, 'averagerating'),
494
ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'),
495
categories: galleryExtension.categories || [],
496
tags: galleryExtension.tags || [],
497
releaseDate: Date.parse(galleryExtension.releaseDate),
498
lastUpdated: Date.parse(galleryExtension.lastUpdated),
499
allTargetPlatforms,
500
assets,
501
properties: {
502
dependencies: getExtensions(version, PropertyType.Dependency),
503
extensionPack: getExtensions(version, PropertyType.ExtensionPack),
504
engine: getEngine(version),
505
enabledApiProposals: getEnabledApiProposals(version),
506
localizedLanguages: getLocalizedLanguages(version),
507
targetPlatform: getTargetPlatformForExtensionVersion(version),
508
isPreReleaseVersion: isPreReleaseVersion(version),
509
executesCode: executesCode(version)
510
},
511
hasPreReleaseVersion: hasPreReleaseForExtension(id, productService) ?? isPreReleaseVersion(latestVersion),
512
hasReleaseVersion: true,
513
private: isPrivateExtension(latestVersion),
514
preview: getIsPreview(galleryExtension.flags),
515
isSigned: !!assets.signature,
516
queryContext,
517
supportLink: getSupportLink(latestVersion),
518
detailsLink: detailsViewUri ? format2(detailsViewUri, { publisher: galleryExtension.publisher.publisherName, name: galleryExtension.extensionName }) : undefined,
519
publisherLink: publisherViewUri ? format2(publisherViewUri, { publisher: galleryExtension.publisher.publisherName }) : undefined,
520
ratingLink: ratingViewUri ? format2(ratingViewUri, { publisher: galleryExtension.publisher.publisherName, name: galleryExtension.extensionName }) : undefined,
521
};
522
}
523
524
interface IRawExtensionsControlManifest {
525
malicious: string[];
526
learnMoreLinks?: IStringDictionary<string>;
527
migrateToPreRelease?: IStringDictionary<{
528
id: string;
529
displayName: string;
530
migrateStorage?: boolean;
531
engine?: string;
532
}>;
533
deprecated?: IStringDictionary<boolean | {
534
disallowInstall?: boolean;
535
extension?: {
536
id: string;
537
displayName: string;
538
};
539
settings?: string[];
540
additionalInfo?: string;
541
}>;
542
search?: ISearchPrefferedResults[];
543
autoUpdate?: IStringDictionary<string>;
544
}
545
546
export abstract class AbstractExtensionGalleryService implements IExtensionGalleryService {
547
548
declare readonly _serviceBrand: undefined;
549
550
private readonly extensionsControlUrl: string | undefined;
551
private readonly unpkgResourceApi: string | undefined;
552
553
private readonly commonHeadersPromise: Promise<IHeaders>;
554
private readonly extensionsEnabledWithApiProposalVersion: string[];
555
556
constructor(
557
storageService: IStorageService | undefined,
558
private readonly assignmentService: IAssignmentService | undefined,
559
@IRequestService private readonly requestService: IRequestService,
560
@ILogService private readonly logService: ILogService,
561
@IEnvironmentService private readonly environmentService: IEnvironmentService,
562
@ITelemetryService private readonly telemetryService: ITelemetryService,
563
@IFileService private readonly fileService: IFileService,
564
@IProductService private readonly productService: IProductService,
565
@IConfigurationService private readonly configurationService: IConfigurationService,
566
@IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService,
567
@IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService,
568
) {
569
this.extensionsControlUrl = productService.extensionsGallery?.controlUrl;
570
this.unpkgResourceApi = productService.extensionsGallery?.extensionUrlTemplate;
571
this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? [];
572
this.commonHeadersPromise = resolveMarketplaceHeaders(
573
productService.version,
574
productService,
575
this.environmentService,
576
this.configurationService,
577
this.fileService,
578
storageService,
579
this.telemetryService);
580
}
581
582
isEnabled(): boolean {
583
return this.extensionGalleryManifestService.extensionGalleryManifestStatus === ExtensionGalleryManifestStatus.Available;
584
}
585
586
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, token: CancellationToken): Promise<IGalleryExtension[]>;
587
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, token: CancellationToken): Promise<IGalleryExtension[]>;
588
async getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, arg1: any, arg2?: any): Promise<IGalleryExtension[]> {
589
const extensionGalleryManifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest();
590
if (!extensionGalleryManifest) {
591
throw new Error('No extension gallery service configured.');
592
}
593
594
const options = CancellationToken.isCancellationToken(arg1) ? {} : arg1 as IExtensionQueryOptions;
595
const token = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2 as CancellationToken;
596
597
const resourceApi = await this.getResourceApi(extensionGalleryManifest);
598
const result = resourceApi
599
? await this.getExtensionsUsingResourceApi(extensionInfos, options, resourceApi, extensionGalleryManifest, token)
600
: await this.getExtensionsUsingQueryApi(extensionInfos, options, extensionGalleryManifest, token);
601
602
const uuids = result.map(r => r.identifier.uuid);
603
const extensionInfosByName: IExtensionInfo[] = [];
604
for (const e of extensionInfos) {
605
if (e.uuid && !uuids.includes(e.uuid)) {
606
extensionInfosByName.push({ ...e, uuid: undefined });
607
}
608
}
609
610
if (extensionInfosByName.length) {
611
// report telemetry data for additional query
612
this.telemetryService.publicLog2<
613
{ count: number },
614
{
615
owner: 'sandy081';
616
comment: 'Report the query to the Marketplace for fetching extensions by name';
617
readonly count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extensions to fetch' };
618
}>('galleryService:additionalQueryByName', {
619
count: extensionInfosByName.length
620
});
621
622
const extensions = await this.getExtensionsUsingQueryApi(extensionInfosByName, options, extensionGalleryManifest, token);
623
result.push(...extensions);
624
}
625
626
return result;
627
}
628
629
private async getResourceApi(extensionGalleryManifest: IExtensionGalleryManifest): Promise<{ uri: string; fallback?: string } | undefined> {
630
const latestVersionResource = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionLatestVersionUri);
631
if (latestVersionResource) {
632
return {
633
uri: latestVersionResource,
634
fallback: this.unpkgResourceApi
635
};
636
}
637
return undefined;
638
}
639
640
private async getExtensionsUsingQueryApi(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<IGalleryExtension[]> {
641
const names: string[] = [],
642
ids: string[] = [],
643
includePreRelease: (IExtensionIdentifier & { includePreRelease: boolean })[] = [],
644
versions: (IExtensionIdentifier & { version: string })[] = [];
645
let isQueryForReleaseVersionFromPreReleaseVersion = true;
646
647
for (const extensionInfo of extensionInfos) {
648
if (extensionInfo.uuid) {
649
ids.push(extensionInfo.uuid);
650
} else {
651
names.push(extensionInfo.id);
652
}
653
if (extensionInfo.version) {
654
versions.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, version: extensionInfo.version });
655
} else {
656
includePreRelease.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, includePreRelease: !!extensionInfo.preRelease });
657
}
658
isQueryForReleaseVersionFromPreReleaseVersion = isQueryForReleaseVersionFromPreReleaseVersion && (!!extensionInfo.hasPreRelease && !extensionInfo.preRelease);
659
}
660
661
if (!ids.length && !names.length) {
662
return [];
663
}
664
665
let query = new Query().withPage(1, extensionInfos.length);
666
if (ids.length) {
667
query = query.withFilter(FilterType.ExtensionId, ...ids);
668
}
669
if (names.length) {
670
query = query.withFilter(FilterType.ExtensionName, ...names);
671
}
672
if (options.queryAllVersions) {
673
query = query.withFlags(...query.flags, Flag.IncludeVersions);
674
}
675
if (options.source) {
676
query = query.withSource(options.source);
677
}
678
679
const { extensions } = await this.queryGalleryExtensions(
680
query,
681
{
682
targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM,
683
includePreRelease,
684
versions,
685
compatible: !!options.compatible,
686
productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date },
687
isQueryForReleaseVersionFromPreReleaseVersion
688
},
689
extensionGalleryManifest,
690
token);
691
692
if (options.source) {
693
extensions.forEach((e, index) => setTelemetry(e, index, options.source));
694
}
695
696
return extensions;
697
}
698
699
private async getExtensionsUsingResourceApi(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, resourceApi: { uri: string; fallback?: string }, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<IGalleryExtension[]> {
700
701
const result: IGalleryExtension[] = [];
702
const toQuery: IExtensionInfo[] = [];
703
const toFetchLatest: IExtensionInfo[] = [];
704
705
for (const extensionInfo of extensionInfos) {
706
if (!EXTENSION_IDENTIFIER_REGEX.test(extensionInfo.id)) {
707
continue;
708
}
709
if (extensionInfo.version) {
710
toQuery.push(extensionInfo);
711
} else {
712
toFetchLatest.push(extensionInfo);
713
}
714
}
715
716
await Promise.all(toFetchLatest.map(async extensionInfo => {
717
let galleryExtension: IGalleryExtension | null | 'NOT_FOUND';
718
try {
719
galleryExtension = await this.getLatestGalleryExtension(extensionInfo, options, resourceApi, extensionGalleryManifest, token);
720
if (galleryExtension === 'NOT_FOUND') {
721
if (extensionInfo.uuid) {
722
// Fallback to query if extension with UUID is not found. Probably extension is renamed.
723
toQuery.push(extensionInfo);
724
}
725
return;
726
}
727
if (galleryExtension) {
728
result.push(galleryExtension);
729
}
730
} catch (error) {
731
if (error instanceof ExtensionGalleryError) {
732
switch (error.code) {
733
case ExtensionGalleryErrorCode.Offline:
734
case ExtensionGalleryErrorCode.Cancelled:
735
case ExtensionGalleryErrorCode.Timeout:
736
throw error;
737
}
738
}
739
740
// fallback to query
741
this.logService.error(`Error while getting the latest version for the extension ${extensionInfo.id}.`, getErrorMessage(error));
742
this.telemetryService.publicLog2<
743
{
744
extension: string;
745
preRelease: boolean;
746
compatible: boolean;
747
errorCode: string;
748
},
749
{
750
owner: 'sandy081';
751
comment: 'Report the fallback to the Marketplace query for fetching extensions';
752
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension id' };
753
preRelease: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Get pre-release version' };
754
compatible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Get compatible version' };
755
errorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Error code' };
756
}>('galleryService:fallbacktoquery', {
757
extension: extensionInfo.id,
758
preRelease: !!extensionInfo.preRelease,
759
compatible: !!options.compatible,
760
errorCode: error instanceof ExtensionGalleryError ? error.code : 'Unknown'
761
});
762
toQuery.push(extensionInfo);
763
}
764
765
}));
766
767
if (toQuery.length) {
768
const extensions = await this.getExtensionsUsingQueryApi(toQuery, options, extensionGalleryManifest, token);
769
result.push(...extensions);
770
}
771
772
return result;
773
}
774
775
private async getLatestGalleryExtension(extensionInfo: IExtensionInfo, options: IExtensionQueryOptions, resourceApi: { uri: string; fallback?: string }, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<IGalleryExtension | null | 'NOT_FOUND'> {
776
const rawGalleryExtension = await this.getLatestRawGalleryExtensionWithFallback(extensionInfo, resourceApi, token);
777
778
if (!rawGalleryExtension) {
779
return 'NOT_FOUND';
780
}
781
782
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
783
const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion(
784
rawGalleryExtension,
785
{
786
targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM,
787
compatible: !!options.compatible,
788
productVersion: options.productVersion ?? {
789
version: this.productService.version,
790
date: this.productService.date
791
},
792
version: extensionInfo.preRelease ? VersionKind.Latest : VersionKind.Release
793
}, allTargetPlatforms);
794
795
if (rawGalleryExtensionVersion) {
796
return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService);
797
}
798
799
return null;
800
}
801
802
async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise<IGalleryExtension | null> {
803
if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) {
804
return null;
805
}
806
if (await this.isExtensionCompatible(extension, includePreRelease, targetPlatform)) {
807
return extension;
808
}
809
if (this.allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName }) !== true) {
810
return null;
811
}
812
const result = await this.getExtensions([{
813
...extension.identifier,
814
preRelease: includePreRelease,
815
hasPreRelease: extension.hasPreReleaseVersion,
816
}], {
817
compatible: true,
818
productVersion,
819
queryAllVersions: true,
820
targetPlatform,
821
}, CancellationToken.None);
822
823
return result[0] ?? null;
824
}
825
826
async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise<boolean> {
827
return this.isValidVersion(
828
{
829
id: extension.identifier.id,
830
version: extension.version,
831
isPreReleaseVersion: extension.properties.isPreReleaseVersion,
832
targetPlatform: extension.properties.targetPlatform,
833
manifestAsset: extension.assets.manifest,
834
engine: extension.properties.engine,
835
enabledApiProposals: extension.properties.enabledApiProposals
836
},
837
{
838
targetPlatform,
839
compatible: true,
840
productVersion,
841
version: includePreRelease ? VersionKind.Latest : VersionKind.Release
842
},
843
extension.publisherDisplayName,
844
extension.allTargetPlatforms
845
);
846
}
847
848
private async isValidVersion(
849
extension: { id: string; version: string; isPreReleaseVersion: boolean; targetPlatform: TargetPlatform; manifestAsset: IGalleryExtensionAsset | null; engine: string | undefined; enabledApiProposals: string[] | undefined },
850
{ targetPlatform, compatible, productVersion, version }: Omit<ExtensionVersionCriteria, 'targetPlatform'> & { targetPlatform: TargetPlatform | undefined },
851
publisherDisplayName: string,
852
allTargetPlatforms: TargetPlatform[]
853
): Promise<boolean> {
854
855
const hasPreRelease = hasPreReleaseForExtension(extension.id, this.productService);
856
const excludeVersionRange = getExcludeVersionRangeForExtension(extension.id, this.productService);
857
858
if (extension.isPreReleaseVersion && hasPreRelease === false /* Skip if hasPreRelease is not defined for this extension */) {
859
return false;
860
}
861
862
if (excludeVersionRange && semver.satisfies(extension.version, excludeVersionRange)) {
863
return false;
864
}
865
866
// Specific version
867
if (isString(version)) {
868
if (extension.version !== version) {
869
return false;
870
}
871
}
872
873
// Prerelease or release version kind
874
else if (version === VersionKind.Release || version === VersionKind.Prerelease) {
875
if (extension.isPreReleaseVersion !== (version === VersionKind.Prerelease)) {
876
return false;
877
}
878
}
879
880
if (targetPlatform && !isTargetPlatformCompatible(extension.targetPlatform, allTargetPlatforms, targetPlatform)) {
881
return false;
882
}
883
884
if (compatible) {
885
if (this.allowedExtensionsService.isAllowed({ id: extension.id, publisherDisplayName, version: extension.version, prerelease: extension.isPreReleaseVersion, targetPlatform: extension.targetPlatform }) !== true) {
886
return false;
887
}
888
889
if (!this.areApiProposalsCompatible(extension.id, extension.enabledApiProposals)) {
890
return false;
891
}
892
893
if (!(await this.isEngineValid(extension.id, extension.version, extension.engine, extension.manifestAsset, productVersion))) {
894
return false;
895
}
896
}
897
898
return true;
899
}
900
901
private areApiProposalsCompatible(extensionId: string, enabledApiProposals: string[] | undefined): boolean {
902
if (!enabledApiProposals) {
903
return true;
904
}
905
if (!this.extensionsEnabledWithApiProposalVersion.includes(extensionId.toLowerCase())) {
906
return true;
907
}
908
return areApiProposalsCompatible(enabledApiProposals);
909
}
910
911
private async isEngineValid(extensionId: string, version: string, engine: string | undefined, manifestAsset: IGalleryExtensionAsset | null, productVersion: IProductVersion): Promise<boolean> {
912
if (!engine) {
913
if (!manifestAsset) {
914
this.logService.error(`Missing engine and manifest asset for the extension ${extensionId} with version ${version}`);
915
return false;
916
}
917
try {
918
type GalleryServiceEngineFallbackClassification = {
919
owner: 'sandy081';
920
comment: 'Fallback request when engine is not found in properties of an extension version';
921
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' };
922
extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' };
923
};
924
type GalleryServiceEngineFallbackEvent = {
925
extension: string;
926
extensionVersion: string;
927
};
928
this.telemetryService.publicLog2<GalleryServiceEngineFallbackEvent, GalleryServiceEngineFallbackClassification>('galleryService:engineFallback', { extension: extensionId, extensionVersion: version });
929
930
const headers = { 'Accept-Encoding': 'gzip' };
931
const context = await this.getAsset(extensionId, manifestAsset, AssetType.Manifest, version, { headers });
932
const manifest = await asJson<IExtensionManifest>(context);
933
if (!manifest) {
934
this.logService.error(`Manifest was not found for the extension ${extensionId} with version ${version}`);
935
return false;
936
}
937
engine = manifest.engines.vscode;
938
} catch (error) {
939
this.logService.error(`Error while getting the engine for the version ${version}.`, getErrorMessage(error));
940
return false;
941
}
942
}
943
944
return isEngineValid(engine, productVersion.version, productVersion.date);
945
}
946
947
async query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>> {
948
const extensionGalleryManifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest();
949
950
if (!extensionGalleryManifest) {
951
throw new Error('No extension gallery service configured.');
952
}
953
954
let text = options.text || '';
955
const pageSize = options.pageSize ?? 50;
956
957
let query = new Query()
958
.withPage(1, pageSize);
959
960
if (text) {
961
// Use category filter instead of "category:themes"
962
text = text.replace(/\bcategory:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedCategory, category) => {
963
query = query.withFilter(FilterType.Category, category || quotedCategory);
964
return '';
965
});
966
967
// Use tag filter instead of "tag:debuggers"
968
text = text.replace(/\btag:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedTag, tag) => {
969
query = query.withFilter(FilterType.Tag, tag || quotedTag);
970
return '';
971
});
972
973
// Use featured filter
974
text = text.replace(/\bfeatured(\s+|\b|$)/g, () => {
975
query = query.withFilter(FilterType.Featured);
976
return '';
977
});
978
979
text = text.trim();
980
981
if (text) {
982
text = text.length < 200 ? text : text.substring(0, 200);
983
query = query.withFilter(FilterType.SearchText, text);
984
}
985
986
if (extensionGalleryManifest.capabilities.extensionQuery.sorting?.some(c => c.name === SortBy.NoneOrRelevance)) {
987
query = query.withSortBy(SortBy.NoneOrRelevance);
988
}
989
} else {
990
if (extensionGalleryManifest.capabilities.extensionQuery.sorting?.some(c => c.name === SortBy.InstallCount)) {
991
query = query.withSortBy(SortBy.InstallCount);
992
}
993
}
994
995
if (options.sortBy && extensionGalleryManifest.capabilities.extensionQuery.sorting?.some(c => c.name === options.sortBy)) {
996
query = query.withSortBy(options.sortBy);
997
}
998
999
if (typeof options.sortOrder === 'number') {
1000
query = query.withSortOrder(options.sortOrder);
1001
}
1002
1003
if (options.source) {
1004
query = query.withSource(options.source);
1005
}
1006
1007
const runQuery = async (query: Query, token: CancellationToken) => {
1008
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);
1009
extensions.forEach((e, index) => setTelemetry(e, ((query.pageNumber - 1) * query.pageSize) + index, options.source));
1010
return { extensions, total };
1011
};
1012
const { extensions, total } = await runQuery(query, token);
1013
const getPage = async (pageIndex: number, ct: CancellationToken) => {
1014
if (ct.isCancellationRequested) {
1015
throw new CancellationError();
1016
}
1017
const { extensions } = await runQuery(query.withPage(pageIndex + 1), ct);
1018
return extensions;
1019
};
1020
1021
return { firstPage: extensions, total, pageSize: query.pageSize, getPage };
1022
}
1023
1024
private async queryGalleryExtensions(query: Query, criteria: ExtensionsCriteria, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> {
1025
if (
1026
this.productService.quality !== 'stable'
1027
&& (await this.assignmentService?.getTreatment<boolean>('useLatestPrereleaseAndStableVersionFlag'))
1028
) {
1029
return this.queryGalleryExtensionsUsingIncludeLatestPrereleaseAndStableVersionFlag(query, criteria, extensionGalleryManifest, token);
1030
}
1031
1032
return this.queryGalleryExtensionsWithAllVersionsAsFallback(query, criteria, extensionGalleryManifest, token);
1033
}
1034
1035
private async queryGalleryExtensionsWithAllVersionsAsFallback(query: Query, criteria: ExtensionsCriteria, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> {
1036
const flags = query.flags;
1037
1038
/**
1039
* If both version flags (IncludeLatestVersionOnly and IncludeVersions) are included, then only include latest versions (IncludeLatestVersionOnly) flag.
1040
*/
1041
if (query.flags.includes(Flag.IncludeLatestVersionOnly) && query.flags.includes(Flag.IncludeVersions)) {
1042
query = query.withFlags(...query.flags.filter(flag => flag !== Flag.IncludeVersions));
1043
}
1044
1045
/**
1046
* If version flags (IncludeLatestVersionOnly and IncludeVersions) are not included, default is to query for latest versions (IncludeLatestVersionOnly).
1047
*/
1048
if (!query.flags.includes(Flag.IncludeLatestVersionOnly) && !query.flags.includes(Flag.IncludeVersions)) {
1049
query = query.withFlags(...query.flags, Flag.IncludeLatestVersionOnly);
1050
}
1051
1052
/**
1053
* 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.
1054
*/
1055
if (criteria.versions?.length || criteria.isQueryForReleaseVersionFromPreReleaseVersion) {
1056
query = query.withFlags(...query.flags.filter(flag => flag !== Flag.IncludeLatestVersionOnly), Flag.IncludeVersions);
1057
}
1058
1059
/**
1060
* Add necessary extension flags
1061
*/
1062
query = query.withFlags(...query.flags, Flag.IncludeAssetUri, Flag.IncludeCategoryAndTags, Flag.IncludeFiles, Flag.IncludeStatistics, Flag.IncludeVersionProperties);
1063
const { galleryExtensions: rawGalleryExtensions, total, context } = await this.queryRawGalleryExtensions(query, extensionGalleryManifest, token);
1064
1065
const hasAllVersions: boolean = !query.flags.includes(Flag.IncludeLatestVersionOnly);
1066
if (hasAllVersions) {
1067
const extensions: IGalleryExtension[] = [];
1068
for (const rawGalleryExtension of rawGalleryExtensions) {
1069
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
1070
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
1071
const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease;
1072
const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion(
1073
rawGalleryExtension,
1074
{
1075
compatible: criteria.compatible,
1076
targetPlatform: criteria.targetPlatform,
1077
productVersion: criteria.productVersion,
1078
version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version
1079
?? (includePreRelease ? VersionKind.Latest : VersionKind.Release)
1080
},
1081
allTargetPlatforms
1082
);
1083
if (rawGalleryExtensionVersion) {
1084
extensions.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService, context));
1085
}
1086
}
1087
return { extensions, total };
1088
}
1089
1090
const result: [number, IGalleryExtension][] = [];
1091
const needAllVersions = new Map<string, number>();
1092
for (let index = 0; index < rawGalleryExtensions.length; index++) {
1093
const rawGalleryExtension = rawGalleryExtensions[index];
1094
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
1095
const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease;
1096
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
1097
if (criteria.compatible) {
1098
// Skip looking for all versions if requested for a web-compatible extension and it is not a web extension.
1099
if (isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) {
1100
continue;
1101
}
1102
// Skip looking for all versions if the extension is not allowed.
1103
if (this.allowedExtensionsService.isAllowed({ id: extensionIdentifier.id, publisherDisplayName: rawGalleryExtension.publisher.displayName }) !== true) {
1104
continue;
1105
}
1106
}
1107
const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion(
1108
rawGalleryExtension,
1109
{
1110
compatible: criteria.compatible,
1111
targetPlatform: criteria.targetPlatform,
1112
productVersion: criteria.productVersion,
1113
version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version
1114
?? (includePreRelease ? VersionKind.Latest : VersionKind.Release)
1115
},
1116
allTargetPlatforms
1117
);
1118
const extension = rawGalleryExtensionVersion ? toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService, context) : null;
1119
if (!extension
1120
/** Need all versions if the extension is a pre-release version but
1121
* - the query is to look for a release version or
1122
* - the extension has no release version
1123
* Get all versions to get or check the release version
1124
*/
1125
|| (extension.properties.isPreReleaseVersion && (!includePreRelease || !extension.hasReleaseVersion))
1126
/**
1127
* Need all versions if the extension is a release version with a different target platform than requested and also has a pre-release version
1128
* Because, this is a platform specific extension and can have a newer release version supporting this platform.
1129
* See https://github.com/microsoft/vscode/issues/139628
1130
*/
1131
|| (!extension.properties.isPreReleaseVersion && extension.properties.targetPlatform !== criteria.targetPlatform && extension.hasPreReleaseVersion)
1132
) {
1133
needAllVersions.set(rawGalleryExtension.extensionId, index);
1134
} else {
1135
result.push([index, extension]);
1136
}
1137
}
1138
1139
if (needAllVersions.size) {
1140
const stopWatch = new StopWatch();
1141
const query = new Query()
1142
.withFlags(...flags.filter(flag => flag !== Flag.IncludeLatestVersionOnly), Flag.IncludeVersions)
1143
.withPage(1, needAllVersions.size)
1144
.withFilter(FilterType.ExtensionId, ...needAllVersions.keys());
1145
const { extensions } = await this.queryGalleryExtensions(query, criteria, extensionGalleryManifest, token);
1146
this.telemetryService.publicLog2<GalleryServiceAdditionalQueryEvent, GalleryServiceAdditionalQueryClassification>('galleryService:additionalQuery', {
1147
duration: stopWatch.elapsed(),
1148
count: needAllVersions.size
1149
});
1150
for (const extension of extensions) {
1151
const index = needAllVersions.get(extension.identifier.uuid)!;
1152
result.push([index, extension]);
1153
}
1154
}
1155
1156
return { extensions: result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension), total };
1157
}
1158
1159
private async queryGalleryExtensionsUsingIncludeLatestPrereleaseAndStableVersionFlag(query: Query, criteria: ExtensionsCriteria, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> {
1160
1161
/**
1162
* If versions criteria exist, then remove latest flags and add all versions flag.
1163
*/
1164
if (criteria.versions?.length) {
1165
query = query.withFlags(...query.flags.filter(flag => flag !== Flag.IncludeLatestVersionOnly && flag !== Flag.IncludeLatestPrereleaseAndStableVersionOnly), Flag.IncludeVersions);
1166
}
1167
1168
/**
1169
* If the query does not specify all versions flag, handle latest versions.
1170
*/
1171
else if (!query.flags.includes(Flag.IncludeVersions)) {
1172
const includeLatest = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : criteria.includePreRelease.every(({ includePreRelease }) => includePreRelease);
1173
query = includeLatest ? query.withFlags(...query.flags.filter(flag => flag !== Flag.IncludeLatestPrereleaseAndStableVersionOnly), Flag.IncludeLatestVersionOnly) : query.withFlags(...query.flags.filter(flag => flag !== Flag.IncludeLatestVersionOnly), Flag.IncludeLatestPrereleaseAndStableVersionOnly);
1174
}
1175
1176
/**
1177
* If all versions flag is set, remove latest flags.
1178
*/
1179
if (query.flags.includes(Flag.IncludeVersions) && (query.flags.includes(Flag.IncludeLatestVersionOnly) || query.flags.includes(Flag.IncludeLatestPrereleaseAndStableVersionOnly))) {
1180
query = query.withFlags(...query.flags.filter(flag => flag !== Flag.IncludeLatestVersionOnly && flag !== Flag.IncludeLatestPrereleaseAndStableVersionOnly), Flag.IncludeVersions);
1181
}
1182
1183
/**
1184
* Add necessary extension flags
1185
*/
1186
query = query.withFlags(...query.flags, Flag.IncludeAssetUri, Flag.IncludeCategoryAndTags, Flag.IncludeFiles, Flag.IncludeStatistics, Flag.IncludeVersionProperties);
1187
const { galleryExtensions: rawGalleryExtensions, total, context } = await this.queryRawGalleryExtensions(query, extensionGalleryManifest, token);
1188
1189
const extensions: IGalleryExtension[] = [];
1190
for (let index = 0; index < rawGalleryExtensions.length; index++) {
1191
const rawGalleryExtension = rawGalleryExtensions[index];
1192
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
1193
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
1194
if (criteria.compatible) {
1195
// Skip looking for all versions if requested for a web-compatible extension and it is not a web extension.
1196
if (isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) {
1197
continue;
1198
}
1199
// Skip looking for all versions if the extension is not allowed.
1200
if (this.allowedExtensionsService.isAllowed({ id: extensionIdentifier.id, publisherDisplayName: rawGalleryExtension.publisher.displayName }) !== true) {
1201
continue;
1202
}
1203
}
1204
1205
const version = criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version
1206
?? ((isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease) ? VersionKind.Latest : VersionKind.Release);
1207
const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion(
1208
rawGalleryExtension,
1209
{
1210
compatible: criteria.compatible,
1211
targetPlatform: criteria.targetPlatform,
1212
productVersion: criteria.productVersion,
1213
version
1214
},
1215
allTargetPlatforms
1216
);
1217
if (rawGalleryExtensionVersion) {
1218
extensions.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService, context));
1219
}
1220
}
1221
1222
return { extensions, total };
1223
}
1224
1225
private async getRawGalleryExtensionVersion(rawGalleryExtension: IRawGalleryExtension, criteria: ExtensionVersionCriteria, allTargetPlatforms: TargetPlatform[]): Promise<IRawGalleryExtensionVersion | null> {
1226
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
1227
const rawGalleryExtensionVersions = sortExtensionVersions(rawGalleryExtension.versions, criteria.targetPlatform);
1228
1229
if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) {
1230
return null;
1231
}
1232
1233
const version = isString(criteria.version) ? criteria.version : undefined;
1234
1235
for (let index = 0; index < rawGalleryExtensionVersions.length; index++) {
1236
const rawGalleryExtensionVersion = rawGalleryExtensionVersions[index];
1237
if (await this.isValidVersion(
1238
{
1239
id: extensionIdentifier.id,
1240
version: rawGalleryExtensionVersion.version,
1241
isPreReleaseVersion: isPreReleaseVersion(rawGalleryExtensionVersion),
1242
targetPlatform: getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion),
1243
engine: getEngine(rawGalleryExtensionVersion),
1244
manifestAsset: getVersionAsset(rawGalleryExtensionVersion, AssetType.Manifest),
1245
enabledApiProposals: getEnabledApiProposals(rawGalleryExtensionVersion)
1246
},
1247
criteria,
1248
rawGalleryExtension.publisher.displayName,
1249
allTargetPlatforms)
1250
) {
1251
return rawGalleryExtensionVersion;
1252
}
1253
if (version && rawGalleryExtensionVersion.version === version) {
1254
return null;
1255
}
1256
}
1257
1258
if (version || criteria.compatible) {
1259
return null;
1260
}
1261
1262
/**
1263
* Fallback: Return the latest version
1264
* This can happen when the extension does not have a release version or does not have a version compatible with the given target platform.
1265
*/
1266
return rawGalleryExtension.versions[0];
1267
}
1268
1269
private async queryRawGalleryExtensions(query: Query, extensionGalleryManifest: IExtensionGalleryManifest, token: CancellationToken): Promise<IRawGalleryExtensionsResult> {
1270
const extensionsQueryApi = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionQueryService);
1271
1272
if (!extensionsQueryApi) {
1273
throw new Error('No extension gallery query service configured.');
1274
}
1275
1276
query = query
1277
/* Always exclude non validated extensions */
1278
.withFlags(...query.flags, Flag.ExcludeNonValidated)
1279
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
1280
1281
const unpublishedFlag = extensionGalleryManifest.capabilities.extensionQuery.flags?.find(f => f.name === Flag.Unpublished);
1282
/* Always exclude unpublished extensions */
1283
if (unpublishedFlag) {
1284
query = query.withFilter(FilterType.ExcludeWithFlags, String(unpublishedFlag.value));
1285
}
1286
1287
const data = JSON.stringify({
1288
filters: [
1289
{
1290
criteria: query.criteria.reduce<{ filterType: number; value?: string }[]>((criteria, c) => {
1291
const criterium = extensionGalleryManifest.capabilities.extensionQuery.filtering?.find(f => f.name === c.filterType);
1292
if (criterium) {
1293
criteria.push({
1294
filterType: criterium.value,
1295
value: c.value,
1296
});
1297
}
1298
return criteria;
1299
}, []),
1300
pageNumber: query.pageNumber,
1301
pageSize: query.pageSize,
1302
sortBy: extensionGalleryManifest.capabilities.extensionQuery.sorting?.find(s => s.name === query.sortBy)?.value,
1303
sortOrder: query.sortOrder,
1304
}
1305
],
1306
assetTypes: query.assetTypes,
1307
flags: query.flags.reduce<number>((flags, flag) => {
1308
const flagValue = extensionGalleryManifest.capabilities.extensionQuery.flags?.find(f => f.name === flag);
1309
if (flagValue) {
1310
flags |= flagValue.value;
1311
}
1312
return flags;
1313
}, 0)
1314
});
1315
1316
const commonHeaders = await this.commonHeadersPromise;
1317
const headers = {
1318
...commonHeaders,
1319
'Content-Type': 'application/json',
1320
'Accept': 'application/json;api-version=3.0-preview.1',
1321
'Accept-Encoding': 'gzip',
1322
'Content-Length': String(data.length),
1323
};
1324
1325
const stopWatch = new StopWatch();
1326
let context: IRequestContext | undefined, errorCode: ExtensionGalleryErrorCode | undefined, total: number = 0;
1327
1328
try {
1329
context = await this.requestService.request({
1330
type: 'POST',
1331
url: extensionsQueryApi,
1332
data,
1333
headers
1334
}, token);
1335
1336
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) {
1337
return { galleryExtensions: [], total };
1338
}
1339
1340
const result = await asJson<IRawGalleryQueryResult>(context);
1341
if (result) {
1342
const r = result.results[0];
1343
const galleryExtensions = r.extensions;
1344
const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0];
1345
total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0;
1346
1347
return {
1348
galleryExtensions,
1349
total,
1350
context: context.res.headers['activityid'] ? {
1351
[SEARCH_ACTIVITY_HEADER_NAME]: context.res.headers['activityid']
1352
} : {}
1353
};
1354
}
1355
return { galleryExtensions: [], total };
1356
1357
} catch (e) {
1358
if (isCancellationError(e)) {
1359
errorCode = ExtensionGalleryErrorCode.Cancelled;
1360
throw e;
1361
} else {
1362
const errorMessage = getErrorMessage(e);
1363
errorCode = isOfflineError(e)
1364
? ExtensionGalleryErrorCode.Offline
1365
: errorMessage.startsWith('XHR timeout')
1366
? ExtensionGalleryErrorCode.Timeout
1367
: ExtensionGalleryErrorCode.Failed;
1368
throw new ExtensionGalleryError(errorMessage, errorCode);
1369
}
1370
} finally {
1371
this.telemetryService.publicLog2<GalleryServiceQueryEvent, GalleryServiceQueryClassification>('galleryService:query', {
1372
filterTypes: query.criteria.map(criterium => criterium.filterType),
1373
flags: query.flags,
1374
sortBy: query.sortBy,
1375
sortOrder: String(query.sortOrder),
1376
pageNumber: String(query.pageNumber),
1377
source: query.source,
1378
searchTextLength: query.searchText.length,
1379
requestBodySize: String(data.length),
1380
duration: stopWatch.elapsed(),
1381
success: !!context && isSuccess(context),
1382
responseBodySize: context?.res.headers['Content-Length'],
1383
statusCode: context ? String(context.res.statusCode) : undefined,
1384
errorCode,
1385
count: String(total),
1386
server: this.getHeaderValue(context?.res.headers, SERVER_HEADER_NAME),
1387
activityId: this.getHeaderValue(context?.res.headers, ACTIVITY_HEADER_NAME),
1388
endToEndId: this.getHeaderValue(context?.res.headers, END_END_ID_HEADER_NAME),
1389
});
1390
}
1391
}
1392
1393
private getHeaderValue(headers: IHeaders | undefined, name: string): TelemetryTrustedValue<string> | undefined {
1394
const headerValue = headers?.[name.toLowerCase()];
1395
const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
1396
return value ? new TelemetryTrustedValue(value) : undefined;
1397
}
1398
1399
private async getLatestRawGalleryExtensionWithFallback(extensionInfo: IExtensionInfo, resourceApi: { uri: string; fallback?: string }, token: CancellationToken): Promise<IRawGalleryExtension | null> {
1400
const [publisher, name] = extensionInfo.id.split('.');
1401
let errorCode: string | undefined;
1402
try {
1403
const uri = URI.parse(format2(resourceApi.uri, { publisher, name }));
1404
return await this.getLatestRawGalleryExtension(extensionInfo.id, uri, token);
1405
} catch (error) {
1406
if (error instanceof ExtensionGalleryError) {
1407
errorCode = error.code;
1408
switch (error.code) {
1409
case ExtensionGalleryErrorCode.Offline:
1410
case ExtensionGalleryErrorCode.Cancelled:
1411
case ExtensionGalleryErrorCode.Timeout:
1412
case ExtensionGalleryErrorCode.ClientError:
1413
throw error;
1414
}
1415
} else {
1416
errorCode = 'Unknown';
1417
}
1418
if (!resourceApi.fallback) {
1419
throw error;
1420
}
1421
} finally {
1422
this.telemetryService.publicLog2<
1423
{
1424
extension: string;
1425
errorCode?: string;
1426
},
1427
{
1428
owner: 'sandy081';
1429
comment: 'Report fetching latest version of an extension';
1430
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the extension' };
1431
errorCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error code in case of error' };
1432
}
1433
>('galleryService:getmarketplacelatest', {
1434
extension: extensionInfo.id,
1435
errorCode,
1436
});
1437
}
1438
1439
this.logService.error(`Error while getting the latest version for the extension ${extensionInfo.id} from ${resourceApi.uri}. Trying the fallback ${resourceApi.fallback}`, errorCode);
1440
try {
1441
const uri = URI.parse(format2(resourceApi.fallback, { publisher, name }));
1442
return await this.getLatestRawGalleryExtension(extensionInfo.id, uri, token);
1443
} catch (error) {
1444
errorCode = error instanceof ExtensionGalleryError ? error.code : 'Unknown';
1445
throw error;
1446
} finally {
1447
this.telemetryService.publicLog2<
1448
{
1449
extension: string;
1450
errorCode?: string;
1451
},
1452
{
1453
owner: 'sandy081';
1454
comment: 'Report the fallback to the unpkg service for getting latest extension';
1455
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension id' };
1456
errorCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error code in case of error' };
1457
}>('galleryService:fallbacktounpkg', {
1458
extension: extensionInfo.id,
1459
errorCode,
1460
});
1461
}
1462
}
1463
1464
private async getLatestRawGalleryExtension(extension: string, uri: URI, token: CancellationToken): Promise<IRawGalleryExtension | null> {
1465
let context;
1466
let errorCode: string | undefined;
1467
const stopWatch = new StopWatch();
1468
1469
try {
1470
const commonHeaders = await this.commonHeadersPromise;
1471
const headers = {
1472
...commonHeaders,
1473
'Content-Type': 'application/json',
1474
'Accept': 'application/json;api-version=7.2-preview',
1475
'Accept-Encoding': 'gzip',
1476
};
1477
1478
context = await this.requestService.request({
1479
type: 'GET',
1480
url: uri.toString(true),
1481
headers,
1482
timeout: REQUEST_TIME_OUT
1483
}, token);
1484
1485
if (context.res.statusCode === 404) {
1486
errorCode = 'NotFound';
1487
return null;
1488
}
1489
1490
if (context.res.statusCode && context.res.statusCode !== 200) {
1491
throw new Error('Unexpected HTTP response: ' + context.res.statusCode);
1492
}
1493
1494
const result = await asJson<IRawGalleryExtension>(context);
1495
if (!result) {
1496
errorCode = 'NoData';
1497
}
1498
return result;
1499
}
1500
1501
catch (error) {
1502
let galleryErrorCode: ExtensionGalleryErrorCode;
1503
if (isCancellationError(error)) {
1504
galleryErrorCode = ExtensionGalleryErrorCode.Cancelled;
1505
} else if (isOfflineError(error)) {
1506
galleryErrorCode = ExtensionGalleryErrorCode.Offline;
1507
} else if (getErrorMessage(error).startsWith('XHR timeout')) {
1508
galleryErrorCode = ExtensionGalleryErrorCode.Timeout;
1509
} else if (context && isClientError(context)) {
1510
galleryErrorCode = ExtensionGalleryErrorCode.ClientError;
1511
} else if (context && isServerError(context)) {
1512
galleryErrorCode = ExtensionGalleryErrorCode.ServerError;
1513
} else {
1514
galleryErrorCode = ExtensionGalleryErrorCode.Failed;
1515
}
1516
errorCode = galleryErrorCode;
1517
throw new ExtensionGalleryError(error, galleryErrorCode);
1518
}
1519
1520
finally {
1521
type GalleryServiceGetLatestEventClassification = {
1522
owner: 'sandy081';
1523
comment: 'Report the query to the Marketplace for fetching latest version of an extension';
1524
host: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The host of the end point' };
1525
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the extension' };
1526
duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Duration in ms for the query' };
1527
errorCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error code in case of error' };
1528
statusCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The status code in case of error' };
1529
server?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The server of the end point' };
1530
activityId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The activity ID of the request' };
1531
endToEndId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The end-to-end ID of the request' };
1532
};
1533
type GalleryServiceGetLatestEvent = {
1534
extension: string;
1535
host: string;
1536
duration: number;
1537
errorCode?: string;
1538
statusCode?: string;
1539
server?: TelemetryTrustedValue<string>;
1540
activityId?: TelemetryTrustedValue<string>;
1541
endToEndId?: TelemetryTrustedValue<string>;
1542
};
1543
this.telemetryService.publicLog2<GalleryServiceGetLatestEvent, GalleryServiceGetLatestEventClassification>('galleryService:getLatest', {
1544
extension,
1545
host: uri.authority,
1546
duration: stopWatch.elapsed(),
1547
errorCode,
1548
statusCode: context?.res.statusCode && context?.res.statusCode !== 200 ? `${context.res.statusCode}` : undefined,
1549
server: this.getHeaderValue(context?.res.headers, SERVER_HEADER_NAME),
1550
activityId: this.getHeaderValue(context?.res.headers, ACTIVITY_HEADER_NAME),
1551
endToEndId: this.getHeaderValue(context?.res.headers, END_END_ID_HEADER_NAME),
1552
});
1553
}
1554
}
1555
1556
async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void> {
1557
const manifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest();
1558
if (!manifest) {
1559
return undefined;
1560
}
1561
1562
let url: string;
1563
1564
if (isWeb) {
1565
const resource = getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.WebExtensionStatisticsUri);
1566
if (!resource) {
1567
return;
1568
}
1569
url = format2(resource, { publisher, name, version, statTypeValue: type === StatisticType.Install ? '1' : '3' });
1570
} else {
1571
const resource = getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.ExtensionStatisticsUri);
1572
if (!resource) {
1573
return;
1574
}
1575
url = format2(resource, { publisher, name, version, statTypeName: type });
1576
}
1577
1578
const Accept = isWeb ? 'api-version=6.1-preview.1' : '*/*;api-version=4.0-preview.1';
1579
const commonHeaders = await this.commonHeadersPromise;
1580
const headers = { ...commonHeaders, Accept };
1581
try {
1582
await this.requestService.request({
1583
type: 'POST',
1584
url,
1585
headers
1586
}, CancellationToken.None);
1587
} catch (error) { /* Ignore */ }
1588
}
1589
1590
async download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void> {
1591
this.logService.trace('ExtensionGalleryService#download', extension.identifier.id);
1592
const data = getGalleryExtensionTelemetryData(extension);
1593
const startTime = new Date().getTime();
1594
1595
const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : '';
1596
const downloadAsset = operationParam ? {
1597
uri: `${extension.assets.download.uri}${URI.parse(extension.assets.download.uri).query ? '&' : '?'}${operationParam}=true`,
1598
fallbackUri: `${extension.assets.download.fallbackUri}${URI.parse(extension.assets.download.fallbackUri).query ? '&' : '?'}${operationParam}=true`
1599
} : extension.assets.download;
1600
1601
const headers: IHeaders | undefined = extension.queryContext?.[SEARCH_ACTIVITY_HEADER_NAME] ? { [SEARCH_ACTIVITY_HEADER_NAME]: extension.queryContext[SEARCH_ACTIVITY_HEADER_NAME] } : undefined;
1602
const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, extension.version, headers ? { headers } : undefined);
1603
1604
try {
1605
await this.fileService.writeFile(location, context.stream);
1606
} catch (error) {
1607
try {
1608
await this.fileService.del(location);
1609
} catch (e) {
1610
/* ignore */
1611
this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e));
1612
}
1613
throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting);
1614
}
1615
1616
/* __GDPR__
1617
"galleryService:downloadVSIX" : {
1618
"owner": "sandy081",
1619
"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
1620
"${include}": [
1621
"${GalleryExtensionTelemetryData}"
1622
]
1623
}
1624
*/
1625
this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration: new Date().getTime() - startTime });
1626
}
1627
1628
async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise<void> {
1629
if (!extension.assets.signature) {
1630
throw new Error('No signature asset found');
1631
}
1632
1633
this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id);
1634
1635
const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature, extension.version);
1636
try {
1637
await this.fileService.writeFile(location, context.stream);
1638
} catch (error) {
1639
try {
1640
await this.fileService.del(location);
1641
} catch (e) {
1642
/* ignore */
1643
this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e));
1644
}
1645
throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting);
1646
}
1647
1648
}
1649
1650
async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
1651
if (extension.assets.readme) {
1652
const context = await this.getAsset(extension.identifier.id, extension.assets.readme, AssetType.Details, extension.version, {}, token);
1653
const content = await asTextOrError(context);
1654
return content || '';
1655
}
1656
return '';
1657
}
1658
1659
async getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null> {
1660
if (extension.assets.manifest) {
1661
const context = await this.getAsset(extension.identifier.id, extension.assets.manifest, AssetType.Manifest, extension.version, {}, token);
1662
const text = await asTextOrError(context);
1663
return text ? JSON.parse(text) : null;
1664
}
1665
return null;
1666
}
1667
1668
async getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise<ITranslation | null> {
1669
const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0];
1670
if (asset) {
1671
const context = await this.getAsset(extension.identifier.id, asset[1], asset[0], extension.version);
1672
const text = await asTextOrError(context);
1673
return text ? JSON.parse(text) : null;
1674
}
1675
return null;
1676
}
1677
1678
async getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
1679
if (extension.assets.changelog) {
1680
const context = await this.getAsset(extension.identifier.id, extension.assets.changelog, AssetType.Changelog, extension.version, {}, token);
1681
const content = await asTextOrError(context);
1682
return content || '';
1683
}
1684
return '';
1685
}
1686
1687
async getAllVersions(extensionIdentifier: IExtensionIdentifier): Promise<IGalleryExtensionVersion[]> {
1688
return this.getVersions(extensionIdentifier);
1689
}
1690
1691
async getAllCompatibleVersions(extensionIdentifier: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
1692
return this.getVersions(extensionIdentifier, { version: includePreRelease ? VersionKind.Latest : VersionKind.Release, targetPlatform });
1693
}
1694
1695
private async getVersions(extensionIdentifier: IExtensionIdentifier, onlyCompatible?: { version: VersionKind; targetPlatform: TargetPlatform }): Promise<IGalleryExtensionVersion[]> {
1696
const extensionGalleryManifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest();
1697
if (!extensionGalleryManifest) {
1698
throw new Error('No extension gallery service configured.');
1699
}
1700
1701
let query = new Query()
1702
.withFlags(Flag.IncludeVersions, Flag.IncludeCategoryAndTags, Flag.IncludeFiles, Flag.IncludeVersionProperties)
1703
.withPage(1, 1);
1704
1705
if (extensionIdentifier.uuid) {
1706
query = query.withFilter(FilterType.ExtensionId, extensionIdentifier.uuid);
1707
} else {
1708
query = query.withFilter(FilterType.ExtensionName, extensionIdentifier.id);
1709
}
1710
1711
const { galleryExtensions } = await this.queryRawGalleryExtensions(query, extensionGalleryManifest, CancellationToken.None);
1712
if (!galleryExtensions.length) {
1713
return [];
1714
}
1715
1716
const allTargetPlatforms = getAllTargetPlatforms(galleryExtensions[0]);
1717
if (onlyCompatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, onlyCompatible.targetPlatform)) {
1718
return [];
1719
}
1720
1721
const versions: IRawGalleryExtensionVersion[] = [];
1722
const productVersion = { version: this.productService.version, date: this.productService.date };
1723
await Promise.all(galleryExtensions[0].versions.map(async (version) => {
1724
try {
1725
if (
1726
(await this.isValidVersion(
1727
{
1728
id: extensionIdentifier.id,
1729
version: version.version,
1730
isPreReleaseVersion: isPreReleaseVersion(version),
1731
targetPlatform: getTargetPlatformForExtensionVersion(version),
1732
engine: getEngine(version),
1733
manifestAsset: getVersionAsset(version, AssetType.Manifest),
1734
enabledApiProposals: getEnabledApiProposals(version)
1735
},
1736
{
1737
compatible: !!onlyCompatible,
1738
productVersion,
1739
targetPlatform: onlyCompatible?.targetPlatform,
1740
version: onlyCompatible?.version ?? version.version
1741
},
1742
galleryExtensions[0].publisher.displayName,
1743
allTargetPlatforms))
1744
) {
1745
versions.push(version);
1746
}
1747
} catch (error) { /* Ignore error and skip version */ }
1748
}));
1749
1750
const result: IGalleryExtensionVersion[] = [];
1751
const seen = new Map<string, number>();
1752
for (const version of sortExtensionVersions(versions, onlyCompatible?.targetPlatform ?? CURRENT_TARGET_PLATFORM)) {
1753
const index = seen.get(version.version);
1754
const existing = index !== undefined ? result[index] : undefined;
1755
const targetPlatform = getTargetPlatformForExtensionVersion(version);
1756
if (!existing) {
1757
seen.set(version.version, result.length);
1758
result.push({ version: version.version, date: version.lastUpdated, isPreReleaseVersion: isPreReleaseVersion(version), targetPlatforms: [targetPlatform] });
1759
} else {
1760
existing.targetPlatforms.push(targetPlatform);
1761
}
1762
}
1763
1764
return result;
1765
}
1766
1767
private async getAsset(extension: string, asset: IGalleryExtensionAsset, assetType: string, extensionVersion: string, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): Promise<IRequestContext> {
1768
const commonHeaders = await this.commonHeadersPromise;
1769
const baseOptions = { type: 'GET' };
1770
const headers = { ...commonHeaders, ...(options.headers || {}) };
1771
options = { ...options, ...baseOptions, headers };
1772
1773
const url = asset.uri;
1774
const fallbackUrl = asset.fallbackUri;
1775
const firstOptions = { ...options, url, timeout: REQUEST_TIME_OUT };
1776
1777
let context;
1778
try {
1779
context = await this.requestService.request(firstOptions, token);
1780
if (context.res.statusCode === 200) {
1781
return context;
1782
}
1783
const message = await asTextOrError(context);
1784
throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`);
1785
} catch (err) {
1786
if (isCancellationError(err)) {
1787
throw err;
1788
}
1789
1790
const message = getErrorMessage(err);
1791
type GalleryServiceCDNFallbackClassification = {
1792
owner: 'sandy081';
1793
comment: 'Fallback request information when the primary asset request to CDN fails';
1794
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' };
1795
assetType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'asset that failed' };
1796
message: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error message' };
1797
extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' };
1798
readonly server?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'server that handled the query' };
1799
readonly endToEndId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'end to end operation id' };
1800
readonly activityId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'activity id' };
1801
};
1802
type GalleryServiceCDNFallbackEvent = {
1803
extension: string;
1804
assetType: string;
1805
message: string;
1806
extensionVersion: string;
1807
server?: TelemetryTrustedValue<string>;
1808
endToEndId?: TelemetryTrustedValue<string>;
1809
activityId?: TelemetryTrustedValue<string>;
1810
};
1811
this.telemetryService.publicLog2<GalleryServiceCDNFallbackEvent, GalleryServiceCDNFallbackClassification>('galleryService:cdnFallback', {
1812
extension,
1813
assetType,
1814
message,
1815
extensionVersion,
1816
server: this.getHeaderValue(context?.res.headers, SERVER_HEADER_NAME),
1817
activityId: this.getHeaderValue(context?.res.headers, ACTIVITY_HEADER_NAME),
1818
endToEndId: this.getHeaderValue(context?.res.headers, END_END_ID_HEADER_NAME),
1819
});
1820
1821
const fallbackOptions = { ...options, url: fallbackUrl, timeout: REQUEST_TIME_OUT };
1822
return this.requestService.request(fallbackOptions, token);
1823
}
1824
}
1825
1826
async getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
1827
if (!this.isEnabled()) {
1828
throw new Error('No extension gallery service configured.');
1829
}
1830
1831
if (!this.extensionsControlUrl) {
1832
return { malicious: [], deprecated: {}, search: [], autoUpdate: {} };
1833
}
1834
1835
const context = await this.requestService.request({
1836
type: 'GET',
1837
url: this.extensionsControlUrl,
1838
timeout: REQUEST_TIME_OUT
1839
}, CancellationToken.None);
1840
1841
if (context.res.statusCode !== 200) {
1842
throw new Error('Could not get extensions report.');
1843
}
1844
1845
const result = await asJson<IRawExtensionsControlManifest>(context);
1846
const malicious: Array<MaliciousExtensionInfo> = [];
1847
const deprecated: IStringDictionary<IDeprecationInfo> = {};
1848
const search: ISearchPrefferedResults[] = [];
1849
const autoUpdate: IStringDictionary<string> = result?.autoUpdate ?? {};
1850
if (result) {
1851
for (const id of result.malicious) {
1852
if (!isString(id)) {
1853
continue;
1854
}
1855
const publisherOrExtension = EXTENSION_IDENTIFIER_REGEX.test(id) ? { id } : id;
1856
malicious.push({ extensionOrPublisher: publisherOrExtension, learnMoreLink: result.learnMoreLinks?.[id] });
1857
}
1858
if (result.migrateToPreRelease) {
1859
for (const [unsupportedPreReleaseExtensionId, preReleaseExtensionInfo] of Object.entries(result.migrateToPreRelease)) {
1860
if (!preReleaseExtensionInfo.engine || isEngineValid(preReleaseExtensionInfo.engine, this.productService.version, this.productService.date)) {
1861
deprecated[unsupportedPreReleaseExtensionId.toLowerCase()] = {
1862
disallowInstall: true,
1863
extension: {
1864
id: preReleaseExtensionInfo.id,
1865
displayName: preReleaseExtensionInfo.displayName,
1866
autoMigrate: { storage: !!preReleaseExtensionInfo.migrateStorage },
1867
preRelease: true
1868
}
1869
};
1870
}
1871
}
1872
}
1873
if (result.deprecated) {
1874
for (const [deprecatedExtensionId, deprecationInfo] of Object.entries(result.deprecated)) {
1875
if (deprecationInfo) {
1876
deprecated[deprecatedExtensionId.toLowerCase()] = isBoolean(deprecationInfo) ? {} : deprecationInfo;
1877
}
1878
}
1879
}
1880
if (result.search) {
1881
for (const s of result.search) {
1882
search.push(s);
1883
}
1884
}
1885
}
1886
1887
return { malicious, deprecated, search, autoUpdate };
1888
}
1889
1890
}
1891
1892
export class ExtensionGalleryService extends AbstractExtensionGalleryService {
1893
1894
constructor(
1895
@IStorageService storageService: IStorageService,
1896
@IRequestService requestService: IRequestService,
1897
@ILogService logService: ILogService,
1898
@IEnvironmentService environmentService: IEnvironmentService,
1899
@ITelemetryService telemetryService: ITelemetryService,
1900
@IFileService fileService: IFileService,
1901
@IProductService productService: IProductService,
1902
@IConfigurationService configurationService: IConfigurationService,
1903
@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
1904
@IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService,
1905
) {
1906
super(storageService, undefined, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService, allowedExtensionsService, extensionGalleryManifestService);
1907
}
1908
}
1909
1910
export class ExtensionGalleryServiceWithNoStorageService extends AbstractExtensionGalleryService {
1911
1912
constructor(
1913
@IRequestService requestService: IRequestService,
1914
@ILogService logService: ILogService,
1915
@IEnvironmentService environmentService: IEnvironmentService,
1916
@ITelemetryService telemetryService: ITelemetryService,
1917
@IFileService fileService: IFileService,
1918
@IProductService productService: IProductService,
1919
@IConfigurationService configurationService: IConfigurationService,
1920
@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
1921
@IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService,
1922
) {
1923
super(undefined, undefined, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService, allowedExtensionsService, extensionGalleryManifestService);
1924
}
1925
}
1926
1927