mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Convert TemplateEngine to AsyncHandlers
This commit is contained in:
committed by
Joachim Van Herwegen
parent
a3c7baf6d3
commit
cf74ce3d2a
@@ -55,7 +55,7 @@ export class HtmlViewHandler extends InteractionHandler {
|
||||
public async handle({ operation, oidcInteraction }: InteractionHandlerInput): Promise<Representation> {
|
||||
const template = this.templates[operation.target.path];
|
||||
const contents = { idpIndex: this.idpIndex, authenticating: Boolean(oidcInteraction) };
|
||||
const result = await this.templateEngine.render(contents, { templateFile: template });
|
||||
const result = await this.templateEngine.handleSafe({ contents, template: { templateFile: template }});
|
||||
return new BasicRepresentation(result, operation.target, TEXT_HTML);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export class ForgotPasswordHandler extends BaseInteractionHandler {
|
||||
private async sendResetMail(recordId: string, email: string): Promise<void> {
|
||||
this.logger.info(`Sending password reset to ${email}`);
|
||||
const resetLink = `${this.resetRoute.getPath()}?rid=${encodeURIComponent(recordId)}`;
|
||||
const renderedEmail = await this.templateEngine.render({ resetLink });
|
||||
const renderedEmail = await this.templateEngine.handleSafe({ contents: { resetLink }});
|
||||
await this.emailSender.handleSafe({
|
||||
recipient: email,
|
||||
subject: 'Reset your password',
|
||||
|
||||
@@ -453,8 +453,11 @@ export * from './util/map/WrappedSetMultiMap';
|
||||
// Util/Templates
|
||||
export * from './util/templates/ChainedTemplateEngine';
|
||||
export * from './util/templates/EjsTemplateEngine';
|
||||
export * from './util/templates/ExtensionBasedTemplateEngine';
|
||||
export * from './util/templates/HandlebarsTemplateEngine';
|
||||
export * from './util/templates/StaticTemplateEngine';
|
||||
export * from './util/templates/TemplateEngine';
|
||||
export * from './util/templates/TemplateUtil';
|
||||
|
||||
// Util
|
||||
export * from './util/ContentTypes';
|
||||
|
||||
@@ -76,7 +76,7 @@ export class SetupHttpHandler extends OperationHttpHandler {
|
||||
* Returns the HTML representation of the setup page.
|
||||
*/
|
||||
private async handleGet(operation: Operation): Promise<ResponseDescription> {
|
||||
const result = await this.templateEngine.render({});
|
||||
const result = await this.templateEngine.handleSafe({ contents: {}});
|
||||
const representation = new BasicRepresentation(result, operation.target, TEXT_HTML);
|
||||
return new OkResponseDescription(representation.metadata, representation.data);
|
||||
}
|
||||
|
||||
@@ -206,9 +206,9 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
|
||||
/**
|
||||
* Creates a read stream from the file and applies the template if necessary.
|
||||
*/
|
||||
private async processFile(link: TemplateResourceLink, options: Dict<string>): Promise<Guarded<Readable>> {
|
||||
private async processFile(link: TemplateResourceLink, contents: Dict<string>): Promise<Guarded<Readable>> {
|
||||
if (link.isTemplate) {
|
||||
const rendered = await this.templateEngine.render(options, { templateFile: link.filePath });
|
||||
const rendered = await this.templateEngine.handleSafe({ contents, template: { templateFile: link.filePath }});
|
||||
return guardedStreamFrom(rendered);
|
||||
}
|
||||
return guardStream(createReadStream(link.filePath));
|
||||
|
||||
@@ -43,13 +43,13 @@ export class ContainerToTemplateConverter extends BaseTypedRepresentationConvert
|
||||
}
|
||||
|
||||
public async handle({ identifier, representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||
const rendered = await this.templateEngine.render({
|
||||
const rendered = await this.templateEngine.handleSafe({ contents: {
|
||||
identifier: identifier.path,
|
||||
name: this.getLocalName(identifier.path),
|
||||
container: true,
|
||||
children: await this.getChildResources(identifier, representation.data),
|
||||
parents: this.getParentContainers(identifier),
|
||||
});
|
||||
}});
|
||||
return new BasicRepresentation(rendered, representation.metadata, this.contentType);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ export class DynamicJsonToTemplateConverter extends RepresentationConverter {
|
||||
return representation;
|
||||
}
|
||||
|
||||
const json = JSON.parse(await readableToString(representation.data));
|
||||
const contents = JSON.parse(await readableToString(representation.data));
|
||||
|
||||
const rendered = await this.templateEngine.render(json, { templateFile: typeMap[type] });
|
||||
const rendered = await this.templateEngine.handleSafe({ contents, template: { templateFile: typeMap[type] }});
|
||||
const metadata = new RepresentationMetadata(representation.metadata, { [CONTENT_TYPE]: type });
|
||||
|
||||
return new BasicRepresentation(rendered, metadata);
|
||||
|
||||
@@ -65,8 +65,8 @@ export class ErrorToTemplateConverter extends BaseTypedRepresentationConverter {
|
||||
try {
|
||||
const templateFile = `${error.errorCode}${this.extension}`;
|
||||
assert(isValidFileName(templateFile), 'Invalid error template name');
|
||||
description = await this.templateEngine.render(error.details ?? {},
|
||||
{ templateFile, templatePath: this.codeTemplatesPath });
|
||||
description = await this.templateEngine.handleSafe({ contents: error.details ?? {},
|
||||
template: { templateFile, templatePath: this.codeTemplatesPath }});
|
||||
} catch {
|
||||
// In case no template is found, or rendering errors, we still want to convert
|
||||
}
|
||||
@@ -74,8 +74,9 @@ export class ErrorToTemplateConverter extends BaseTypedRepresentationConverter {
|
||||
|
||||
// Render the main template, embedding the rendered error description
|
||||
const { name, message, stack } = error;
|
||||
const variables = { name, message, stack, description };
|
||||
const rendered = await this.templateEngine.render(variables, { templateFile: this.mainTemplatePath });
|
||||
const contents = { name, message, stack, description };
|
||||
const rendered = await this.templateEngine
|
||||
.handleSafe({ contents, template: { templateFile: this.mainTemplatePath }});
|
||||
|
||||
return new BasicRepresentation(rendered, representation.metadata, this.contentType);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class MarkdownToHtmlConverter extends BaseTypedRepresentationConverter {
|
||||
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||
const markdown = await readableToString(representation.data);
|
||||
const htmlBody = marked(markdown);
|
||||
const html = await this.templateEngine.render({ htmlBody });
|
||||
const html = await this.templateEngine.handleSafe({ contents: { htmlBody }});
|
||||
|
||||
return new BasicRepresentation(html, representation.metadata, TEXT_HTML);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Template, TemplateEngine } from './TemplateEngine';
|
||||
import type { TemplateEngineInput } from './TemplateEngine';
|
||||
import { TemplateEngine } from './TemplateEngine';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
/**
|
||||
@@ -9,7 +10,7 @@ import Dict = NodeJS.Dict;
|
||||
* All subsequent engines will be called with no template parameter.
|
||||
* Contents will still be passed along and another entry will be added for the body of the previous output.
|
||||
*/
|
||||
export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
|
||||
export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> extends TemplateEngine<T> {
|
||||
private readonly firstEngine: TemplateEngine<T>;
|
||||
private readonly chainedEngines: TemplateEngine[];
|
||||
private readonly renderedName: string;
|
||||
@@ -19,6 +20,7 @@ export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> implements T
|
||||
* @param renderedName - The name of the key used to pass the body of one engine to the next.
|
||||
*/
|
||||
public constructor(engines: TemplateEngine[], renderedName = 'body') {
|
||||
super();
|
||||
if (engines.length === 0) {
|
||||
throw new Error('At least 1 engine needs to be provided.');
|
||||
}
|
||||
@@ -27,12 +29,14 @@ export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> implements T
|
||||
this.renderedName = renderedName;
|
||||
}
|
||||
|
||||
public async render(contents: T): Promise<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
|
||||
let body = await this.firstEngine.render(contents, template!);
|
||||
public async canHandle(input: TemplateEngineInput<T>): Promise<void> {
|
||||
return this.firstEngine.canHandle(input);
|
||||
}
|
||||
|
||||
public async handle({ contents, template }: TemplateEngineInput<T>): Promise<string> {
|
||||
let body = await this.firstEngine.handle({ contents, template });
|
||||
for (const engine of this.chainedEngines) {
|
||||
body = await engine.render({ ...contents, [this.renderedName]: body });
|
||||
body = await engine.handleSafe({ contents: { ...contents, [this.renderedName]: body }});
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
// tsdoc/syntax cannot handle `@range`
|
||||
import type { TemplateFunction } from 'ejs';
|
||||
import { compile, render } from 'ejs';
|
||||
import type { TemplateEngine, Template } from './TemplateEngine';
|
||||
import { getTemplateFilePath, readTemplate } from './TemplateEngine';
|
||||
import { render } from 'ejs';
|
||||
import { ExtensionBasedTemplateEngine } from './ExtensionBasedTemplateEngine';
|
||||
import type { TemplateEngineInput } from './TemplateEngine';
|
||||
import { getTemplateFilePath, readTemplate } from './TemplateUtil';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
/**
|
||||
* Fills in EJS templates.
|
||||
*/
|
||||
export class EjsTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
|
||||
private readonly applyTemplate: Promise<TemplateFunction>;
|
||||
export class EjsTemplateEngine<T extends Dict<any> = Dict<any>> extends ExtensionBasedTemplateEngine<T> {
|
||||
private readonly baseUrl: string;
|
||||
|
||||
/**
|
||||
* @param baseUrl - Base URL of the server.
|
||||
* @param template - The default template @range {json}
|
||||
* @param supportedExtensions - The extensions that are supported by this template engine (defaults to 'ejs').
|
||||
*/
|
||||
public constructor(baseUrl: string, template?: Template) {
|
||||
// EJS requires the `filename` parameter to be able to include partial templates
|
||||
const filename = getTemplateFilePath(template);
|
||||
public constructor(baseUrl: string, supportedExtensions = [ 'ejs' ]) {
|
||||
super(supportedExtensions);
|
||||
this.baseUrl = baseUrl;
|
||||
|
||||
this.applyTemplate = readTemplate(template)
|
||||
.then((templateString: string): TemplateFunction => compile(templateString, { filename }));
|
||||
}
|
||||
|
||||
public async render(contents: T): Promise<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
|
||||
public async handle({ contents, template }: TemplateEngineInput<T>): Promise<string> {
|
||||
const options = { ...contents, filename: getTemplateFilePath(template), baseUrl: this.baseUrl };
|
||||
return template ? render(await readTemplate(template), options) : (await this.applyTemplate)(options);
|
||||
return render(await readTemplate(template), options);
|
||||
}
|
||||
}
|
||||
|
||||
34
src/util/templates/ExtensionBasedTemplateEngine.ts
Normal file
34
src/util/templates/ExtensionBasedTemplateEngine.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { NotImplementedHttpError } from '../errors/NotImplementedHttpError';
|
||||
import { getExtension } from '../PathUtil';
|
||||
import type { TemplateEngineInput } from './TemplateEngine';
|
||||
import { TemplateEngine } from './TemplateEngine';
|
||||
import { getTemplateFilePath } from './TemplateUtil';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
/**
|
||||
* Parent class for template engines that accept handling based on whether the template extension is supported.
|
||||
*/
|
||||
export abstract class ExtensionBasedTemplateEngine<T extends Dict<any> = Dict<any>> extends TemplateEngine<T> {
|
||||
protected readonly supportedExtensions: string[];
|
||||
|
||||
/**
|
||||
* Constructor for ExtensionBasedTemplateEngine.
|
||||
*
|
||||
* @param supportedExtensions - Array of the extensions supported by the template engine (e.g. [ 'ejs' ]).
|
||||
*/
|
||||
protected constructor(supportedExtensions: string[]) {
|
||||
super();
|
||||
this.supportedExtensions = supportedExtensions;
|
||||
}
|
||||
|
||||
public async canHandle({ template }: TemplateEngineInput<T>): Promise<void> {
|
||||
if (typeof template === 'undefined') {
|
||||
throw new NotImplementedHttpError('No template was provided.');
|
||||
}
|
||||
// Check if the target template extension is supported.
|
||||
const filepath = getTemplateFilePath(template);
|
||||
if (typeof filepath === 'undefined' || !this.supportedExtensions.includes(getExtension(filepath))) {
|
||||
throw new NotImplementedHttpError('The provided template is not supported.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,26 @@
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
// tsdoc/syntax cannot handle `@range`
|
||||
import type { TemplateDelegate } from 'handlebars';
|
||||
import { compile } from 'handlebars';
|
||||
import type { TemplateEngine, Template } from './TemplateEngine';
|
||||
import { readTemplate } from './TemplateEngine';
|
||||
import { ExtensionBasedTemplateEngine } from './ExtensionBasedTemplateEngine';
|
||||
import type { TemplateEngineInput } from './TemplateEngine';
|
||||
import { readTemplate } from './TemplateUtil';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
/**
|
||||
* Fills in Handlebars templates.
|
||||
*/
|
||||
export class HandlebarsTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
|
||||
private readonly applyTemplate: Promise<TemplateDelegate>;
|
||||
export class HandlebarsTemplateEngine<T extends Dict<any> = Dict<any>> extends ExtensionBasedTemplateEngine<T> {
|
||||
private readonly baseUrl: string;
|
||||
|
||||
/**
|
||||
* @params baseUrl - Base URL of the server.
|
||||
* @param template - The default template @range {json}
|
||||
* @param baseUrl - Base URL of the server.
|
||||
* @param supportedExtensions - The extensions that are supported by this template engine (defaults to 'hbs').
|
||||
*/
|
||||
public constructor(baseUrl: string, template?: Template) {
|
||||
public constructor(baseUrl: string, supportedExtensions = [ 'hbs' ]) {
|
||||
super(supportedExtensions);
|
||||
this.baseUrl = baseUrl;
|
||||
this.applyTemplate = readTemplate(template)
|
||||
.then((templateString: string): TemplateDelegate => compile(templateString));
|
||||
}
|
||||
|
||||
public async render(contents: T): Promise<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
|
||||
const applyTemplate = template ? compile(await readTemplate(template)) : await this.applyTemplate;
|
||||
public async handle({ contents, template }: TemplateEngineInput<T>): Promise<string> {
|
||||
const applyTemplate = compile(await readTemplate(template));
|
||||
return applyTemplate({ ...contents, baseUrl: this.baseUrl });
|
||||
}
|
||||
}
|
||||
|
||||
36
src/util/templates/StaticTemplateEngine.ts
Normal file
36
src/util/templates/StaticTemplateEngine.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { AsyncHandler } from '../handlers/AsyncHandler';
|
||||
import type { TemplateEngineInput, Template } from './TemplateEngine';
|
||||
import { TemplateEngine } from './TemplateEngine';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
/**
|
||||
* Template engine that renders output based on a static template file.
|
||||
*/
|
||||
export class StaticTemplateEngine<T extends Dict<any> = Dict<any>> extends TemplateEngine<T> {
|
||||
private readonly template: Template;
|
||||
private readonly templateEngine: AsyncHandler<TemplateEngineInput<T>, string>;
|
||||
|
||||
/**
|
||||
* Creates a new StaticTemplateEngine.
|
||||
*
|
||||
* @param templateEngine - The template engine that should be used for processing the template.
|
||||
* @param template - The static template to be used.
|
||||
*/
|
||||
public constructor(templateEngine: AsyncHandler<TemplateEngineInput<T>, string>, template: Template) {
|
||||
super();
|
||||
this.template = template;
|
||||
this.templateEngine = templateEngine;
|
||||
}
|
||||
|
||||
public async canHandle({ contents, template }: TemplateEngineInput<T>): Promise<void> {
|
||||
if (typeof template !== 'undefined') {
|
||||
throw new Error('StaticTemplateEngine does not support template as handle input, ' +
|
||||
'provide a template via the constructor instead!');
|
||||
}
|
||||
return this.templateEngine.canHandle({ contents, template: this.template });
|
||||
}
|
||||
|
||||
public async handle({ contents }: TemplateEngineInput<T>): Promise<string> {
|
||||
return this.templateEngine.handle({ contents, template: this.template });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { joinFilePath, resolveAssetPath } from '../PathUtil';
|
||||
import { AsyncHandler } from '../handlers/AsyncHandler';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
export type Template = TemplateFileName | TemplateString | TemplatePath;
|
||||
@@ -18,50 +17,19 @@ export interface TemplatePath {
|
||||
templatePath?: string;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||
/**
|
||||
* Utility interface for representing TemplateEngine input.
|
||||
*/
|
||||
export interface TemplateEngineInput<T> {
|
||||
// The contents to render
|
||||
contents: T;
|
||||
// The template to use for rendering (optional)
|
||||
template?: Template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic interface for classes that implement a template engine.
|
||||
* A template engine renders content into a template.
|
||||
*/
|
||||
export interface TemplateEngine<T extends Dict<any> = Dict<any>> {
|
||||
/**
|
||||
* Renders the given contents into the template.
|
||||
*
|
||||
* @param contents - The contents to render.
|
||||
* @param template - The template to use for rendering;
|
||||
* if omitted, a default template is used.
|
||||
* @returns The rendered contents.
|
||||
*/
|
||||
render(contents: T): Promise<string>;
|
||||
render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/method-signature-style */
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the template.
|
||||
* Returns undefined if the input does not contain a file path.
|
||||
*/
|
||||
export function getTemplateFilePath(template?: Template): string | undefined {
|
||||
// The template has been passed as a filename
|
||||
if (typeof template === 'string') {
|
||||
return getTemplateFilePath({ templateFile: template });
|
||||
}
|
||||
// The template has already been given as a string so no known path
|
||||
if (!template || 'templateString' in template) {
|
||||
return;
|
||||
}
|
||||
const { templateFile, templatePath } = template;
|
||||
const fullTemplatePath = templatePath ? joinFilePath(templatePath, templateFile) : templateFile;
|
||||
return resolveAssetPath(fullTemplatePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the template and returns it as a string.
|
||||
*/
|
||||
export async function readTemplate(template: Template = { templateString: '' }): Promise<string> {
|
||||
// The template has already been given as a string
|
||||
if (typeof template === 'object' && 'templateString' in template) {
|
||||
return template.templateString;
|
||||
}
|
||||
// The template needs to be read from disk
|
||||
return fsPromises.readFile(getTemplateFilePath(template)!, 'utf8');
|
||||
}
|
||||
export abstract class TemplateEngine<T extends Dict<any> = Dict<any>>
|
||||
extends AsyncHandler<TemplateEngineInput<T>, string> {}
|
||||
|
||||
33
src/util/templates/TemplateUtil.ts
Normal file
33
src/util/templates/TemplateUtil.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { joinFilePath, resolveAssetPath } from '../PathUtil';
|
||||
import type { Template } from './TemplateEngine';
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the template.
|
||||
* Returns undefined if the input does not contain a file path.
|
||||
*/
|
||||
export function getTemplateFilePath(template?: Template): string | undefined {
|
||||
// The template has been passed as a filename
|
||||
if (typeof template === 'string') {
|
||||
return getTemplateFilePath({ templateFile: template });
|
||||
}
|
||||
// The template has already been given as a string so no known path
|
||||
if (!template || 'templateString' in template) {
|
||||
return;
|
||||
}
|
||||
const { templateFile, templatePath } = template;
|
||||
const fullTemplatePath = templatePath ? joinFilePath(templatePath, templateFile) : templateFile;
|
||||
return resolveAssetPath(fullTemplatePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the template and returns it as a string.
|
||||
*/
|
||||
export async function readTemplate(template: Template = { templateString: '' }): Promise<string> {
|
||||
// The template has already been given as a string
|
||||
if (typeof template === 'object' && 'templateString' in template) {
|
||||
return template.templateString;
|
||||
}
|
||||
// The template needs to be read from disk
|
||||
return fsPromises.readFile(getTemplateFilePath(template)!, 'utf8');
|
||||
}
|
||||
Reference in New Issue
Block a user