diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/CollectionQueryParameters.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/CollectionQueryParameters.java new file mode 100644 index 00000000000..2d78d01d8a3 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/CollectionQueryParameters.java @@ -0,0 +1,237 @@ +/* + * 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 + */ +package org.exist.xquery.functions.fn; + +import org.exist.xquery.ErrorCodes; +import org.exist.xquery.Expression; +import org.exist.xquery.XPathException; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Saxon-style query string parameters shared by {@link FunUriCollection} + * (database collections) and {@link ExtCollection} (file: URI collections). + * + *
Supports four parameters:
+ *Each consumer specifies which subset of parameters it accepts via the {@code allowedKeys} + * argument to {@link #parse(String, Set, Expression)}. Unknown or invalid keys/values raise + * {@link ErrorCodes#FODC0004}.
+ */ +public final class CollectionQueryParameters { + + public static final String KEY_SELECT = "select"; + public static final String KEY_MATCH = "match"; + public static final String KEY_CONTENT_TYPE = "content-type"; + public static final String KEY_STABLE = "stable"; + + public static final String VALUE_CONTENT_TYPE_DOCUMENT = "application/vnd.existdb.document"; + public static final String VALUE_CONTENT_TYPE_DOCUMENT_BINARY = "application/vnd.existdb.document+binary"; + public static final String VALUE_CONTENT_TYPE_DOCUMENT_XML = "application/vnd.existdb.document+xml"; + public static final String VALUE_CONTENT_TYPE_SUBCOLLECTION = "application/vnd.existdb.collection"; + public static final String[] VALUE_CONTENT_TYPES = { + VALUE_CONTENT_TYPE_DOCUMENT, + VALUE_CONTENT_TYPE_DOCUMENT_BINARY, + VALUE_CONTENT_TYPE_DOCUMENT_XML, + VALUE_CONTENT_TYPE_SUBCOLLECTION + }; + + public static final String VALUE_STABLE_NO = "no"; + public static final String VALUE_STABLE_YES = "yes"; + public static final String[] VALUE_STABLES = { + VALUE_STABLE_NO, + VALUE_STABLE_YES + }; + + /** Keys accepted by fn:uri-collection (no select). */ + public static final Set+ * Supports Saxon-style query string parameters (aligned with fn:uri-collection): + *
+ *+ * Only DBA users can access the file system directly. + *
+ */ + private void getFileCollectionItems(final URI collectionUri, final String rawQueryString, final Sequence items) throws XPathException { + // Security: only DBA users can access file: URIs + if (!context.getBroker().getCurrentSubject().hasDbaRole()) { + throw new XPathException(this, ErrorCodes.FODC0002, + "Permission denied: only DBA users can access file: URIs in fn:collection()"); + } + + // Parse Saxon-style query parameters from the raw query string. + // We use rawQueryString (passed separately from the URI) because the query + // may contain regex characters like ^, [, ], +, $ that Java's URI class rejects. + final CollectionQueryParameters params = CollectionQueryParameters.parse( + rawQueryString != null ? "?" + rawQueryString : null, + CollectionQueryParameters.FILE_COLLECTION_KEYS, + this); + + // fn:collection() returns documents (XML), so a content-type that excludes XML + // would yield an empty result. Detect that early. + if (params.hasContentType() && !params.includesXmlDocuments()) { + return; + } + + // Default glob pattern is *.xml (XML files only). User-supplied select overrides. + final String globPattern = params.getSelect() != null ? params.getSelect() : "*.xml"; + + // Compile match regex if present + final Pattern matchPattern = (params.getMatch() != null && !params.getMatch().isEmpty()) + ? Pattern.compile(params.getMatch()) + : null; + + final Path dir = Paths.get(collectionUri.getPath()); + if (!Files.isDirectory(dir)) { + throw new XPathException(this, ErrorCodes.FODC0002, + "Directory does not exist: " + dir); + } + + // Collect candidate files matching all filters + final List
+ * Creates a temp directory with a mix of files (XML, non-XML, malformed) and verifies
+ * the {@code select}, {@code match}, {@code content-type}, and {@code stable} parameters.
+ */
+public class CollectionFileUriTest {
+
+ @ClassRule
+ public static final ExistEmbeddedServer existEmbeddedServer = new ExistEmbeddedServer(true, true);
+
+ private static Path tempDir;
+
+ @BeforeClass
+ public static void setUp() throws IOException {
+ tempDir = Files.createTempDirectory("exist-collection-file-uri-test-");
+
+ // 5 well-formed XML files
+ Files.writeString(tempDir.resolve("doc1.xml"), "1");
+ Files.writeString(tempDir.resolve("doc2.xml"), "2");
+ Files.writeString(tempDir.resolve("doc3.xml"), "3");
+ Files.writeString(tempDir.resolve("alpha.xml"), "alpha");
+ Files.writeString(tempDir.resolve("beta.xml"), "beta");
+
+ // Non-XML files (should be excluded by default *.xml glob)
+ Files.writeString(tempDir.resolve("readme.txt"), "not xml");
+ Files.writeString(tempDir.resolve("data.json"), "{\"k\":1}");
+ }
+
+ @AfterClass
+ public static void tearDown() throws IOException {
+ if (tempDir != null && Files.exists(tempDir)) {
+ try (final Stream