001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.io.IOException;
005import java.io.InputStream;
006import java.util.stream.IntStream;
007
008import javax.xml.XMLConstants;
009import javax.xml.parsers.DocumentBuilder;
010import javax.xml.parsers.DocumentBuilderFactory;
011import javax.xml.parsers.ParserConfigurationException;
012import javax.xml.parsers.SAXParser;
013import javax.xml.parsers.SAXParserFactory;
014import javax.xml.stream.XMLInputFactory;
015import javax.xml.transform.TransformerConfigurationException;
016import javax.xml.transform.TransformerFactory;
017import javax.xml.validation.Schema;
018import javax.xml.validation.SchemaFactory;
019import javax.xml.validation.SchemaFactoryConfigurationError;
020import javax.xml.validation.Validator;
021
022import org.w3c.dom.Document;
023import org.w3c.dom.Element;
024import org.w3c.dom.Node;
025import org.w3c.dom.NodeList;
026import org.xml.sax.InputSource;
027import org.xml.sax.SAXException;
028import org.xml.sax.SAXNotRecognizedException;
029import org.xml.sax.SAXNotSupportedException;
030import org.xml.sax.helpers.DefaultHandler;
031
032/**
033 * XML utils, mainly used to construct safe factories.
034 * @since 13901
035 */
036public final class XmlUtils {
037
038    private static final String FEATURE_DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";
039
040    private XmlUtils() {
041        // Hide default constructor for utils classes
042    }
043
044    /**
045     * Returns the W3C XML Schema factory implementation. Robust method dealing with ContextClassLoader problems.
046     * @return the W3C XML Schema factory implementation
047     */
048    public static SchemaFactory newXmlSchemaFactory() {
049        try {
050            return SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
051        } catch (SchemaFactoryConfigurationError e) {
052            Logging.debug(e);
053            // Can happen with icedtea-web. Use workaround from https://issues.apache.org/jira/browse/GERONIMO-6185
054            Thread currentThread = Thread.currentThread();
055            ClassLoader old = currentThread.getContextClassLoader();
056            currentThread.setContextClassLoader(null);
057            try {
058                return SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
059            } finally {
060                currentThread.setContextClassLoader(old);
061            }
062        }
063    }
064
065    /**
066     * Returns a new secure DOM builder, supporting XML namespaces.
067     * @return a new secure DOM builder, supporting XML namespaces
068     * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration.
069     */
070    public static DocumentBuilder newSafeDOMBuilder() throws ParserConfigurationException {
071        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
072        builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
073        builderFactory.setNamespaceAware(true);
074        builderFactory.setValidating(false);
075        return builderFactory.newDocumentBuilder();
076    }
077
078    /**
079     * Parse the content given {@link InputStream} as XML.
080     * This method uses a secure DOM builder, supporting XML namespaces.
081     *
082     * @param is The InputStream containing the content to be parsed.
083     * @return the result DOM document
084     * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration.
085     * @throws IOException if any IO errors occur.
086     * @throws SAXException for SAX errors.
087     */
088    public static Document parseSafeDOM(InputStream is) throws ParserConfigurationException, IOException, SAXException {
089        Stopwatch stopwatch = Stopwatch.createStarted();
090        Logging.debug("Starting DOM parsing of {0}", is);
091        Document result = newSafeDOMBuilder().parse(is);
092        Logging.debug(stopwatch.toString("DOM parsing"));
093        return result;
094    }
095
096    /**
097     * Returns a new secure SAX parser, supporting XML namespaces.
098     * @return a new secure SAX parser, supporting XML namespaces
099     * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration.
100     * @throws SAXException for SAX errors.
101     */
102    public static SAXParser newSafeSAXParser() throws ParserConfigurationException, SAXException {
103        SAXParserFactory parserFactory = SAXParserFactory.newInstance();
104        parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
105        parserFactory.setFeature(FEATURE_DISALLOW_DOCTYPE_DECL, true);
106        parserFactory.setNamespaceAware(true);
107        return parserFactory.newSAXParser();
108    }
109
110    /**
111     * Parse the content given {@link org.xml.sax.InputSource} as XML using the specified {@link org.xml.sax.helpers.DefaultHandler}.
112     * This method uses a secure SAX parser, supporting XML namespaces.
113     *
114     * @param is The InputSource containing the content to be parsed.
115     * @param dh The SAX DefaultHandler to use.
116     * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration.
117     * @throws SAXException for SAX errors.
118     * @throws IOException if any IO errors occur.
119     */
120    public static void parseSafeSAX(InputSource is, DefaultHandler dh) throws ParserConfigurationException, SAXException, IOException {
121        Stopwatch stopwatch = Stopwatch.createStarted();
122        Logging.debug("Starting SAX parsing of {0} using {1}", is, dh);
123        newSafeSAXParser().parse(is, dh);
124        Logging.debug(stopwatch.toString("SAX parsing"));
125    }
126
127    /**
128     * Returns a new secure {@link XMLInputFactory}.
129     * @return a new secure {@code XMLInputFactory}, for which external entities are not loaded
130     */
131    public static XMLInputFactory newSafeXMLInputFactory() {
132        XMLInputFactory factory = XMLInputFactory.newInstance();
133        // do not try to load external entities, nor validate the XML
134        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
135        factory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
136        factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
137        return factory;
138    }
139
140    /**
141     * Returns a new secure {@link TransformerFactory}.
142     * @return a new secure {@link TransformerFactory}
143     * @throws TransformerConfigurationException if the factory or the Transformers or Templates it creates cannot support this feature.
144     */
145    public static TransformerFactory newSafeTransformerFactory() throws TransformerConfigurationException {
146        TransformerFactory factory = TransformerFactory.newInstance();
147        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
148        return factory;
149    }
150
151    /**
152     * Returns a new secure {@link Validator}.
153     * @param schema XML schema
154     * @return a new secure {@link Validator}
155     * @since 14441
156     */
157    public static Validator newSafeValidator(Schema schema) {
158        Validator validator = schema.newValidator();
159        try {
160            validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
161            validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
162        } catch (SAXNotRecognizedException | SAXNotSupportedException e) {
163            // All implementations that implement JAXP 1.5 or newer are required to support these two properties
164            Logging.trace(e);
165        }
166        return validator;
167    }
168
169    /**
170     * Get the first child element
171     * @param parent parent node
172     * @return the first child element
173     * @since 14348
174     */
175    public static Element getFirstChildElement(Node parent) {
176        NodeList children = parent.getChildNodes();
177        return (Element) IntStream.range(0, children.getLength())
178                .mapToObj(children::item)
179                .filter(child -> child instanceof Element)
180                .findFirst().orElse(null);
181    }
182}