This is an automated email from the ASF dual-hosted git repository.
jsedding pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
The following commit(s) were added to refs/heads/master by this push:
new b70eb04f SLING-13265: Wrong ResourceMetadata for alias resolution with
unreadable ancestor (#215)
b70eb04f is described below
commit b70eb04f20766e7a72908a85ff267f45021b5f98
Author: Julian Sedding <[email protected]>
AuthorDate: Mon Aug 3 09:30:06 2026 +0200
SLING-13265: Wrong ResourceMetadata for alias resolution with unreadable
ancestor (#215)
---
pom.xml | 6 +-
.../impl/ResourceResolverImpl.java | 89 ++++++-----
.../ResolutionWithInaccessiblePathsTest.java | 173 +++++++++++++++++++++
3 files changed, 222 insertions(+), 46 deletions(-)
diff --git a/pom.xml b/pom.xml
index 429a650d..cde5ff73 100644
--- a/pom.xml
+++ b/pom.xml
@@ -154,12 +154,12 @@
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter-engine</artifactId>
+ <artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.junit.vintage</groupId>
- <artifactId>junit-vintage-engine</artifactId>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
diff --git
a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
index ff04067f..9f7b0248 100644
---
a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
+++
b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
@@ -333,6 +333,11 @@ public class ResourceResolverImpl extends SlingAdaptable
implements ResourceReso
absPath = "/" + absPath;
}
+ // replace multiple slashes with single slashes to support paths like
e.g. //bin/browser.html
+ while (absPath.startsWith("//")) {
+ absPath = absPath.substring(1);
+ }
+
// check for special namespace prefix treatment
absPath = unmangleNamespaces(absPath);
@@ -809,58 +814,56 @@ public class ResourceResolverImpl extends SlingAdaptable
implements ResourceReso
* the {@link ResourcePathIterator} to resolve the resource.
*/
public Resource resolveInternal(final String absPath, final Map<String,
String> parameters) {
- Resource resource = null;
if (absPath != null && !absPath.isEmpty() && !absPath.startsWith("/"))
{
logger.debug("resolveInternal: absolute path expected {} ",
absPath);
- return resource; // resource is null at this point
+ return null;
}
String curPath = absPath;
try {
final ResourcePathIterator it = new ResourcePathIterator(absPath);
+ Resource resource = null;
while (it.hasNext() && resource == null) {
curPath = it.next();
resource = getAbsoluteResourceInternal(null, curPath,
parameters, true);
}
+ // SLING-627: set the part cut off from the uriPath as
+ // sling.resolutionPathInfo property such that
+ // uriPath = curPath + sling.resolutionPathInfo
+ if (resource != null) {
+
+ final String rpi = absPath.substring(curPath.length());
+
resource.getResourceMetadata().setResolutionPath(absPath.substring(0,
curPath.length()));
+ resource.getResourceMetadata().setResolutionPathInfo(rpi);
+ resource.getResourceMetadata().setParameterMap(parameters);
+
+ logger.debug(
+ "resolveInternal: Found resource {} with path info {}
for {}",
+ new Object[] {resource, rpi, absPath});
+ return resource;
+ }
} catch (final Exception ex) {
throw new SlingException("Problem trying " + curPath + " for
request path " + absPath, ex);
}
- // SLING-627: set the part cut off from the uriPath as
- // sling.resolutionPathInfo property such that
- // uriPath = curPath + sling.resolutionPathInfo
+ // no direct resource found, so we have to drill down into the
+ // resource tree to find a match
+ Resource resource = getAbsoluteResourceInternal(absPath, parameters,
true);
if (resource != null) {
-
- final String rpi = absPath.substring(curPath.length());
-
resource.getResourceMetadata().setResolutionPath(absPath.substring(0,
curPath.length()));
- resource.getResourceMetadata().setResolutionPathInfo(rpi);
- resource.getResourceMetadata().setParameterMap(parameters);
-
- logger.debug(
- "resolveInternal: Found resource {} with path info {} for
{}",
- new Object[] {resource, rpi, absPath});
-
- } else {
-
- String tokenizedPath = absPath;
-
- // no direct resource found, so we have to drill down into the
- // resource tree to find a match
- resource = getAbsoluteResourceInternal(null, "/", parameters,
true);
-
- // no read access on / drilling further down
- // SLING-5638
- if (resource == null) {
- resource = getAbsoluteResourceInternal(absPath, parameters,
true);
- if (resource != null) {
- tokenizedPath =
tokenizedPath.substring(resource.getPath().length());
- }
- }
-
final StringBuilder resolutionPath = new StringBuilder();
+ String tokenizedPath = Objects.equals(resource.getPath(), "/")
+ ? absPath
+ : absPath.substring(resource.getPath().length());
final StringTokenizer tokener = new StringTokenizer(tokenizedPath,
"/");
final int delimCount = StringUtils.countMatches(tokenizedPath,
'/');
final int redundantDelimCount = delimCount - tokener.countTokens();
+ // we found an ancestor resource, so we need to prefix the
resolutionPath with it
+ // unless it is the root path (that would result in a double slash
prefix "//")
+ String resolutionPathPrefix =
resource.getResourceMetadata().getResolutionPath();
+ if (!Objects.equals(resolutionPathPrefix, "/")) {
+ resolutionPath.append(resolutionPathPrefix);
+ }
+
while (resource != null && tokener.hasMoreTokens()) {
final String childNameRaw = tokener.nextToken();
@@ -1000,20 +1003,20 @@ public class ResourceResolverImpl extends
SlingAdaptable implements ResourceReso
return null;
}
- absPath = absPath.substring(absPath.indexOf("/"));
- Resource resource = getAbsoluteResourceInternal(null, absPath,
parameters, isResolved);
-
- absPath = absPath.substring(0, absPath.lastIndexOf("/"));
-
- while (!absPath.equals("")) {
- Resource r = getAbsoluteResourceInternal(null, absPath,
parameters, true);
-
+ String candidatePath = absPath.substring(absPath.indexOf("/"));
+ while (candidatePath != null) {
+ // If this was a "getResource" call, i.e. isResolved = false,
+ // then the first iteration will return a Resource; in case the
+ // resource doesn't exist a SyntheticResource. I.e. for
+ // "getResource" calls this loop should always return in during
+ // the first iteration.
+ Resource r = getAbsoluteResourceInternal(null, candidatePath,
parameters, isResolved);
if (r != null) {
- resource = r;
+ return r;
}
- absPath = absPath.substring(0, absPath.lastIndexOf("/"));
+ candidatePath = ResourceUtil.getParent(candidatePath);
}
- return resource;
+ return null;
}
/**
diff --git
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResolutionWithInaccessiblePathsTest.java
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResolutionWithInaccessiblePathsTest.java
new file mode 100644
index 00000000..47f3ef2e
--- /dev/null
+++
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResolutionWithInaccessiblePathsTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.resourceresolver.impl.mapping;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.resourceresolver.impl.ResourceAccessSecurityTracker;
+import org.apache.sling.resourceresolver.impl.ResourceResolverFactoryActivator;
+import org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContext;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContextExtension;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.osgi.util.tracker.ServiceTracker;
+
+import static
org.apache.sling.spi.resource.provider.ResourceProvider.PROPERTY_NAME;
+import static
org.apache.sling.spi.resource.provider.ResourceProvider.PROPERTY_ROOT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class ResolutionWithInaccessiblePathsTest {
+
+ @RegisterExtension
+ OsgiContextExtension osgiContextExtension = new OsgiContextExtension();
+
+ @ParameterizedTest(name = "[{index}] inaccessible path: {0}")
+ @ValueSource(
+ strings = {
+ "none (equivalent to admin access)",
+ "/",
+ "/content",
+ "/content/en",
+ "/content/en/solutions",
+ "/content/en/solutions/airlines",
+ "/content/en/solutions/airlines/products"
+ })
+ void simpleResolution(String inaccessiblePath, OsgiContext ctx) throws
InterruptedException, LoginException {
+ ResourceResolverFactory resourceResolverFactory =
registerResourceResolverfactory(ctx, p -> {
+ Stream.of(
+ "/",
+ "/content",
+ "/content/en",
+ "/content/en/solutions",
+ "/content/en/solutions/airlines",
+ "/content/en/solutions/airlines/products")
+ .filter(path -> !Objects.equals(path, inaccessiblePath))
+ .forEach(p::putResource);
+ p.putResource("/content/en/solutions/airlines/products/wings");
+ });
+
+ ResourceResolver resolver =
resourceResolverFactory.getResourceResolver(Collections.emptyMap());
+ Resource resource =
resolver.resolve("/content/en/solutions/airlines/products/wings.html");
+ assertEquals(
+ "/content/en/solutions/airlines/products/wings",
+ resource.getResourceMetadata().getResolutionPath());
+ assertEquals(".html",
resource.getResourceMetadata().getResolutionPathInfo());
+ assertEquals("/content/en/solutions/airlines/products/wings",
resource.getPath());
+ }
+
+ @ParameterizedTest(name = "[{index}] inaccessible path: {0}")
+ @ValueSource(strings = {"none (equivalent to admin access)", "/",
"/content", "/content/es"})
+ void aliasResolution(String inaccessiblePath, OsgiContext ctx) throws
InterruptedException, LoginException {
+ ResourceResolverFactory resourceResolverFactory =
registerResourceResolverfactory(ctx, p -> {
+ Stream.of("/", "/content", "/content/es")
+ .filter(path -> !Objects.equals(path, inaccessiblePath))
+ .forEach(p::putResource);
+ p.putResource("/content/es/solutions");
+ p.putResource("/content/es/solutions/airlines", "sling:alias",
"aerolineas");
+ p.putResource("/content/es/solutions/airlines/products",
"sling:alias", "productos");
+ p.putResource("/content/es/solutions/airlines/products/wings",
"sling:alias", "alas");
+ });
+
+ ResourceResolver resolver =
resourceResolverFactory.getResourceResolver(Collections.emptyMap());
+ Resource resource;
+
+ resource =
resolver.resolve("/content/es/solutions/aerolineas/productos/alas.html");
+ assertEquals(
+ "/content/es/solutions/aerolineas/productos/alas",
+ resource.getResourceMetadata().getResolutionPath());
+ assertEquals(".html",
resource.getResourceMetadata().getResolutionPathInfo());
+ assertEquals("/content/es/solutions/airlines/products/wings",
resource.getPath());
+
+ resource =
resolver.resolve("/content/es/solutions/aerolineas/productos/wings.mobile.html");
+ assertEquals(
+ "/content/es/solutions/aerolineas/productos/wings",
+ resource.getResourceMetadata().getResolutionPath());
+ assertEquals(".mobile.html",
resource.getResourceMetadata().getResolutionPathInfo());
+ assertEquals("/content/es/solutions/airlines/products/wings",
resource.getPath());
+
+ resource =
resolver.resolve("/content/es/solutions/airlines/productos/alas.json");
+ assertEquals(
+ "/content/es/solutions/airlines/productos/alas",
+ resource.getResourceMetadata().getResolutionPath());
+ assertEquals(".json",
resource.getResourceMetadata().getResolutionPathInfo());
+ assertEquals("/content/es/solutions/airlines/products/wings",
resource.getPath());
+ }
+
+ @ParameterizedTest(name = "[{index}] inaccessible path: {0}")
+ @ValueSource(strings = {"none (equivalent to admin access)", "/",
"/content"})
+ void emptySegmentResolution(String inaccessiblePath, OsgiContext ctx)
throws InterruptedException, LoginException {
+ ResourceResolverFactory resourceResolverFactory =
registerResourceResolverfactory(ctx, p -> {
+ Stream.of("/", "/content", "/content/en")
+ .filter(path -> !Objects.equals(path, inaccessiblePath))
+ .forEach(p::putResource);
+ });
+
+ ResourceResolver resolver =
resourceResolverFactory.getResourceResolver(Collections.emptyMap());
+ Resource resource = resolver.resolve("//content/en.html"); // leading
double slash
+ assertEquals("/content/en",
resource.getResourceMetadata().getResolutionPath());
+ assertEquals(".html",
resource.getResourceMetadata().getResolutionPathInfo());
+ assertEquals("/content/en", resource.getPath());
+ }
+
+ private static ResourceResolverFactory registerResourceResolverfactory(
+ OsgiContext ctx, Consumer<InMemoryResourceProvider>
resourceInitializer) throws InterruptedException {
+ ctx.registerInjectActivateService(new ServiceUserMapperImpl());
+ ctx.registerInjectActivateService(new ResourceAccessSecurityTracker());
+ ctx.registerInjectActivateService(new
StringInterpolationProviderImpl());
+
+ InMemoryResourceProvider provider = new
InMemoryResourceProvider(false);
+ resourceInitializer.accept(provider);
+
+ // we fake the fact that we are the JCR resource provider since it's
the required one
+ ctx.registerService(ResourceProvider.class, provider, PROPERTY_ROOT,
"/", PROPERTY_NAME, "JCR");
+
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("resource.resolver.optimize.alias.resolution", true);
+ properties.put("resource.resolver.alias.cache.in.background", false);
+ properties.put("resource.resolver.mapping", new String[] {"/:/"});
+
ctx.registerInjectActivateService(ResourceResolverFactoryActivator.class,
properties);
+
+ final ResourceResolverFactory factory;
+ final ServiceTracker<ResourceResolverFactory, ResourceResolverFactory>
tracker =
+ new ServiceTracker<>(ctx.bundleContext(),
ResourceResolverFactory.class, null);
+ try {
+ tracker.open();
+ factory = tracker.waitForService(TimeUnit.SECONDS.toMillis(300));
+ } finally {
+ tracker.close();
+ }
+
+ assertNotNull(factory);
+ return factory;
+ }
+}