Repository: solid/community-server (ts)

Issue:
Selectively enable/disable ConstantConverter for certain MIME types
### Discussed in https://github.com/solid/community-server/discussions/895

<div type='discussions-op-text'>

<sup>Originally posted by **angelo-v** August  6, 2021</sup>
I changed set `"options_document": true` in the mashlib recipe, so also files get rendered via data browser.

But I want to download certain file types, especially PDFs, which does not work with this setting anymore, since the data browser renders instead. How can I exclude certain file types from the data browser rendering?</div>

---

As a solution,  propose optional constructor arguments for string arrays of `enableMimeTypes` and `disableMimeTypes`, defaulting to `[ '*/* ]` and `[]`, respectively. MIME checks should be bypassed for containers.

Pull request description:
feat: Allow filtering in ConstantConverter based on type
Closes #896.

Patch:
diff --git a/README.md b/README.md
index 40972ced7..aeee635ec 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,8 @@ npm start -- # add parameters if needed
 ### 📦 Running via Docker
 Docker allows you to run the server without having Node.js installed:
 ```shell
+git clone https://github.com/solid/community-server.git
+cd community-server
 # Build the Docker image
 docker build --rm -f Dockerfile -t css:latest .
 # Run the image, serving your `~/Solid` directory on `http://localhost:3000`
diff --git a/config/storage/backend/regex.json b/config/storage/backend/regex.json
index 09dc72e24..c4578c5fd 100644
--- a/config/storage/backend/regex.json
+++ b/config/storage/backend/regex.json
@@ -56,11 +56,17 @@
       "accessor": { "@id": "urn:solid-server:default:MemoryDataAccessor" }
     },
     {
+      "comment": "SparqlDataAccessor only accepts quad objects so data to that route needs to be converted",
       "@id": "urn:solid-server:default:SparqlResourceStore",
-      "@type": "DataAccessorBasedStore",
-      "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" },
-      "auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" },
-      "accessor": { "@id": "urn:solid-server:default:SparqlDataAccessor" }
+      "@type": "RepresentationConvertingStore",
+      "options_inConverter": { "@id": "urn:solid-server:default:RepresentationConverter" },
+      "options_inType": "internal/quads",
+      "source": {
+        "@type": "DataAccessorBasedStore",
+        "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" },
+        "auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" },
+        "accessor": { "@id": "urn:solid-server:default:SparqlDataAccessor" }
+      }
     }
   ]
 }
diff --git a/package-lock.json b/package-lock.json
index 0cc02f911..1b60db42e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,7 @@
         "@types/cors": "^2.8.10",
         "@types/end-of-stream": "^1.4.0",
         "@types/lodash.orderby": "^4.6.6",
-        "@types/marked": "^2.0.3",
+        "@types/marked": "^3.0.0",
         "@types/mime-types": "^2.1.0",
         "@types/n3": "^1.10.0",
         "@types/node": "^15.12.5",
@@ -45,7 +45,7 @@
         "handlebars": "^4.7.7",
         "jose": "^3.11.6",
         "lodash.orderby": "^4.6.0",
-        "marked": "^2.1.3",
+        "marked": "^3.0.0",
         "mime-types": "^2.1.32",
         "n3": "^1.10.0",
         "nodemailer": "^6.6.2",
@@ -4927,9 +4927,9 @@
       "integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w=="
     },
     "node_modules/@types/marked": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.3.tgz",
-      "integrity": "sha512-lbhSN1rht/tQ+dSWxawCzGgTfxe9DB31iLgiT1ZVT5lshpam/nyOA1m3tKHRoNPctB2ukSL22JZI5Fr+WI/zYg=="
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/marked/-/marked-3.0.0.tgz",
+      "integrity": "sha512-vof90OIWT+Tzq3MBRXgV9fsH8PC3WZ4OQg9Qa04vOtP0TcyiNfl7BTonYCmTapHZ5lRZh6ihUYkAy7St1hmk/A=="
     },
     "node_modules/@types/mime": {
       "version": "1.3.2",
@@ -11805,14 +11805,14 @@
       }
     },
     "node_modules/marked": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz",
-      "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.1.tgz",
+      "integrity": "sha512-GQtec2MkSCp+ZecV2jBe7+J2fiDXeBFKUBr4981srZDjCaXg0WrtTo9eY2CVc2ZC5YQXkkc0Q8sTZ6c1DB1o2Q==",
       "bin": {
         "marked": "bin/marked"
       },
       "engines": {
-        "node": ">= 10"
+        "node": ">= 12"
       }
     },
     "node_modules/media-typer": {
@@ -15391,6 +15391,18 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/typedoc/node_modules/marked": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz",
+      "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==",
+      "dev": true,
+      "bin": {
+        "marked": "bin/marked"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
     "node_modules/typescript": {
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
@@ -19939,9 +19951,9 @@
       "integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w=="
     },
     "@types/marked": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.3.tgz",
-      "integrity": "sha512-lbhSN1rht/tQ+dSWxawCzGgTfxe9DB31iLgiT1ZVT5lshpam/nyOA1m3tKHRoNPctB2ukSL22JZI5Fr+WI/zYg=="
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/marked/-/marked-3.0.0.tgz",
+      "integrity": "sha512-vof90OIWT+Tzq3MBRXgV9fsH8PC3WZ4OQg9Qa04vOtP0TcyiNfl7BTonYCmTapHZ5lRZh6ihUYkAy7St1hmk/A=="
     },
     "@types/mime": {
       "version": "1.3.2",
@@ -25212,9 +25224,9 @@
       "dev": true
     },
     "marked": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz",
-      "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA=="
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.1.tgz",
+      "integrity": "sha512-GQtec2MkSCp+ZecV2jBe7+J2fiDXeBFKUBr4981srZDjCaXg0WrtTo9eY2CVc2ZC5YQXkkc0Q8sTZ6c1DB1o2Q=="
     },
     "media-typer": {
       "version": "0.3.0",
@@ -28001,6 +28013,12 @@
             "once": "^1.3.0",
             "path-is-absolute": "^1.0.0"
           }
+        },
+        "marked": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz",
+          "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==",
+          "dev": true
         }
       }
     },
diff --git a/package.json b/package.json
index 31ff5eb41..d0a1f0051 100644
--- a/package.json
+++ b/package.json
@@ -84,7 +84,7 @@
     "@types/cors": "^2.8.10",
     "@types/end-of-stream": "^1.4.0",
     "@types/lodash.orderby": "^4.6.6",
-    "@types/marked": "^2.0.3",
+    "@types/marked": "^3.0.0",
     "@types/mime-types": "^2.1.0",
     "@types/n3": "^1.10.0",
     "@types/node": "^15.12.5",
@@ -111,7 +111,7 @@
     "handlebars": "^4.7.7",
     "jose": "^3.11.6",
     "lodash.orderby": "^4.6.0",
-    "marked": "^2.1.3",
+    "marked": "^3.0.0",
     "mime-types": "^2.1.32",
     "n3": "^1.10.0",
     "nodemailer": "^6.6.2",
diff --git a/src/storage/conversion/ConstantConverter.ts b/src/storage/conversion/ConstantConverter.ts
index 040d5279a..37c8042ef 100644
--- a/src/storage/conversion/ConstantConverter.ts
+++ b/src/storage/conversion/ConstantConverter.ts
@@ -23,6 +23,14 @@ export interface ConstantConverterOptions {
    * The minimum requested quality/preference before this should trigger.
    */
   minQuality?: number;
+  /**
+   * Media ranges for which the conversion should happen.
+   */
+  enabledMediaRanges?: string[];
+  /**
+   * Media ranges for which the conversion should not happen.
+   */
+  disabledMediaRanges?: string[];
 }
 
 /**
@@ -57,6 +65,8 @@ export class ConstantConverter extends RepresentationConverter {
       container: options.container ?? true,
       document: options.document ?? true,
       minQuality: options.minQuality ?? 0,
+      enabledMediaRanges: options.enabledMediaRanges ?? [ '*/*' ],
+      disabledMediaRanges: options.disabledMediaRanges ?? [],
     };
   }
 
@@ -83,10 +93,19 @@ export class ConstantConverter extends RepresentationConverter {
       throw new NotImplementedHttpError(`Preference is lower than the specified minimum quality`);
     }
 
+    const sourceContentType = representation.metadata.contentType ?? '';
     // Do not replace the representation if it already has our content type
-    if (matchesMediaType(representation.metadata.contentType ?? '', this.contentType)) {
+    if (matchesMediaType(sourceContentType, this.contentType)) {
       throw new NotImplementedHttpError(`Representation is already ${this.contentType}`);
     }
+
+    // Only replace the representation if it matches the media range settings
+    if (!this.options.enabledMediaRanges.some((type): boolean => matchesMediaType(sourceContentType, type))) {
+      throw new NotImplementedHttpError(`${sourceContentType} is not one of the enabled media types.`);
+    }
+    if (this.options.disabledMediaRanges.some((type): boolean => matchesMediaType(sourceContentType, type))) {
+      throw new NotImplementedHttpError(`${sourceContentType} is one of the disabled media types.`);
+    }
   }
 
   public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {


Test patch:
diff --git a/test/unit/storage/conversion/ConstantConverter.test.ts b/test/unit/storage/conversion/ConstantConverter.test.ts
index 27e81ff78..964dc359d 100644
--- a/test/unit/storage/conversion/ConstantConverter.test.ts
+++ b/test/unit/storage/conversion/ConstantConverter.test.ts
@@ -3,6 +3,7 @@ import arrayifyStream from 'arrayify-stream';
 import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import type { ConstantConverterOptions } from '../../../../src/storage/conversion/ConstantConverter';
 import { ConstantConverter } from '../../../../src/storage/conversion/ConstantConverter';
+import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
 
 const createReadStream = jest.spyOn(fs, 'createReadStream').mockReturnValue('file contents' as any);
 
@@ -12,7 +13,7 @@ describe('A ConstantConverter', (): void => {
   let converter: ConstantConverter;
 
   beforeEach(async(): Promise<void> => {
-    options = { container: true, document: true, minQuality: 1 };
+    options = { container: true, document: true, minQuality: 1, enabledMediaRanges: [ '*/*' ], disabledMediaRanges: []};
     converter = new ConstantConverter('abc/def/index.html', 'text/html', options);
   });
 
@@ -69,6 +70,26 @@ describe('A ConstantConverter', (): void => {
     await expect(converter.canHandle(args)).rejects.toThrow('Representation is already text/html');
   });
 
+  it('does not support representations if their content-type is not enabled.', async(): Promise<void> => {
+    const preferences = { type: { 'text/html': 1 }};
+    const representation = { metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/plain' }) } as any;
+    const args = { identifier: { path: 'container/' }, representation, preferences };
+
+    converter = new ConstantConverter('abc/def/index.html', 'text/html', { enabledMediaRanges: [ 'text/turtle' ]});
+
+    await expect(converter.canHandle(args)).rejects.toThrow('text/plain is not one of the enabled media types.');
+  });
+
+  it('does not support representations if their content-type is disabled.', async(): Promise<void> => {
+    const preferences = { type: { 'text/html': 1 }};
+    const representation = { metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/plain' }) } as any;
+    const args = { identifier: { path: 'container/' }, representation, preferences };
+
+    converter = new ConstantConverter('abc/def/index.html', 'text/html', { disabledMediaRanges: [ 'text/*' ]});
+
+    await expect(converter.canHandle(args)).rejects.toThrow('text/plain is one of the disabled media types.');
+  });
+
   it('supports representations with an unknown content type.', async(): Promise<void> => {
     const preferences = { type: { 'text/html': 1 }};
     const metadata = new RepresentationMetadata();