Skip to content

Commit 7330a7f

Browse files
authored
Pull image urls out of pr comment html (#6976)
* Pull image urls out of pr comment html Fixes #6175 * Support GHE and GHCE * typo * Escape strings in regexp
1 parent 85916ec commit 7330a7f

4 files changed

Lines changed: 104 additions & 14 deletions

File tree

src/common/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,3 +995,7 @@ export async function batchPromiseAll<T>(items: readonly T[], batchSize: number,
995995
}
996996
}
997997

998+
export function escapeRegExp(string: string) {
999+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1000+
}
1001+

src/github/prComment.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { emojify, ensureEmojis } from '../common/emoji';
1010
import Logger from '../common/logger';
1111
import { DataUri } from '../common/uri';
1212
import { ALLOWED_USERS, JSDOC_NON_USERS, PHPDOC_NON_USERS } from '../common/user';
13-
import { stringReplaceAsync } from '../common/utils';
13+
import { escapeRegExp, stringReplaceAsync } from '../common/utils';
1414
import { GitHubRepository } from './githubRepository';
1515
import { IAccount } from './interface';
1616
import { updateCommentReactions } from './utils';
@@ -208,6 +208,7 @@ export class TemporaryComment extends CommentBase {
208208

209209
const SUGGESTION_EXPRESSION = /```suggestion(\u0020*(\r\n|\n))((?<suggestion>[\s\S]*?)(\r\n|\n))?```/;
210210
const IMG_EXPRESSION = /<img .*src=['"](?<src>.+?)['"].*?>/g;
211+
const UUID_EXPRESSION = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}/;
211212

212213
export class GHPRComment extends CommentBase {
213214
private static ID = 'GHPRComment';
@@ -221,14 +222,17 @@ export class GHPRComment extends CommentBase {
221222

222223
private _rawBody: string | vscode.MarkdownString;
223224
private replacedBody: string;
225+
private githubRepository: GitHubRepository | undefined;
224226

225-
constructor(private readonly context: vscode.ExtensionContext, comment: IComment, parent: GHPRCommentThread, private readonly githubRepositories?: GitHubRepository[]) {
227+
constructor(private readonly context: vscode.ExtensionContext, comment: IComment, parent: GHPRCommentThread, githubRepositories?: GitHubRepository[]) {
226228
super(parent);
227229
this.rawComment = comment;
228230
this.originalAuthor = {
229231
name: comment.user?.specialDisplayName ?? comment.user!.login,
230232
iconPath: comment.user && comment.user.avatarUrl ? vscode.Uri.parse(comment.user.avatarUrl) : undefined,
231233
};
234+
const url = vscode.Uri.parse(comment.url);
235+
this.githubRepository = githubRepositories?.find(repo => repo.remote.host === url.authority);
232236

233237
const avatarUrisPromise = comment.user ? DataUri.avatarCirclesAsImageDataUris(context, [comment.user], 28, 28) : Promise.resolve([]);
234238
this.doSetBody(comment.body, !comment.user).then(async () => { // only refresh if there's no user. If there's a user, we'll refresh in the then.
@@ -364,20 +368,18 @@ ${args[3] ?? ''}
364368
}
365369

366370
private async replacePermalink(body: string): Promise<string> {
367-
const githubRepositories = this.githubRepositories;
368-
if (!githubRepositories || githubRepositories.length === 0) {
371+
const githubRepository = this.githubRepository;
372+
if (!githubRepository) {
369373
return body;
370374
}
371375

372-
const expression = new RegExp(`https://github.com/(.+)/${githubRepositories[0].remote.repositoryName}/blob/([0-9a-f]{40})/(.*)#L([0-9]+)(-L([0-9]+))?`, 'g');
376+
const repoName = escapeRegExp(githubRepository.remote.repositoryName);
377+
const expression = new RegExp(`https://github.com/(.+)/${repoName}/blob/([0-9a-f]{40})/(.*)#L([0-9]+)(-L([0-9]+))?`, 'g');
373378
return stringReplaceAsync(body, expression, async (match: string, owner: string, sha: string, file: string, start: string, _endGroup?: string, end?: string, index?: number) => {
374379
if (index && (index > 0) && (body.charAt(index - 1) === '(')) {
375380
return match;
376381
}
377-
const githubRepository = githubRepositories.find(repository => repository.remote.owner.toLocaleLowerCase() === owner.toLocaleLowerCase());
378-
if (!githubRepository) {
379-
return match;
380-
}
382+
381383
const startLine = parseInt(start);
382384
const endLine = end ? parseInt(end) : startLine + 1;
383385
const lineContents = await githubRepository.getLines(sha, file, startLine, endLine);
@@ -398,6 +400,15 @@ ${lineContents}
398400
});
399401
}
400402

403+
private replaceImages(body: string): string {
404+
const html = this.rawComment.bodyHTML;
405+
if (!html) {
406+
return body;
407+
}
408+
409+
return replaceImages(body, html, this.githubRepository?.remote.host);
410+
}
411+
401412
private replaceNewlines(body: string) {
402413
return body.replace(/(?<!\s)(\r\n|\n)/g, ' \n');
403414
}
@@ -416,7 +427,8 @@ ${lineContents}
416427
const permalinkReplaced = await this.replacePermalink(body.value);
417428
return this.replaceImg(this.replaceSuggestion(permalinkReplaced));
418429
}
419-
const newLinesReplaced = this.replaceNewlines(body);
430+
const imagesReplaced = this.replaceImages(body);
431+
const newLinesReplaced = this.replaceNewlines(imagesReplaced);
420432
const documentLanguage = (await vscode.workspace.openTextDocument(this.parent.uri)).languageId;
421433
const replacerRegex = new RegExp(`([^/\[\`]|^)@(${ALLOWED_USERS})`, 'g');
422434
// Replace user
@@ -471,3 +483,24 @@ ${lineContents}
471483
return new vscode.MarkdownString(this.rawComment.body);
472484
}
473485
}
486+
487+
export function replaceImages(markdownBody: string, htmlBody: string, host: string = 'github.com') {
488+
const originalExpression = new RegExp(`https:\/\/${host}\/.+\/assets\/([^\/]+\/)?(?<uuid>${UUID_EXPRESSION.source})`);
489+
let originalMatch = markdownBody.match(originalExpression);
490+
const htmlHost = escapeRegExp(host === 'github.com' ? 'githubusercontent.com' : host);
491+
492+
while (originalMatch) {
493+
if (originalMatch.groups?.uuid) {
494+
const uuid = escapeRegExp(originalMatch.groups.uuid);
495+
const htmlExpression = new RegExp(`https:\/\/([^"]*${htmlHost})\/[^?]+${uuid}[^"]+`);
496+
const htmlMatch = htmlBody.match(htmlExpression);
497+
if (htmlMatch && htmlMatch[0]) {
498+
markdownBody = markdownBody.replace(originalMatch[0], htmlMatch[0]);
499+
} else {
500+
return markdownBody;
501+
}
502+
}
503+
originalMatch = markdownBody.match(originalExpression);
504+
}
505+
return markdownBody;
506+
}

src/issues/issueTodoProvider.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@
55

66
import * as vscode from 'vscode';
77
import { CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE } from '../common/settingKeys';
8+
import { escapeRegExp } from '../common/utils';
89
import { ISSUE_OR_URL_EXPRESSION } from '../github/utils';
910
import { MAX_LINE_LENGTH } from './util';
1011

11-
function escapeRegExp(string: string) {
12-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13-
}
14-
1512
export class IssueTodoProvider implements vscode.CodeActionProvider {
1613
private expression: RegExp | undefined;
1714

src/test/github/prComment.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 { default as assert } from 'assert';
7+
import { replaceImages } from '../../github/prComment';
8+
9+
describe('replace images', function () {
10+
it('github.com', function () {
11+
const markdownBody = `Test image
12+
![image](https://github.com/user-attachments/assets/714215c1-e994-4c69-be20-2276c558f7c3)
13+
test again
14+
![image](https://github.com/user-attachments/assets/3f2c170a-d0c3-4ac7-a9e5-ea13bf71a5bc)`;
15+
const htmlBody = `
16+
<p dir="auto">Test image</p><p dir="auto"><a target="_blank" rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/38270282/445632993-714215c1-e994-4c69-be20-2276c558f7c3.png?jwt=TEST"><img src="https://private-user-images.githubusercontent.com/38270282/445632993-714215c1-e994-4c69-be20-2276c558f7c3.png?jwt=TEST" alt="image" style="max-width: 100%;"></a></p>
17+
<p dir="auto">test again</p>
18+
<p dir="auto"><a target="_blank" rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/38270282/445689518-3f2c170a-d0c3-4ac7-a9e5-ea13bf71a5bc.png?jwt=TEST"><img src="https://private-user-images.githubusercontent.com/38270282/445689518-3f2c170a-d0c3-4ac7-a9e5-ea13bf71a5bc.png?jwt=TEST" alt="image" style="max-width: 100%;"></a></p>`;
19+
const host = 'github.com';
20+
const replaced = replaceImages(markdownBody, htmlBody, host);
21+
const expected = `Test image
22+
![image](https://private-user-images.githubusercontent.com/38270282/445632993-714215c1-e994-4c69-be20-2276c558f7c3.png?jwt=TEST)
23+
test again
24+
![image](https://private-user-images.githubusercontent.com/38270282/445689518-3f2c170a-d0c3-4ac7-a9e5-ea13bf71a5bc.png?jwt=TEST)`;
25+
assert.strictEqual(replaced, expected);
26+
});
27+
28+
it('GHCE', function () {
29+
const markdownBody = `Test image
30+
![image](https://test.ghe.com/user-attachments/assets/d81c6ab2-52a6-4ebf-b0c8-125492bd9662)`;
31+
const htmlBody = `
32+
<p dir="auto">Test image</p>
33+
<p dir="auto"><a target="_blank" rel="noopener noreferrer" href="https://test.ghe.com/github-production-user-asset-6210df/11296/2514616-d81c6ab2-52a6-4ebf-b0c8-125492bd9662.png?TEST"><img src="https://objects-origin.test.ghe.com/github-production-user-asset-6210df/11296/2514616-d81c6ab2-52a6-4ebf-b0c8-125492bd9662.png?TEST" alt="image" style="max-width: 100%;"></a></p>`;
34+
const host = 'test.ghe.com';
35+
const replaced = replaceImages(markdownBody, htmlBody, host);
36+
const expected = `Test image
37+
![image](https://test.ghe.com/github-production-user-asset-6210df/11296/2514616-d81c6ab2-52a6-4ebf-b0c8-125492bd9662.png?TEST)`;
38+
39+
assert.strictEqual(replaced, expected);
40+
});
41+
42+
it('GHE', function () {
43+
const markdownBody = `Test
44+
![image](https://alexr00-my-test-instance.ghe-test.com/my-user/my-repo/assets/6/c267d6ce-fbdd-41a0-b86d-760882bd0c82)
45+
`;
46+
const htmlBody = ` <p dir="auto">Test<br>
47+
<a target="_blank" rel="noopener noreferrer" href="https://media.alexr00-my-test-instance.ghe-test.com/user/6/files/c267d6ce-fbdd-41a0-b86d-760882bd0c82?TEST"><img src="https://media.alexr00-my-test-instance.ghe-test.com/user/6/files/c267d6ce-fbdd-41a0-b86d-760882bd0c82?TEST" alt="image" style="max-width: 100%;"></a></p>`;
48+
const host = 'alexr00-my-test-instance.ghe-test.com';
49+
const replaced = replaceImages(markdownBody, htmlBody, host);
50+
const expected = `Test
51+
![image](https://media.alexr00-my-test-instance.ghe-test.com/user/6/files/c267d6ce-fbdd-41a0-b86d-760882bd0c82?TEST)
52+
`;
53+
54+
assert.strictEqual(replaced, expected);
55+
});
56+
});

0 commit comments

Comments
 (0)