Skip to content

Commit

Permalink
Media image API handler #10882
Browse files Browse the repository at this point in the history
  • Loading branch information
anatol-sialitski committed Feb 21, 2025
1 parent ddf8427 commit 211cfa9
Show file tree
Hide file tree
Showing 15 changed files with 428 additions and 668 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,32 @@

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

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

return Hashing.sha1()
.newHasher()
.putString( attachment.getSha512().substring( 0, 32 ), StandardCharsets.UTF_8 )
.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 resolveAttachmentHash( final Media media )
public static String resolveImageHash( final Media media )
{
final Attachment attachment = media.getMediaAttachment();
return attachment.getSha512() != null ? attachment.getSha512().substring( 0, 32 ) : null;

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 @@ -13,28 +13,32 @@
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.content.Media;
import com.enonic.xp.portal.PortalResponse;
import com.enonic.xp.portal.handler.PortalHandlerWorker;
import com.enonic.xp.portal.impl.MediaHashResolver;
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.trace.Trace;
import com.enonic.xp.trace.Tracer;
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>
public abstract class AbstractAttachmentHandlerWorker
{
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 @@ -55,22 +59,20 @@ public abstract class AbstractAttachmentHandlerWorker<T extends Content>

public Branch branch;

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

@Override
public PortalResponse execute()
throws Exception
{
final T content = cast( getContent( this.id ) );
final Attachment attachment = resolveAttachment( content, this.name );
final Media media = castToMedia( getContent( this.id ) );
final Attachment attachment = resolveAttachment( media, this.name );
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() );
Expand All @@ -85,10 +87,12 @@ public PortalResponse execute()
}
else
{
contentType = shouldConvert( content, this.name ) ? MediaTypes.instance().fromFile( this.name ) : attachmentMimeType;
body = transform( content, binaryReference, binary, contentType );
contentType = shouldConvert( media, this.name ) ? MediaTypes.instance().fromFile( this.name ) : attachmentMimeType;
body = transform( media, binaryReference, binary, contentType );
}

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

if ( contentType.is( SVG_MEDIA_TYPE ) )
{
if ( isSvgz )
Expand All @@ -100,21 +104,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 ) &&
final boolean isPublic = media.getPermissions().isAllowedFor( RoleKeys.EVERYONE, Permission.READ ) &&
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( media, attachment, binaryReference ) ) )
{
portalResponse.header( HttpHeaders.CACHE_CONTROL, cacheControlHeaderConfig );
}
Expand All @@ -125,23 +127,32 @@ public PortalResponse execute()
portalResponse.header( HttpHeaders.CONTENT_DISPOSITION, contentDispositionAttachment( attachment.getName() ) );
}

addTrace( content );
addTrace( media );

writeResponseContent( portalResponse, contentType, body );

return portalResponse.build();
}

protected ByteSource transform( final T content, final BinaryReference binaryReference, final ByteSource binary,
protected ByteSource transform( final Media content, final BinaryReference binaryReference, final ByteSource binary,
final MediaType contentType )
throws IOException
{
return binary;
}

protected String resolveHash( final T content, final BinaryReference binaryReference )
private String resolveHash( final Media content, final Attachment attachment, final BinaryReference binaryReference )
{
return this.contentService.getBinaryKey( content.getId(), binaryReference );
if ( legacyMode )
{
final String hash = this.contentService.getBinaryKey( content.getId(), binaryReference );
return content.isImage() ? MediaHashResolver.resolveImageHash( content, hash ) : hash;
}
else
{
final String hash = MediaHashResolver.resolveAttachmentHash( attachment );
return content.isImage() ? MediaHashResolver.resolveImageHash( content, hash ) : hash;
}
}

protected Attachment resolveAttachment( final Content content, final String name )
Expand All @@ -167,9 +178,24 @@ protected void writeResponseContent( final PortalResponse.Builder portalResponse
new RangeRequestHelper().handleRangeRequest( request, portalResponse, body, contentType );
}

protected abstract T cast( Content content );
protected Media castToMedia( final Content content )
{
if ( content instanceof Media )
{
return (Media) content;
}
throw WebException.notFound( String.format( "Content with id [%s] is not a Media", content.getId() ) );
}

protected abstract void addTrace( T content );
protected void addTrace( final Media media )
{
final Trace trace = Tracer.current();
if ( trace != null )
{
trace.put( "contentPath", media.getPath() );
trace.put( "type", media.isImage() ? "image" : "attachment" );
}
}

private Content getContent( final ContentId contentId )
{
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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.enonic.xp.portal.impl.handler;

import java.util.Objects;

import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;

import com.enonic.xp.content.ContentService;
import com.enonic.xp.context.ContextAccessor;
import com.enonic.xp.context.ContextBuilder;
import com.enonic.xp.portal.impl.PortalConfig;
import com.enonic.xp.portal.impl.handler.attachment.AttachmentHandlerWorker;
import com.enonic.xp.portal.universalapi.UniversalApiHandler;
import com.enonic.xp.web.HttpMethod;
import com.enonic.xp.web.WebRequest;
import com.enonic.xp.web.WebResponse;

@Component(service = UniversalApiHandler.class, property = {"applicationKey=media", "apiKey=attachment", "displayName=Attachment Media API",
"allowedPrincipals=role:system.everyone", "mount=true"}, configurationPid = "com.enonic.xp.portal")
public class AttachmentMediaHandler
extends MediaHandlerBase
{
@Activate
public AttachmentMediaHandler( @Reference final ContentService contentService )
{
super( contentService );
}

@Activate
@Modified
public void activate( final PortalConfig config )
{
doActivate( config );
}

@Override
public WebResponse handle( final WebRequest webRequest )
{
final String path = Objects.requireNonNullElse( webRequest.getEndpointPath(), webRequest.getRawPath() );
final AttachmentPathParser pathParser = new AttachmentPathParser( path );
final PathMetadata pathMetadata = pathParser.parse();

if ( webRequest.getMethod() == HttpMethod.OPTIONS )
{
return HandlerHelper.handleDefaultOptions( ALLOWED_METHODS );
}

checkArguments( webRequest, pathMetadata );

return ContextBuilder.copyOf( ContextAccessor.current() )
.repositoryId( pathMetadata.repositoryId )
.branch( pathMetadata.branch )
.build()
.callWith( () -> {
final AttachmentHandlerWorker worker = new AttachmentHandlerWorker( webRequest, this.contentService );

worker.download = HandlerHelper.getParameter( webRequest, "download" ) != null;
worker.id = pathMetadata.contentId;
worker.fingerprint = pathMetadata.fingerprint;
worker.name = pathMetadata.name;
worker.privateCacheControlHeaderConfig = this.privateCacheControlHeaderConfig;
worker.publicCacheControlHeaderConfig = this.publicCacheControlHeaderConfig;
worker.contentSecurityPolicy = this.contentSecurityPolicy;
worker.contentSecurityPolicySvg = this.contentSecurityPolicySvg;
worker.branch = pathMetadata.branch;

return worker.execute();
} );
}


private static final class AttachmentPathParser
extends PathParser<PathMetadata>
{
// Attachment path is: "{project[:draft]}/{id[:fingerprint]}/{name}"

static final String FRAMED_API_HANDLER_KEY = "/media:attachment/";

static final int NAME_INDEX = 2;

static final int PATH_VARIABLES_LIMIT = 4;

AttachmentPathParser( final String path )
{
super( path, FRAMED_API_HANDLER_KEY, PATH_VARIABLES_LIMIT );
}

PathMetadata parse()
{
final PathMetadata metadata = doParse();

metadata.name = pathVariables[NAME_INDEX];

return metadata;
}

@Override
PathMetadata createMetadata()
{
return new PathMetadata();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public PortalResponse handle( final WebRequest webRequest )
}

final ImageHandlerWorker worker =
new ImageHandlerWorker( (PortalRequest) webRequest, this.contentService, this.imageService, this.mediaInfoService );
new ImageHandlerWorker( webRequest, this.contentService, this.imageService, this.mediaInfoService );

worker.id = ContentId.from( matcher.group( 1 ) );
worker.fingerprint = matcher.group( 2 );
Expand Down
Loading

0 comments on commit 211cfa9

Please sign in to comment.