mirror of
https://github.com/microsoft/vscode.git
synced 2026-10-08 19:10:39 +01:00
* notifications: Harden untrusted notification text Render core notification and progress strings literally, with explicit typed links and a narrowly scoped extension compatibility capability. Preserve existing link actions, accessibility, and extension behavior, and guard reflected text in built-in extensions with consistent fallback helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * notifications: Address capability export and accessibility feedback Follow scope-aware aliases and returned values when checking exports of the legacy notification parsing capability. Cover named functions/classes, shadowing, recursive references, and legitimate bridge use. Make notification link guidance conditional for link-free Accessible View content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
132 lines
8.5 KiB
TypeScript
132 lines
8.5 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import assert from 'assert';
|
|
import { ESLint, RuleTester } from 'eslint';
|
|
import { suite, test } from 'node:test';
|
|
import * as path from 'path';
|
|
import tseslint from 'typescript-eslint';
|
|
import rule from '../../../.eslint-plugin-local/code-no-legacy-notification-parsing.ts';
|
|
|
|
RuleTester.describe = suite;
|
|
RuleTester.it = test;
|
|
|
|
const root = path.resolve(import.meta.dirname, '../../..');
|
|
const allowedFiles = [
|
|
'src/vs/workbench/api/browser/mainThreadMessageService.ts',
|
|
'src/vs/workbench/api/browser/mainThreadProgress.ts',
|
|
'src/vs/workbench/test/common/notifications.test.ts',
|
|
];
|
|
const coreFile = path.join(root, 'src/vs/workbench/contrib/example/browser/example.ts');
|
|
const source = '../../../../platform/notification/common/notificationLegacy.js';
|
|
const bridgeSource = '../../../platform/notification/common/notificationLegacy.js';
|
|
const options = [{ allowedFiles }];
|
|
|
|
new RuleTester({ languageOptions: { parser: tseslint.parser } }).run('code-no-legacy-notification-parsing', rule, {
|
|
valid: [
|
|
...allowedFiles.map(filename => ({
|
|
filename: path.join(root, filename),
|
|
options,
|
|
code: `import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; const options = { legacyExtensionLinkParsing: capability };`,
|
|
})),
|
|
...[
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; function unrelated(capability) { return capability; } export { unrelated };`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; { const alias = capability; consume(alias); } const alias = 'safe'; export { alias };`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export function notify() { service.notify({ legacyExtensionLinkParsing: capability }); return 'done'; }`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export class Bridge { notify() { return new Promise(resolve => { service.notify({ legacyExtensionLinkParsing: capability }); resolve(); }); } }`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export function isCapability(value) { return value === capability; }`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; function unused() { return capability; } export function notify() { return 'done'; }`,
|
|
`const first = () => second(); const second = () => first(); export { first };`,
|
|
].map(code => ({ filename: path.join(root, allowedFiles[0]), options, code })),
|
|
...[
|
|
`import type { LegacyExtensionLinkParsing } from '${source}';`,
|
|
`import { type LegacyExtensionLinkParsing } from '${source}';`,
|
|
`import type * as legacy from '${source}';`,
|
|
`import { isLegacyExtensionLinkParsing } from '${source}';`,
|
|
`export type { LegacyExtensionLinkParsing } from '${source}';`,
|
|
`export { type LegacyExtensionLinkParsing } from '${source}';`,
|
|
`export type * from '${source}';`,
|
|
`import { NotificationText } from '../../../../platform/notification/common/notificationMessage.js'; const message = NotificationText.link('Logs', 'command:showLogs');`,
|
|
`const message = { isTrusted: true };`,
|
|
`import { legacyExtensionLinkParsing } from './unrelated.js';`,
|
|
].map(code => ({ filename: coreFile, options, code })),
|
|
],
|
|
invalid: [
|
|
...[
|
|
`import { legacyExtensionLinkParsing } from '${source}';`,
|
|
`import { legacyExtensionLinkParsing as harmless } from '${source}';`,
|
|
`import { legacyExtensionLinkParsing } from 'vs/platform/notification/common/notificationLegacy.js';`,
|
|
`import { legacyExtensionLinkParsing } from '${source.replace('.js', '.ts')}';`,
|
|
].map(code => ({ filename: coreFile, options, code, errors: [{ messageId: 'restrictedCapability' }] })),
|
|
{
|
|
name: 'does not exempt arbitrary core tests',
|
|
filename: path.join(root, 'src/vs/workbench/test/common/another.test.ts'),
|
|
options,
|
|
code: `import { legacyExtensionLinkParsing } from '${bridgeSource}';`,
|
|
errors: [{ messageId: 'restrictedCapability' }],
|
|
},
|
|
...[
|
|
`import * as legacy from '${source}';`,
|
|
`import legacy from '${source}';`,
|
|
`const legacy = await import('${source}');`,
|
|
`const legacy = await import(\`${source}\`);`,
|
|
`const legacy = require('${source}');`,
|
|
`import legacy = require('${source}');`,
|
|
].map(code => ({ filename: coreFile, options, code, errors: [{ messageId: 'explicitImport' }] })),
|
|
...[
|
|
`export { legacyExtensionLinkParsing } from '${source}';`,
|
|
`export { legacyExtensionLinkParsing as other } from '${source}';`,
|
|
`export * from '${source}';`,
|
|
`export * as legacy from '${source}';`,
|
|
].map(code => ({ filename: coreFile, options, code, errors: [{ messageId: 'restrictedExport' }] })),
|
|
...[
|
|
`export { legacyExtensionLinkParsing } from '${bridgeSource}';`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export { capability };`,
|
|
`export { capability }; import { legacyExtensionLinkParsing as capability } from '${bridgeSource}';`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export default capability;`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export const compatibility = capability;`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export default { capability };`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export const compatibility = () => capability;`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; const alias = capability; export { alias };`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; const first = capability; const second = first; export { second as compatibility };`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; const alias = capability; export default alias;`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; const alias = capability; export const compatibility = { alias };`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export function getCapability() { return capability; }`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; function getCapability() { return capability; } export { getCapability };`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; const alias = capability; export function getCapability() { if (ready) { return alias; } }`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; function getCapability() { return capability; } export function getOptions() { return { legacyExtensionLinkParsing: getCapability() }; }`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; export class Compatibility { getCapability() { return capability; } }`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; class Compatibility { static get capability() { return capability; } } export { Compatibility };`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; const alias = capability; export class Compatibility { value = alias; }`,
|
|
`import { legacyExtensionLinkParsing as capability } from '${bridgeSource}'; const first = () => second(); const second = () => ready ? first() : capability; export { first };`,
|
|
].map(code => ({
|
|
filename: path.join(root, allowedFiles[0]),
|
|
options,
|
|
code,
|
|
errors: [{ messageId: 'restrictedExport' }],
|
|
})),
|
|
],
|
|
});
|
|
|
|
test('notification capability restriction is enabled for core in the repository config', async () => {
|
|
const linter = new ESLint({ cwd: root });
|
|
const code = `import { legacyExtensionLinkParsing } from '${bridgeSource}';`;
|
|
const filenames = [
|
|
allowedFiles[0],
|
|
allowedFiles[1],
|
|
'src/vs/workbench/api/browser/mainThreadSomethingElse.ts',
|
|
'src/vs/workbench/test/common/another.test.ts',
|
|
];
|
|
const results = await Promise.all(filenames.map(filePath => linter.lintText(code, { filePath: path.join(root, filePath) })));
|
|
assert.deepStrictEqual(results.map(result => result.flatMap(file => file.messages
|
|
.filter(message => message.ruleId === 'local/code-no-legacy-notification-parsing')
|
|
.map(message => ({ id: message.messageId, severity: message.severity })))), [
|
|
[],
|
|
[],
|
|
[{ id: 'restrictedCapability', severity: 2 }],
|
|
[{ id: 'restrictedCapability', severity: 2 }],
|
|
]);
|
|
});
|