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 <azerr@redhat.com>
  • Loading branch information
angelozerr authored and datho7561 committed Jan 14, 2021
1 parent ee4950e commit 6e5a906
Show file tree
Hide file tree
Showing 10 changed files with 637 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) 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 {
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 {

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 6e5a906

Please sign in to comment.