This is an automated email from the ASF dual-hosted git repository.

reschke pushed a commit to branch SLING-13265-1.x
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git

commit eee516d6d2f30ddacb30fab9b48d60def7fc8a0d
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                                            |   9 +-
 .../impl/ResourceResolverImpl.java                 |  89 ++++++-----
 .../ResolutionWithInaccessiblePathsTest.java       | 173 +++++++++++++++++++++
 3 files changed, 222 insertions(+), 49 deletions(-)

diff --git a/pom.xml b/pom.xml
index d24c108b..f57a131e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -143,19 +143,16 @@
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
-            <version>5.9.2</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-engine</artifactId>
-            <version>5.9.2</version>
+            <artifactId>junit-jupiter-params</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.junit.vintage</groupId>
-            <artifactId>junit-vintage-engine</artifactId>
-            <version>5.9.2</version>
+            <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 f911e894..cbd56019 100644
--- 
a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
+++ 
b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
@@ -305,6 +305,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);
 
@@ -766,58 +771,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();
 
@@ -957,20 +960,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;
+    }
+}

Reply via email to