mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: return contenttype header value string with parameters
This commit is contained in:
committed by
Joachim Van Herwegen
parent
e0954cf2a7
commit
311f8756ec
@@ -3,6 +3,7 @@
|
||||
"import": [
|
||||
"css:config/ldp/metadata-writer/writers/allow-accept.json",
|
||||
"css:config/ldp/metadata-writer/writers/constant.json",
|
||||
"css:config/ldp/metadata-writer/writers/content-type.json",
|
||||
"css:config/ldp/metadata-writer/writers/link-rel.json",
|
||||
"css:config/ldp/metadata-writer/writers/mapped.json",
|
||||
"css:config/ldp/metadata-writer/writers/modified.json",
|
||||
@@ -17,6 +18,7 @@
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_AllowAccept" },
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_Constant" },
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_ContentType" },
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_Mapped" },
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_Modified" },
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_LinkRel" },
|
||||
|
||||
10
config/ldp/metadata-writer/writers/content-type.json
Normal file
10
config/ldp/metadata-writer/writers/content-type.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Adds the Content-Type header with value and parameters (if available).",
|
||||
"@id": "urn:solid-server:default:MetadataWriter_ContentType",
|
||||
"@type": "ContentTypeMetadataWriter"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,10 +6,6 @@
|
||||
"@id": "urn:solid-server:default:MetadataWriter_Mapped",
|
||||
"@type": "MappedMetadataWriter",
|
||||
"headerMap": [
|
||||
{
|
||||
"MappedMetadataWriter:_headerMap_key": "http://www.w3.org/ns/ma-ont#format",
|
||||
"MappedMetadataWriter:_headerMap_value": "Content-Type"
|
||||
},
|
||||
{
|
||||
"MappedMetadataWriter:_headerMap_key": "urn:npm:solid:community-server:http:location",
|
||||
"MappedMetadataWriter:_headerMap_value": "Location"
|
||||
|
||||
@@ -4,7 +4,6 @@ import { MetadataParser } from './MetadataParser';
|
||||
|
||||
/**
|
||||
* Parser for the `content-type` header.
|
||||
* Currently only stores the media type and ignores other parameters such as charset.
|
||||
*/
|
||||
export class ContentTypeParser extends MetadataParser {
|
||||
public async handle(input: { request: HttpRequest; metadata: RepresentationMetadata }): Promise<void> {
|
||||
|
||||
15
src/http/output/metadata/ContentTypeMetadataWriter.ts
Normal file
15
src/http/output/metadata/ContentTypeMetadataWriter.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { HttpResponse } from '../../../server/HttpResponse';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { MetadataWriter } from './MetadataWriter';
|
||||
|
||||
/**
|
||||
* Adds the `Content-Type` header containing value and parameters (if available).
|
||||
*/
|
||||
export class ContentTypeMetadataWriter extends MetadataWriter {
|
||||
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
|
||||
const { contentTypeObject } = input.metadata;
|
||||
if (contentTypeObject) {
|
||||
input.response.setHeader('Content-Type', contentTypeObject.toHeaderValueString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import { DataFactory, Store } from 'n3';
|
||||
import type { BlankNode, DefaultGraph, Literal, NamedNode, Quad, Term } from 'rdf-js';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import type { ContentType } from '../../util/HeaderUtil';
|
||||
import { parseContentType } from '../../util/HeaderUtil';
|
||||
import { ContentType, parseContentType } from '../../util/HeaderUtil';
|
||||
import { toNamedTerm, toObjectTerm, isTerm, toLiteral } from '../../util/TermUtil';
|
||||
import { CONTENT_TYPE_TERM, CONTENT_LENGTH_TERM, XSD, SOLID_META, RDFS } from '../../util/Vocabularies';
|
||||
import type { ResourceIdentifier } from './ResourceIdentifier';
|
||||
@@ -369,18 +368,16 @@ export class RepresentationMetadata {
|
||||
return;
|
||||
}
|
||||
const params = this.getAll(SOLID_META.terms.contentTypeParameter);
|
||||
return {
|
||||
value,
|
||||
parameters: Object.fromEntries(params.map((param): [string, string] => {
|
||||
const labels = this.store.getObjects(param, RDFS.terms.label, null);
|
||||
const values = this.store.getObjects(param, SOLID_META.terms.value, null);
|
||||
if (labels.length !== 1 || values.length !== 1) {
|
||||
this.logger.error(`Detected invalid content-type metadata for ${this.id.value}`);
|
||||
return [ 'invalid', '' ];
|
||||
}
|
||||
return [ labels[0].value, values[0].value ];
|
||||
})),
|
||||
};
|
||||
const parameters = Object.fromEntries(params.map((param): [string, string] => {
|
||||
const labels = this.store.getObjects(param, RDFS.terms.label, null);
|
||||
const values = this.store.getObjects(param, SOLID_META.terms.value, null);
|
||||
if (labels.length !== 1 || values.length !== 1) {
|
||||
this.logger.error(`Detected invalid content-type metadata for ${this.id.value}`);
|
||||
return [ 'invalid', '' ];
|
||||
}
|
||||
return [ labels[0].value, values[0].value ];
|
||||
}));
|
||||
return new ContentType(value, parameters);
|
||||
}
|
||||
|
||||
private removeContentType(): void {
|
||||
|
||||
@@ -96,6 +96,7 @@ export * from './http/output/error/SafeErrorHandler';
|
||||
// HTTP/Output/Metadata
|
||||
export * from './http/output/metadata/AllowAcceptHeaderWriter';
|
||||
export * from './http/output/metadata/ConstantMetadataWriter';
|
||||
export * from './http/output/metadata/ContentTypeMetadataWriter';
|
||||
export * from './http/output/metadata/LinkRelMetadataWriter';
|
||||
export * from './http/output/metadata/MappedMetadataWriter';
|
||||
export * from './http/output/metadata/MetadataWriter';
|
||||
|
||||
@@ -106,9 +106,18 @@ export interface AcceptDatetime extends AcceptHeader { }
|
||||
* Contents of a HTTP Content-Type Header.
|
||||
* Optional parameters Record is included.
|
||||
*/
|
||||
export interface ContentType {
|
||||
value: string;
|
||||
parameters: Record<string, string>;
|
||||
export class ContentType {
|
||||
public constructor(public value: string, public parameters: Record<string, string> = {}) {}
|
||||
|
||||
/**
|
||||
* Serialize this ContentType object to a ContentType header appropriate value string.
|
||||
* @returns The value string, including parameters, if present.
|
||||
*/
|
||||
public toHeaderValueString(): string {
|
||||
return Object.entries(this.parameters)
|
||||
.sort((entry1, entry2): number => entry1[0].localeCompare(entry2[0]))
|
||||
.reduce((acc, entry): string => `${acc}; ${entry[0]}=${entry[1]}`, this.value);
|
||||
}
|
||||
}
|
||||
|
||||
export interface LinkEntryParameters extends Record<string, string> {
|
||||
@@ -463,7 +472,7 @@ export function parseContentType(input: string): ContentType {
|
||||
prev.parameters[cur.name] = cur.value;
|
||||
return prev;
|
||||
},
|
||||
{ value, parameters: {}},
|
||||
new ContentType(value),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const baseUrl = `http://localhost:${port}`;
|
||||
const documents = [
|
||||
[ '/turtle', 'text/turtle', '# Test' ],
|
||||
[ '/markdown', 'text/markdown', '# Test' ],
|
||||
[ '/plain', 'text/plain; charset=utf-8', '# Test' ],
|
||||
];
|
||||
|
||||
const cases: [string, string, string][] = [
|
||||
@@ -27,6 +28,8 @@ const cases: [string, string, string][] = [
|
||||
[ '/markdown', 'text/html', 'text/html,*/*;q=0.8' ],
|
||||
[ '/markdown', 'text/html', 'text/markdown;q=0.1, text/html;q=0.9' ],
|
||||
[ '/markdown', 'application/octet-stream', 'application/octet-stream' ],
|
||||
[ '/plain', 'text/plain; charset=utf-8', 'text/plain' ],
|
||||
[ '/plain', 'text/plain; charset=utf-8', 'text/plain;q=0.1, text/markdown;q=0.9' ],
|
||||
];
|
||||
|
||||
describe('Content negotiation', (): void => {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createResponse } from 'node-mocks-http';
|
||||
import { ContentTypeMetadataWriter } from '../../../../../src/http/output/metadata/ContentTypeMetadataWriter';
|
||||
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
|
||||
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
||||
|
||||
describe('A ContentTypeMetadataWriter', (): void => {
|
||||
const writer = new ContentTypeMetadataWriter();
|
||||
let response: HttpResponse;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
response = createResponse() as HttpResponse;
|
||||
});
|
||||
|
||||
it('adds no header if there is no relevant metadata.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata();
|
||||
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
||||
expect(response.getHeaders()).toEqual({ });
|
||||
});
|
||||
|
||||
it('adds a Content-Type header with parameters if present.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata('text/plain; charset=utf-8');
|
||||
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
||||
|
||||
expect(response.getHeaders()).toEqual({
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
});
|
||||
|
||||
const metadata2 = new RepresentationMetadata('text/plain; charset="utf-8"');
|
||||
await expect(writer.handle({ response, metadata: metadata2 })).resolves.toBeUndefined();
|
||||
expect(response.getHeaders()).toEqual({
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a Content-Type header without parameters.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata('text/plain');
|
||||
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
||||
|
||||
expect(response.getHeaders()).toEqual({
|
||||
'content-type': 'text/plain',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import 'jest-rdf';
|
||||
import type { BlankNode } from 'n3';
|
||||
import { DataFactory } from 'n3';
|
||||
import type { NamedNode, Quad } from 'rdf-js';
|
||||
import { ContentType } from '../../../../src';
|
||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||
import { CONTENT_TYPE_TERM, SOLID_META, RDFS } from '../../../../src/util/Vocabularies';
|
||||
const { defaultGraph, literal, namedNode, quad } = DataFactory;
|
||||
@@ -320,13 +321,13 @@ describe('A RepresentationMetadata', (): void => {
|
||||
it('has a shorthand for Content-Type as object.', async(): Promise<void> => {
|
||||
expect(metadata.contentType).toBeUndefined();
|
||||
expect(metadata.contentTypeObject).toBeUndefined();
|
||||
metadata.contentTypeObject = {
|
||||
value: 'text/plain',
|
||||
parameters: {
|
||||
metadata.contentTypeObject = new ContentType(
|
||||
'text/plain',
|
||||
{
|
||||
charset: 'utf-8',
|
||||
test: 'value1',
|
||||
},
|
||||
};
|
||||
);
|
||||
expect(metadata.contentTypeObject).toEqual({
|
||||
value: 'text/plain',
|
||||
parameters: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
||||
import {
|
||||
import { ContentType,
|
||||
addHeader,
|
||||
hasScheme,
|
||||
matchesAuthorizationScheme,
|
||||
@@ -11,8 +11,7 @@ import {
|
||||
parseAcceptLanguage,
|
||||
parseContentType,
|
||||
parseForwarded,
|
||||
parseLinkHeader,
|
||||
} from '../../../src/util/HeaderUtil';
|
||||
parseLinkHeader } from '../../../src/util/HeaderUtil';
|
||||
|
||||
describe('HeaderUtil', (): void => {
|
||||
describe('#parseAccept', (): void => {
|
||||
@@ -469,4 +468,21 @@ describe('HeaderUtil', (): void => {
|
||||
expect(hasScheme('wss://example.com', 'http', 'WSS')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('A ContentType instance', (): void => {
|
||||
it('can serialize to a correct header value string with parameters.', (): void => {
|
||||
const contentType: ContentType = new ContentType(
|
||||
'text/plain',
|
||||
{
|
||||
charset: 'utf-8',
|
||||
extra: 'test',
|
||||
},
|
||||
);
|
||||
expect(contentType.toHeaderValueString()).toBe('text/plain; charset=utf-8; extra=test');
|
||||
});
|
||||
|
||||
it('can serialize to a correct header value string without parameters.', (): void => {
|
||||
const contentType: ContentType = new ContentType('text/plain');
|
||||
expect(contentType.toHeaderValueString()).toBe('text/plain');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user