Skip to content

Commit

Permalink
Implement HashedFlexibleImageUriFactory/-Parser
Browse files Browse the repository at this point in the history
  • Loading branch information
eschleb committed Nov 16, 2023
1 parent 02fc49e commit 77ac2c9
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 12 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,27 @@ public class CustomImageOperationProvider extends ImageOperationProvider {
<type>com.merkle.oss.magnolia.imaging.flexible.generator.ImageOperationProvider</type>
<implementation>com.somepackage.CustomImageOperationProvider</implementation>
</component>
```

## Hashed uri configuration
To prevent unnecessary generation of images, hashed uri factory/parser can be bound, which generate a hash over the
created uri and checks it when parsing, resulting in 404 if it has been modified.
### Guice binding
```xml
<components>
...
<component>
<type>com.merkle.oss.magnolia.imaging.flexible.generator.uri.FlexibleImageUriParser</type>
<implementation>com.merkle.oss.magnolia.imaging.flexible.generator.uri.HashedFlexibleImageUriParser</implementation>
</component>
<component>
<type>com.merkle.oss.magnolia.imaging.flexible.generator.uri.FlexibleImageUriFactory</type>
<implementation>com.merkle.oss.magnolia.imaging.flexible.generator.uri.HashedFlexibleImageUriFactory</implementation>
</component>
...
</components>
```
### Salt property
```properties
com.merkle.oss.magnolia.imaging.flexible.generator.uri.hash.salt=someEncryptedSalt
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.merkle.oss.magnolia.imaging.flexible.generator;

import com.merkle.oss.magnolia.imaging.flexible.generator.uri.FlexibleImageUriParser;
import com.merkle.oss.magnolia.imaging.flexible.model.FlexibleParameter;
import info.magnolia.imaging.ParameterProvider;
import info.magnolia.imaging.ParameterProviderFactory;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.merkle.oss.magnolia.imaging.flexible.generator;
package com.merkle.oss.magnolia.imaging.flexible.generator.uri;

import com.machinezoo.noexception.Exceptions;
import com.merkle.oss.magnolia.imaging.flexible.generator.FlexibleImageGenerator;
import com.merkle.oss.magnolia.imaging.flexible.model.FlexibleParameter;
import info.magnolia.context.MgnlContext;
import org.apache.commons.lang3.StringUtils;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.merkle.oss.magnolia.imaging.flexible.generator;
package com.merkle.oss.magnolia.imaging.flexible.generator.uri;

import com.merkle.oss.magnolia.imaging.flexible.model.FlexibleParameter;
import com.merkle.oss.magnolia.imaging.flexible.model.bundle.ProcessedBundlesProvider;
Expand Down Expand Up @@ -38,13 +38,13 @@ public FlexibleImageUriParser(

public Optional<FlexibleParameter> parse(final HttpServletRequest request) {
final String uri = request.getRequestURI();
return getAsset(uri)
.flatMap(asset ->
flexibleParameterFactory.create(asset, key -> getParameter(uri, key))
)
.filter(parameter ->
isSizeValid(parameter.getWidth(), parameter.getRatio().orElse(null))
);
return getAsset(uri).flatMap(asset -> parse(uri, asset));
}

protected Optional<FlexibleParameter> parse(final String uri, final Asset asset) {
return flexibleParameterFactory.create(asset, key -> getParameter(uri, key)).filter(parameter ->
isSizeValid(parameter.getWidth(), parameter.getRatio().orElse(null))
);
}

private Optional<String> getParameter(final String uri, final String key) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.merkle.oss.magnolia.imaging.flexible.generator.uri;

import com.merkle.oss.magnolia.imaging.flexible.model.FlexibleParameter;

import javax.inject.Inject;
import java.net.URI;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class HashedFlexibleImageUriFactory extends FlexibleImageUriFactory {
private final ImageDigest imageDigest;

@Inject
public HashedFlexibleImageUriFactory(final ImageDigest imageDigest) {
this.imageDigest = imageDigest;
}

/*
* /<context>/.imaging/flex/assetItemKey/hash/hashValue/param1Key/param1Value/.../fileName
* e.g. /author/.imaging/flex/jcr:b3ee7444-4830-4454-abbb-20fc35387032/crop/true/hash/2a2ca0ea6452010c507f67d3e2dbb823//height/316/width/560/dummy1-1600x900.jpg
*/
public URI create(final FlexibleParameter parameter) {
final URI uri = super.create(parameter);

return super.create(new HashedFlexibleParameter(
parameter,
imageDigest.getMD5Hex(uri.toString())
));
}

static class HashedFlexibleParameter extends FlexibleParameter {
static final String HASH_PARAM = "hash";
private final String hash;

public HashedFlexibleParameter(
final FlexibleParameter wrapped,
final String hash
) {
super(wrapped.getDynamicImageParameter().orElse(null), wrapped.getRatio().orElse(null), wrapped.getWidth(), wrapped);
this.hash = hash;
}

@Override
public Map<String, String> toMap() {
return Stream.concat(
super.toMap().entrySet().stream(),
Map.of(HASH_PARAM, hash).entrySet().stream()
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.merkle.oss.magnolia.imaging.flexible.generator.uri;

import com.merkle.oss.magnolia.imaging.flexible.model.FlexibleParameter;
import com.merkle.oss.magnolia.imaging.flexible.model.bundle.ProcessedBundlesProvider;
import info.magnolia.dam.api.Asset;
import info.magnolia.dam.templating.functions.DamTemplatingFunctions;

import javax.inject.Inject;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.merkle.oss.magnolia.imaging.flexible.generator.uri.HashedFlexibleImageUriFactory.HashedFlexibleParameter.HASH_PARAM;

public class HashedFlexibleImageUriParser extends FlexibleImageUriParser {
/*
* /<context>/.imaging/flex/assetItemKey/hash/hashValue/param1Key/param1Value/.../fileName
* e.g. /author/.imaging/flex/jcr:b3ee7444-4830-4454-abbb-20fc35387032/crop/true/hash/2a2ca0ea6452010c507f67d3e2dbb823/height/316/width/560/dummy1-1600x900.jpg
*/
private static final Pattern HASH_PATH_PARAM_PATTERN = Pattern.compile("(.*)/" + HASH_PARAM + "/([^/]+)(/.+)");
private final ImageDigest imageDigest;

@Inject
public HashedFlexibleImageUriParser(
final DamTemplatingFunctions damTemplatingFunctions,
final ProcessedBundlesProvider bundlesProvider,
final FlexibleParameter.Factory flexibleParameterFactory,
final ImageDigest imageDigest
) {
super(damTemplatingFunctions, bundlesProvider, flexibleParameterFactory);
this.imageDigest = imageDigest;
}

protected Optional<FlexibleParameter> parse(final String uri, final Asset asset) {
final Matcher matcher = HASH_PATH_PARAM_PATTERN.matcher(uri);
if(matcher.matches()) {
final String urlWithoutHashParam = matcher.group(1) + matcher.group(3);
final String hashParamValue = matcher.group(2);
if(Objects.equals(imageDigest.getMD5Hex(urlWithoutHashParam), hashParamValue)) {
return super.parse(uri, asset);
}
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.merkle.oss.magnolia.imaging.flexible.generator.uri;

import info.magnolia.init.MagnoliaConfigurationProperties;
import org.apache.commons.codec.digest.DigestUtils;

import javax.inject.Inject;
import javax.inject.Provider;
import java.security.MessageDigest;
import java.util.Optional;

public class ImageDigest {
static final String SALT_PROPERTY_KEY = "com.merkle.oss.magnolia.imaging.flexible.generator.uri.hash.salt";
private final Provider<String> stringProvider;

@Inject
public ImageDigest(final MagnoliaConfigurationProperties properties) {
this.stringProvider = () -> Optional.ofNullable(properties.getProperty(SALT_PROPERTY_KEY)).orElseThrow(() ->
new NullPointerException("salt not configured! Set "+SALT_PROPERTY_KEY+" in magnolia.properties")
);
}

public String getMD5Hex(final String input) {
final MessageDigest md = DigestUtils.getMd5Digest();
md.update(stringProvider.get().getBytes());
return DigestUtils.md5Hex(md.digest(input.getBytes()));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.merkle.oss.magnolia.imaging.flexible.model;

import com.merkle.oss.magnolia.imaging.flexible.generator.FlexibleImageUriFactory;
import com.merkle.oss.magnolia.imaging.flexible.generator.uri.FlexibleImageUriFactory;
import com.merkle.oss.magnolia.imaging.flexible.model.bundle.ProcessedBundle;
import com.merkle.oss.magnolia.imaging.flexible.model.bundle.ProcessedBundlesProvider;
import info.magnolia.dam.api.Asset;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ public int hashCode() {
return Objects.hash(dynamicImageParameter, ratio, width);
}

@Override
public String toString() {
return "FlexibleParameter{" +
"dynamicImageParameter=" + dynamicImageParameter +
", ratio='" + ratio + '\'' +
", width=" + width +
"} " + super.toString();
}

public static class Factory {
public static final String WIDTH_PARAM = "width";
public static final String RATIO_PARAM = "ratio";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.merkle.oss.magnolia.imaging.flexible.generator;
package com.merkle.oss.magnolia.imaging.flexible.generator.uri;

import com.merkle.oss.magnolia.imaging.flexible.model.DynamicImageParameter;
import com.merkle.oss.magnolia.imaging.flexible.model.FlexibleParameter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.merkle.oss.magnolia.imaging.flexible.generator;
package com.merkle.oss.magnolia.imaging.flexible.generator.uri;

import com.merkle.oss.magnolia.imaging.flexible.model.DynamicImageParameter;
import com.merkle.oss.magnolia.imaging.flexible.model.FlexibleParameter;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.merkle.oss.magnolia.imaging.flexible.generator.uri;

import com.merkle.oss.magnolia.imaging.flexible.model.DynamicImageParameter;
import com.merkle.oss.magnolia.imaging.flexible.model.FlexibleParameter;
import info.magnolia.context.MgnlContext;
import info.magnolia.dam.api.Asset;
import info.magnolia.dam.api.ItemKey;
import info.magnolia.test.TestMagnoliaConfigurationProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

class HashedFlexibleImageUriFactoryTest {
private HashedFlexibleImageUriFactory uriFactory;

@BeforeEach
void setUp() throws IOException {
uriFactory = new HashedFlexibleImageUriFactory(
new ImageDigest(new TestMagnoliaConfigurationProperties(ImageDigest.SALT_PROPERTY_KEY, "someSalt"))
);
}

@Test
void create() throws URISyntaxException {
final Asset asset = mock(Asset.class);
doReturn(ItemKey.from("jcr:b3ee7444-4830-4454-abbb-20fc35387032")).when(asset).getItemKey();
doReturn("someImage.jpg").when(asset).getFileName();
final FlexibleParameter parameter = new FlexibleParameter(new DynamicImageParameter(true), "16:9", 100, asset);

try (MockedStatic<MgnlContext> mgnlContext = Mockito.mockStatic(MgnlContext.class)) {
mgnlContext.when(MgnlContext::getContextPath).thenReturn("/author");

assertEquals(
new URI("/author/.imaging/flex/jcr:b3ee7444-4830-4454-abbb-20fc35387032/crop/true/hash/1952df994c77d7b92999fef87833207f/ratio/16:9/width/100/someImage.jpg"),
uriFactory.create(parameter)
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.merkle.oss.magnolia.imaging.flexible.generator.uri;

import com.merkle.oss.magnolia.imaging.flexible.model.DynamicImageParameter;
import com.merkle.oss.magnolia.imaging.flexible.model.FlexibleParameter;
import com.merkle.oss.magnolia.imaging.flexible.model.bundle.ProcessedBundle;
import com.merkle.oss.magnolia.imaging.flexible.model.bundle.ProcessedBundlesProvider;
import info.magnolia.dam.api.Asset;
import info.magnolia.dam.templating.functions.DamTemplatingFunctions;
import info.magnolia.test.TestMagnoliaConfigurationProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

class HashedFlexibleImageUriParserTest {
private final Asset asset = mock(Asset.class);
private FlexibleImageUriParser flexibleImageUriParser;

@BeforeEach
void setUp() throws IOException {
final DamTemplatingFunctions damTemplatingFunctions = mock(DamTemplatingFunctions.class);
doReturn(asset).when(damTemplatingFunctions).getAsset("jcr:b3ee7444-4830-4454-abbb-20fc35387032");

final ProcessedBundlesProvider processedBundlesProvider = mock(ProcessedBundlesProvider.class);
final ProcessedBundle.ImageSize imageSize = mock(ProcessedBundle.ImageSize.class);
doReturn(100).when(imageSize).getWidth();
doReturn(Optional.of("16:9")).when(imageSize).getRatio();
final ProcessedBundle processedBundle = mock(ProcessedBundle.class);
doReturn(List.of(imageSize)).when(processedBundle).getImageSizes();
doReturn(Collections.emptyList()).when(processedBundle).getCustomRenditions();
doReturn(List.of(processedBundle)).when(processedBundlesProvider).get();


flexibleImageUriParser = new HashedFlexibleImageUriParser(
damTemplatingFunctions,
processedBundlesProvider,
new FlexibleParameter.Factory(new DynamicImageParameter.Factory()),
new ImageDigest(new TestMagnoliaConfigurationProperties(ImageDigest.SALT_PROPERTY_KEY, "someSalt"))
);
}

@Test
void parse_valid() {
assertEquals(
Optional.of(new FlexibleParameter(new DynamicImageParameter(true), "16:9", 100, asset)),
flexibleImageUriParser.parse(createRequest("/author/.imaging/flex/jcr:b3ee7444-4830-4454-abbb-20fc35387032/crop/true/hash/1952df994c77d7b92999fef87833207f/ratio/16:9/width/100/someImage.jpg"))
);
}

@Test
void parse_invalid_hash() {
assertEquals(
Optional.empty(),
flexibleImageUriParser.parse(createRequest("/author/.imaging/flex/jcr:b3ee7444-4830-4454-abbb-20fc35387032/crop/true/hash/INVALID_HASH/ratio/16:9/width/100/someImage.jpg"))
);
}

@Test
void parse_invalid_param() {
assertEquals(
Optional.empty(),
flexibleImageUriParser.parse(createRequest("/author/.imaging/flex/jcr:b3ee7444-4830-4454-abbb-20fc35387032/crop/INVALID/hash/1952df994c77d7b92999fef87833207f/ratio/16:9/width/100/someImage.jpg"))
);
}

private HttpServletRequest createRequest(final String uri) {
final HttpServletRequest request = mock(HttpServletRequest.class);
doReturn(uri).when(request).getRequestURI();
return request;
}
}

0 comments on commit 77ac2c9

Please sign in to comment.