8354084: Streamline XPath API's extension function control

Reviewed-by: rriggs, naoto
This commit is contained in:
Joe Wang 2025-05-01 17:03:29 +00:00
parent 6b553acbaa
commit cf0db96314
9 changed files with 223 additions and 253 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
@ -41,7 +41,7 @@ import javax.xml.transform.TransformerException;
* the expression executes, it calls ExtensionsTable#extFunction, and then
* converts the result to the appropriate XObject.
* @xsl.usage advanced
* @LastModified: May 2022
* @LastModified: Apr 2025
*/
public class FuncExtFunction extends Function
{
@ -186,12 +186,6 @@ public class FuncExtFunction extends Function
*/
public XObject execute(XPathContext xctxt) throws TransformerException
{
if (xctxt.isSecureProcessing())
throw new javax.xml.transform.TransformerException(
XPATHMessages.createXPATHMessage(
XPATHErrorResources.ER_EXTENSION_FUNCTION_CANNOT_BE_INVOKED,
new Object[] {toString()}));
XObject result;
List<XObject> argVec = new ArrayList<>();
int nArgs = m_argVec.size();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
@ -37,26 +37,16 @@ import jdk.xml.internal.JdkXmlFeatures;
/**
*
* @author Ramesh Mandava
* @LastModified: Nov 2017
* @LastModified: Apr 2025
*/
public class JAXPExtensionsProvider implements ExtensionsProvider {
private final XPathFunctionResolver resolver;
private boolean extensionInvocationDisabled = false;
public JAXPExtensionsProvider(XPathFunctionResolver resolver) {
this.resolver = resolver;
this.extensionInvocationDisabled = false;
}
public JAXPExtensionsProvider(XPathFunctionResolver resolver,
boolean featureSecureProcessing, JdkXmlFeatures featureManager ) {
this.resolver = resolver;
if (featureSecureProcessing &&
!featureManager.getFeature(JdkXmlFeatures.XmlFeature.ENABLE_EXTENSION_FUNCTION)) {
this.extensionInvocationDisabled = true;
}
}
/**
* Is the extension function available?
@ -111,16 +101,6 @@ public class JAXPExtensionsProvider implements ExtensionsProvider {
//Find the XPathFunction corresponding to namespace and funcName
javax.xml.namespace.QName myQName = new QName( ns, funcName );
// JAXP 1.3 spec says When XMLConstants.FEATURE_SECURE_PROCESSING
// feature is set then invocation of extension functions need to
// throw XPathFunctionException
if ( extensionInvocationDisabled ) {
String fmsg = XSLMessages.createXPATHMessage(
XPATHErrorResources.ER_EXTENSION_FUNCTION_CANNOT_BE_INVOKED,
new Object[] { myQName.toString() } );
throw new XPathFunctionException ( fmsg );
}
// Assuming user is passing all the needed parameters ( including
// default values )
int arity = argVec.size();
@ -167,16 +147,6 @@ public class JAXPExtensionsProvider implements ExtensionsProvider {
javax.xml.namespace.QName myQName =
new javax.xml.namespace.QName( namespace, functionName );
// JAXP 1.3 spec says When XMLConstants.FEATURE_SECURE_PROCESSING
// feature is set then invocation of extension functions need to
// throw XPathFunctionException
if ( extensionInvocationDisabled ) {
String fmsg = XSLMessages.createXPATHMessage(
XPATHErrorResources.ER_EXTENSION_FUNCTION_CANNOT_BE_INVOKED,
new Object[] { myQName.toString() } );
throw new XPathFunctionException ( fmsg );
}
XPathFunction xpathFunction =
resolver.resolveFunction( myQName, arity );

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -54,7 +54,7 @@ import org.xml.sax.SAXException;
* This class contains several utility methods used by XPathImpl and
* XPathExpressionImpl
*
* @LastModified: Jan 2022
* @LastModified: Apr 2025
*/
class XPathImplUtil {
XPathFunctionResolver functionResolver;
@ -85,8 +85,7 @@ class XPathImplUtil {
new Object[] {}));
}
if (functionResolver != null) {
JAXPExtensionsProvider jep = new JAXPExtensionsProvider(
functionResolver, featureSecureProcessing, featureManager);
JAXPExtensionsProvider jep = new JAXPExtensionsProvider(functionResolver);
xpathSupport = new com.sun.org.apache.xpath.internal.XPathContext(jep);
} else {
xpathSupport = new com.sun.org.apache.xpath.internal.XPathContext();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
@ -31,7 +31,7 @@ import java.util.ListResourceBundle;
* Also you need to update the count of messages(MAX_CODE)or
* the count of warnings(MAX_WARNING) [ Information purpose only]
* @xsl.usage advanced
* @LastModified: Nov 2024
* @LastModified: Apr 2025
*/
public class XPATHErrorResources extends ListResourceBundle
{
@ -305,7 +305,6 @@ public static final String ER_IGNORABLE_WHITESPACE_NOT_HANDLED =
public static final String ER_XPATH_ERROR = "ER_XPATH_ERROR";
//BEGIN: Keys needed for exception messages of JAXP 1.3 XPath API implementation
public static final String ER_EXTENSION_FUNCTION_CANNOT_BE_INVOKED = "ER_EXTENSION_FUNCTION_CANNOT_BE_INVOKED";
public static final String ER_RESOLVE_VARIABLE_RETURNS_NULL = "ER_RESOLVE_VARIABLE_RETURNS_NULL";
public static final String ER_NO_XPATH_VARIABLE_RESOLVER = "ER_NO_XPATH_VARIABLE_RESOLVER";
public static final String ER_NO_XPATH_FUNCTION_PROVIDER = "ER_NO_XPATH_FUNCTION_PROVIDER";
@ -766,11 +765,6 @@ public static final String ER_IGNORABLE_WHITESPACE_NOT_HANDLED =
//BEGIN: Definitions of error keys used in exception messages of JAXP 1.3 XPath API implementation
/** Field ER_EXTENSION_FUNCTION_CANNOT_BE_INVOKED */
{ ER_EXTENSION_FUNCTION_CANNOT_BE_INVOKED,
"Extension function: ''{0}'' can not be invoked when the XMLConstants.FEATURE_SECURE_PROCESSING feature is set to true."},
/** Field ER_RESOLVE_VARIABLE_RETURNS_NULL */
{ ER_RESOLVE_VARIABLE_RETURNS_NULL,

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -868,19 +868,19 @@
* </tr>
* <tr>
* <td id="ExtFunc">{@systemProperty jdk.xml.enableExtensionFunctions}</td>
* <td>Determines if XSLT and XPath extension functions are to be allowed.
* <td>Determines whether extension functions in the Transform API are to be allowed.
* The extension functions in the XPath API are not affected by this property.
* </td>
* <td style="text-align:center" rowspan="5">yes</td>
* <td style="text-align:center" rowspan="3">Boolean</td>
* <td>
* true or false. True indicates that extension functions are allowed; False otherwise.
* </td>
* <td style="text-align:center">true</td>
* <td style="text-align:center">false</td>
* <td style="text-align:center">false</td>
* <td style="text-align:center">Yes</td>
* <td style="text-align:center">
* <a href="#Transform">Transform</a><br>
* <a href="#XPATH">XPath</a>
* </td>
* <td style="text-align:center"><a href="#Processor">Method 2</a></td>
* <td style="text-align:center">8</td>

View File

@ -1,195 +0,0 @@
/*
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package xpath;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFactoryConfigurationException;
import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathFunctionException;
import javax.xml.xpath.XPathFunctionResolver;
import org.testng.Assert;
import org.testng.annotations.Test;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/*
* @test
* @library /javax/xml/jaxp/libs /javax/xml/jaxp/unittest
* @run testng/othervm xpath.SecureProcessingTest
* @summary Test when FEATURE_SECURE_PROCESSING is true, calling an external function will cause XPathFunctionException.
*/
@Test
public class SecureProcessingTest {
public final void testSecureProcessing() {
boolean _isSecureMode = System.getSecurityManager() != null;
final String XPATH_EXPRESSION = "ext:helloWorld()";
// the xml source
InputStream xmlStream = this.getClass().getResourceAsStream("SecureProcessingTest.xml");
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = null;
Document document = null;
try {
documentBuilder = documentBuilderFactory.newDocumentBuilder();
document = documentBuilder.parse(xmlStream);
} catch (ParserConfigurationException parserConfigurationException) {
parserConfigurationException.printStackTrace();
Assert.fail(parserConfigurationException.toString());
} catch (SAXException saxException) {
saxException.printStackTrace();
Assert.fail(saxException.toString());
} catch (IOException ioException) {
ioException.printStackTrace();
Assert.fail(ioException.toString());
}
// the XPath
XPathFactory xPathFactory = null;
XPath xPath = null;
String xPathResult = null;
// SECURE_PROCESSING == false
// evaluate an expression with a user defined function with a non-secure
// XPath
// expect success
if (!_isSecureMode) { // jaxp secure feature can not be turned off when
// security manager is present
try {
xPathFactory = xPathFactory.newInstance();
xPathFactory.setXPathFunctionResolver(new MyXPathFunctionResolver());
xPathFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
xPath = xPathFactory.newXPath();
xPath.setNamespaceContext(new MyNamespaceContext());
xPathResult = xPath.evaluate(XPATH_EXPRESSION, document);
} catch (XPathFactoryConfigurationException xPathFactoryConfigurationException) {
xPathFactoryConfigurationException.printStackTrace();
Assert.fail(xPathFactoryConfigurationException.toString());
} catch (XPathExpressionException xPathExpressionException) {
xPathExpressionException.printStackTrace();
Assert.fail(xPathExpressionException.toString());
}
// expected success
System.out.println("XPath result (SECURE_PROCESSING == false) = \"" + xPathResult + "\"");
}
// now try with SECURE_PROCESSING == true
// evaluate an expression with a user defined function with a secure
// XPath
// expect Exception
boolean securityException = false;
try {
xPathFactory = xPathFactory.newInstance();
xPathFactory.setXPathFunctionResolver(new MyXPathFunctionResolver());
xPathFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
xPath = xPathFactory.newXPath();
xPath.setNamespaceContext(new MyNamespaceContext());
xPathResult = xPath.evaluate(XPATH_EXPRESSION, document);
} catch (XPathFactoryConfigurationException xPathFactoryConfigurationException) {
xPathFactoryConfigurationException.printStackTrace();
Assert.fail(xPathFactoryConfigurationException.toString());
} catch (XPathFunctionException xPathFunctionException) {
// expected security exception
securityException = true;
xPathFunctionException.printStackTrace(System.out);
} catch (XPathExpressionException xPathExpressionException) {
xPathExpressionException.printStackTrace();
Assert.fail(xPathExpressionException.toString());
}
// expected Exception
if (!securityException) {
Assert.fail("XPath result (SECURE_PROCESSING == true) = \"" + xPathResult + "\"");
}
}
private class MyXPathFunctionResolver implements XPathFunctionResolver {
public XPathFunction resolveFunction(QName functionName, int arity) {
// not a real ewsolver, always return a default XPathFunction
return new MyXPathFunction();
}
}
private class MyXPathFunction implements XPathFunction {
public Object evaluate(List list) throws XPathFunctionException {
return "Hello World";
}
}
private class MyNamespaceContext implements NamespaceContext {
public String getNamespaceURI(String prefix) {
if (prefix == null) {
throw new IllegalArgumentException("The prefix cannot be null.");
}
if (prefix.equals("ext")) {
return "http://ext.com";
} else {
return null;
}
}
public String getPrefix(String namespace) {
if (namespace == null) {
throw new IllegalArgumentException("The namespace uri cannot be null.");
}
if (namespace.equals("http://ext.com")) {
return "ext";
} else {
return null;
}
}
public Iterator getPrefixes(String namespace) {
return null;
}
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0"?>
<helloWorld/>

View File

@ -0,0 +1,198 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package xpath;
import java.util.List;
import java.util.stream.Stream;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
import jaxp.library.JUnitTestUtil;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*
* @test
* @bug 8354084
* @summary Verifies that extensions to XPathFunction.
* @library /javax/xml/jaxp/libs /javax/xml/jaxp/unittest /test/lib
* @run junit xpath.XPathFunctionTest
*/
public class XPathFunctionTest {
/*
* Arguments for XPath Extension Function Test, refer to the test below.
*/
private static Stream<Arguments> testData() {
// expected result when a resolver is set properly
String result = "id=2 price=20";
return Stream.of(
// cases where the result is as expected when a resolver is set
Arguments.of(true, false, false, false, false, result, null),
Arguments.of(true, true, false, false, false, result, null),
Arguments.of(true, true, false, true, false, result, null),
Arguments.of(true, true, false, true, true, result, null),
Arguments.of(true, true, true, true, true, result, null),
// cases XPathExpressionException was thrown before the change even though there's a resolver
Arguments.of(true, true, true, false, false, result, null),
Arguments.of(true, true, true, true, false, result, null),
// XPathExpressionException will continue to be thrown due to missing resolver, though it was
// thrown for a different reason (FSP is turned on) before the change
Arguments.of(false, false, false, false, false, result, XPathExpressionException.class),
Arguments.of(false, true, true, false, false, result, XPathExpressionException.class),
Arguments.of(false, true, true, true, false, result, XPathExpressionException.class)
);
}
/**
* Verifies the control over XPath Extension Functions.
* @param useResolver indicates whether there is a custom resolver
* @param setFSP indicates whether FSP is to be set
* @param FSPValue the FSP value
* @param setProperty indicates whether the property {@code jdk.xml.enableExtensionFunctions}
* is to be set
* @param propertyValue the property value
* @param expected the expected result
* @param expectedType the expected throw type
* @throws Exception if the test fails other than the expected Exception, which
* would indicate an issue in configuring the test
*/
@ParameterizedTest
@MethodSource("testData")
public void test(boolean useResolver, boolean setFSP, boolean FSPValue,
boolean setProperty, boolean propertyValue, String expected, Class<Throwable> expectedType) throws Exception {
if (expectedType != null) {
assertThrows(expectedType, () -> findToy(useResolver, setFSP, FSPValue, setProperty, propertyValue, expectedType));
} else {
String result = findToy(useResolver, setFSP, FSPValue, setProperty, propertyValue, expectedType);
assertEquals(expected, result);
}
}
public String findToy(boolean useResolver, boolean setFSP, boolean FSPValue,
boolean setProperty, boolean propertyValue, Class<Throwable> expectedType)
throws Exception {
Document doc = getDocument(JUnitTestUtil.SRC_DIR + "/XPathFunctionTest.xml");
XPathFactory xpf = XPathFactory.newDefaultInstance();
if (useResolver) {
xpf.setXPathFunctionResolver(new FunctionResolver(doc));
}
if (setFSP) {
xpf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, FSPValue);
}
if (setProperty) {
xpf.setFeature("jdk.xml.enableExtensionFunctions", propertyValue);
}
XPath xPath = xpf.newXPath();
XPathExpression exp = xPath.compile("ext:findToy('name', 'Another toy')");
Node toyNode = (Node)exp.evaluate(doc, XPathConstants.NODE);
String id = "", price = "";
if (toyNode != null && toyNode.getNodeType() == Node.ELEMENT_NODE) {
Element toyElement = (Element)toyNode;
id = toyElement.getAttribute("id");
price = toyElement.getElementsByTagName("price").item(0).getTextContent();
}
return "id=" + id + " price=" + price;
}
/**
* Returns a DOM Document.
* @param xmlFile the XML document to be parsed
* @return a DOM Document
* @throws Exception if error occurs
*/
Document getDocument(String xmlFile)
throws Exception {
try {
DocumentBuilder builder = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder();
Document out = builder.parse(xmlFile);
return out;
} catch (Exception e) {
// won't happen, parsing a valid file
}
return null;
}
// XPathFunctionResolver customized for the FindFunction
class FunctionResolver implements XPathFunctionResolver {
private final Document doc;
public FunctionResolver(Document doc) {
this.doc = doc;
}
@Override
public XPathFunction resolveFunction(QName functionName, int arity) {
if ("findToy".equals(functionName.getLocalPart()) && arity == 2) {
return new FindFunction(doc);
}
return null;
}
}
// The Find function
class FindFunction implements XPathFunction {
private final Document doc;
public FindFunction(Document doc) {
this.doc = doc;
}
@SuppressWarnings("rawtypes")
public Object evaluate(List list) throws XPathFunctionException {
if (list == null || list.size() != 2) {
throw new XPathFunctionException("FindToy requires two args: name and value");
}
String eleName = (String)list.get(0);
String eleValue = (String)list.get(1);
NodeList toys = doc.getElementsByTagName("toy");
for (int i = 0; i<toys.getLength(); i++) {
Element toy = (Element)toys.item(i);
NodeList children = toy.getElementsByTagName(eleName);
if (children.getLength() > 0) {
String text = children.item(0).getTextContent();
if (eleValue.equals(text)) {
return toy;
}
}
}
return null;
}
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<toys>
<toy id="1">
<name>Some toy</name>
<price>10</price>
</toy>
<toy id="2">
<name>Another toy</name>
<price>20</price>
</toy>
</toys>