Skip to content

Commit 3308922

Browse files
authored
Merge branch 'main' into copilot/fix-259564
2 parents e78492b + 9ace93e commit 3308922

File tree

57 files changed

+1234
-517
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1234
-517
lines changed

.eslint-plugin-local/tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"compilerOptions": {
3-
"target": "es2020",
3+
"target": "es2024",
44
"lib": [
5-
"ES2020"
5+
"ES2024"
66
],
77
"module": "commonjs",
88
"esModuleInterop": true,

.github/prompts/implement.prompt.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ Please write a high quality, general purpose solution. Implement a solution that
77

88
Focus on understanding the problem requirements and implementing the correct algorithm. Tests are there to verify correctness, not to define the solution. Provide a principled implementation that follows best practices and software design principles.
99

10-
If the task is unreasonable or infeasible, or if any of the tests are incorrect, please tell me. The solution should be robust, maintainable, and extendable.
10+
If the task is unreasonable or infeasible, or if any of the tests are incorrect, please tell the user. The solution should be robust, maintainable, and extendable.

build/gulpfile.vscode.linux.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ function prepareDebPackage(arch) {
107107

108108
const postinst = gulp.src('resources/linux/debian/postinst.template', { base: '.' })
109109
.pipe(replace('@@NAME@@', product.applicationName))
110+
.pipe(replace('@@ARCHITECTURE@@', debArch))
110111
.pipe(rename('DEBIAN/postinst'));
111112

112113
const templates = gulp.src('resources/linux/debian/templates.template', { base: '.' })

build/npm/jsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"compilerOptions": {
3-
"target": "es2020",
3+
"target": "es2024",
44
"lib": [
5-
"ES2020"
5+
"ES2024"
66
],
77
"module": "node16",
88
"checkJs": true,

build/tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"compilerOptions": {
3-
"target": "es2022",
3+
"target": "es2024",
44
"lib": [
5-
"ES2020"
5+
"ES2024"
66
],
77
"module": "nodenext",
88
"alwaysStrict": true,

extensions/git/src/commands.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3480,15 +3480,32 @@ export class CommandCenter {
34803480
return;
34813481
}
34823482

3483-
// Check whether the selected branch is checked out in an existing worktree
3484-
const worktree = repository.worktrees.find(worktree => worktree.ref === choice.refId);
3485-
if (worktree) {
3486-
const message = l10n.t('Branch "{0}" is already checked out in the worktree at "{1}".', choice.refName, worktree.path);
3487-
await this.handleWorktreeConflict(worktree.path, message);
3488-
return;
3489-
}
3483+
if (choice.refName === repository.HEAD?.name) {
3484+
const message = l10n.t('Branch "{0}" is already checked out in the current repository.', choice.refName);
3485+
const createBranch = l10n.t('Create New Branch');
3486+
const pick = await window.showWarningMessage(message, { modal: true }, createBranch);
3487+
3488+
if (pick === createBranch) {
3489+
branch = await this.promptForBranchName(repository);
34903490

3491-
commitish = choice.refName;
3491+
if (!branch) {
3492+
return;
3493+
}
3494+
3495+
commitish = 'HEAD';
3496+
} else {
3497+
return;
3498+
}
3499+
} else {
3500+
// Check whether the selected branch is checked out in an existing worktree
3501+
const worktree = repository.worktrees.find(worktree => worktree.ref === choice.refId);
3502+
if (worktree) {
3503+
const message = l10n.t('Branch "{0}" is already checked out in the worktree at "{1}".', choice.refName, worktree.path);
3504+
await this.handleWorktreeConflict(worktree.path, message);
3505+
return;
3506+
}
3507+
commitish = choice.refName;
3508+
}
34923509
}
34933510

34943511
const worktreeName = ((branch ?? commitish).startsWith(branchPrefix)

resources/linux/debian/postinst.template

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Types: deb
9696
URIs: https://packages.microsoft.com/repos/code
9797
Suites: stable
9898
Components: main
99-
Architectures: amd64,arm64,armhf
99+
Architectures: @@ARCHITECTURE@@
100100
Signed-By: $CODE_TRUSTED_PART
101101
EOF
102102
if [ -f "$CODE_SOURCE_PART" ]; then
@@ -105,7 +105,7 @@ EOF
105105
else
106106
echo "### THIS FILE IS AUTOMATICALLY CONFIGURED ###
107107
# You may comment out this entry, but any other modifications may be lost.
108-
deb [arch=amd64,arm64,armhf] https://packages.microsoft.com/repos/code stable main" > $CODE_SOURCE_PART
108+
deb [arch=@@ARCHITECTURE@@] https://packages.microsoft.com/repos/code stable main" > $CODE_SOURCE_PART
109109
fi
110110

111111
# Sourced from https://packages.microsoft.com/keys/microsoft.asc

src/vs/base/browser/domSanitize.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,15 @@ export interface DomSanitizerConfig {
184184
readonly override?: readonly string[];
185185
};
186186

187+
/**
188+
* If set, replaces unsupported tags with their plaintext representation instead of removing them.
189+
*
190+
* For example, <p><bad>"text"</bad></p> becomes <p>"<bad>text</bad>"</p>.
191+
*/
192+
readonly replaceWithPlaintext?: boolean;
193+
187194
// TODO: move these into more controlled api
188195
readonly _do_not_use_hooks?: {
189-
readonly uponSanitizeElement?: UponSanitizeElementCb;
190196
readonly uponSanitizeAttribute?: UponSanitizeAttributeCb;
191197
};
192198
}
@@ -238,8 +244,8 @@ export function sanitizeHtml(untrusted: string, config?: DomSanitizerConfig): Tr
238244
config?.allowedLinkProtocols?.override ?? [Schemas.http, Schemas.https],
239245
config?.allowedMediaProtocols?.override ?? [Schemas.http, Schemas.https]));
240246

241-
if (config?._do_not_use_hooks?.uponSanitizeElement) {
242-
store.add(addDompurifyHook('uponSanitizeElement', config?._do_not_use_hooks.uponSanitizeElement));
247+
if (config?.replaceWithPlaintext) {
248+
store.add(addDompurifyHook('uponSanitizeElement', replaceWithPlainTextHook));
243249
}
244250

245251
if (config?._do_not_use_hooks?.uponSanitizeAttribute) {
@@ -255,6 +261,56 @@ export function sanitizeHtml(untrusted: string, config?: DomSanitizerConfig): Tr
255261
}
256262
}
257263

264+
const selfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
265+
266+
function replaceWithPlainTextHook(element: Element, data: dompurify.SanitizeElementHookEvent, _config: dompurify.Config) {
267+
if (!data.allowedTags[data.tagName] && data.tagName !== 'body') {
268+
const replacement = convertTagToPlaintext(element);
269+
if (element.nodeType === Node.COMMENT_NODE) {
270+
// Workaround for https://github.com/cure53/DOMPurify/issues/1005
271+
// The comment will be deleted in the next phase. However if we try to remove it now, it will cause
272+
// an exception. Instead we insert the text node before the comment.
273+
element.parentElement?.insertBefore(replacement, element);
274+
} else {
275+
element.parentElement?.replaceChild(replacement, element);
276+
}
277+
}
278+
}
279+
280+
export function convertTagToPlaintext(element: Element): DocumentFragment {
281+
let startTagText: string;
282+
let endTagText: string | undefined;
283+
if (element.nodeType === Node.COMMENT_NODE) {
284+
startTagText = `<!--${element.textContent}-->`;
285+
} else {
286+
const tagName = element.tagName.toLowerCase();
287+
const isSelfClosing = selfClosingTags.includes(tagName);
288+
const attrString = element.attributes.length ?
289+
' ' + Array.from(element.attributes)
290+
.map(attr => `${attr.name}="${attr.value}"`)
291+
.join(' ')
292+
: '';
293+
startTagText = `<${tagName}${attrString}>`;
294+
if (!isSelfClosing) {
295+
endTagText = `</${tagName}>`;
296+
}
297+
}
298+
299+
const fragment = document.createDocumentFragment();
300+
const textNode = element.ownerDocument.createTextNode(startTagText);
301+
fragment.appendChild(textNode);
302+
while (element.firstChild) {
303+
fragment.appendChild(element.firstChild);
304+
}
305+
306+
const endTagTextNode = endTagText ? element.ownerDocument.createTextNode(endTagText) : undefined;
307+
if (endTagTextNode) {
308+
fragment.appendChild(endTagTextNode);
309+
}
310+
311+
return fragment;
312+
}
313+
258314
/**
259315
* Sanitizes the given `value` and reset the given `node` with it.
260316
*/

src/vs/base/browser/markdownRenderer.ts

Lines changed: 19 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { escape } from '../common/strings.js';
2020
import { URI } from '../common/uri.js';
2121
import * as DOM from './dom.js';
2222
import * as domSanitize from './domSanitize.js';
23+
import { convertTagToPlaintext } from './domSanitize.js';
2324
import { DomEmitter } from './event.js';
2425
import { FormattedTextRenderOptions } from './formattedTextRenderer.js';
2526
import { StandardKeyboardEvent } from './keyboardEvent.js';
@@ -211,6 +212,20 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
211212
}));
212213
}
213214

215+
// Remove/disable inputs
216+
for (const input of [...element.getElementsByTagName('input')]) {
217+
if (input.attributes.getNamedItem('type')?.value === 'checkbox') {
218+
input.setAttribute('disabled', '');
219+
} else {
220+
if (options.sanitizerConfig?.replaceWithPlaintext) {
221+
const replacement = convertTagToPlaintext(input);
222+
input.parentElement?.replaceChild(replacement, input);
223+
} else {
224+
input.remove();
225+
}
226+
}
227+
}
228+
214229
return {
215230
element,
216231
dispose: () => {
@@ -412,15 +427,12 @@ function resolveWithBaseUri(baseUri: URI, href: string): string {
412427
}
413428
}
414429

415-
416-
const selfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
417-
418430
function sanitizeRenderedMarkdown(
419431
renderedMarkdown: string,
420432
isTrusted: boolean | MarkdownStringTrustedOptions,
421433
options: MarkdownSanitizerConfig = {},
422434
): TrustedHTML {
423-
const sanitizerConfig = getSanitizerOptions(isTrusted, options);
435+
const sanitizerConfig = getDomSanitizerConfig(isTrusted, options);
424436
return domSanitize.sanitizeHtml(renderedMarkdown, sanitizerConfig);
425437
}
426438

@@ -434,7 +446,6 @@ export const allowedMarkdownHtmlAttributes = [
434446
'autoplay',
435447
'alt',
436448
'checked',
437-
'class',
438449
'colspan',
439450
'controls',
440451
'disabled',
@@ -452,6 +463,7 @@ export const allowedMarkdownHtmlAttributes = [
452463
'type',
453464
'width',
454465
'start',
466+
'value',
455467

456468
// Custom markdown attributes
457469
'data-code',
@@ -462,7 +474,7 @@ export const allowedMarkdownHtmlAttributes = [
462474
'class',
463475
];
464476

465-
function getSanitizerOptions(isTrusted: boolean | MarkdownStringTrustedOptions, options: MarkdownSanitizerConfig): domSanitize.DomSanitizerConfig {
477+
function getDomSanitizerConfig(isTrusted: boolean | MarkdownStringTrustedOptions, options: MarkdownSanitizerConfig): domSanitize.DomSanitizerConfig {
466478
const allowedLinkSchemes = [
467479
Schemas.http,
468480
Schemas.https,
@@ -507,6 +519,7 @@ function getSanitizerOptions(isTrusted: boolean | MarkdownStringTrustedOptions,
507519
Schemas.vscodeRemoteResource,
508520
]
509521
},
522+
replaceWithPlaintext: options.replaceWithPlaintext,
510523
_do_not_use_hooks: {
511524
uponSanitizeAttribute: (element, e) => {
512525
if (options.customAttrSanitizer) {
@@ -545,61 +558,6 @@ function getSanitizerOptions(isTrusted: boolean | MarkdownStringTrustedOptions,
545558
e.keepAttr = false;
546559
}
547560
},
548-
uponSanitizeElement: (element, e) => {
549-
let wantsReplaceWithPlaintext = false;
550-
if (e.tagName === 'input') {
551-
if (element.attributes.getNamedItem('type')?.value === 'checkbox') {
552-
element.setAttribute('disabled', '');
553-
} else if (options.replaceWithPlaintext) {
554-
wantsReplaceWithPlaintext = true;
555-
} else {
556-
element.remove();
557-
return;
558-
}
559-
}
560-
561-
if (options.replaceWithPlaintext && (wantsReplaceWithPlaintext || (!e.allowedTags[e.tagName] && e.tagName !== 'body'))) {
562-
if (element.parentElement) {
563-
let startTagText: string;
564-
let endTagText: string | undefined;
565-
if (e.tagName === '#comment') {
566-
startTagText = `<!--${element.textContent}-->`;
567-
} else {
568-
const isSelfClosing = selfClosingTags.includes(e.tagName);
569-
const attrString = element.attributes.length ?
570-
' ' + Array.from(element.attributes)
571-
.map(attr => `${attr.name}="${attr.value}"`)
572-
.join(' ')
573-
: '';
574-
startTagText = `<${e.tagName}${attrString}>`;
575-
if (!isSelfClosing) {
576-
endTagText = `</${e.tagName}>`;
577-
}
578-
}
579-
580-
const fragment = document.createDocumentFragment();
581-
const textNode = element.parentElement.ownerDocument.createTextNode(startTagText);
582-
fragment.appendChild(textNode);
583-
const endTagTextNode = endTagText ? element.parentElement.ownerDocument.createTextNode(endTagText) : undefined;
584-
while (element.firstChild) {
585-
fragment.appendChild(element.firstChild);
586-
}
587-
588-
if (endTagTextNode) {
589-
fragment.appendChild(endTagTextNode);
590-
}
591-
592-
if (element.nodeType === Node.COMMENT_NODE) {
593-
// Workaround for https://github.com/cure53/DOMPurify/issues/1005
594-
// The comment will be deleted in the next phase. However if we try to remove it now, it will cause
595-
// an exception. Instead we insert the text node before the comment.
596-
element.parentElement.insertBefore(fragment, element);
597-
} else {
598-
element.parentElement.replaceChild(fragment, element);
599-
}
600-
}
601-
}
602-
}
603561
}
604562
};
605563
}

src/vs/base/test/browser/domSanitize.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,65 @@ suite('DomSanitize', () => {
113113

114114
assert.ok(str.includes('src="data:image/png;base64,'));
115115
});
116+
117+
suite('replaceWithPlaintext', () => {
118+
119+
test('replaces unsupported tags with plaintext representation', () => {
120+
const html = '<div>safe<script>alert(1)</script>content</div>';
121+
const result = sanitizeHtml(html, {
122+
replaceWithPlaintext: true
123+
});
124+
const str = result.toString();
125+
assert.strictEqual(str, `<div>safe&lt;script&gt;alert(1)&lt;/script&gt;content</div>`);
126+
});
127+
128+
test('handles self-closing tags correctly', () => {
129+
const html = '<div><input type="text"><custom-input /></div>';
130+
const result = sanitizeHtml(html, {
131+
replaceWithPlaintext: true
132+
});
133+
assert.strictEqual(result.toString(), '<div>&lt;input type="text"&gt;&lt;custom-input&gt;&lt;/custom-input&gt;</div>');
134+
});
135+
136+
test('handles tags with attributes', () => {
137+
const html = '<div><unknown-tag class="test" id="myid">content</unknown-tag></div>';
138+
const result = sanitizeHtml(html, {
139+
replaceWithPlaintext: true
140+
});
141+
assert.strictEqual(result.toString(), '<div>&lt;unknown-tag class="test" id="myid"&gt;content&lt;/unknown-tag&gt;</div>');
142+
});
143+
144+
test('handles nested unsupported tags', () => {
145+
const html = '<div><outer><inner>nested</inner></outer></div>';
146+
const result = sanitizeHtml(html, {
147+
replaceWithPlaintext: true
148+
});
149+
assert.strictEqual(result.toString(), '<div>&lt;outer&gt;&lt;inner&gt;nested&lt;/inner&gt;&lt;/outer&gt;</div>');
150+
});
151+
152+
test('handles comments correctly', () => {
153+
const html = '<div><!-- this is a comment -->content</div>';
154+
const result = sanitizeHtml(html, {
155+
replaceWithPlaintext: true
156+
});
157+
assert.strictEqual(result.toString(), '<div>&lt;!-- this is a comment --&gt;content</div>');
158+
});
159+
160+
test('handles empty tags', () => {
161+
const html = '<div><empty></empty></div>';
162+
const result = sanitizeHtml(html, {
163+
replaceWithPlaintext: true
164+
});
165+
assert.strictEqual(result.toString(), '<div>&lt;empty&gt;&lt;/empty&gt;</div>');
166+
});
167+
168+
test('works with custom allowed tags configuration', () => {
169+
const html = '<div><custom>allowed</custom><forbidden>not allowed</forbidden></div>';
170+
const result = sanitizeHtml(html, {
171+
replaceWithPlaintext: true,
172+
allowedTags: { augment: ['custom'] }
173+
});
174+
assert.strictEqual(result.toString(), '<div><custom>allowed</custom>&lt;forbidden&gt;not allowed&lt;/forbidden&gt;</div>');
175+
});
176+
});
116177
});

0 commit comments

Comments
 (0)