Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Media image API handler #10882 #10924

Merged
merged 9 commits into from
Feb 27, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
public final class Cropping
{

public static final Cropping DEFAULT = Cropping.create().build();

private final double top;

private final double left;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public enum ImageOrientation
RightBottom( 7 ), // 0th row at right, 0th column at bottom
LeftBottom( 8 ); // 0th row at left, 0th column at bottom

private static final ImageOrientation DEFAULT = ImageOrientation.TopLeft; // no rotation needed
public static final ImageOrientation DEFAULT = ImageOrientation.TopLeft; // no rotation needed

private static final Map<Integer, ImageOrientation> LOOKUP_TABLE =
Arrays.stream( values() ).collect( Collectors.toMap( e -> e.value, Function.identity() ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ protected void buildToString( final MoreObjects.ToStringHelper helper )

public ImageUrlParams validate()
{
Preconditions.checkState( getPortalRequest().getContent() != null || id != null || path != null,
Preconditions.checkState( (getPortalRequest() != null && getPortalRequest().getContent() != null) || id != null || path != null,
"id, path or content must be set" );
return this;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.enonic.xp.portal.impl;

import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import java.util.Objects;

import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;

import com.enonic.xp.attachment.Attachment;
import com.enonic.xp.content.Media;
import com.enonic.xp.image.Cropping;
import com.enonic.xp.image.FocalPoint;
import com.enonic.xp.media.ImageOrientation;

public final class MediaHashResolver

Check warning on line 17 in modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/MediaHashResolver.java

View check run for this annotation

Codecov / codecov/patch

modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/MediaHashResolver.java#L17

Added line #L17 was not covered by tests
{
public static String resolveLegacyImageHash( final Media media, final String hash )
{
return Hashing.sha1()
.newHasher()
.putString( String.valueOf( hash ), StandardCharsets.UTF_8 )
.putString( String.valueOf( media.getFocalPoint() ), StandardCharsets.UTF_8 )
.putString( String.valueOf( media.getCropping() ), StandardCharsets.UTF_8 )
.putString( String.valueOf( media.getOrientation() ), StandardCharsets.UTF_8 )
.hash()
.toString();
}

public static String resolveImageHash( final Media media, final String hash )
{
if ( hash == null )
{
return null;

Check warning on line 35 in modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/MediaHashResolver.java

View check run for this annotation

Codecov / codecov/patch

modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/MediaHashResolver.java#L35

Added line #L35 was not covered by tests
}

final Hasher hasher = Hashing.sha512().newHasher();

hasher.putBytes( HashCode.fromString( hash ).asBytes() );

final FocalPoint focalPoint = Objects.requireNonNullElse( media.getFocalPoint(), FocalPoint.DEFAULT );

hasher.putDouble( focalPoint.xOffset() );
hasher.putDouble( focalPoint.yOffset() );

final Cropping cropping = Objects.requireNonNullElse( media.getCropping(), Cropping.DEFAULT );

hasher.putDouble( cropping.top() );
hasher.putDouble( cropping.left() );
hasher.putDouble( cropping.bottom() );
hasher.putDouble( cropping.right() );
hasher.putDouble( cropping.zoom() );

final ImageOrientation orientation = Objects.requireNonNullElse( media.getOrientation(), ImageOrientation.DEFAULT );
hasher.putInt( orientation.ordinal() );

return HexFormat.of().formatHex( hasher.hash().asBytes(), 0, 16 );
}

public static String resolveImageHash( final Media media )
{
final Attachment attachment = media.getMediaAttachment();

if ( attachment == null || attachment.getSha512() == null )
{
return null;
}

return resolveImageHash( media, resolveAttachmentHash( attachment ) );
}

public static String resolveAttachmentHash( final Attachment attachment )
{
return attachment == null || attachment.getSha512() == null ? null : attachment.getSha512().substring( 0, 32 );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

public final class VirtualHostContextHelper
{
public static final String MEDIA_SERVICE_BASE_URL = "mediaService.baseUrl";

public static final String MEDIA_SERVICE_SCOPE = "mediaService.scope";

private VirtualHostContextHelper()
Expand All @@ -17,11 +15,6 @@ public static String getProperty( final String property )
return (String) ContextAccessor.current().getAttribute( property );
}

public static String getMediaServiceBaseUrl()
{
return getProperty( MEDIA_SERVICE_BASE_URL );
}

public static String getMediaServiceScope()
{
return getProperty( MEDIA_SERVICE_SCOPE );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,33 @@

import com.enonic.xp.attachment.Attachment;
import com.enonic.xp.attachment.Attachments;
import com.enonic.xp.branch.Branch;
import com.enonic.xp.content.Content;
import com.enonic.xp.content.ContentConstants;
import com.enonic.xp.content.ContentId;
import com.enonic.xp.content.ContentService;
import com.enonic.xp.portal.PortalRequest;
import com.enonic.xp.portal.PortalResponse;
import com.enonic.xp.portal.handler.PortalHandlerWorker;
import com.enonic.xp.portal.impl.handler.attachment.RangeRequestHelper;
import com.enonic.xp.security.RoleKeys;
import com.enonic.xp.security.acl.Permission;
import com.enonic.xp.util.BinaryReference;
import com.enonic.xp.util.MediaTypes;
import com.enonic.xp.web.WebException;
import com.enonic.xp.web.WebRequest;

import static com.enonic.xp.web.servlet.ServletRequestUrlHelper.contentDispositionAttachment;
import static com.google.common.base.Strings.nullToEmpty;

public abstract class AbstractAttachmentHandlerWorker<T extends Content>
extends PortalHandlerWorker<PortalRequest>
{
private static final MediaType SVG_MEDIA_TYPE = MediaType.SVG_UTF_8.withoutParameters();

private static final MediaType AVIF_MEDIA_TYPE = MediaType.create( "image", "avif" );

protected ContentService contentService;

protected WebRequest request;

public ContentId id;

public String name;
Expand All @@ -50,13 +51,16 @@ public abstract class AbstractAttachmentHandlerWorker<T extends Content>

public String contentSecurityPolicySvg;

public AbstractAttachmentHandlerWorker( final PortalRequest request, final ContentService contentService )
public boolean legacyMode;

public Branch branch;

public AbstractAttachmentHandlerWorker( final WebRequest request, final ContentService contentService )
{
super( request );
this.contentService = contentService;
this.request = request;
}

@Override
public PortalResponse execute()
throws Exception
{
Expand All @@ -65,15 +69,14 @@ public PortalResponse execute()
final BinaryReference binaryReference = attachment.getBinaryReference();
final ByteSource binary = getBinary( this.id, binaryReference );

final PortalResponse.Builder portalResponse = PortalResponse.create();
final boolean isSvgz = "svgz".equals( attachment.getExtension() );

final MediaType attachmentMimeType = isSvgz ? SVG_MEDIA_TYPE : MediaType.parse( attachment.getMimeType() );

final MediaType contentType;
final ByteSource body;
if ( attachmentMimeType.is( MediaType.GIF ) || attachmentMimeType.is(
AVIF_MEDIA_TYPE ) || attachmentMimeType.is( MediaType.WEBP ) || attachmentMimeType.is( SVG_MEDIA_TYPE ) )
if ( attachmentMimeType.is( MediaType.GIF ) || attachmentMimeType.is( AVIF_MEDIA_TYPE ) ||
attachmentMimeType.is( MediaType.WEBP ) || attachmentMimeType.is( SVG_MEDIA_TYPE ) )
{
contentType = attachmentMimeType;
body = binary;
Expand All @@ -84,6 +87,7 @@ public PortalResponse execute()
body = transform( content, binaryReference, binary, contentType );
}

final PortalResponse.Builder portalResponse = PortalResponse.create();

if ( contentType.is( SVG_MEDIA_TYPE ) )
{
Expand All @@ -96,21 +100,19 @@ public PortalResponse execute()
portalResponse.header( HttpHeaders.CONTENT_SECURITY_POLICY, contentSecurityPolicySvg );
}
}
else
else if ( !nullToEmpty( contentSecurityPolicy ).isBlank() )
{
if ( !nullToEmpty( contentSecurityPolicy ).isBlank() )
{
portalResponse.header( HttpHeaders.CONTENT_SECURITY_POLICY, contentSecurityPolicy );
}
portalResponse.header( HttpHeaders.CONTENT_SECURITY_POLICY, contentSecurityPolicy );
}

if ( !nullToEmpty( this.fingerprint ).isBlank() )
{
final boolean isPublic = content.getPermissions().isAllowedFor( RoleKeys.EVERYONE, Permission.READ ) &&
ContentConstants.BRANCH_MASTER.equals( request.getBranch() );
ContentConstants.BRANCH_MASTER.equals( branch );
final String cacheControlHeaderConfig = isPublic ? publicCacheControlHeaderConfig : privateCacheControlHeaderConfig;

if ( !nullToEmpty( cacheControlHeaderConfig ).isBlank() && this.fingerprint.equals( resolveHash( content, binaryReference ) ) )
if ( !nullToEmpty( cacheControlHeaderConfig ).isBlank() &&
this.fingerprint.equals( resolveHash( content, attachment, binaryReference ) ) )
{
portalResponse.header( HttpHeaders.CACHE_CONTROL, cacheControlHeaderConfig );
}
Expand All @@ -135,10 +137,7 @@ protected ByteSource transform( final T content, final BinaryReference binaryRef
return binary;
}

protected String resolveHash( final T content, final BinaryReference binaryReference )
{
return this.contentService.getBinaryKey( content.getId(), binaryReference );
}
protected abstract String resolveHash( T content, Attachment attachment, BinaryReference binaryReference );

protected Attachment resolveAttachment( final Content content, final String name )
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ public class ApiDispatcher

private final ErrorHandler errorHandler;

private final MediaHandler mediaHandler;

private volatile boolean legacyImageServiceEnabled;

private volatile boolean legacyAttachmentServiceEnabled;
Expand All @@ -52,8 +50,7 @@ public class ApiDispatcher
public ApiDispatcher( @Reference final SlashApiHandler apiHandler, @Reference final ComponentHandler componentHandler,
@Reference final AssetHandler assetHandler, @Reference final ServiceHandler serviceHandler,
@Reference final IdentityHandler identityHandler, @Reference final ImageHandler imageHandler,
@Reference final AttachmentHandler attachmentHandler, @Reference final ErrorHandler errorHandler,
@Reference final MediaHandler mediaHandler )
@Reference final AttachmentHandler attachmentHandler, @Reference final ErrorHandler errorHandler )
{
super( 1 );

Expand All @@ -65,7 +62,6 @@ public ApiDispatcher( @Reference final SlashApiHandler apiHandler, @Reference fi
this.imageHandler = imageHandler;
this.attachmentHandler = attachmentHandler;
this.errorHandler = errorHandler;
this.mediaHandler = mediaHandler;
}

@Activate
Expand Down Expand Up @@ -95,7 +91,6 @@ protected WebResponse doHandle( final WebRequest webRequest, final WebResponse w
doHandleLegacyHandler( webResponse, legacyAttachmentServiceEnabled, () -> attachmentHandler.handle( webRequest ) );
case "image" -> doHandleLegacyHandler( webResponse, legacyImageServiceEnabled, () -> imageHandler.handle( webRequest ) );
case "service" -> doHandleLegacyHandler( webResponse, legacyHttpServiceEnabled, () -> serviceHandler.handle( webRequest ) );
case "media" -> mediaHandler.handle( webRequest );
case "error" -> errorHandler.handle( webRequest );
case "idprovider" -> identityHandler.handle( webRequest, webResponse );
case "asset" -> assetHandler.handle( webRequest );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public PortalResponse handle( final WebRequest webRequest )
return HandlerHelper.handleDefaultOptions( ALLOWED_METHODS );
}

final AttachmentHandlerWorker worker = new AttachmentHandlerWorker( (PortalRequest) webRequest, this.contentService );
final AttachmentHandlerWorker worker = new AttachmentHandlerWorker( webRequest, this.contentService );
worker.download = "download".equals( matcher.group( 1 ) );
worker.id = ContentId.from( matcher.group( 2 ) );
worker.fingerprint = matcher.group( 3 );
Expand All @@ -97,6 +97,9 @@ public PortalResponse handle( final WebRequest webRequest )
worker.publicCacheControlHeaderConfig = this.publicCacheControlHeaderConfig;
worker.contentSecurityPolicy = this.contentSecurityPolicy;
worker.contentSecurityPolicySvg = this.contentSecurityPolicySvg;
worker.legacyMode = true;
worker.branch = ( (PortalRequest) webRequest ).getBranch();

return worker.execute();
}
}
Loading