Skip to content

Commit

Permalink
Media image API handler #10882 (#10924)
Browse files Browse the repository at this point in the history
Media image API handler #10882

Media attachment API handler #10883
  • Loading branch information
anatol-sialitski authored Feb 27, 2025
1 parent f8a1540 commit 5686987
Show file tree
Hide file tree
Showing 33 changed files with 1,982 additions and 1,418 deletions.
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
{
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;
}

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

0 comments on commit 5686987

Please sign in to comment.