|
1 | | -import { EventEmitter, Uri } from 'vscode'; |
| 1 | +import type { MessageItem } from 'vscode'; |
| 2 | +import { EventEmitter, Uri, window, workspace } from 'vscode'; |
2 | 3 | import type { CommitAuthor } from '@gitlens/git/models/author.js'; |
3 | 4 | import { CustomRemoteProvider } from '@gitlens/git/remotes/custom.js'; |
4 | 5 | import { getGitHubNoReplyAddressParts } from '@gitlens/git/remotes/github.js'; |
@@ -247,18 +248,19 @@ async function getAvatarUriFromRemoteProvider( |
247 | 248 | }); |
248 | 249 | } |
249 | 250 |
|
250 | | - if (!account) { |
| 251 | + if (!account?.avatarUrl) { |
251 | 252 | const remoteWithProvider = await Container.instance.git |
252 | 253 | .getRepositoryService(repoPathOrCommit.repoPath) |
253 | 254 | .remotes.getBestRemoteWithProvider(); |
254 | 255 |
|
255 | 256 | if (remoteWithProvider?.provider instanceof CustomRemoteProvider) { |
256 | | - const avatarUrl = remoteWithProvider.provider.getUrlForAvatar(email, size); |
| 257 | + const avatarUrl = getApprovedCustomRemoteAvatarUrl(remoteWithProvider.provider, email, size); |
257 | 258 | if (avatarUrl != null) { |
258 | 259 | avatar.uri = Uri.parse(avatarUrl); |
259 | 260 | avatar.timestamp = Date.now(); |
260 | 261 | avatar.retries = 0; |
261 | 262 | avatarCache.set(`${md5(email.trim().toLowerCase())}:${size}`, { ...avatar }); |
| 263 | + _onDidFetchAvatar.fire({ email: email }); |
262 | 264 | return avatar.uri; |
263 | 265 | } |
264 | 266 | } |
@@ -316,6 +318,71 @@ export function getPresenceDataUri(status: ContactPresenceStatus): string { |
316 | 318 | return dataUri; |
317 | 319 | } |
318 | 320 |
|
| 321 | +const promptedAvatarTemplates = new Set<string>(); |
| 322 | + |
| 323 | +function getApprovedCustomRemoteAvatarUrl( |
| 324 | + provider: CustomRemoteProvider, |
| 325 | + email: string, |
| 326 | + size: number, |
| 327 | +): string | undefined { |
| 328 | + // Avatar templates from `gitlens.remotes` may originate from workspace settings |
| 329 | + // — only honor them in a trusted workspace |
| 330 | + if (!workspace.isTrusted) return undefined; |
| 331 | + |
| 332 | + const template = provider.avatarUrlTemplate; |
| 333 | + if (template == null) return undefined; |
| 334 | + |
| 335 | + const approval = getAvatarTemplateApproval(template); |
| 336 | + if (approval === 'allow') { |
| 337 | + return provider.getUrlForAvatar(email, size); |
| 338 | + } |
| 339 | + if (approval === 'deny') { |
| 340 | + return undefined; |
| 341 | + } |
| 342 | + // Unknown template — prompt the user (non-blocking) and fall through to fallback avatar |
| 343 | + void promptForAvatarTemplateApproval(template); |
| 344 | + return undefined; |
| 345 | +} |
| 346 | + |
| 347 | +function getAvatarTemplateApproval(template: string): 'allow' | 'deny' | undefined { |
| 348 | + return Container.instance.storage.get('avatars:approvedRemoteTemplates')?.[template]; |
| 349 | +} |
| 350 | + |
| 351 | +async function setAvatarTemplateApproval(template: string, decision: 'allow' | 'deny'): Promise<void> { |
| 352 | + const approvals = { ...(Container.instance.storage.get('avatars:approvedRemoteTemplates') ?? {}) }; |
| 353 | + approvals[template] = decision; |
| 354 | + await Container.instance.storage.store('avatars:approvedRemoteTemplates', approvals); |
| 355 | +} |
| 356 | + |
| 357 | +async function promptForAvatarTemplateApproval(template: string): Promise<void> { |
| 358 | + if (promptedAvatarTemplates.has(template)) return; |
| 359 | + promptedAvatarTemplates.add(template); |
| 360 | + |
| 361 | + const allow: MessageItem = { title: 'Allow' }; |
| 362 | + const never: MessageItem = { title: 'Never' }; |
| 363 | + const notNow: MessageItem = { title: 'Not Now', isCloseAffordance: true }; |
| 364 | + |
| 365 | + const result = await window.showInformationMessage( |
| 366 | + `GitLens wants to fetch contributor avatars from a URL configured by the current workspace. This URL will be requested for every commit author shown in this repository.\n\nTemplate: ${template}`, |
| 367 | + allow, |
| 368 | + never, |
| 369 | + notNow, |
| 370 | + ); |
| 371 | + |
| 372 | + if (result === allow) { |
| 373 | + await setAvatarTemplateApproval(template, 'allow'); |
| 374 | + resetAvatarCache('failed'); |
| 375 | + } else if (result === never) { |
| 376 | + await setAvatarTemplateApproval(template, 'deny'); |
| 377 | + } |
| 378 | +} |
| 379 | + |
| 380 | +export function resetApprovedAvatarTemplates(): Promise<void> { |
| 381 | + promptedAvatarTemplates.clear(); |
| 382 | + resetAvatarCache('failed'); |
| 383 | + return Container.instance.storage.delete('avatars:approvedRemoteTemplates'); |
| 384 | +} |
| 385 | + |
319 | 386 | export function resetAvatarCache(reset: 'all' | 'failed' | 'fallback'): void { |
320 | 387 | switch (reset) { |
321 | 388 | case 'all': |
|
0 commit comments