Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
new DefaultAttachmentMalwareScanner(persistenceService, attachmentService, scanClient);

EndTransactionMalwareScanProvider malwareScanEndTransactionListener =
(attachmentEntity, contentId) ->
(attachmentEntity, contentId, inlinePrefix) ->
new EndTransactionMalwareScanRunner(
attachmentEntity, contentId, malwareScanner, runtime);
attachmentEntity, contentId, inlinePrefix, malwareScanner, runtime);

// register event handlers for attachment service
configurer.eventHandler(
Expand All @@ -129,7 +129,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
eventFactory, attachmentsReader, outboxedAttachmentService, storage, defaultMaxSize));
configurer.eventHandler(new DeleteAttachmentsHandler(attachmentsReader, deleteEvent));
EndTransactionMalwareScanRunner scanRunner =
new EndTransactionMalwareScanRunner(null, null, malwareScanner, runtime);
new EndTransactionMalwareScanRunner(
null, null, Optional.empty(), malwareScanner, runtime);
configurer.eventHandler(
new ReadAttachmentsHandler(
attachmentService, new AttachmentStatusValidator(), scanRunner, persistenceService));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.sap.cds.services.handler.annotations.ServiceName;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -52,9 +53,17 @@ void processBefore(CdsDeleteEventContext context) {
context.getModel(), context.getTarget(), context.getCqn());

Converter converter =
(path, element, value) ->
deleteEvent.processEvent(
path, (InputStream) value, Attachments.of(path.target().values()), context);
(path, element, value) -> {
Optional<String> inlinePrefix =
ApplicationHandlerHelper.getInlineAttachmentPrefix(
path.target().entity(), element.getName());
return deleteEvent.processEvent(
path,
(InputStream) value,
Attachments.of(path.target().values()),
context,
inlinePrefix);
};

CdsDataProcessor.create()
.addConverter(ApplicationHandlerHelper.MEDIA_CONTENT_FILTER, converter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.slf4j.Logger;
Expand Down Expand Up @@ -99,8 +100,11 @@ void processBefore(CdsReadEventContext context) {
CdsModel cdsModel = context.getModel();
List<String> fieldNames =
getAttachmentAssociations(cdsModel, context.getTarget(), "", new ArrayList<>());
if (!fieldNames.isEmpty()) {
CqnSelect resultCqn = CQL.copy(context.getCqn(), new BeforeReadItemsModifier(fieldNames));
List<String> inlinePrefixes =
ApplicationHandlerHelper.getInlineAttachmentFieldNames(context.getTarget());
if (!fieldNames.isEmpty() || !inlinePrefixes.isEmpty()) {
CqnSelect resultCqn =
CQL.copy(context.getCqn(), new BeforeReadItemsModifier(fieldNames, inlinePrefixes));
context.setCqn(resultCqn);
}
}
Expand All @@ -114,10 +118,21 @@ void processAfter(CdsReadEventContext context, List<CdsData> data) {

Converter converter =
(path, element, value) -> {
Attachments attachment = Attachments.of(path.target().values());
Attachments attachment;
// Check if this is an inline attachment field
Optional<String> inlinePrefix =
ApplicationHandlerHelper.getInlineAttachmentPrefix(
path.target().type(), element.getName());
if (inlinePrefix.isPresent()) {
attachment =
ApplicationHandlerHelper.extractInlineAttachment(
path.target().values(), inlinePrefix.get());
} else {
attachment = Attachments.of(path.target().values());
}
InputStream content = attachment.getContent();
if (nonNull(attachment.getContentId())) {
verifyStatus(path, attachment);
verifyStatus(path, attachment, inlinePrefix);
Supplier<InputStream> supplier =
nonNull(content)
? () -> content
Expand All @@ -137,7 +152,7 @@ void processAfter(CdsReadEventContext context, List<CdsData> data) {
private List<String> getAttachmentAssociations(
CdsModel model, CdsEntity entity, String associationName, List<String> processedEntities) {
List<String> associationNames = new ArrayList<>();
if (ApplicationHandlerHelper.isMediaEntity(entity)) {
if (ApplicationHandlerHelper.isDirectMediaEntity(entity)) {
associationNames.add(associationName);
}

Expand Down Expand Up @@ -167,7 +182,7 @@ private List<String> getAttachmentAssociations(
return associationNames;
}

private void verifyStatus(Path path, Attachments attachment) {
private void verifyStatus(Path path, Attachments attachment, Optional<String> inlinePrefix) {
if (areKeysEmpty(path.target().keys())) {
String currentStatus = attachment.getStatus();
logger.debug(
Expand All @@ -176,13 +191,13 @@ private void verifyStatus(Path path, Attachments attachment) {
currentStatus);
if (needsScan(currentStatus, attachment.getScannedAt())) {
if (StatusCode.CLEAN.equals(currentStatus)) {
transitionToScanning(path.target().entity(), attachment);
transitionToScanning(path.target().entity(), attachment, inlinePrefix);
}
logger.debug(
"Scanning content with ID {} for malware, has current status {}",
attachment.getContentId(),
currentStatus);
scanExecutor.scanAsync(path.target().entity(), attachment.getContentId());
scanExecutor.scanAsync(path.target().entity(), attachment.getContentId(), inlinePrefix);
}
statusValidator.verifyStatus(attachment.getStatus());
}
Expand All @@ -201,26 +216,34 @@ private boolean isScanStale(Instant scannedAt) {
return scannedAt == null || Instant.now().isAfter(scannedAt.plus(RESCAN_THRESHOLD));
}

private void transitionToScanning(CdsEntity entity, Attachments attachment) {
private void transitionToScanning(
CdsEntity entity, Attachments attachment, Optional<String> inlinePrefix) {
logger.debug(
"Attachment {} has stale scan (scannedAt={}), transitioning to SCANNING for rescan.",
attachment.getContentId(),
attachment.getScannedAt());

String contentIdCol = resolveColumn(Attachments.CONTENT_ID, inlinePrefix);
String statusCol = resolveColumn(Attachments.STATUS, inlinePrefix);

Attachments updateData = Attachments.create();
updateData.setStatus(StatusCode.SCANNING);
updateData.put(statusCol, StatusCode.SCANNING);

// Filter by contentId because primary keys are unavailable during content-only reads
// (areKeysEmpty returns true). This is consistent with DefaultAttachmentMalwareScanner.
CqnUpdate update =
Update.entity(entity)
.data(updateData)
.where(entry -> entry.get(Attachments.CONTENT_ID).eq(attachment.getContentId()));
.where(entry -> entry.get(contentIdCol).eq(attachment.getContentId()));
persistenceService.run(update);

attachment.setStatus(StatusCode.SCANNING);
}

private static String resolveColumn(String fieldName, Optional<String> inlinePrefix) {
return inlinePrefix.map(p -> p + "_" + fieldName).orElse(fieldName);
}

private boolean areKeysEmpty(Map<String, Object> keys) {
return keys.values().stream().allMatch(Objects::isNull);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,25 @@ void processBefore(CdsUpdateEventContext context, List<CdsData> data) {
}

private boolean associationsAreUnchanged(CdsEntity entity, List<CdsData> data) {
// TODO: check if this should be replaced with
// entity.assocations().noneMatch(...)
return entity
.compositions()
.noneMatch(
association -> data.stream().anyMatch(d -> d.containsKey(association.getName())));
// Check composition associations
boolean compositionsUnchanged =
entity
.compositions()
.noneMatch(
association -> data.stream().anyMatch(d -> d.containsKey(association.getName())));

// Also check inline attachment fields
List<String> inlinePrefixes = ApplicationHandlerHelper.getInlineAttachmentFieldNames(entity);
boolean inlineUnchanged =
inlinePrefixes.stream()
.noneMatch(
prefix ->
data.stream()
.anyMatch(
d ->
d.keySet().stream().anyMatch(key -> key.startsWith(prefix + "_"))));

return compositionsUnchanged && inlineUnchanged;
}

private void deleteRemovedAttachments(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public final class ModifyApplicationHandlerHelper {

Expand Down Expand Up @@ -51,14 +52,19 @@ public static void handleAttachmentForEntities(
ApplicationHandlerHelper.condenseAttachments(existingAttachments, entity);

Converter converter =
(path, element, value) ->
handleAttachmentForEntity(
condensedExistingAttachments,
eventFactory,
eventContext,
path,
(InputStream) value,
defaultMaxSize);
(path, element, value) -> {
Optional<String> inlinePrefix =
ApplicationHandlerHelper.getInlineAttachmentPrefix(
path.target().entity(), element.getName());
return handleAttachmentForEntity(
condensedExistingAttachments,
eventFactory,
eventContext,
path,
(InputStream) value,
defaultMaxSize,
inlinePrefix);
};

CdsDataProcessor.create()
.addConverter(ApplicationHandlerHelper.MEDIA_CONTENT_FILTER, converter)
Expand All @@ -74,6 +80,7 @@ public static void handleAttachmentForEntities(
* @param path the {@link Path} of the attachment
* @param content the content of the attachment
* @param defaultMaxSize the default max size to use when no annotation is present
* @param inlinePrefix the inline attachment field prefix, or empty for composition-based
* @return the processed content as an {@link InputStream}
*/
public static InputStream handleAttachmentForEntity(
Expand All @@ -82,11 +89,20 @@ public static InputStream handleAttachmentForEntity(
EventContext eventContext,
Path path,
InputStream content,
String defaultMaxSize) {
String defaultMaxSize,
Optional<String> inlinePrefix) {
Map<String, Object> keys = ApplicationHandlerHelper.removeDraftKey(path.target().keys());
ReadonlyDataContextEnhancer.restoreReadonlyFields((CdsData) path.target().values());
Attachments attachment = getExistingAttachment(keys, existingAttachments);
String contentId = (String) path.target().values().get(Attachments.CONTENT_ID);

// For inline attachment fields, extract contentId using the known prefix
String contentId;
if (inlinePrefix.isPresent()) {
contentId =
(String) path.target().values().get(inlinePrefix.get() + "_" + Attachments.CONTENT_ID);
} else {
contentId = (String) path.target().values().get(Attachments.CONTENT_ID);
}
String contentLength = eventContext.getParameterInfo().getHeader("Content-Length");
String maxSizeStr = getValMaxValue(path.target().entity(), defaultMaxSize);
eventContext.put(
Expand All @@ -112,7 +128,8 @@ public static InputStream handleAttachmentForEntity(
ModifyAttachmentEvent eventToProcess =
eventFactory.getEvent(wrappedContent, contentId, attachment);
try {
return eventToProcess.processEvent(path, wrappedContent, attachment, eventContext);
return eventToProcess.processEvent(
path, wrappedContent, attachment, eventContext, inlinePrefix);
} catch (Exception e) {
if (wrappedContent != null && wrappedContent.isLimitExceeded()) {
throw tooLargeException;
Expand All @@ -122,8 +139,20 @@ public static InputStream handleAttachmentForEntity(
}

private static String getValMaxValue(CdsEntity entity, String defaultMaxSize) {
// Try direct content element first (composition-based)
return entity
.findElement("content")
.or(
() -> {
// Try inline attachment content elements (e.g. profilePicture_content)
List<String> prefixes =
ApplicationHandlerHelper.getInlineAttachmentFieldNames(entity);
for (String prefix : prefixes) {
var found = entity.findElement(prefix + "_content");
if (found.isPresent()) return found;
}
return Optional.empty();
})
.flatMap(e -> e.findAnnotation("Validation.Maximum"))
.map(CdsAnnotation::getValue)
.filter(v -> !"true".equals(v.toString()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.sap.cds.reflect.CdsEntity;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
* The class {@link ReadonlyDataContextEnhancer} provides methods to backup and restore readonly
Expand All @@ -35,14 +36,38 @@ public static void preserveReadonlyFields(CdsEntity target, List<CdsData> data,
Validator validator =
(path, element, value) -> {
if (isDraft) {
Attachments values = Attachments.of(path.target().values());
Attachments attachment = Attachments.create();
attachment.setContentId(values.getContentId());
attachment.setStatus(values.getStatus());
attachment.setScannedAt(values.getScannedAt());
path.target().values().put(DRAFT_READONLY_CONTEXT, attachment);
// Determine if this is an inline attachment field
Optional<String> inlinePrefix =
ApplicationHandlerHelper.getInlineAttachmentPrefix(
path.target().type(), element.getName());
if (inlinePrefix.isPresent()) {
// Inline attachment: use prefixed field names
String prefix = inlinePrefix.get() + "_";
Attachments attachment = Attachments.create();
attachment.setContentId(
(String) path.target().values().get(prefix + Attachments.CONTENT_ID));
attachment.setStatus(
(String) path.target().values().get(prefix + Attachments.STATUS));
attachment.setScannedAt(
(java.time.Instant) path.target().values().get(prefix + Attachments.SCANNED_AT));
path.target().values().put(prefix + DRAFT_READONLY_CONTEXT, attachment);
} else {
// Composition-based attachment: use direct field names
Attachments values = Attachments.of(path.target().values());
Attachments attachment = Attachments.create();
attachment.setContentId(values.getContentId());
attachment.setStatus(values.getStatus());
attachment.setScannedAt(values.getScannedAt());
path.target().values().put(DRAFT_READONLY_CONTEXT, attachment);
}
} else {
path.target().values().remove(DRAFT_READONLY_CONTEXT);
// Also remove inline prefixed draft readonly contexts
List<String> prefixes =
ApplicationHandlerHelper.getInlineAttachmentFieldNames(path.target().type());
for (String prefix : prefixes) {
path.target().values().remove(prefix + "_" + DRAFT_READONLY_CONTEXT);
}
}
};

Expand All @@ -53,18 +78,37 @@ public static void preserveReadonlyFields(CdsEntity target, List<CdsData> data,

/**
* Restores the readonly fields with the backup from the data in the custom field {@value
* #DRAFT_READONLY_CONTEXT}.
* #DRAFT_READONLY_CONTEXT}. Supports both composition-based and inline attachment fields.
*
* @param data the {@link CdsData data} to restore with readonly fields
*/
public static void restoreReadonlyFields(CdsData data) {
// Restore composition-based readonly fields
CdsData readOnlyData = (CdsData) data.get(DRAFT_READONLY_CONTEXT);
if (Objects.nonNull(readOnlyData)) {
data.put(Attachments.CONTENT_ID, readOnlyData.get(Attachments.CONTENT_ID));
data.put(Attachments.STATUS, readOnlyData.get(Attachments.STATUS));
data.put(Attachments.SCANNED_AT, readOnlyData.get(Attachments.SCANNED_AT));
data.remove(DRAFT_READONLY_CONTEXT);
}

// Restore inline attachment readonly fields
for (String key : List.copyOf(data.keySet())) {
if (key.endsWith("_" + DRAFT_READONLY_CONTEXT)) {
String prefix = key.substring(0, key.length() - DRAFT_READONLY_CONTEXT.length() - 1);
CdsData inlineReadOnlyData = (CdsData) data.get(key);
if (Objects.nonNull(inlineReadOnlyData)) {
data.put(
prefix + "_" + Attachments.CONTENT_ID,
inlineReadOnlyData.get(Attachments.CONTENT_ID));
data.put(prefix + "_" + Attachments.STATUS, inlineReadOnlyData.get(Attachments.STATUS));
data.put(
prefix + "_" + Attachments.SCANNED_AT,
inlineReadOnlyData.get(Attachments.SCANNED_AT));
data.remove(key);
}
}
}
}

private ReadonlyDataContextEnhancer() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static void validateMediaAttachments(
CdsModel cdsModel = cdsRuntime.getCdsModel();

List<String> mediaEntityNames =
ApplicationHandlerHelper.isMediaEntity(entity)
ApplicationHandlerHelper.isDirectMediaEntity(entity)
? List.of(entity.getQualifiedName())
: cascader.findMediaEntityNames(cdsModel, entity);

Expand Down
Loading
Loading