Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable XSD validation when xsi:schemaLocation doesn't declare the hint for the document element root. #953

Merged
merged 1 commit into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
package org.eclipse.lemminx.extensions.contentmodel;

import java.util.Objects;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.extensions.contentmodel.commands.XMLValidationAllFilesCommand;
import org.eclipse.lemminx.extensions.contentmodel.commands.XMLValidationFileCommand;
Expand All @@ -24,6 +26,7 @@
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelTypeDefinitionParticipant;
import org.eclipse.lemminx.extensions.contentmodel.participants.diagnostics.ContentModelDiagnosticsParticipant;
import org.eclipse.lemminx.extensions.contentmodel.settings.ContentModelSettings;
import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationSettings;
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.IXMLValidationService;
import org.eclipse.lemminx.services.extensions.ICodeActionParticipant;
Expand Down Expand Up @@ -69,6 +72,8 @@ public class ContentModelPlugin implements IXMLExtension {

private ContentModelSettings cmSettings;

private XMLValidationSettings currentValidationSettings;

public ContentModelPlugin() {
completionParticipant = new ContentModelCompletionParticipant();
hoverParticipant = new ContentModelHoverParticipant();
Expand Down Expand Up @@ -105,6 +110,8 @@ private void updateSettings(ISaveContext saveContext) {
cmSettings = ContentModelSettings.getContentModelXMLSettings(initializationOptionsSettings);
if (cmSettings != null) {
updateSettings(cmSettings, saveContext);
} else {
currentValidationSettings = null;
}
}

Expand Down Expand Up @@ -144,6 +151,12 @@ private void updateSettings(ContentModelSettings settings, ISaveContext context)
// Update symbols
boolean showReferencedGrammars = settings.isShowReferencedGrammars();
symbolsProviderParticipant.setEnabled(showReferencedGrammars);
// Track if validation settings has changed
XMLValidationSettings oldValidationSettings = currentValidationSettings;
currentValidationSettings = cmSettings.getValidation();
if (oldValidationSettings != null && !Objects.equals(oldValidationSettings, currentValidationSettings)) {
context.collectDocumentToValidate(d -> true);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,31 @@
package org.eclipse.lemminx.extensions.contentmodel.participants.diagnostics;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.xerces.impl.XMLEntityManager;
import org.apache.xerces.parsers.SAXParser;
import org.apache.xerces.util.URI.MalformedURIException;
import org.apache.xerces.xni.grammars.XMLGrammarPool;
import org.apache.xerces.xni.parser.XMLEntityResolver;
import org.eclipse.lemminx.dom.DOMAttr;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.NoNamespaceSchemaLocation;
import org.eclipse.lemminx.dom.SchemaLocationHint;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSyntaxErrorCode;
import org.eclipse.lemminx.extensions.contentmodel.settings.SchemaEnabled;
import org.eclipse.lemminx.extensions.contentmodel.settings.XMLSchemaSettings;
import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationSettings;
import org.eclipse.lemminx.services.extensions.diagnostics.LSPContentHandler;
import org.eclipse.lemminx.uriresolver.CacheResourceDownloadingException;
Expand Down Expand Up @@ -82,19 +90,24 @@ public static void doDiagnostics(DOMDocument document, XMLEntityResolver entityR
// Add LSP content handler to stop XML parsing if monitor is canceled.
parser.setContentHandler(new LSPContentHandler(monitor));

boolean hasSchemaGrammar = document.hasSchemaLocation() || document.hasNoNamespaceSchemaLocation()
|| hasExternalSchemaGrammar(document);
boolean hasGrammar = document.hasDTD() || hasSchemaGrammar || document.hasExternalGrammar();
// If diagnostics for Schema preference is enabled
if ((validationSettings == null) || validationSettings.isSchema()) {
// warn if XML document is not bound to a grammar according the settings
warnNoGrammar(document, diagnostics, validationSettings);
// Update external grammar location (file association)
updateExternalGrammarLocation(document, parser);

updateExternalGrammarLocation(document, parser);
parser.setFeature("http://apache.org/xml/features/validation/schema", hasSchemaGrammar); //$NON-NLS-1$
boolean hasSchemaLocation = document.hasSchemaLocation();
boolean hasNoNamespaceSchemaLocation = document.hasNoNamespaceSchemaLocation();
boolean hasSchemaGrammar = hasSchemaLocation || hasNoNamespaceSchemaLocation
|| hasExternalSchemaGrammar(document);
boolean schemaValidationEnabled = (hasSchemaGrammar
&& isSchemaValidationEnabled(document, validationSettings)
|| (hasNoNamespaceSchemaLocation
&& isNoNamespaceSchemaValidationEnabled(document, validationSettings)));
parser.setFeature("http://apache.org/xml/features/validation/schema", schemaValidationEnabled); //$NON-NLS-1$

// warn if XML document is not bound to a grammar according the settings
warnNoGrammar(document, diagnostics, validationSettings);
} else {
hasGrammar = false; // validation for Schema was disabled
boolean hasGrammar = document.hasDTD() || hasSchemaGrammar || document.hasExternalGrammar();
if (hasSchemaGrammar && !schemaValidationEnabled) {
hasGrammar = false;
}
parser.setFeature("http://xml.org/sax/features/validation", hasGrammar); //$NON-NLS-1$

Expand All @@ -114,6 +127,125 @@ public static void doDiagnostics(DOMDocument document, XMLEntityResolver entityR
}
}

private static boolean isSchemaValidationEnabled(DOMDocument document, XMLValidationSettings validationSettings) {
if (validationSettings == null) {
return true;
}
SchemaEnabled enabled = SchemaEnabled.always;
XMLSchemaSettings schemaSettings = validationSettings.getSchema();
if (schemaSettings != null && schemaSettings.getEnabled() != null) {
enabled = schemaSettings.getEnabled();
}
switch (enabled) {
case always:
return true;
case never:
return false;
case onValidSchema:
return isValidSchemaLocation(document);
default:
return true;
}
}

/**
* Returns true if the given DOM document declares a xsi:schemaLocation hint for
* the document root element is valid and false otherwise.
*
* The xsi:schemaLocation is valid if:
*
* <ul>
* <li>xsi:schemaLocation defines an URI for the namespace of the document
* element.</li>
* <li>the URI can be opened</li>
* </ul>
*
* @param document the DOM document.
* @return true if the given DOM document declares a xsi:schemaLocation hint for
* the document root element is valid and false otherwise.
*/
private static boolean isValidSchemaLocation(DOMDocument document) {
if (!document.hasSchemaLocation()) {
return false;
}
String namespaceURI = document.getNamespaceURI();
SchemaLocationHint hint = document.getSchemaLocation().getLocationHint(namespaceURI);
if (hint == null) {
return false;
}
String location = hint.getHint();
return isValidLocation(document.getDocumentURI(), location);
}

private static boolean isNoNamespaceSchemaValidationEnabled(DOMDocument document,
XMLValidationSettings validationSettings) {
if (validationSettings == null) {
return true;
}
SchemaEnabled enabled = SchemaEnabled.always;
XMLSchemaSettings schemaSettings = validationSettings.getSchema();
if (schemaSettings != null && schemaSettings.getEnabled() != null) {
enabled = schemaSettings.getEnabled();
}
switch (enabled) {
case always:
return true;
case never:
return false;
case onValidSchema:
return isValidNoNamespaceSchemaLocation(document);
default:
return true;
}
}

/**
* Returns true if the given DOM document declares a
* xsi:noNamespaceSchemaLocation which is valid and false otherwise.
*
* The xsi:noNamespaceSchemaLocation is valid if:
*
* <ul>
* <li>xsi:noNamespaceSchemaLocation defines an URI.</li>
* <li>the URI can be opened</li>
* </ul>
*
* @param document the DOM document.
* @return true if the given DOM document declares a xsi:schemaLocation hint for
* the document root element is valid and false otherwise.
*/
private static boolean isValidNoNamespaceSchemaLocation(DOMDocument document) {
NoNamespaceSchemaLocation noNamespaceSchemaLocation = document.getNoNamespaceSchemaLocation();
if (noNamespaceSchemaLocation == null) {
return false;
}
String location = noNamespaceSchemaLocation.getLocation();
return isValidLocation(document.getDocumentURI(), location);
}

private static boolean isValidLocation(String documentURI, String location) {
String resolvedLocation = getResolvedLocation(documentURI, location);
if (resolvedLocation == null) {
return false;
}
try (InputStream is = new URL(resolvedLocation).openStream()) {
return true;
} catch (Exception e) {
return false;
}
}

private static String getResolvedLocation(String documentURI, String location) {
if (StringUtils.isBlank(location)) {
return null;
}
try {
return XMLEntityManager.expandSystemId(location, documentURI, false);
} catch (MalformedURIException e) {
return location;
}
}

private static boolean hasExternalSchemaGrammar(DOMDocument document) {
if (document.getExternalGrammarFromNamespaceURI() != null) {
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2021 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*/
package org.eclipse.lemminx.extensions.contentmodel.settings;

public enum SchemaEnabled {
angelozerr marked this conversation as resolved.
Show resolved Hide resolved
always, never, onValidSchema;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2021 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*/
package org.eclipse.lemminx.extensions.contentmodel.settings;

/**
* XML Schema settings.
*
*/
public class XMLSchemaSettings {
angelozerr marked this conversation as resolved.
Show resolved Hide resolved

public XMLSchemaSettings() {
setEnabled(SchemaEnabled.always);
}

private SchemaEnabled enabled;

public void setEnabled(SchemaEnabled enabled) {
this.enabled = enabled;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((enabled == null) ? 0 : enabled.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
XMLSchemaSettings other = (XMLSchemaSettings) obj;
if (enabled != other.enabled)
return false;
return true;
}

public SchemaEnabled getEnabled() {
return enabled;
}
}
Loading