extensionGalleryService.ts 10.8 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { TPromise } from 'vs/base/common/winjs.base';
J
Joao Moreno 已提交
7
import { IExtension, IExtensionGalleryService, IGalleryVersion, IQueryOptions, IQueryResult } from 'vs/platform/extensionManagement/common/extensionManagement';
J
Joao Moreno 已提交
8
import { isUndefined } from 'vs/base/common/types';
J
Joao Moreno 已提交
9
import { IXHRResponse } from 'vs/base/common/http';
10
import { assign, getOrDefault } from 'vs/base/common/objects';
E
Erich Gamma 已提交
11
import { IRequestService } from 'vs/platform/request/common/request';
J
Joao Moreno 已提交
12
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
J
Joao Moreno 已提交
13
import { matchesContiguousSubString } from 'vs/base/common/filters';
J
Joao Moreno 已提交
14
import { getExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
15
import pkg from 'vs/platform/package';
J
Joao Moreno 已提交
16
import product from 'vs/platform/product';
E
Erich Gamma 已提交
17 18 19 20 21 22 23 24

export interface IGalleryExtensionFile {
	assetType: string;
}

export interface IGalleryExtensionVersion {
	version: string;
	lastUpdated: string;
J
Joao Moreno 已提交
25
	assetUri: string;
E
Erich Gamma 已提交
26 27 28 29 30 31 32 33 34 35 36
	files: IGalleryExtensionFile[];
}

export interface IGalleryExtension {
	extensionId: string;
	extensionName: string;
	displayName: string;
	shortDescription: string;
	publisher: { displayName: string, publisherId: string, publisherName: string; };
	versions: IGalleryExtensionVersion[];
	galleryApiUrl: string;
J
Joao Moreno 已提交
37
	statistics: IGalleryExtensionStatistics[];
38 39
}

J
Joao Moreno 已提交
40
export interface IGalleryExtensionStatistics {
41 42
	statisticName: string;
	value: number;
E
Erich Gamma 已提交
43 44
}

J
Joao Moreno 已提交
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
enum Flags {
	None = 0x0,
	IncludeVersions = 0x1,
	IncludeFiles = 0x2,
	IncludeCategoryAndTags = 0x4,
	IncludeSharedAccounts = 0x8,
	IncludeVersionProperties = 0x10,
	ExcludeNonValidated = 0x20,
	IncludeInstallationTargets = 0x40,
	IncludeAssetUri = 0x80,
	IncludeStatistics = 0x100,
	IncludeLatestVersionOnly = 0x200
}

enum FilterType {
	Tag = 1,
	ExtensionId = 4,
	Category = 5,
	ExtensionName = 7,
	Target = 8,
	Featured = 9,
	SearchText = 10
}

enum SortBy {
	NoneOrRelevance = 0,
	LastUpdatedDate = 1,
	Title = 2,
	PublisherName = 3,
	InstallCount = 4,
	PublishedDate = 5,
	AverageRating = 6
}

enum SortOrder {
	Default = 0,
	Ascending = 1,
	Descending = 2
}

interface ICriterium {
	filterType: FilterType;
	value?: string;
}

J
Joao Moreno 已提交
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
const DefaultPageSize = 10;

interface IQueryState {
	pageNumber: number;
	pageSize: number;
	sortBy: SortBy;
	sortOrder: SortOrder;
	flags: Flags;
	criteria: ICriterium[];
}

const DefaultQueryState: IQueryState = {
	pageNumber: 1,
	pageSize: DefaultPageSize,
	sortBy: SortBy.NoneOrRelevance,
	sortOrder: SortOrder.Default,
	flags: Flags.None,
	criteria: []
};

J
Joao Moreno 已提交
110 111
class Query {

J
Joao Moreno 已提交
112 113
	constructor(private state = DefaultQueryState) {}

J
Joao Moreno 已提交
114
	get pageNumber(): number { return this.state.pageNumber; }
J
Joao Moreno 已提交
115
	get pageSize(): number { return this.state.pageSize; }
J
Joao Moreno 已提交
116 117 118
	get sortBy(): number { return this.state.sortBy; }
	get sortOrder(): number { return this.state.sortOrder; }
	get flags(): number { return this.state.flags; }
J
Joao Moreno 已提交
119

J
paging!  
Joao Moreno 已提交
120
	withPage(pageNumber: number, pageSize: number = this.state.pageSize): Query {
J
Joao Moreno 已提交
121
		return new Query(assign({}, this.state, { pageNumber, pageSize }));
J
Joao Moreno 已提交
122 123 124 125 126 127 128 129 130
	}

	withFilter(filterType: FilterType, value?: string): Query {
		const criterium: ICriterium = { filterType };

		if (!isUndefined(value)) {
			criterium.value = value;
		}

J
Joao Moreno 已提交
131 132 133
		const criteria = this.state.criteria.slice();
		criteria.push(criterium);
		return new Query(assign({}, this.state, { criteria }));
J
Joao Moreno 已提交
134 135 136
	}

	withSort(sortBy: SortBy, sortOrder = SortOrder.Default): Query {
J
Joao Moreno 已提交
137
		return new Query(assign({}, this.state, { sortBy, sortOrder }));
J
Joao Moreno 已提交
138 139 140
	}

	withFlags(...flags: Flags[]): Query {
J
Joao Moreno 已提交
141
		return new Query(assign({}, this.state, { flags: flags.reduce((r, f) => r | f, 0) }));
J
Joao Moreno 已提交
142 143 144 145 146
	}

	get raw(): any {
		return {
			filters: [{
J
Joao Moreno 已提交
147 148 149 150 151
				criteria: this.state.criteria,
				pageNumber: this.state.pageNumber,
				pageSize: this.state.pageSize,
				sortBy: this.state.sortBy,
				sortOrder: this.state.sortOrder
J
Joao Moreno 已提交
152
			}],
J
Joao Moreno 已提交
153
			flags: this.state.flags
J
Joao Moreno 已提交
154 155 156 157
		};
	}
}

J
Joao Moreno 已提交
158 159 160 161 162 163 164 165 166
function getInstallCount(statistics: IGalleryExtensionStatistics[]): number {
	if (!statistics) {
		return 0;
	}

	const result = statistics.filter(s => s.statisticName === 'install')[0];
	return result ? result.value : 0;
}

J
Joao Moreno 已提交
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
function toExtension(galleryExtension: IGalleryExtension, extensionsGalleryUrl: string, downloadHeaders: any): IExtension {
	const versions = galleryExtension.versions.map<IGalleryVersion>(v => ({
		version: v.version,
		date: v.lastUpdated,
		downloadHeaders,
		downloadUrl: `${ v.assetUri }/Microsoft.VisualStudio.Services.VSIXPackage?install=true`,
		manifestUrl: `${ v.assetUri }/Microsoft.VisualStudio.Code.Manifest`
	}));

	return {
		name: galleryExtension.extensionName,
		displayName: galleryExtension.displayName || galleryExtension.extensionName,
		publisher: galleryExtension.publisher.publisherName,
		version: versions[0].version,
		engines: { vscode: void 0 }, // TODO: ugly
		description: galleryExtension.shortDescription || '',
		galleryInformation: {
			galleryApiUrl: extensionsGalleryUrl,
			id: galleryExtension.extensionId,
			publisherId: galleryExtension.publisher.publisherId,
			publisherDisplayName: galleryExtension.publisher.displayName,
			installCount: getInstallCount(galleryExtension.statistics),
			versions
		}
	};
}

J
Joao Moreno 已提交
194 195 196 197 198 199 200 201 202 203 204
const FIVE_MINUTES = 1000 * 60 * 5;

function extensionFilter(input: string): (e: IExtension) => boolean {
	return extension => {
		return !!matchesContiguousSubString(input, `${ extension.publisher }.${ extension.name }`)
			|| !!matchesContiguousSubString(input, extension.name)
			|| !!matchesContiguousSubString(input, extension.displayName)
			|| !!matchesContiguousSubString(input, extension.description);
	};
}

J
Joao Moreno 已提交
205
export class ExtensionGalleryService implements IExtensionGalleryService {
E
Erich Gamma 已提交
206

J
Joao Moreno 已提交
207
	serviceId = IExtensionGalleryService;
E
Erich Gamma 已提交
208 209

	private extensionsGalleryUrl: string;
J
Joao Moreno 已提交
210
	private extensionsCacheUrl: string;
211
	private machineId: TPromise<string>;
E
Erich Gamma 已提交
212 213 214

	constructor(
		@IRequestService private requestService: IRequestService,
J
Joao Moreno 已提交
215
		@ITelemetryService private telemetryService: ITelemetryService
E
Erich Gamma 已提交
216
	) {
217
		const config = product.extensionsGallery;
J
Joao Moreno 已提交
218
		this.extensionsGalleryUrl = config && config.serviceUrl;
J
Joao Moreno 已提交
219
		this.extensionsCacheUrl = config && config.cacheUrl;
220
		this.machineId = telemetryService.getTelemetryInfo().then(({ machineId }) => machineId);
E
Erich Gamma 已提交
221 222 223 224 225 226
	}

	private api(path = ''): string {
		return `${ this.extensionsGalleryUrl }${ path }`;
	}

J
Joao Moreno 已提交
227
	isEnabled(): boolean {
E
Erich Gamma 已提交
228 229 230
		return !!this.extensionsGalleryUrl;
	}

J
Joao Moreno 已提交
231
	query(options: IQueryOptions = {}): TPromise<IQueryResult> {
J
Joao Moreno 已提交
232
		if (!this.isEnabled()) {
E
Erich Gamma 已提交
233 234 235
			return TPromise.wrapError(new Error('No extension gallery service configured.'));
		}

J
Joao Moreno 已提交
236
		const type = options.ids ? 'ids' : (options.text ? 'text' : 'all');
J
Joao Moreno 已提交
237 238
		const text = options.text || '';
		this.telemetryService.publicLog('galleryService:query', { type, text });
J
Joao Moreno 已提交
239

J
Joao Moreno 已提交
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
		const cache = this.queryCache().then(result => {
			const rawLastModified = result.getResponseHeader('last-modified');

			if (!rawLastModified) {
				return TPromise.wrapError('no last modified header');
			}

			const lastModified = new Date(rawLastModified).getTime();
			const now = new Date().getTime();
			const diff = now - lastModified;

			if (diff > FIVE_MINUTES) {
				return TPromise.wrapError('stale');
			}

			return this.getRequestHeaders().then(downloadHeaders => {
				const rawExtensions: IGalleryExtension[] = JSON.parse(result.responseText).results[0].extensions || [];
				let extensions = rawExtensions
					.map(e => toExtension(e, this.extensionsGalleryUrl, downloadHeaders));

				if (options.ids) {
					extensions = extensions.filter(e => options.ids.indexOf(getExtensionId(e)) > -1);
				} else if (options.text) {
					extensions = extensions.filter(extensionFilter(options.text));
				}

				extensions = extensions
					.sort((a, b) => b.galleryInformation.installCount - a.galleryInformation.installCount);

				return {
					firstPage: extensions,
					total: extensions.length,
					pageSize: extensions.length,
					getPage: () => TPromise.as([])
				};
			});
		});

		return cache.then(null, _ => this._query(options));
	}

	private _query(options: IQueryOptions = {}): TPromise<IQueryResult> {
282
		const text = getOrDefault(options, o => o.text, '');
283
		const pageSize = getOrDefault(options, o => o.pageSize, 50);
J
Joao Moreno 已提交
284

J
Joao Moreno 已提交
285 286
		let query = new Query()
			.withFlags(Flags.IncludeVersions, Flags.IncludeCategoryAndTags, Flags.IncludeAssetUri, Flags.IncludeStatistics)
J
Joao Moreno 已提交
287
			.withPage(1, pageSize)
J
Joao Moreno 已提交
288 289 290 291 292
			.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
			.withSort(SortBy.InstallCount);

		if (text) {
			query = query.withFilter(FilterType.SearchText, text);
293 294 295 296
		} else if (options.ids) {
			options.ids.forEach(id => {
				query = query.withFilter(FilterType.ExtensionName, id);
			});
J
Joao Moreno 已提交
297 298
		}

J
Joao Moreno 已提交
299 300 301 302
		return this.queryGallery(query).then(({ galleryExtensions, total }) => {
			return this.getRequestHeaders().then(downloadHeaders => {
				const extensions = galleryExtensions.map(e => toExtension(e, this.extensionsGalleryUrl, downloadHeaders));
				const pageSize = query.pageSize;
J
paging!  
Joao Moreno 已提交
303
				const getPage = pageIndex => this.queryGallery(query.withPage(pageIndex + 1))
J
Joao Moreno 已提交
304
					.then(({ galleryExtensions }) => galleryExtensions.map(e => toExtension(e, this.extensionsGalleryUrl, downloadHeaders)));
J
Joao Moreno 已提交
305

J
Joao Moreno 已提交
306
				return { firstPage: extensions, total, pageSize, getPage };
307
			});
J
Joao Moreno 已提交
308 309
		});
	}
310

J
Joao Moreno 已提交
311 312
	private queryGallery(query: Query): TPromise<{ galleryExtensions: IGalleryExtension[], total: number; }> {
		const data = JSON.stringify(query.raw);
J
Joao Moreno 已提交
313

J
Joao Moreno 已提交
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
		return this.getRequestHeaders()
			.then(headers => {
				headers = assign(headers, {
					'Content-Type': 'application/json',
					'Accept': 'application/json;api-version=3.0-preview.1',
					'Content-Length': data.length
				});

				const request = {
					type: 'POST',
					url: this.api('/extensionquery'),
					data,
					headers
				};

				return this.requestService.makeRequest(request);
			})
			.then(r => JSON.parse(r.responseText).results[0])
			.then(r => {
				const galleryExtensions = r.extensions;
				const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0];
				const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0;

				return { galleryExtensions, total };
			});
J
Joao Moreno 已提交
339
	}
340

J
Joao Moreno 已提交
341 342 343 344 345 346 347 348 349 350
	private queryCache(): TPromise<IXHRResponse> {
		const url = this.extensionsCacheUrl;

		if (!url) {
			return TPromise.wrapError(new Error('No cache configured.'));
		}

		return this.requestService.makeRequest({ url });
	}

351 352 353
	private getRequestHeaders(): TPromise<any> {
		return this.machineId.then(machineId => {
			const result = {
354 355
				'X-Market-Client-Id': `VSCode ${ pkg.version }`,
				'User-Agent': `VSCode ${ pkg.version }`
356 357 358 359 360 361 362 363 364
			};

			if (machineId) {
				result['X-Market-User-Id'] = machineId;
			}

			return result;
		});
	}
E
Erich Gamma 已提交
365
}