Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ private XdmValue ofNode(final Node node) throws XPathException {
final DocumentBuilder sourceBuilder = newDocumentBuilder();
try {
if (node instanceof Document) {
// a document node (in-memory or persistent) can be built directly
return sourceBuilder.build(new DOMSource(node));
} else {
//The source must be part of a document
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ enum Format {
this.serializationProperties = serializationProperties;
}

final Destination createDestination(final Xslt30Transformer xslt30Transformer, final boolean forceCreation) {
final Destination createDestination(final Xslt30Transformer xslt30Transformer) {
switch (format) {
case DOCUMENT:
if (!forceCreation) {
this.builder = context.getDocumentBuilder();
} else {
this.builder = new MemTreeBuilder(context);
this.builder.startDocument();
}
// NOTE: Always build the result into a fresh document builder.
// The shared builder of the XQueryContext may already be in use
// by an enclosing expression (e.g. an element constructor).
// convert() returns the builder's whole document - using the shared builder corrupts both.
this.builder = new MemTreeBuilder(context);
this.builder.startDocument();
return new SAXDestination(new DocumentBuilderReceiver(builder));
case SERIALIZED:
final Serializer serializer = xslt30Transformer.newSerializer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@
import net.sf.saxon.s9api.XdmValue;
import org.apache.commons.lang3.StringUtils;
import org.exist.dom.memtree.NamespaceNode;
import org.exist.dom.persistent.NodeProxy;
import org.exist.security.PermissionDeniedException;
import org.exist.xquery.ErrorCodes;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.util.DocUtils;
import org.exist.xquery.functions.array.ArrayType;
import org.exist.xquery.functions.fn.FnTransform;
import org.exist.xquery.functions.map.MapType;
Expand Down Expand Up @@ -166,7 +169,12 @@ class Options {
stylesheetBaseUri = xsltSource._1;
}
if (!StringUtils.isEmpty(stylesheetBaseUri)) {
resolvedStylesheetBaseURI = Optional.of(resolveURI(new AnyURIValue(stylesheetBaseUri), context.getBaseURI()));
// Only resolve if it's not already absolute (database URIs start with "/" or "xmldb:")
if (stylesheetBaseUri.startsWith("/") || stylesheetBaseUri.startsWith("xmldb:") || stylesheetBaseUri.startsWith("exist://")) {
resolvedStylesheetBaseURI = Optional.of(new AnyURIValue(stylesheetBaseUri));
} else {
resolvedStylesheetBaseURI = Optional.of(resolveURI(new AnyURIValue(stylesheetBaseUri), context.getBaseURI()));
}
} else {
resolvedStylesheetBaseURI = Optional.empty();
}
Expand Down Expand Up @@ -467,7 +475,7 @@ private Tuple2<String, Source> getStylesheet(final MapType options) throws XPath
final List<Tuple2<String, Source>> results = new ArrayList<>(1);
final Optional<String> stylesheetLocation = Options.STYLESHEET_LOCATION.get(options).map(StringValue::getStringValue);
if (stylesheetLocation.isPresent()) {
results.add(Tuple(stylesheetLocation.get(), resolveStylesheetLocation(stylesheetLocation.get())));
results.add(resolveStylesheetLocation(stylesheetLocation.get()));
}

final Optional<Node> stylesheetNode = Options.STYLESHEET_NODE.get(options).map(NodeValue::getNode);
Expand Down Expand Up @@ -496,19 +504,68 @@ private Tuple2<String, Source> getStylesheet(final MapType options) throws XPath
* It may be a dynamically configured document.
* Or a document within the database.
* </p>
* <p>
* A relative location is first resolved the way {@code fn:doc} resolves
* relative paths: against the base URI of the query (where a collection
* path is treated as a "directory") and/or the location of the querying
* module within the database. If that does not find a document, the
* location is resolved strictly against the static base URI according
* to RFC 3986 (e.g. for file: or http: base URIs).
* See <a href="https://github.com/eXist-db/exist/issues/5052">issue 5052</a>.
* </p>
* @param stylesheetLocation path or URI of stylesheet
* @return a source wrapping the contents of the stylesheet
* @return a Tuple whose first value is the actual location of the resolved
* stylesheet, and whose second value is a source wrapping its contents
* @throws XPathException if there is a problem resolving the location.
*/
private Source resolveStylesheetLocation(final String stylesheetLocation) throws XPathException {
private Tuple2<String, Source> resolveStylesheetLocation(final String stylesheetLocation) throws XPathException {

final URI uri = URI.create(stylesheetLocation);
if (uri.isAbsolute()) {
return URIResolution.resolveDocument(stylesheetLocation, context, fnTransform);
} else {
return resolvePossibleStylesheetLocation(stylesheetLocation);
}

try {
return resolvePossibleStylesheetLocation(stylesheetLocation);
} catch (final XPathException e) {
final AnyURIValue resolved = resolveURI(new AnyURIValue(stylesheetLocation), context.getBaseURI());
return URIResolution.resolveDocument(resolved.getStringValue(), context, fnTransform);
return resolvePossibleStylesheetLocation(resolved.getStringValue());
}
}

/**
* Resolve a stylesheet location
*
* @param location of the stylesheet
* @return a Tuple whose first value is the actual location of the resolved
* stylesheet (used as its base URI), and whose second value is the
* resolved stylesheet as a source
* @throws XPathException if the item does not exist, or is not a document
*/
private Tuple2<String, Source> resolvePossibleStylesheetLocation(final String location) throws XPathException {

Sequence document;
try {
document = DocUtils.getDocument(context, location);
} catch (final PermissionDeniedException e) {
throw new XPathException(fnTransform, ErrorCodes.FODC0002,
"Can not access '" + location + "'" + e.getMessage());
}
if (document != null && document.hasOne() && Type.subTypeOf(document.getItemType(), Type.NODE)) {
if (document instanceof NodeProxy) {
final DOMSource source = new DOMSource(((NodeProxy) document).getNode());
Comment thread
line-o marked this conversation as resolved.
Outdated
source.setSystemId(location);
return Tuple(location, source);
}
else if (document.itemAt(0) instanceof Node) {
final Node node = (Node) document.itemAt(0);
Comment thread
line-o marked this conversation as resolved.
Outdated
final DOMSource source = new DOMSource(node);
source.setSystemId(location);
return Tuple(location, source);
}
}
throw new XPathException(fnTransform, ErrorCodes.FODC0002,
"Location '"+ location + "' returns an item which is not a document node");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
xslt30Transformer.setResultDocumentHandler(resultDocumentURI -> {
final Delivery resultDelivery = new Delivery(context, options.deliveryFormat, serializationProperties);
resultDocuments.put(resultDocumentURI, resultDelivery);
return resultDelivery.createDestination(xslt30Transformer, true);
return resultDelivery.createDestination(xslt30Transformer);
});

if (options.globalContextItem.isPresent()) {
Expand Down Expand Up @@ -330,7 +330,7 @@ private class TemplateInvocation {
this.options = options;
this.sourceNode = sourceNode;
this.delivery = delivery;
this.destination = delivery.createDestination(xslt30Transformer, false);
this.destination = delivery.createDestination(xslt30Transformer);
this.xslt30Transformer = xslt30Transformer;
this.resultDocuments = resultDocuments;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ static AnyURIValue resolveURI(final AnyURIValue relative, final AnyURIValue base
if (relativeURI.isAbsolute()) {
return relative;
}
var baseURI = new URI(base.getStringValue() );
if (!baseURI.isAbsolute()) {
var baseString = base.getStringValue();
Comment thread
line-o marked this conversation as resolved.
Outdated
var baseURI = new URI(baseString);
// Treat database paths (starting with "/" or "xmldb:") as absolute for resolution
var isAbsoluteBase = baseURI.isAbsolute() || baseString.startsWith("/") || baseString.startsWith("xmldb:");
if (!isAbsoluteBase) {
return relative;
}
try {
Expand Down Expand Up @@ -123,10 +126,14 @@ static Source resolveDocument(final String location, final XQueryContext xQueryC
}
if (document.hasOne() && Type.subTypeOf(document.getItemType(), Type.NODE)) {
if (document instanceof NodeProxy proxy) {
return new DOMSource(proxy.getNode());
final DOMSource source = new DOMSource(proxy.getNode());
source.setSystemId(location);
return source;
}
else if (document.itemAt(0) instanceof Node node) {
return new DOMSource(node);
final DOMSource source = new DOMSource(node);
source.setSystemId(location);
return source;
}
}
throw new XPathException(containingExpression, ErrorCodes.FODC0002,
Expand Down
199 changes: 199 additions & 0 deletions exist-core/src/test/xquery/xquery3/transform/fnTransform5052.xqm
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
(:
: eXist-db Open Source Native XML Database
: Copyright (C) 2001 The eXist-db Authors
:
: info@exist-db.org
: http://www.exist-db.org
:
: This library is free software; you can redistribute it and/or
: modify it under the terms of the GNU Lesser General Public
: License as published by the Free Software Foundation; either
: version 2.1 of the License, or (at your option) any later version.
:
: This library 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
: Lesser General Public License for more details.
:
: You should have received a copy of the GNU Lesser General Public
: License along with this library; if not, write to the Free Software
: Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
:)
xquery version "3.1";

(:~
: Tests for issue 5052: fn:transform does not resolve relative URIs
: against the database.
:
: Covers:
: - relative xsl:include / xsl:import hrefs in stylesheets stored in the
: database (resolved from the containing collection of the stylesheet)
: - a relative "stylesheet-location" in a query stored in the database
: (resolved from the containing collection of the query, consistent
: with fn:doc and transform:transform)
:
: @see https://github.com/eXist-db/exist/issues/5052
:)
module namespace t5052="http://exist-db.org/xquery/test/fn-transform-5052";

import module namespace xmldb="http://exist-db.org/xquery/xmldb";
import module namespace util="http://exist-db.org/xquery/util";

declare namespace test="http://exist-db.org/xquery/xqsuite";
declare namespace xsl="http://www.w3.org/1999/XSL/Transform";

declare variable $t5052:coll-name := "fn-transform-5052";
declare variable $t5052:coll := "/db/" || $t5052:coll-name;

declare variable $t5052:plain-xsl :=
<xsl:stylesheet version="3.0">
<xsl:template match="/"><plain-ok/></xsl:template>
</xsl:stylesheet>;

declare variable $t5052:included-xsl :=
<xsl:stylesheet version="3.0">
<xsl:template name="hello"><hello>included</hello></xsl:template>
</xsl:stylesheet>;

declare variable $t5052:main-include-xsl :=
<xsl:stylesheet version="3.0">
<xsl:include href="included.xsl"/>
<xsl:template match="/"><result><xsl:call-template name="hello"/></result></xsl:template>
</xsl:stylesheet>;

declare variable $t5052:imported-xsl :=
<xsl:stylesheet version="3.0">
<xsl:template name="greet"><greet>imported</greet></xsl:template>
</xsl:stylesheet>;

declare variable $t5052:main-import-xsl :=
<xsl:stylesheet version="3.0">
<xsl:import href="sub/imported.xsl"/>
<xsl:template match="/"><result><xsl:call-template name="greet"/></result></xsl:template>
</xsl:stylesheet>;

declare variable $t5052:nested-a-xsl :=
<xsl:stylesheet version="3.0">
<xsl:include href="nested-b.xsl"/>
</xsl:stylesheet>;

declare variable $t5052:nested-b-xsl :=
<xsl:stylesheet version="3.0">
<xsl:template name="deep"><deep>nested</deep></xsl:template>
</xsl:stylesheet>;

declare variable $t5052:main-nested-xsl :=
<xsl:stylesheet version="3.0">
<xsl:include href="sub/nested-a.xsl"/>
<xsl:template match="/"><result><xsl:call-template name="deep"/></result></xsl:template>
</xsl:stylesheet>;

(: a query stored in the database, using a stylesheet-location relative to its collection :)
declare variable $t5052:relative-location-xq :=
'xquery version "3.1";
fn:transform(map{
"stylesheet-location": "plain.xsl",
"source-node": document { <input/> }
})?output';

(: as above, but with a base-uri declared in the prolog: the collection URI without a
: trailing slash, as reported in https://github.com/eXist-db/exist/issues/5052 :)
declare variable $t5052:relative-location-base-uri-xq :=
'xquery version "3.1";
declare base-uri "/db/fn-transform-5052";
fn:transform(map{
"stylesheet-location": "plain.xsl",
"source-node": document { <input/> }
})?output';

declare
%test:setUp
function t5052:setup() {
xmldb:create-collection("/db", $t5052:coll-name),
xmldb:create-collection($t5052:coll, "sub"),
xmldb:store($t5052:coll, "plain.xsl", $t5052:plain-xsl),
xmldb:store($t5052:coll, "included.xsl", $t5052:included-xsl),
xmldb:store($t5052:coll, "main-include.xsl", $t5052:main-include-xsl),
xmldb:store($t5052:coll || "/sub", "imported.xsl", $t5052:imported-xsl),
xmldb:store($t5052:coll, "main-import.xsl", $t5052:main-import-xsl),
xmldb:store($t5052:coll || "/sub", "nested-a.xsl", $t5052:nested-a-xsl),
xmldb:store($t5052:coll || "/sub", "nested-b.xsl", $t5052:nested-b-xsl),
xmldb:store($t5052:coll, "main-nested.xsl", $t5052:main-nested-xsl),
xmldb:store($t5052:coll, "relative-location.xq", $t5052:relative-location-xq, "application/xquery"),
xmldb:store($t5052:coll, "relative-location-base-uri.xq", $t5052:relative-location-base-uri-xq, "application/xquery")
};

declare
%test:tearDown
function t5052:tearDown() {
xmldb:remove($t5052:coll)
};

(:~ Control: absolute stylesheet-location without includes works before and after the fix. :)
declare
%test:assertEquals("<plain-ok/>")
function t5052:absolute-location-no-include() {
fn:transform(map{
"stylesheet-location": $t5052:coll || "/plain.xsl",
"source-node": document { <input/> }
})?output
};

(:~ Relative xsl:include, resolved from the collection containing the stylesheet. :)
declare
%test:assertEquals("<result><hello>included</hello></result>")
function t5052:include-relative-same-collection() {
fn:serialize(fn:transform(map{
"stylesheet-location": $t5052:coll || "/main-include.xsl",
"source-node": document { <input/> }
})?output)
};

(:~ Relative xsl:import into a sub-collection. :)
declare
%test:assertEquals("<result><greet>imported</greet></result>")
function t5052:import-relative-sub-collection() {
fn:serialize(fn:transform(map{
"stylesheet-location": $t5052:coll || "/main-import.xsl",
"source-node": document { <input/> }
})?output)
};

(:~ Base URI must be propagated per stylesheet module: sub/nested-a.xsl includes
: nested-b.xsl which lives next to it in sub/. :)
declare
%test:assertEquals("<result><deep>nested</deep></result>")
function t5052:include-relative-nested() {
fn:serialize(fn:transform(map{
"stylesheet-location": $t5052:coll || "/main-nested.xsl",
"source-node": document { <input/> }
})?output)
};

(:~ Relative xsl:include where the stylesheet is passed as a persistent
: stylesheet-node stored in the database. :)
declare
%test:assertEquals("<result><hello>included</hello></result>")
function t5052:stylesheet-node-stored-include() {
fn:serialize(fn:transform(map{
"stylesheet-node": doc($t5052:coll || "/main-include.xsl"),
"source-node": document { <input/> }
})?output)
};

(:~ A query stored in the database resolves a relative stylesheet-location
: from its own collection (as fn:doc and transform:transform do). :)
declare
%test:assertEquals("<plain-ok/>")
function t5052:relative-location-stored-query() {
fn:serialize(util:eval(xs:anyURI($t5052:coll || "/relative-location.xq")))
};

(:~ As reported in issue 5052: a declared base-uri pointing to a collection
: (no trailing slash) must not lose its last path segment when a relative
: stylesheet-location is resolved against it. :)
declare
%test:assertEquals("<plain-ok/>")
function t5052:relative-location-collection-base-uri() {
fn:serialize(util:eval(xs:anyURI($t5052:coll || "/relative-location-base-uri.xq")))
};
Loading
Loading