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