/*******************************************************************************
 * Copyright (c) 2018, 2019 Ericsson
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/

package org.eclipse.tracecompass.common.core.xml;

import java.io.IOException;
import java.io.InputStream;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.common.core.Activator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;

/**
 * XML Utilities. Useful to avoid copy-pasting secure code generation. Utils
 * here should be OWASP compliant.
 *
 * @author Matthew Khouzam
 * @since 3.2
 */
public final class XmlUtils {

    private static final String EMPTY = ""; //$NON-NLS-1$
    private static final String DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl"; //$NON-NLS-1$
    private static final String NONVALIDATING_LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; //$NON-NLS-1$
    private static final String USE_EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; //$NON-NLS-1$
    private static final String ACCESS_EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; //$NON-NLS-1$

    private XmlUtils() {
        // Do nothing
    }

    /**
     * <p>
     * Create a new <code>Transformer</code> that performs a copy of the
     * <code>Source</code> to the <code>Result</code>. i.e. the "<em>identity
     * transform</em>".
     * </p>
     * <p>
     * This is thread safe.
     * </p>
     * <p>
     * Use {@link XMLConstants#FEATURE_SECURE_PROCESSING} to ensure that the
     * transformer is secure.
     * </p>
     *
     * @return A Transformer object that may be used to perform a transformation
     *         in a single thread, never null.
     *
     * @throws TransformerConfigurationException
     *             When it is not possible to create a <code>Transformer</code>
     *             instance.
     */
    public static Transformer newSecureTransformer() throws TransformerConfigurationException {
        TransformerFactory factory = TransformerFactory.newInstance();
        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        return factory.newTransformer();
    }

    /**
     * Create a document builder factory that is safe according to the OWASP
     * injection prevention cheat sheet.
     *
     * @return the documentBuilderFactory
     * @since 4.1
     */
    public static DocumentBuilderFactory newSafeDocumentBuilderFactory() {
        String feature = null;
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        try {
            // This one is from Sonar (squid:S2755)
            dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            // This is the PRIMARY defense. If DTDs (doctypes) are disallowed,
            // almost all
            // XML entity attacks are prevented
            // Xerces 2 only -
            // http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
            feature = DISALLOW_DOCTYPE_DECL;
            dbf.setFeature(feature, true);

            // If you can't completely disable DTDs, then at least do the
            // following:
            // Xerces 1 -
            // http://xerces.apache.org/xerces-j/features.html#external-general-entities
            // Xerces 2 -
            // http://xerces.apache.org/xerces2-j/features.html#external-general-entities
            // JDK7+ - http://xml.org/sax/features/external-general-entities
            feature = ACCESS_EXTERNAL_GENERAL_ENTITIES;
            dbf.setFeature(feature, false);

            // Xerces 1 -
            // http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
            // Xerces 2 -
            // http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
            // JDK7+ - http://xml.org/sax/features/external-parameter-entities
            feature = USE_EXTERNAL_PARAMETER_ENTITIES;
            dbf.setFeature(feature, false);

            // Disable external DTDs as well
            feature = NONVALIDATING_LOAD_EXTERNAL_DTD;
            dbf.setFeature(feature, false);

            // and these as well, per Timothy Morgan's 2014 paper: "XML Schema,
            // DTD, and Entity Attacks"
            dbf.setXIncludeAware(false);
            dbf.setExpandEntityReferences(false);

            // And, per Timothy Morgan:
            // "If for some reason support for inline  DOCTYPEs are a requirement, then  ensure
            // the entity settings are disabled (as shown above) and beware that SSRF attacks
            // (http://cwe.mitre.org/data/definitions/918.html)
            // and denial of service attacks (such as billion laughs or decompression bombs via
            // "jar:") are a risk."

        } catch (ParserConfigurationException e) {
            // This should catch a failed setFeature feature
            Activator.instance().logInfo("ParserConfigurationException was thrown. The feature '" + feature //$NON-NLS-1$
                    + "' is probably not supported by your XML processor.", e); //$NON-NLS-1$
        }
        return dbf;
    }

    /**
     * Create a new safe {@link XMLStreamReader} from an {@link InputStream}
     *
     * @param inputStream
     *            the {@link InputStream} to read
     * @return an XML stream reader
     * @throws XMLStreamException
     *             if the stream could not be read or is badly formatted
     * @since 4.2
     */
    public static @Nullable XMLStreamReader newSafeXmlStreamReader(InputStream inputStream) throws XMLStreamException {
        XMLInputFactory factory = XMLInputFactory.newInstance();
        /*
         * Disable XML External entity (XXE) parsing
         */
        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
        return factory.createXMLStreamReader(inputStream);
    }

    /**
     * Get a safe {@link SchemaFactory}
     *
     * @return a safe {@link SchemaFactory}
     * @throws SAXException
     *             parse exception with
     *             {@link XMLConstants#W3C_XML_SCHEMA_NS_URI}
     * @since 4.2
     */
    public static SchemaFactory newSafeSchemaFactory() throws SAXException {
        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, EMPTY);
        factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, EMPTY);
        return factory;
    }

    /**
     * Validate a source, throws an exception if invalid.
     *
     * @param schema
     *            the schema to verify against. Use
     *            {@link #newSafeSchemaFactory()} for a safe schema
     * @param source
     *            the source to verify
     * @throws SAXException
     *             when the underlying XMLReader cannot set the security
     *             properties -or- when the source fails validation
     * @throws IOException
     *             the source failed validation due to file reasons, e.g.
     *             permissions
     * @since 4.2
     */
    public static void safeValidate(Schema schema, Source source) throws SAXException, IOException {
        Validator validator = schema.newValidator();
        validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, EMPTY);
        validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, EMPTY);
        validator.validate(source);
    }

    /**
     * Create a new secure {@link SAXParserFactory}
     *
     * @return a new secure {@link SAXParserFactory}
     * @throws ParserConfigurationException
     *             The parser cannot be created with the default configuration
     * @throws SAXNotRecognizedException
     *             When the underlying XMLReader does not recognize the property
     *             name.
     * @throws SAXNotSupportedException
     *             When the underlying XMLReader does recognize the property
     *             name but does not support it
     * @since 4.2
     */
    public static SAXParserFactory newSafeSaxParserFactory() throws SAXNotRecognizedException, SAXNotSupportedException, ParserConfigurationException {
        SAXParserFactory parserFactory = SAXParserFactory.newInstance();
        parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        parserFactory.setFeature(ACCESS_EXTERNAL_GENERAL_ENTITIES, false);
        parserFactory.setFeature(USE_EXTERNAL_PARAMETER_ENTITIES, false);
        parserFactory.setFeature(NONVALIDATING_LOAD_EXTERNAL_DTD, false);
        parserFactory.setFeature(DISALLOW_DOCTYPE_DECL, true);
        parserFactory.setNamespaceAware(true);
        return parserFactory;
    }
}
