mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-26 03:29:00 +01:00
[json] add trustedDomains settings (#287639)
* use trusted schemas * [json] add trustedDomains settings
This commit is contained in:
committed by
GitHub
parent
4641b2abb8
commit
067cb03d18
@@ -7,9 +7,9 @@ export type JSONLanguageStatus = { schemas: string[] };
|
||||
|
||||
import {
|
||||
workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation,
|
||||
Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange,
|
||||
Diagnostic, StatusBarAlignment, TextDocument, FormattingOptions, CancellationToken, FoldingRange,
|
||||
ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n,
|
||||
RelativePattern
|
||||
RelativePattern, CodeAction, CodeActionKind, CodeActionContext
|
||||
} from 'vscode';
|
||||
import {
|
||||
LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, DocumentDiagnosticReportKind,
|
||||
@@ -20,8 +20,9 @@ import {
|
||||
|
||||
|
||||
import { hash } from './utils/hash';
|
||||
import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem } from './languageStatus';
|
||||
import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem, createSchemaLoadIssueItem, createSchemaLoadStatusItem } from './languageStatus';
|
||||
import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';
|
||||
import { matchesUrlPattern } from './utils/urlMatch';
|
||||
|
||||
namespace VSCodeContentRequest {
|
||||
export const type: RequestType<string, string, any> = new RequestType('vscode/content');
|
||||
@@ -42,6 +43,7 @@ namespace LanguageStatusRequest {
|
||||
namespace ValidateContentRequest {
|
||||
export const type: RequestType<{ schemaUri: string; content: string }, LSPDiagnostic[], any> = new RequestType('json/validateContent');
|
||||
}
|
||||
|
||||
interface SortOptions extends LSPFormattingOptions {
|
||||
}
|
||||
|
||||
@@ -110,6 +112,7 @@ export namespace SettingIds {
|
||||
export const enableKeepLines = 'json.format.keepLines';
|
||||
export const enableValidation = 'json.validate.enable';
|
||||
export const enableSchemaDownload = 'json.schemaDownload.enable';
|
||||
export const trustedDomains = 'json.schemaDownload.trustedDomains';
|
||||
export const maxItemsComputed = 'json.maxItemsComputed';
|
||||
export const editorFoldingMaximumRegions = 'editor.foldingMaximumRegions';
|
||||
export const editorColorDecoratorsLimit = 'editor.colorDecoratorsLimit';
|
||||
@@ -119,6 +122,17 @@ export namespace SettingIds {
|
||||
export const colorDecoratorsLimit = 'colorDecoratorsLimit';
|
||||
}
|
||||
|
||||
export namespace CommandIds {
|
||||
export const workbenchActionOpenSettings = 'workbench.action.openSettings';
|
||||
export const workbenchTrustManage = 'workbench.trust.manage';
|
||||
export const retryResolveSchemaCommandId = '_json.retryResolveSchema';
|
||||
export const configureTrustedDomainsCommandId = '_json.configureTrustedDomains';
|
||||
export const showAssociatedSchemaList = '_json.showAssociatedSchemaList';
|
||||
export const clearCacheCommandId = 'json.clearCache';
|
||||
export const validateCommandId = 'json.validate';
|
||||
export const sortCommandId = 'json.sort';
|
||||
}
|
||||
|
||||
export interface TelemetryReporter {
|
||||
sendTelemetryEvent(eventName: string, properties?: {
|
||||
[key: string]: string;
|
||||
@@ -143,6 +157,16 @@ export interface SchemaRequestService {
|
||||
clearCache?(): Promise<string[]>;
|
||||
}
|
||||
|
||||
export enum SchemaRequestServiceErrors {
|
||||
UntrustedWorkspaceError = 1,
|
||||
UntrustedSchemaError = 2,
|
||||
OpenTextDocumentAccessError = 3,
|
||||
HTTPDisabledError = 4,
|
||||
HTTPError = 5,
|
||||
VSCodeAccessError = 6,
|
||||
UntitledAccessError = 7,
|
||||
}
|
||||
|
||||
export const languageServerDescription = l10n.t('JSON Language Server');
|
||||
|
||||
let resultLimit = 5000;
|
||||
@@ -191,6 +215,8 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
const toDispose: Disposable[] = [];
|
||||
|
||||
let rangeFormatting: Disposable | undefined = undefined;
|
||||
let settingsCache: Settings | undefined = undefined;
|
||||
let schemaAssociationsCache: Promise<ISchemaAssociation[]> | undefined = undefined;
|
||||
|
||||
const documentSelector = languageParticipants.documentSelector;
|
||||
|
||||
@@ -200,14 +226,18 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
toDispose.push(schemaResolutionErrorStatusBarItem);
|
||||
|
||||
const fileSchemaErrors = new Map<string, string>();
|
||||
let schemaDownloadEnabled = true;
|
||||
let schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload);
|
||||
let trustedDomains = workspace.getConfiguration().get<Record<string, boolean>>(SettingIds.trustedDomains, {});
|
||||
|
||||
let isClientReady = false;
|
||||
|
||||
const documentSymbolsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentSymbolsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit));
|
||||
toDispose.push(documentSymbolsLimitStatusbarItem);
|
||||
|
||||
toDispose.push(commands.registerCommand('json.clearCache', async () => {
|
||||
const schemaLoadStatusItem = createSchemaLoadStatusItem((diagnostic: Diagnostic) => createSchemaLoadIssueItem(documentSelector, schemaDownloadEnabled, diagnostic));
|
||||
toDispose.push(schemaLoadStatusItem);
|
||||
|
||||
toDispose.push(commands.registerCommand(CommandIds.clearCacheCommandId, async () => {
|
||||
if (isClientReady && runtime.schemaRequests.clearCache) {
|
||||
const cachedSchemas = await runtime.schemaRequests.clearCache();
|
||||
await client.sendNotification(SchemaContentChangeNotification.type, cachedSchemas);
|
||||
@@ -215,12 +245,12 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
window.showInformationMessage(l10n.t('JSON schema cache cleared.'));
|
||||
}));
|
||||
|
||||
toDispose.push(commands.registerCommand('json.validate', async (schemaUri: Uri, content: string) => {
|
||||
toDispose.push(commands.registerCommand(CommandIds.validateCommandId, async (schemaUri: Uri, content: string) => {
|
||||
const diagnostics: LSPDiagnostic[] = await client.sendRequest(ValidateContentRequest.type, { schemaUri: schemaUri.toString(), content });
|
||||
return diagnostics.map(client.protocol2CodeConverter.asDiagnostic);
|
||||
}));
|
||||
|
||||
toDispose.push(commands.registerCommand('json.sort', async () => {
|
||||
toDispose.push(commands.registerCommand(CommandIds.sortCommandId, async () => {
|
||||
|
||||
if (isClientReady) {
|
||||
const textEditor = window.activeTextEditor;
|
||||
@@ -239,17 +269,10 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
}
|
||||
}));
|
||||
|
||||
function filterSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] {
|
||||
const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError);
|
||||
if (schemaErrorIndex !== -1) {
|
||||
const schemaResolveDiagnostic = diagnostics[schemaErrorIndex];
|
||||
fileSchemaErrors.set(uri.toString(), schemaResolveDiagnostic.message);
|
||||
if (!schemaDownloadEnabled) {
|
||||
diagnostics = diagnostics.filter(d => !isSchemaResolveError(d));
|
||||
}
|
||||
if (window.activeTextEditor && window.activeTextEditor.document.uri.toString() === uri.toString()) {
|
||||
schemaResolutionErrorStatusBarItem.show();
|
||||
}
|
||||
function handleSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] {
|
||||
schemaLoadStatusItem.update(uri, diagnostics);
|
||||
if (!schemaDownloadEnabled) {
|
||||
return diagnostics.filter(d => !isSchemaResolveError(d));
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
@@ -270,18 +293,18 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
},
|
||||
middleware: {
|
||||
workspace: {
|
||||
didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() })
|
||||
didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings(true) })
|
||||
},
|
||||
provideDiagnostics: async (uriOrDoc, previousResolutId, token, next) => {
|
||||
const diagnostics = await next(uriOrDoc, previousResolutId, token);
|
||||
if (diagnostics && diagnostics.kind === DocumentDiagnosticReportKind.Full) {
|
||||
const uri = uriOrDoc instanceof Uri ? uriOrDoc : uriOrDoc.uri;
|
||||
diagnostics.items = filterSchemaErrorDiagnostics(uri, diagnostics.items);
|
||||
diagnostics.items = handleSchemaErrorDiagnostics(uri, diagnostics.items);
|
||||
}
|
||||
return diagnostics;
|
||||
},
|
||||
handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => {
|
||||
diagnostics = filterSchemaErrorDiagnostics(uri, diagnostics);
|
||||
diagnostics = handleSchemaErrorDiagnostics(uri, diagnostics);
|
||||
next(uri, diagnostics);
|
||||
},
|
||||
// testing the replace / insert mode
|
||||
@@ -373,7 +396,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
const uri = Uri.parse(uriPath);
|
||||
const uriString = uri.toString(true);
|
||||
if (uri.scheme === 'untitled') {
|
||||
throw new ResponseError(3, l10n.t('Unable to load {0}', uriString));
|
||||
throw new ResponseError(SchemaRequestServiceErrors.UntitledAccessError, l10n.t('Unable to load {0}', uriString));
|
||||
}
|
||||
if (uri.scheme === 'vscode') {
|
||||
try {
|
||||
@@ -382,7 +405,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
const content = await workspace.fs.readFile(uri);
|
||||
return new TextDecoder().decode(content);
|
||||
} catch (e) {
|
||||
throw new ResponseError(5, e.toString(), e);
|
||||
throw new ResponseError(SchemaRequestServiceErrors.VSCodeAccessError, e.toString(), e);
|
||||
}
|
||||
} else if (uri.scheme !== 'http' && uri.scheme !== 'https') {
|
||||
try {
|
||||
@@ -390,9 +413,15 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
schemaDocuments[uriString] = true;
|
||||
return document.getText();
|
||||
} catch (e) {
|
||||
throw new ResponseError(2, e.toString(), e);
|
||||
throw new ResponseError(SchemaRequestServiceErrors.OpenTextDocumentAccessError, e.toString(), e);
|
||||
}
|
||||
} else if (schemaDownloadEnabled) {
|
||||
if (!workspace.isTrusted) {
|
||||
throw new ResponseError(SchemaRequestServiceErrors.UntrustedWorkspaceError, l10n.t('Downloading schemas is disabled in untrusted workspaces'));
|
||||
}
|
||||
if (!await isTrusted(uri)) {
|
||||
throw new ResponseError(SchemaRequestServiceErrors.UntrustedSchemaError, l10n.t('Location {0} is untrusted', uriString));
|
||||
}
|
||||
} else if (schemaDownloadEnabled && workspace.isTrusted) {
|
||||
if (runtime.telemetry && uri.authority === 'schema.management.azure.com') {
|
||||
/* __GDPR__
|
||||
"json.schema" : {
|
||||
@@ -406,13 +435,10 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
try {
|
||||
return await runtime.schemaRequests.getContent(uriString);
|
||||
} catch (e) {
|
||||
throw new ResponseError(4, e.toString());
|
||||
throw new ResponseError(SchemaRequestServiceErrors.HTTPError, e.toString(), e);
|
||||
}
|
||||
} else {
|
||||
if (!workspace.isTrusted) {
|
||||
throw new ResponseError(1, l10n.t('Downloading schemas is disabled in untrusted workspaces'));
|
||||
}
|
||||
throw new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload));
|
||||
throw new ResponseError(SchemaRequestServiceErrors.HTTPDisabledError, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -427,19 +453,6 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const handleActiveEditorChange = (activeEditor?: TextEditor) => {
|
||||
if (!activeEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeDocUri = activeEditor.document.uri.toString();
|
||||
|
||||
if (activeDocUri && fileSchemaErrors.has(activeDocUri)) {
|
||||
schemaResolutionErrorStatusBarItem.show();
|
||||
} else {
|
||||
schemaResolutionErrorStatusBarItem.hide();
|
||||
}
|
||||
};
|
||||
const handleContentClosed = (uriString: string) => {
|
||||
if (handleContentChange(uriString)) {
|
||||
delete schemaDocuments[uriString];
|
||||
@@ -484,59 +497,81 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString())));
|
||||
toDispose.push(workspace.onDidCloseTextDocument(d => handleContentClosed(d.uri.toString())));
|
||||
|
||||
toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange));
|
||||
toDispose.push(commands.registerCommand(CommandIds.retryResolveSchemaCommandId, triggerValidation));
|
||||
|
||||
const handleRetryResolveSchemaCommand = () => {
|
||||
if (window.activeTextEditor) {
|
||||
schemaResolutionErrorStatusBarItem.text = '$(watch)';
|
||||
const activeDocUri = window.activeTextEditor.document.uri.toString();
|
||||
client.sendRequest(ForceValidateRequest.type, activeDocUri).then((diagnostics) => {
|
||||
const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError);
|
||||
if (schemaErrorIndex !== -1) {
|
||||
// Show schema resolution errors in status bar only; ref: #51032
|
||||
const schemaResolveDiagnostic = diagnostics[schemaErrorIndex];
|
||||
fileSchemaErrors.set(activeDocUri, schemaResolveDiagnostic.message);
|
||||
} else {
|
||||
schemaResolutionErrorStatusBarItem.hide();
|
||||
toDispose.push(commands.registerCommand(CommandIds.configureTrustedDomainsCommandId, configureTrustedDomains));
|
||||
|
||||
toDispose.push(languages.registerCodeActionsProvider(documentSelector, {
|
||||
provideCodeActions(_document: TextDocument, _range: Range, context: CodeActionContext): CodeAction[] {
|
||||
const codeActions: CodeAction[] = [];
|
||||
|
||||
for (const diagnostic of context.diagnostics) {
|
||||
if (typeof diagnostic.code !== 'number') {
|
||||
continue;
|
||||
}
|
||||
schemaResolutionErrorStatusBarItem.text = '$(alert)';
|
||||
});
|
||||
switch (diagnostic.code) {
|
||||
case ErrorCodes.UntrustedSchemaError: {
|
||||
const title = l10n.t('Configure Trusted Domains...');
|
||||
const action = new CodeAction(title, CodeActionKind.QuickFix);
|
||||
const schemaUri = diagnostic.relatedInformation?.[0]?.location.uri;
|
||||
if (schemaUri) {
|
||||
action.command = { command: CommandIds.configureTrustedDomainsCommandId, arguments: [schemaUri.toString()], title };
|
||||
} else {
|
||||
action.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.trustedDomains], title };
|
||||
}
|
||||
action.diagnostics = [diagnostic];
|
||||
action.isPreferred = true;
|
||||
codeActions.push(action);
|
||||
}
|
||||
break;
|
||||
case ErrorCodes.HTTPDisabledError: {
|
||||
const title = l10n.t('Enable Schema Downloading...');
|
||||
const action = new CodeAction(title, CodeActionKind.QuickFix);
|
||||
action.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.enableSchemaDownload], title };
|
||||
action.diagnostics = [diagnostic];
|
||||
action.isPreferred = true;
|
||||
codeActions.push(action);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return codeActions;
|
||||
}
|
||||
};
|
||||
|
||||
toDispose.push(commands.registerCommand('_json.retryResolveSchema', handleRetryResolveSchemaCommand));
|
||||
|
||||
client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations());
|
||||
|
||||
toDispose.push(extensions.onDidChange(async _ => {
|
||||
client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations());
|
||||
}, {
|
||||
providedCodeActionKinds: [CodeActionKind.QuickFix]
|
||||
}));
|
||||
|
||||
const associationWatcher = workspace.createFileSystemWatcher(new RelativePattern(
|
||||
Uri.parse(`vscode://schemas-associations/`),
|
||||
'**/schemas-associations.json')
|
||||
);
|
||||
client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(false));
|
||||
|
||||
toDispose.push(extensions.onDidChange(async _ => {
|
||||
client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(true));
|
||||
}));
|
||||
|
||||
const associationWatcher = workspace.createFileSystemWatcher(new RelativePattern(Uri.parse(`vscode://schemas-associations/`), '**/schemas-associations.json'));
|
||||
toDispose.push(associationWatcher);
|
||||
toDispose.push(associationWatcher.onDidChange(async _e => {
|
||||
client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations());
|
||||
client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(true));
|
||||
}));
|
||||
|
||||
// manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652.
|
||||
updateFormatterRegistration();
|
||||
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });
|
||||
|
||||
updateSchemaDownloadSetting();
|
||||
|
||||
toDispose.push(workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(SettingIds.enableFormatter)) {
|
||||
updateFormatterRegistration();
|
||||
} else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) {
|
||||
updateSchemaDownloadSetting();
|
||||
schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload);
|
||||
triggerValidation();
|
||||
} else if (e.affectsConfiguration(SettingIds.editorFoldingMaximumRegions) || e.affectsConfiguration(SettingIds.editorColorDecoratorsLimit)) {
|
||||
client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() });
|
||||
client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings(true) });
|
||||
} else if (e.affectsConfiguration(SettingIds.trustedDomains)) {
|
||||
trustedDomains = workspace.getConfiguration().get<Record<string, boolean>>(SettingIds.trustedDomains, {});
|
||||
triggerValidation();
|
||||
}
|
||||
}));
|
||||
toDispose.push(workspace.onDidGrantWorkspaceTrust(updateSchemaDownloadSetting));
|
||||
toDispose.push(workspace.onDidGrantWorkspaceTrust(() => triggerValidation()));
|
||||
|
||||
toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri)));
|
||||
|
||||
@@ -572,20 +607,13 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
}
|
||||
}
|
||||
|
||||
function updateSchemaDownloadSetting() {
|
||||
if (!workspace.isTrusted) {
|
||||
schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Unable to download schemas in untrusted workspaces.');
|
||||
schemaResolutionErrorStatusBarItem.command = 'workbench.trust.manage';
|
||||
return;
|
||||
}
|
||||
schemaDownloadEnabled = workspace.getConfiguration().get(SettingIds.enableSchemaDownload) !== false;
|
||||
if (schemaDownloadEnabled) {
|
||||
schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Unable to resolve schema. Click to retry.');
|
||||
schemaResolutionErrorStatusBarItem.command = '_json.retryResolveSchema';
|
||||
handleRetryResolveSchemaCommand();
|
||||
} else {
|
||||
schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Downloading schemas is disabled. Click to configure.');
|
||||
schemaResolutionErrorStatusBarItem.command = { command: 'workbench.action.openSettings', arguments: [SettingIds.enableSchemaDownload], title: '' };
|
||||
async function triggerValidation() {
|
||||
const activeTextEditor = window.activeTextEditor;
|
||||
if (activeTextEditor && languageParticipants.hasLanguage(activeTextEditor.document.languageId)) {
|
||||
schemaResolutionErrorStatusBarItem.text = '$(watch)';
|
||||
schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Validating...');
|
||||
const activeDocUri = activeTextEditor.document.uri.toString();
|
||||
await client.sendRequest(ForceValidateRequest.type, activeDocUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,6 +640,113 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
});
|
||||
}
|
||||
|
||||
function getSettings(forceRefresh: boolean): Settings {
|
||||
if (!settingsCache || forceRefresh) {
|
||||
settingsCache = computeSettings();
|
||||
}
|
||||
return settingsCache;
|
||||
}
|
||||
|
||||
async function getSchemaAssociations(forceRefresh: boolean): Promise<ISchemaAssociation[]> {
|
||||
if (!schemaAssociationsCache || forceRefresh) {
|
||||
schemaAssociationsCache = computeSchemaAssociations();
|
||||
runtime.logOutputChannel.info(`Computed schema associations: ${(await schemaAssociationsCache).map(a => `${a.uri} -> [${a.fileMatch.join(', ')}]`).join('\n')}`);
|
||||
|
||||
}
|
||||
return schemaAssociationsCache;
|
||||
}
|
||||
|
||||
async function isTrusted(uri: Uri): Promise<boolean> {
|
||||
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
|
||||
return true;
|
||||
}
|
||||
const uriString = uri.toString(true);
|
||||
|
||||
// Check against trustedDomains setting
|
||||
if (matchesUrlPattern(uri, trustedDomains)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const knownAssociations = await getSchemaAssociations(false);
|
||||
for (const association of knownAssociations) {
|
||||
if (association.uri === uriString) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const settingsCache = getSettings(false);
|
||||
if (settingsCache.json && settingsCache.json.schemas) {
|
||||
for (const schemaSetting of settingsCache.json.schemas) {
|
||||
const schemaUri = schemaSetting.url;
|
||||
if (schemaUri === uriString) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function configureTrustedDomains(schemaUri: string): Promise<void> {
|
||||
interface QuickPickItemWithAction {
|
||||
label: string;
|
||||
description?: string;
|
||||
execute: () => Promise<void>;
|
||||
}
|
||||
|
||||
const items: QuickPickItemWithAction[] = [];
|
||||
|
||||
try {
|
||||
const uri = Uri.parse(schemaUri);
|
||||
const domain = `${uri.scheme}://${uri.authority}`;
|
||||
|
||||
// Add "Trust domain" option
|
||||
items.push({
|
||||
label: l10n.t('Trust Domain: {0}', domain),
|
||||
description: l10n.t('Allow all schemas from this domain'),
|
||||
execute: async () => {
|
||||
const config = workspace.getConfiguration();
|
||||
const currentDomains = config.get<Record<string, boolean>>(SettingIds.trustedDomains, {});
|
||||
currentDomains[domain] = true;
|
||||
await config.update(SettingIds.trustedDomains, currentDomains, true);
|
||||
await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains);
|
||||
}
|
||||
});
|
||||
|
||||
// Add "Trust URI" option
|
||||
items.push({
|
||||
label: l10n.t('Trust URI: {0}', schemaUri),
|
||||
description: l10n.t('Allow only this specific schema'),
|
||||
execute: async () => {
|
||||
const config = workspace.getConfiguration();
|
||||
const currentDomains = config.get<Record<string, boolean>>(SettingIds.trustedDomains, {});
|
||||
currentDomains[schemaUri] = true;
|
||||
await config.update(SettingIds.trustedDomains, currentDomains, true);
|
||||
await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
runtime.logOutputChannel.error(`Failed to parse schema URI: ${schemaUri}`);
|
||||
}
|
||||
|
||||
|
||||
// Always add "Configure setting" option
|
||||
items.push({
|
||||
label: l10n.t('Configure Setting'),
|
||||
description: l10n.t('Open settings editor'),
|
||||
execute: async () => {
|
||||
await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains);
|
||||
}
|
||||
});
|
||||
|
||||
const selected = await window.showQuickPick(items, {
|
||||
placeHolder: l10n.t('Select how to configure trusted schema domains')
|
||||
});
|
||||
|
||||
if (selected) {
|
||||
await selected.execute();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
dispose: async () => {
|
||||
await client.stop();
|
||||
@@ -621,9 +756,9 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
|
||||
};
|
||||
}
|
||||
|
||||
async function getSchemaAssociations(): Promise<ISchemaAssociation[]> {
|
||||
return getSchemaExtensionAssociations()
|
||||
.concat(await getDynamicSchemaAssociations());
|
||||
async function computeSchemaAssociations(): Promise<ISchemaAssociation[]> {
|
||||
const extensionAssociations = getSchemaExtensionAssociations();
|
||||
return extensionAssociations.concat(await getDynamicSchemaAssociations());
|
||||
}
|
||||
|
||||
function getSchemaExtensionAssociations(): ISchemaAssociation[] {
|
||||
@@ -680,7 +815,9 @@ async function getDynamicSchemaAssociations(): Promise<ISchemaAssociation[]> {
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSettings(): Settings {
|
||||
|
||||
|
||||
function computeSettings(): Settings {
|
||||
const configuration = workspace.getConfiguration();
|
||||
const httpSettings = workspace.getConfiguration('http');
|
||||
|
||||
@@ -781,8 +918,14 @@ function updateMarkdownString(h: MarkdownString): MarkdownString {
|
||||
return n;
|
||||
}
|
||||
|
||||
function isSchemaResolveError(d: Diagnostic) {
|
||||
return d.code === /* SchemaResolveError */ 0x300;
|
||||
export namespace ErrorCodes {
|
||||
export const SchemaResolveError = 0x10000;
|
||||
export const UntrustedSchemaError = SchemaResolveError + SchemaRequestServiceErrors.UntrustedSchemaError;
|
||||
export const HTTPDisabledError = SchemaResolveError + SchemaRequestServiceErrors.HTTPDisabledError;
|
||||
}
|
||||
|
||||
export function isSchemaResolveError(d: Diagnostic) {
|
||||
return typeof d.code === 'number' && d.code >= ErrorCodes.SchemaResolveError;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
import {
|
||||
window, languages, Uri, Disposable, commands, QuickPickItem,
|
||||
extensions, workspace, Extension, WorkspaceFolder, QuickPickItemKind,
|
||||
ThemeIcon, TextDocument, LanguageStatusSeverity, l10n, DocumentSelector
|
||||
ThemeIcon, TextDocument, LanguageStatusSeverity, l10n, DocumentSelector, Diagnostic
|
||||
} from 'vscode';
|
||||
import { JSONLanguageStatus, JSONSchemaSettings } from './jsonClient';
|
||||
import { CommandIds, ErrorCodes, isSchemaResolveError, JSONLanguageStatus, JSONSchemaSettings, SettingIds } from './jsonClient';
|
||||
|
||||
type ShowSchemasInput = {
|
||||
schemas: string[];
|
||||
@@ -168,7 +168,7 @@ export function createLanguageStatusItem(documentSelector: DocumentSelector, sta
|
||||
statusItem.name = l10n.t('JSON Validation Status');
|
||||
statusItem.severity = LanguageStatusSeverity.Information;
|
||||
|
||||
const showSchemasCommand = commands.registerCommand('_json.showAssociatedSchemaList', showSchemaList);
|
||||
const showSchemasCommand = commands.registerCommand(CommandIds.showAssociatedSchemaList, showSchemaList);
|
||||
|
||||
const activeEditorListener = window.onDidChangeActiveTextEditor(() => {
|
||||
updateLanguageStatus();
|
||||
@@ -195,7 +195,7 @@ export function createLanguageStatusItem(documentSelector: DocumentSelector, sta
|
||||
statusItem.detail = l10n.t('multiple JSON schemas configured');
|
||||
}
|
||||
statusItem.command = {
|
||||
command: '_json.showAssociatedSchemaList',
|
||||
command: CommandIds.showAssociatedSchemaList,
|
||||
title: l10n.t('Show Schemas'),
|
||||
arguments: [{ schemas, uri: document.uri.toString() } satisfies ShowSchemasInput]
|
||||
};
|
||||
@@ -279,3 +279,86 @@ export function createDocumentSymbolsLimitItem(documentSelector: DocumentSelecto
|
||||
}
|
||||
|
||||
|
||||
export function createSchemaLoadStatusItem(newItem: (fileSchemaError: Diagnostic) => Disposable) {
|
||||
let statusItem: Disposable | undefined;
|
||||
const fileSchemaErrors: Map<string, Diagnostic> = new Map();
|
||||
|
||||
const toDispose: Disposable[] = [];
|
||||
toDispose.push(window.onDidChangeActiveTextEditor(textEditor => {
|
||||
statusItem?.dispose();
|
||||
statusItem = undefined;
|
||||
const doc = textEditor?.document;
|
||||
if (doc) {
|
||||
const fileSchemaError = fileSchemaErrors.get(doc.uri.toString());
|
||||
if (fileSchemaError !== undefined) {
|
||||
statusItem = newItem(fileSchemaError);
|
||||
}
|
||||
}
|
||||
}));
|
||||
toDispose.push(workspace.onDidCloseTextDocument(document => {
|
||||
fileSchemaErrors.delete(document.uri.toString());
|
||||
}));
|
||||
|
||||
function update(uri: Uri, diagnostics: Diagnostic[]) {
|
||||
const fileSchemaError = diagnostics.find(isSchemaResolveError);
|
||||
const uriString = uri.toString();
|
||||
|
||||
if (fileSchemaError === undefined) {
|
||||
fileSchemaErrors.delete(uriString);
|
||||
if (statusItem && uriString === window.activeTextEditor?.document.uri.toString()) {
|
||||
statusItem.dispose();
|
||||
statusItem = undefined;
|
||||
}
|
||||
} else {
|
||||
const current = fileSchemaErrors.get(uriString);
|
||||
if (current?.message === fileSchemaError.message) {
|
||||
return;
|
||||
}
|
||||
fileSchemaErrors.set(uriString, fileSchemaError);
|
||||
if (uriString === window.activeTextEditor?.document.uri.toString()) {
|
||||
statusItem?.dispose();
|
||||
statusItem = newItem(fileSchemaError);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
update,
|
||||
dispose() {
|
||||
statusItem?.dispose();
|
||||
toDispose.forEach(d => d.dispose());
|
||||
toDispose.length = 0;
|
||||
statusItem = undefined;
|
||||
fileSchemaErrors.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function createSchemaLoadIssueItem(documentSelector: DocumentSelector, schemaDownloadEnabled: boolean | undefined, diagnostic: Diagnostic): Disposable {
|
||||
const statusItem = languages.createLanguageStatusItem('json.documentSymbolsStatus', documentSelector);
|
||||
statusItem.name = l10n.t('JSON Outline Status');
|
||||
statusItem.severity = LanguageStatusSeverity.Error;
|
||||
statusItem.text = 'Schema download issue';
|
||||
if (!workspace.isTrusted) {
|
||||
statusItem.detail = l10n.t('Workspace untrusted');
|
||||
statusItem.command = { command: CommandIds.workbenchTrustManage, title: 'Configure Trust' };
|
||||
} else if (!schemaDownloadEnabled) {
|
||||
statusItem.detail = l10n.t('Download disabled');
|
||||
statusItem.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.enableSchemaDownload], title: 'Configure' };
|
||||
} else if (typeof diagnostic.code === 'number' && diagnostic.code === ErrorCodes.UntrustedSchemaError) {
|
||||
statusItem.detail = l10n.t('Location untrusted');
|
||||
const schemaUri = diagnostic.relatedInformation?.[0]?.location.uri;
|
||||
if (schemaUri) {
|
||||
statusItem.command = { command: CommandIds.configureTrustedDomainsCommandId, arguments: [schemaUri.toString()], title: 'Configure Trusted Domains' };
|
||||
} else {
|
||||
statusItem.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.trustedDomains], title: 'Configure Trusted Domains' };
|
||||
}
|
||||
} else {
|
||||
statusItem.detail = l10n.t('Unable to resolve schema');
|
||||
statusItem.command = { command: CommandIds.retryResolveSchemaCommandId, title: 'Retry' };
|
||||
}
|
||||
return Disposable.from(statusItem);
|
||||
}
|
||||
|
||||
|
||||
|
||||
107
extensions/json-language-features/client/src/utils/urlMatch.ts
Normal file
107
extensions/json-language-features/client/src/utils/urlMatch.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
/**
|
||||
* Check whether a URL matches the list of trusted domains or URIs.
|
||||
*
|
||||
* trustedDomains is an object where:
|
||||
* - Keys are full domains (https://www.microsoft.com) or full URIs (https://www.test.com/schemas/mySchema.json)
|
||||
* - Keys can include wildcards (https://*.microsoft.com) or glob patterns
|
||||
* - Values are booleans indicating if the domain/URI is trusted (true) or blocked (false)
|
||||
*
|
||||
* @param url The URL to check
|
||||
* @param trustedDomains Object mapping domain patterns to boolean trust values
|
||||
*/
|
||||
export function matchesUrlPattern(url: Uri, trustedDomains: Record<string, boolean>): boolean {
|
||||
// Check localhost
|
||||
if (isLocalhostAuthority(url.authority)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const [pattern, isTrusted] of Object.entries(trustedDomains)) {
|
||||
if (typeof pattern !== 'string' || pattern.trim() === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wildcard matches everything
|
||||
if (pattern === '*') {
|
||||
return isTrusted;
|
||||
}
|
||||
|
||||
try {
|
||||
const patternUri = Uri.parse(pattern);
|
||||
|
||||
// Scheme must match
|
||||
if (url.scheme !== patternUri.scheme) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check authority (host:port)
|
||||
if (!matchesAuthority(url.authority, patternUri.authority)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check path
|
||||
if (!matchesPath(url.path, patternUri.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isTrusted;
|
||||
} catch {
|
||||
// Invalid pattern, skip
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchesAuthority(urlAuthority: string, patternAuthority: string): boolean {
|
||||
urlAuthority = urlAuthority.toLowerCase();
|
||||
patternAuthority = patternAuthority.toLowerCase();
|
||||
|
||||
if (patternAuthority === urlAuthority) {
|
||||
return true;
|
||||
}
|
||||
// Handle wildcard subdomains (e.g., *.github.com)
|
||||
if (patternAuthority.startsWith('*.')) {
|
||||
const patternDomain = patternAuthority.substring(2);
|
||||
// Exact match or subdomain match
|
||||
return urlAuthority === patternDomain || urlAuthority.endsWith('.' + patternDomain);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchesPath(urlPath: string, patternPath: string): boolean {
|
||||
// Empty pattern path or just "/" matches any path
|
||||
if (!patternPath || patternPath === '/') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exact match
|
||||
if (urlPath === patternPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If pattern ends with '/', it matches any path starting with it
|
||||
if (patternPath.endsWith('/')) {
|
||||
return urlPath.startsWith(patternPath);
|
||||
}
|
||||
|
||||
// Otherwise, pattern must be a prefix
|
||||
return urlPath.startsWith(patternPath + '/') || urlPath === patternPath;
|
||||
}
|
||||
|
||||
|
||||
const rLocalhost = /^(.+\.)?localhost(:\d+)?$/i;
|
||||
const r127 = /^127\.0\.0\.1(:\d+)?$/;
|
||||
const rIPv6Localhost = /^\[::1\](:\d+)?$/;
|
||||
|
||||
function isLocalhostAuthority(authority: string): boolean {
|
||||
return rLocalhost.test(authority) || r127.test(authority) || rIPv6Localhost.test(authority);
|
||||
}
|
||||
Reference in New Issue
Block a user