Repository: solid/community-server
Pull request #483 (2021-01-07 15:41:46)

Fix/patch relative
Fixes #438 (but not really since that's a PR).

Seemed better to make a new PR since it now includes changing the interface of the metadata extractor.


Patch:
diff --git a/src/authentication/DPoPWebIdExtractor.ts b/src/authentication/DPoPWebIdExtractor.ts
index ddf137c27..4ddf92cc7 100644
--- a/src/authentication/DPoPWebIdExtractor.ts
+++ b/src/authentication/DPoPWebIdExtractor.ts
@@ -34,7 +34,7 @@ export class DPoPWebIdExtractor extends CredentialsExtractor {
     if (!dpop) {
       throw new BadRequestHttpError('No DPoP header specified.');
     }
-    const resource = await this.targetExtractor.handleSafe(request);
+    const resource = await this.targetExtractor.handleSafe({ request });
 
     try {
       const { webid: webId } = await this.verify(
diff --git a/src/ldp/http/AcceptPreferenceParser.ts b/src/ldp/http/AcceptPreferenceParser.ts
index 66330ee01..bc915d9ff 100644
--- a/src/ldp/http/AcceptPreferenceParser.ts
+++ b/src/ldp/http/AcceptPreferenceParser.ts
@@ -27,7 +27,7 @@ const parsers: {
  * Supports Accept, Accept-Charset, Accept-Encoding, Accept-Language and Accept-DateTime.
  */
 export class AcceptPreferenceParser extends PreferenceParser {
-  public async handle({ headers }: HttpRequest): Promise<RepresentationPreferences> {
+  public async handle({ request: { headers }}: { request: HttpRequest }): Promise<RepresentationPreferences> {
     const preferences: RepresentationPreferences = {};
     for (const { name, header, parse } of parsers) {
       const value = headers[header];
diff --git a/src/ldp/http/BasicRequestParser.ts b/src/ldp/http/BasicRequestParser.ts
index c16aaf474..f7fcb51dc 100644
--- a/src/ldp/http/BasicRequestParser.ts
+++ b/src/ldp/http/BasicRequestParser.ts
@@ -36,9 +36,9 @@ export class BasicRequestParser extends RequestParser {
     if (!method) {
       throw new Error('No method specified on the HTTP request');
     }
-    const target = await this.targetExtractor.handleSafe(request);
-    const preferences = await this.preferenceParser.handleSafe(request);
-    const metadata = await this.metadataExtractor.handleSafe(request);
+    const target = await this.targetExtractor.handleSafe({ request });
+    const preferences = await this.preferenceParser.handleSafe({ request });
+    const metadata = await this.metadataExtractor.handleSafe({ request, target });
     const body = await this.bodyParser.handleSafe({ request, metadata });
 
     return { method, target, preferences, body };
diff --git a/src/ldp/http/BasicTargetExtractor.ts b/src/ldp/http/BasicTargetExtractor.ts
index ae9b55fba..17d400db7 100644
--- a/src/ldp/http/BasicTargetExtractor.ts
+++ b/src/ldp/http/BasicTargetExtractor.ts
@@ -11,7 +11,7 @@ import { TargetExtractor } from './TargetExtractor';
  * TODO: input requires more extensive cleaning/parsing based on headers (see #22).
  */
 export class BasicTargetExtractor extends TargetExtractor {
-  public async handle({ url, connection, headers }: HttpRequest): Promise<ResourceIdentifier> {
+  public async handle({ request: { url, connection, headers }}: { request: HttpRequest }): Promise<ResourceIdentifier> {
     if (!url) {
       throw new Error('Missing URL');
     }
diff --git a/src/ldp/http/PreferenceParser.ts b/src/ldp/http/PreferenceParser.ts
index 73beaa7ba..d8e9c912a 100644
--- a/src/ldp/http/PreferenceParser.ts
+++ b/src/ldp/http/PreferenceParser.ts
@@ -5,4 +5,4 @@ import type { RepresentationPreferences } from '../representation/Representation
 /**
  * Creates {@link RepresentationPreferences} based on the incoming HTTP headers in a {@link HttpRequest}.
  */
-export abstract class PreferenceParser extends AsyncHandler<HttpRequest, RepresentationPreferences> {}
+export abstract class PreferenceParser extends AsyncHandler<{ request: HttpRequest }, RepresentationPreferences> {}
diff --git a/src/ldp/http/SparqlUpdateBodyParser.ts b/src/ldp/http/SparqlUpdateBodyParser.ts
index 45b2501a3..31941ac0b 100644
--- a/src/ldp/http/SparqlUpdateBodyParser.ts
+++ b/src/ldp/http/SparqlUpdateBodyParser.ts
@@ -31,7 +31,7 @@ export class SparqlUpdateBodyParser extends BodyParser {
     let algebra: Algebra.Operation;
     try {
       const sparql = await readableToString(toAlgebraStream);
-      algebra = translate(sparql, { quads: true });
+      algebra = translate(sparql, { quads: true, baseIRI: metadata.identifier.value });
     } catch (error: unknown) {
       this.logger.warn('Could not translate SPARQL query to SPARQL algebra', { error });
       if (error instanceof Error) {
diff --git a/src/ldp/http/TargetExtractor.ts b/src/ldp/http/TargetExtractor.ts
index 8af184257..d6d74b368 100644
--- a/src/ldp/http/TargetExtractor.ts
+++ b/src/ldp/http/TargetExtractor.ts
@@ -5,4 +5,4 @@ import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
 /**
  * Extracts a {@link ResourceIdentifier} from an incoming {@link HttpRequest}.
  */
-export abstract class TargetExtractor extends AsyncHandler<HttpRequest, ResourceIdentifier> {}
+export abstract class TargetExtractor extends AsyncHandler<{ request: HttpRequest }, ResourceIdentifier> {}
diff --git a/src/ldp/http/metadata/BasicMetadataExtractor.ts b/src/ldp/http/metadata/BasicMetadataExtractor.ts
index dbd361731..57f848ec0 100644
--- a/src/ldp/http/metadata/BasicMetadataExtractor.ts
+++ b/src/ldp/http/metadata/BasicMetadataExtractor.ts
@@ -1,5 +1,6 @@
 import type { HttpRequest } from '../../../server/HttpRequest';
 import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
+import type { ResourceIdentifier } from '../../representation/ResourceIdentifier';
 import { MetadataExtractor } from './MetadataExtractor';
 import type { MetadataParser } from './MetadataParser';
 
@@ -14,9 +15,9 @@ export class BasicMetadataExtractor extends MetadataExtractor {
     this.parsers = parsers;
   }
 
-  public async handle(request: HttpRequest):
+  public async handle({ request, target }: { request: HttpRequest; target: ResourceIdentifier }):
   Promise<RepresentationMetadata> {
-    const metadata = new RepresentationMetadata();
+    const metadata = new RepresentationMetadata(target);
     for (const parser of this.parsers) {
       await parser.parse(request, metadata);
     }
diff --git a/src/ldp/http/metadata/MetadataExtractor.ts b/src/ldp/http/metadata/MetadataExtractor.ts
index 1251de244..2af766480 100644
--- a/src/ldp/http/metadata/MetadataExtractor.ts
+++ b/src/ldp/http/metadata/MetadataExtractor.ts
@@ -1,9 +1,10 @@
 import type { HttpRequest } from '../../../server/HttpRequest';
 import { AsyncHandler } from '../../../util/AsyncHandler';
 import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
+import type { ResourceIdentifier } from '../../representation/ResourceIdentifier';
 
 /**
  * Parses the metadata of a {@link HttpRequest} into a {@link RepresentationMetadata}.
  */
 export abstract class MetadataExtractor extends
-  AsyncHandler<HttpRequest, RepresentationMetadata> {}
+  AsyncHandler<{ request: HttpRequest; target: ResourceIdentifier }, RepresentationMetadata> {}


Test patch:
diff --git a/test/integration/LpdHandlerOperations.test.ts b/test/integration/LpdHandlerOperations.test.ts
index c086e44bc..57f5bd59e 100644
--- a/test/integration/LpdHandlerOperations.test.ts
+++ b/test/integration/LpdHandlerOperations.test.ts
@@ -75,7 +75,7 @@ describe('An integrated AuthenticatedLdpHandler', (): void => {
       'POST',
       { 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' },
       [ '<http://test.com/s1> <http://test.com/p1> <http://test.com/o1>.',
-        '<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.' ],
+        '<s2> <http://test.com/p2> <http://test.com/o2>.' ],
     );
     expect(response.statusCode).toBe(201);
     expect(response._getData()).toHaveLength(0);
@@ -89,8 +89,8 @@ describe('An integrated AuthenticatedLdpHandler', (): void => {
       requestUrl,
       'PATCH',
       { 'content-type': 'application/sparql-update', 'transfer-encoding': 'chunked' },
-      [ 'DELETE { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1> }',
-        'INSERT {<http://test.com/s3> <http://test.com/p3> <http://test.com/o3>}',
+      [ 'DELETE { <s1> <http://test.com/p1> <http://test.com/o1> }',
+        'INSERT {<s3> <http://test.com/p3> <http://test.com/o3>}',
         'WHERE {}',
       ],
     );
diff --git a/test/unit/authentication/DPoPWebIdExtractor.test.ts b/test/unit/authentication/DPoPWebIdExtractor.test.ts
index caae34a15..792cff334 100644
--- a/test/unit/authentication/DPoPWebIdExtractor.test.ts
+++ b/test/unit/authentication/DPoPWebIdExtractor.test.ts
@@ -74,7 +74,7 @@ describe('A DPoPWebIdExtractor', (): void => {
     it('calls the target extractor with the correct parameters.', async(): Promise<void> => {
       await webIdExtractor.handleSafe(request);
       expect(targetExtractor.handle).toHaveBeenCalledTimes(1);
-      expect(targetExtractor.handle).toHaveBeenCalledWith(request);
+      expect(targetExtractor.handle).toHaveBeenCalledWith({ request });
     });
 
     it('calls the DPoP verifier with the correct parameters.', async(): Promise<void> => {
diff --git a/test/unit/ldp/http/AcceptPreferenceParser.test.ts b/test/unit/ldp/http/AcceptPreferenceParser.test.ts
index d47068a73..d6551b444 100644
--- a/test/unit/ldp/http/AcceptPreferenceParser.test.ts
+++ b/test/unit/ldp/http/AcceptPreferenceParser.test.ts
@@ -3,41 +3,47 @@ import type { HttpRequest } from '../../../../src/server/HttpRequest';
 
 describe('An AcceptPreferenceParser', (): void => {
   const preferenceParser = new AcceptPreferenceParser();
+  let request: HttpRequest;
+  beforeEach(async(): Promise<void> => {
+    request = { headers: {}} as HttpRequest;
+  });
 
   it('can handle all input.', async(): Promise<void> => {
-    await expect(preferenceParser.canHandle({} as HttpRequest)).resolves.toBeUndefined();
+    await expect(preferenceParser.canHandle({ request })).resolves.toBeUndefined();
   });
 
   it('returns an empty result if there is no relevant input.', async(): Promise<void> => {
-    await expect(preferenceParser.handle({ headers: {}} as HttpRequest)).resolves.toEqual({});
+    await expect(preferenceParser.handle({ request })).resolves.toEqual({});
   });
 
   it('parses accept headers.', async(): Promise<void> => {
-    await expect(preferenceParser.handle({ headers: { accept: 'audio/*; q=0.2, audio/basic' }} as HttpRequest))
+    request.headers = { accept: 'audio/*; q=0.2, audio/basic' };
+    await expect(preferenceParser.handle({ request }))
       .resolves.toEqual({ type: { 'audio/basic': 1, 'audio/*': 0.2 }});
   });
 
   it('parses accept-charset headers.', async(): Promise<void> => {
-    await expect(preferenceParser.handle(
-      { headers: { 'accept-charset': 'iso-8859-5, unicode-1-1;q=0.8' }} as unknown as HttpRequest,
-    )).resolves.toEqual({ charset: { 'iso-8859-5': 1, 'unicode-1-1': 0.8 }});
+    request.headers = { 'accept-charset': 'iso-8859-5, unicode-1-1;q=0.8' };
+    await expect(preferenceParser.handle({ request }))
+      .resolves.toEqual({ charset: { 'iso-8859-5': 1, 'unicode-1-1': 0.8 }});
   });
 
   it('parses accept-datetime headers.', async(): Promise<void> => {
-    await expect(preferenceParser.handle(
-      { headers: { 'accept-datetime': 'Tue, 20 Mar 2001 20:35:00 GMT' }} as unknown as HttpRequest,
-    // eslint-disable-next-line @typescript-eslint/naming-convention
-    )).resolves.toEqual({ datetime: { 'Tue, 20 Mar 2001 20:35:00 GMT': 1 }});
+    request.headers = { 'accept-datetime': 'Tue, 20 Mar 2001 20:35:00 GMT' };
+    await expect(preferenceParser.handle({ request }))
+      // eslint-disable-next-line @typescript-eslint/naming-convention
+      .resolves.toEqual({ datetime: { 'Tue, 20 Mar 2001 20:35:00 GMT': 1 }});
   });
 
   it('parses accept-encoding headers.', async(): Promise<void> => {
-    await expect(preferenceParser.handle(
-      { headers: { 'accept-encoding': 'gzip;q=1.0, identity; q=0.5, *;q=0' }} as unknown as HttpRequest,
-    )).resolves.toEqual({ encoding: { gzip: 1, identity: 0.5, '*': 0 }});
+    request.headers = { 'accept-encoding': 'gzip;q=1.0, identity; q=0.5, *;q=0' };
+    await expect(preferenceParser.handle({ request }))
+      .resolves.toEqual({ encoding: { gzip: 1, identity: 0.5, '*': 0 }});
   });
 
   it('parses accept-language headers.', async(): Promise<void> => {
-    await expect(preferenceParser.handle({ headers: { 'accept-language': 'da, en-gb;q=0.8, en;q=0.7' }} as HttpRequest))
+    request.headers = { 'accept-language': 'da, en-gb;q=0.8, en;q=0.7' };
+    await expect(preferenceParser.handle({ request }))
       .resolves.toEqual({ language: { da: 1, 'en-gb': 0.8, en: 0.7 }});
   });
 });
diff --git a/test/unit/ldp/http/BasicTargetExtractor.test.ts b/test/unit/ldp/http/BasicTargetExtractor.test.ts
index 0520f4055..adc65e814 100644
--- a/test/unit/ldp/http/BasicTargetExtractor.test.ts
+++ b/test/unit/ldp/http/BasicTargetExtractor.test.ts
@@ -8,47 +8,48 @@ describe('A BasicTargetExtractor', (): void => {
   });
 
   it('errors if there is no URL.', async(): Promise<void> => {
-    await expect(extractor.handle({ headers: { host: 'test.com' }} as any)).rejects.toThrow('Missing URL');
+    await expect(extractor.handle({ request: { headers: { host: 'test.com' }} as any })).rejects.toThrow('Missing URL');
   });
 
   it('errors if there is no host.', async(): Promise<void> => {
-    await expect(extractor.handle({ url: 'url', headers: {}} as any)).rejects.toThrow('Missing Host header');
+    await expect(extractor.handle({ request: { url: 'url', headers: {}} as any }))
+      .rejects.toThrow('Missing Host header');
   });
 
   it('errors if the host is invalid.', async(): Promise<void> => {
-    await expect(extractor.handle({ url: 'url', headers: { host: 'test.com/forbidden' }} as any))
+    await expect(extractor.handle({ request: { url: 'url', headers: { host: 'test.com/forbidden' }} as any }))
       .rejects.toThrow('The request has an invalid Host header: test.com/forbidden');
   });
 
   it('returns the input URL.', async(): Promise<void> => {
-    await expect(extractor.handle({ url: 'url', headers: { host: 'test.com' }} as any))
+    await expect(extractor.handle({ request: { url: 'url', headers: { host: 'test.com' }} as any }))
       .resolves.toEqual({ path: 'http://test.com/url' });
   });
 
   it('supports host:port combinations.', async(): Promise<void> => {
-    await expect(extractor.handle({ url: 'url', headers: { host: 'localhost:3000' }} as any))
+    await expect(extractor.handle({ request: { url: 'url', headers: { host: 'localhost:3000' }} as any }))
       .resolves.toEqual({ path: 'http://localhost:3000/url' });
   });
 
   it('uses https protocol if the connection is secure.', async(): Promise<void> => {
     await expect(extractor.handle(
-      { url: 'url', headers: { host: 'test.com' }, connection: { encrypted: true } as any } as any,
+      { request: { url: 'url', headers: { host: 'test.com' }, connection: { encrypted: true } as any } as any },
     )).resolves.toEqual({ path: 'https://test.com/url' });
   });
 
   it('encodes paths.', async(): Promise<void> => {
-    await expect(extractor.handle({ url: '/a%20path%26/name', headers: { host: 'test.com' }} as any))
+    await expect(extractor.handle({ request: { url: '/a%20path%26/name', headers: { host: 'test.com' }} as any }))
       .resolves.toEqual({ path: 'http://test.com/a%20path%26/name' });
 
-    await expect(extractor.handle({ url: '/a path%26/name', headers: { host: 'test.com' }} as any))
+    await expect(extractor.handle({ request: { url: '/a path%26/name', headers: { host: 'test.com' }} as any }))
       .resolves.toEqual({ path: 'http://test.com/a%20path%26/name' });
 
-    await expect(extractor.handle({ url: '/path&%26/name', headers: { host: 'test.com' }} as any))
+    await expect(extractor.handle({ request: { url: '/path&%26/name', headers: { host: 'test.com' }} as any }))
       .resolves.toEqual({ path: 'http://test.com/path%26%26/name' });
   });
 
   it('encodes hosts.', async(): Promise<void> => {
-    await expect(extractor.handle({ url: '/', headers: { host: '點看' }} as any))
+    await expect(extractor.handle({ request: { url: '/', headers: { host: '點看' }} as any }))
       .resolves.toEqual({ path: 'http://xn--c1yn36f/' });
   });
 
@@ -57,7 +58,7 @@ describe('A BasicTargetExtractor', (): void => {
       host: 'test.com',
       forwarded: 'by=203.0.113.60',
     };
-    await expect(extractor.handle({ url: '/foo/bar', headers } as any))
+    await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
       .resolves.toEqual({ path: 'http://test.com/foo/bar' });
   });
 
@@ -66,7 +67,7 @@ describe('A BasicTargetExtractor', (): void => {
       host: 'test.com',
       forwarded: 'proto=https;host=pod.example',
     };
-    await expect(extractor.handle({ url: '/foo/bar', headers } as any))
+    await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
       .resolves.toEqual({ path: 'https://pod.example/foo/bar' });
   });
 });
diff --git a/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts b/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts
index 36cac4713..35b8b63bc 100644
--- a/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts
+++ b/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts
@@ -61,4 +61,24 @@ describe('A SparqlUpdateBodyParser', (): void => {
       [ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }' ],
     );
   });
+
+  it('accepts relative references.', async(): Promise<void> => {
+    input.request = streamifyArray(
+      [ 'INSERT DATA { <#it> <http://test.com/p> <http://test.com/o> }' ],
+    ) as HttpRequest;
+    input.metadata.identifier = namedNode('http://test.com/my-document.ttl');
+    const result = await bodyParser.handle(input);
+    expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT);
+    expect((result.algebra as Algebra.DeleteInsert).insert).toBeRdfIsomorphic([ quad(
+      namedNode('http://test.com/my-document.ttl#it'),
+      namedNode('http://test.com/p'),
+      namedNode('http://test.com/o'),
+    ) ]);
+    expect(result.binary).toBe(true);
+    expect(result.metadata).toBe(input.metadata);
+
+    expect(await arrayifyStream(result.data)).toEqual(
+      [ 'INSERT DATA { <#it> <http://test.com/p> <http://test.com/o> }' ],
+    );
+  });
 });
diff --git a/test/unit/ldp/http/metadata/BasicMetadataExtractor.test.ts b/test/unit/ldp/http/metadata/BasicMetadataExtractor.test.ts
index 78b7d7480..4e244addf 100644
--- a/test/unit/ldp/http/metadata/BasicMetadataExtractor.test.ts
+++ b/test/unit/ldp/http/metadata/BasicMetadataExtractor.test.ts
@@ -32,7 +32,11 @@ describe('A BasicMetadataExtractor', (): void => {
   });
 
   it('will add metadata from the parsers.', async(): Promise<void> => {
-    const metadata = await handler.handle({ headers: { aa: 'valA', bb: 'valB' } as any } as HttpRequest);
+    const target = { path: 'http://test.com/id' };
+    const metadata = await handler.handle(
+      { target, request: { headers: { aa: 'valA', bb: 'valB' } as any } as HttpRequest },
+    );
+    expect(metadata.identifier.value).toBe(target.path);
     expect(metadata.getAll(RDF.type).map((term): any => term.value)).toEqual([ 'valA', 'valB' ]);
   });
 });