Skip to content

Commit

Permalink
Disable XSD validation when xsi:schemaLocation doesn't declare the
Browse files Browse the repository at this point in the history
namespace for the document element root.

See #951

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Jan 13, 2021
1 parent 0d812fb commit 5d80049
Show file tree
Hide file tree
Showing 9 changed files with 622 additions and 149 deletions.
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) 2020 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 {
always, never, onValidSchema;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2020 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 class XMLSchemaSettings {

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

0 comments on commit 5d80049

Please sign in to comment.