001package ca.uhn.fhir.validation;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.ConfigurationException;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.rest.api.EncodingEnum;
026import ca.uhn.fhir.util.ClasspathUtil;
027import org.hl7.fhir.instance.model.api.IBaseResource;
028import org.w3c.dom.ls.LSInput;
029import org.w3c.dom.ls.LSResourceResolver;
030import org.xml.sax.SAXException;
031import org.xml.sax.SAXNotRecognizedException;
032import org.xml.sax.SAXParseException;
033
034import javax.xml.XMLConstants;
035import javax.xml.transform.Source;
036import javax.xml.transform.stream.StreamSource;
037import javax.xml.validation.Schema;
038import javax.xml.validation.SchemaFactory;
039import javax.xml.validation.Validator;
040import java.io.ByteArrayInputStream;
041import java.io.IOException;
042import java.io.StringReader;
043import java.util.Collections;
044import java.util.HashMap;
045import java.util.HashSet;
046import java.util.Map;
047import java.util.Set;
048
049public class SchemaBaseValidator implements IValidatorModule {
050        public static final String RESOURCES_JAR_NOTE = "Note that as of HAPI FHIR 1.2, DSTU2 validation files are kept in a separate JAR (hapi-fhir-validation-resources-XXX.jar) which must be added to your classpath. See the HAPI FHIR download page for more information.";
051
052        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class);
053        private static final Set<String> SCHEMA_NAMES;
054        private static boolean ourJaxp15Supported;
055
056        static {
057                HashSet<String> sn = new HashSet<>();
058                sn.add("xml.xsd");
059                sn.add("xhtml1-strict.xsd");
060                sn.add("fhir-single.xsd");
061                sn.add("fhir-xhtml.xsd");
062                sn.add("tombstone.xsd");
063                sn.add("opensearch.xsd");
064                sn.add("opensearchscore.xsd");
065                sn.add("xmldsig-core-schema.xsd");
066                SCHEMA_NAMES = Collections.unmodifiableSet(sn);
067        }
068
069        private final Map<String, Schema> myKeyToSchema = new HashMap<>();
070        private FhirContext myCtx;
071
072        public SchemaBaseValidator(FhirContext theContext) {
073                myCtx = theContext;
074        }
075
076        private void doValidate(IValidationContext<?> theContext) {
077                Schema schema = loadSchema();
078
079                try {
080                        Validator validator = schema.newValidator();
081                        MyErrorHandler handler = new MyErrorHandler(theContext);
082                        validator.setErrorHandler(handler);
083                        String encodedResource;
084                        if (theContext.getResourceAsStringEncoding() == EncodingEnum.XML) {
085                                encodedResource = theContext.getResourceAsString();
086                        } else {
087                                encodedResource = theContext.getFhirContext().newXmlParser().encodeResourceToString((IBaseResource) theContext.getResource());
088                        }
089
090                        try {
091                                /*
092                                 * See https://github.com/hapifhir/hapi-fhir/issues/339
093                                 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
094                                 */
095                                validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
096                                validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
097                        } catch (SAXNotRecognizedException ex) {
098                                ourLog.debug("Jaxp 1.5 Support not found.", ex);
099                        }
100
101                        validator.validate(new StreamSource(new StringReader(encodedResource)));
102                } catch (SAXParseException e) {
103                        SingleValidationMessage message = new SingleValidationMessage();
104                        message.setLocationLine(e.getLineNumber());
105                        message.setLocationCol(e.getColumnNumber());
106                        message.setMessage(e.getLocalizedMessage());
107                        message.setSeverity(ResultSeverityEnum.FATAL);
108                        theContext.addValidationMessage(message);
109                } catch (SAXException | IOException e) {
110                        // Catch all
111                        throw new ConfigurationException("Could not load/parse schema file", e);
112                }
113        }
114
115        private Schema loadSchema() {
116                String key = "fhir-single.xsd";
117
118                synchronized (myKeyToSchema) {
119                        Schema schema = myKeyToSchema.get(key);
120                        if (schema != null) {
121                                return schema;
122                        }
123
124                        Source baseSource = loadXml("fhir-single.xsd");
125
126                        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
127                        schemaFactory.setResourceResolver(new MyResourceResolver());
128
129                        try {
130                                try {
131                                        /*
132                                         * See https://github.com/hapifhir/hapi-fhir/issues/339
133                                         * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
134                                         */
135                                        schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
136                                        ourJaxp15Supported = true;
137                                } catch (SAXNotRecognizedException e) {
138                                        ourJaxp15Supported = false;
139                                        ourLog.warn("Jaxp 1.5 Support not found.", e);
140                                }
141                                schema = schemaFactory.newSchema(new Source[]{baseSource});
142                        } catch (SAXException e) {
143                                throw new ConfigurationException("Could not load/parse schema file: " + "fhir-single.xsd", e);
144                        }
145                        myKeyToSchema.put(key, schema);
146                        return schema;
147                }
148        }
149
150        Source loadXml(String theSchemaName) {
151                String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName;
152                ourLog.debug("Going to load resource: {}", pathToBase);
153
154                String contents = ClasspathUtil.loadResource(pathToBase, ClasspathUtil.withBom());
155                return new StreamSource(new StringReader(contents), null);
156        }
157
158        @Override
159        public void validateResource(IValidationContext<IBaseResource> theContext) {
160                doValidate(theContext);
161        }
162
163        private final class MyResourceResolver implements LSResourceResolver {
164                private MyResourceResolver() {
165                }
166
167                @Override
168                public LSInput resolveResource(String theType, String theNamespaceURI, String thePublicId, String theSystemId, String theBaseURI) {
169                        if (theSystemId != null && SCHEMA_NAMES.contains(theSystemId)) {
170                                LSInputImpl input = new LSInputImpl();
171                                input.setPublicId(thePublicId);
172                                input.setSystemId(theSystemId);
173                                input.setBaseURI(theBaseURI);
174                                String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSystemId;
175
176                                ourLog.debug("Loading referenced schema file: " + pathToBase);
177
178                                byte[] bytes = ClasspathUtil.loadResourceAsByteArray(pathToBase);
179                                input.setByteStream(new ByteArrayInputStream(bytes));
180                                return input;
181
182                        }
183
184                        throw new ConfigurationException("Unknown schema: " + theSystemId);
185                }
186        }
187
188        private static class MyErrorHandler implements org.xml.sax.ErrorHandler {
189
190                private IValidationContext<?> myContext;
191
192                MyErrorHandler(IValidationContext<?> theContext) {
193                        myContext = theContext;
194                }
195
196                private void addIssue(SAXParseException theException, ResultSeverityEnum theSeverity) {
197                        SingleValidationMessage message = new SingleValidationMessage();
198                        message.setLocationLine(theException.getLineNumber());
199                        message.setLocationCol(theException.getColumnNumber());
200                        message.setMessage(theException.getLocalizedMessage());
201                        message.setSeverity(theSeverity);
202                        myContext.addValidationMessage(message);
203                }
204
205                @Override
206                public void error(SAXParseException theException) {
207                        addIssue(theException, ResultSeverityEnum.ERROR);
208                }
209
210                @Override
211                public void fatalError(SAXParseException theException) {
212                        addIssue(theException, ResultSeverityEnum.FATAL);
213                }
214
215                @Override
216                public void warning(SAXParseException theException) {
217                        addIssue(theException, ResultSeverityEnum.WARNING);
218                }
219
220        }
221
222        public static boolean isJaxp15Supported() {
223                return ourJaxp15Supported;
224        }
225
226}