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; + } +}
