From b1cdf73a2425cf789aff1e3ab874e05d377dfe0f Mon Sep 17 00:00:00 2001
From: Nuno Antunes <afonso.antunes@live.com.pt>
Date: Mon, 23 Sep 2024 08:50:18 +0100
Subject: [PATCH] feat(server): validate rating (#12855)

* feat(server): validate exif rating tag

* fix(server): change allowed range for rating

* refactor: better readibility

* docs: comments

* remove log line
---
 server/src/services/metadata.service.spec.ts | 24 ++++++++++++++++++++
 server/src/services/metadata.service.ts      | 14 +++++++++++-
 2 files changed, 37 insertions(+), 1 deletion(-)

diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts
index 4eac4a4cf9..ad01aa5784 100644
--- a/server/src/services/metadata.service.spec.ts
+++ b/server/src/services/metadata.service.spec.ts
@@ -1107,6 +1107,30 @@ describe(MetadataService.name, () => {
         }),
       );
     });
+
+    it('should handle invalid rating value', async () => {
+      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      metadataMock.readTags.mockResolvedValue({ Rating: 6 });
+
+      await sut.handleMetadataExtraction({ id: assetStub.image.id });
+      expect(assetMock.upsertExif).toHaveBeenCalledWith(
+        expect.objectContaining({
+          rating: null,
+        }),
+      );
+    });
+
+    it('should handle valid rating value', async () => {
+      assetMock.getByIds.mockResolvedValue([assetStub.image]);
+      metadataMock.readTags.mockResolvedValue({ Rating: 5 });
+
+      await sut.handleMetadataExtraction({ id: assetStub.image.id });
+      expect(assetMock.upsertExif).toHaveBeenCalledWith(
+        expect.objectContaining({
+          rating: 5,
+        }),
+      );
+    });
   });
 
   describe('handleQueueSidecar', () => {
diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts
index 60a1e12a5a..bf76be0731 100644
--- a/server/src/services/metadata.service.ts
+++ b/server/src/services/metadata.service.ts
@@ -83,6 +83,18 @@ const validate = <T>(value: T): NonNullable<T> | null => {
   return value ?? null;
 };
 
+const validateRange = (value: number | undefined, min: number, max: number): NonNullable<number> | null => {
+  // reutilizes the validate function
+  const val = validate(value);
+
+  // check if the value is within the range
+  if (val == null || val < min || val > max) {
+    return null;
+  }
+
+  return val;
+};
+
 @Injectable()
 export class MetadataService {
   private storageCore: StorageCore;
@@ -261,7 +273,7 @@ export class MetadataService {
       // comments
       description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
       profileDescription: exifTags.ProfileDescription || null,
-      rating: exifTags.Rating ?? null,
+      rating: validateRange(exifTags.Rating, 0, 5),
 
       // grouping
       livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,