/*
* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*
*/
package org.wso2.carbon.bpmn.core.types.datatypes.xml;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xerces.impl.Constants;
import org.apache.xerces.util.SecurityManager;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.wso2.carbon.bpmn.core.types.datatypes.xml.api.XMLDocument;
import org.xml.sax.SAXException;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
/**
* Util class holding util functions related to XML processing
*/
public class Utils {
private static final Log log = LogFactory.getLog(Utils.class);
private static final int ENTITY_EXPANSION_LIMIT = 0;
/**
* Function to parse string to XMLDocument Object
*
* @param str string containing xml to parse
* @return XMLDocument object which is implementation of org.w3c.dom.Document
* @throws ParserConfigurationException
* @throws IOException If any IO errors occur.
* @throws SAXException If any parse errors occur.
*/
public static XMLDocument parse(String str) throws ParserConfigurationException, IOException, SAXException {
DocumentBuilderFactory docBuilderFactory = getSecuredDocumentBuilder();
DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
try(InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8))) {
Document doc = builder.parse(inputStream);
if (doc != null) {
if (log.isDebugEnabled()) {
log.debug("Parsing to XMLDocument Success. Src string: " + str);
}
return new XMLDocument(doc);
}
}
return null;
}
/**
* Create DocumentBuilderFactory with the XXE and XEE prevention measurements.
*
* @return DocumentBuilderFactory instance
*/
public static DocumentBuilderFactory getSecuredDocumentBuilder() {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
try {
dbf.setFeature(Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_GENERAL_ENTITIES_FEATURE, false);
dbf.setFeature(Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_PARAMETER_ENTITIES_FEATURE, false);
dbf.setFeature(Constants.XERCES_FEATURE_PREFIX + Constants.LOAD_EXTERNAL_DTD_FEATURE, false);
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException e) {
log.error("Failed to load XML Processor Feature " + Constants.EXTERNAL_GENERAL_ENTITIES_FEATURE + " or " +
Constants.EXTERNAL_PARAMETER_ENTITIES_FEATURE + " or " + Constants.LOAD_EXTERNAL_DTD_FEATURE +
" or secure-processing.");
}
SecurityManager securityManager = new SecurityManager();
securityManager.setEntityExpansionLimit(ENTITY_EXPANSION_LIMIT);
dbf.setAttribute(Constants.XERCES_PROPERTY_PREFIX + Constants.SECURITY_MANAGER_PROPERTY, securityManager);
return dbf;
}
/**
* Function to convert XMLDocument to String
*
* @param xmlDoc XMLDocument object to convert
* @return xml in string form
* @throws TransformerException
*/
public static String stringify(XMLDocument xmlDoc) throws TransformerException {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
StringWriter strWriter = new StringWriter();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.transform(new DOMSource(xmlDoc), new StreamResult(strWriter));
if (log.isDebugEnabled()) {
log.debug("XMLDocument to String : " + strWriter.getBuffer().toString());
}
return strWriter.getBuffer().toString();
}
public static Object evaluateXPath(Document doc, String xpathStr, QName returnType) throws XPathExpressionException {
XPath xPath = XPathFactory.newInstance().newXPath();
// Possible xpath injection attack. However this code is reached only when executing a bpmn process
// BPMN process implementor should know the security risk and should evaluate only valid measures
return xPath.evaluate(xpathStr, doc, returnType);
}
/**
* Function to evaluate xpath. This will resolve the NodeList to Node if the result contains only one node
* @param doc
* @param xpathStr
* @return
* @throws XPathExpressionException
*/
public static Object evaluateXPath(Document doc, String xpathStr) throws BPMNXmlException {
Object result = null;
NodeList outputObjList = null;
try {
outputObjList = (NodeList) evaluateXPath(doc, xpathStr, XPathConstants.NODESET);
if (outputObjList.getLength() == 1) {
//If there is only one node
if (outputObjList.item(0) instanceof Text) {
return ((Text)outputObjList.item(0)).getWholeText();
}
return outputObjList.item(0);
}
return outputObjList;
} catch (XPathExpressionException eLevel1) {
//provided xpath cannot be evaluated to NodeList, so it may be evaluated to string
try {
if (log.isDebugEnabled()) {
log.debug("Since evaluating the xpath: "+ xpathStr+ " to NodeList failed, retrying to evaluate it to a STRING");
}
return evaluateXPath(doc, xpathStr, XPathConstants.STRING);
} catch (XPathExpressionException eLevel2) {
if (log.isDebugEnabled()) {
log.debug("Error occurred while evaluating xpath :"+ xpathStr+ " on xml: "+ doc.toString());
}
throw new BPMNXmlException("Error occurred while evaluating xpath :"+ xpathStr+ " due to error in xpath", eLevel2);
}
}
}
}