From 46453b876de3f716a74c28323342eed4935b6234 Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 14:40:46 -0500 Subject: [PATCH 01/15] image viewer --- backend/routers/index.ts | 32 ++++++++++++++++++++++-- tailwind.css | 9 +++++++ views/viewer.pug | 53 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 views/viewer.pug diff --git a/backend/routers/index.ts b/backend/routers/index.ts index bcf6581..60948d2 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -1,4 +1,4 @@ -import { BusBoyFile, AssFile } from 'ass'; +import { BusBoyFile, AssFile, AssUser } from 'ass'; import fs from 'fs-extra'; import bb from 'express-busboy'; @@ -13,6 +13,7 @@ import { random } from '../generators'; import { UserConfig } from '../UserConfig'; import { getFileS3, uploadFileS3 } from '../s3'; import { rateLimiterMiddleware } from '../ratelimit'; +import { DBManager } from '../sql/database'; const router = Router({ caseSensitive: true }); @@ -96,7 +97,34 @@ router.post('/', rateLimiterMiddleware('upload', UserConfig.config?.rateLimit?.u } }); -router.get('/:fakeId', (req, res) => res.redirect(`/direct/${req.params.fakeId}`)); +router.get('/:fakeId', async (req, res) => { + if (!UserConfig.ready) res.redirect('/setup'); + + // Get the ID + const fakeId = req.params.fakeId; + + // Get the file metadata + let _data; + try { _data = await DBManager.get('assfiles', fakeId); } + catch (err) { + log.error('Failed to get', fakeId); + console.error(err); + return res.status(500).send(); + } + + if (!_data) return res.status(404).send(); + else { + let meta = _data as AssFile; + let user = await DBManager.get('assusers', meta.uploader) as AssUser | undefined; + + res.render("viewer", { + url: `/direct/${fakeId}`, + uploader: user?.username ?? 'unknown', + size: meta.size, + time: meta.timestamp + }); + } +}); router.get('/direct/:fakeId', async (req, res) => { if (!UserConfig.ready) res.redirect('/setup'); diff --git a/tailwind.css b/tailwind.css index 6e62757..1e87c7e 100644 --- a/tailwind.css +++ b/tailwind.css @@ -24,6 +24,15 @@ .setup-panel>sl-input { @apply mb-4; } + + .res-image { + @apply max-h-[75vh]; + } + + /* THANKS TAILWIND */ + sl-divider { + border: solid var(--width) var(--color); + } } @layer utilities { diff --git a/views/viewer.pug b/views/viewer.pug new file mode 100644 index 0000000..ef79132 --- /dev/null +++ b/views/viewer.pug @@ -0,0 +1,53 @@ +doctype html +html.dark.sl-theme-dark(lang='en') + head + //- this stuff + meta(charset='UTF-8') + meta(name='viewport', content='width=device-witdh, initial-scale=1.0') + meta(name='theme-color' content='black') + link(rel='stylesheet' href='/.css') + + //- title + title ass 🍑 + + //- embed data + + //- mixins + include ../node_modules/shoelace-fontawesome-pug/sl-fa-mixin.pug + include ../node_modules/shoelace-pug-loader/loader.pug + + //- shoelace + +slTheme('dark') + +slAuto + body.w-screen.h-screen.flex-col + div.w-full.h-full.flex.justify-center.items-center.text-center + main.flex.flex-col + - let borderStyle = { 'border-color': 'var(--sl-color-neutral-200)' }; + header.border-t.border-x.flex.h-8.bg-stone-900(style=borderStyle) + - let dividerStyle = { '--spacing': '0px', 'margin-left': '8px' }; + + //- uploader + span + sl-icon.p-2.align-middle(name='person-fill', label='uploader') + | #{uploader} + sl-divider(vertical, style=dividerStyle) + //- file size + span + sl-icon.p-2.align-middle(name='database-fill', label='size') + sl-format-bytes(value=size) + sl-divider(vertical, style=dividerStyle) + //- upload date + span + //- calendar is a funny word + sl-icon.p-2.align-middle(name='calendar-fill', label='upload date') + sl-format-date(value=time) + sl-divider(vertical, style=dividerStyle) + + //- spacer + div.flex-grow + + //- download button + sl-divider(vertical, style=dividerStyle) + span.float-right + sl-icon-button(name='download', href=url, download, label='download') + img.res-image.border-b.border-x(src=url, style=borderStyle) \ No newline at end of file From 81cb4aa7e3b698badcd89b03992d7a40c4ae2ded Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 14:51:18 -0500 Subject: [PATCH 02/15] embed --- views/viewer.pug | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index ef79132..e466b90 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -1,5 +1,5 @@ doctype html -html.dark.sl-theme-dark(lang='en') +html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') head //- this stuff meta(charset='UTF-8') @@ -11,6 +11,11 @@ html.dark.sl-theme-dark(lang='en') title ass 🍑 //- embed data + meta(property='og:title', content=`image uploaded by ${user}`) + meta(property='og:description', content='a one to two sentence description of your objectg') + meta(property='og:type', content='website') + meta(property='og:image', content=url) + meta(property='og:url', content='.') //- mixins include ../node_modules/shoelace-fontawesome-pug/sl-fa-mixin.pug From 7af42c7c89f3c57e7d06d4911b5ea0a4382d4a10 Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 15:51:34 -0500 Subject: [PATCH 03/15] embed templates --- backend/embed.ts | 26 +++++++++++++++++++++++ backend/routers/index.ts | 7 +++++- common/types.d.ts | 46 ++++++++++++++++++++++++++++++++++++++++ views/viewer.pug | 4 ++-- 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 backend/embed.ts diff --git a/backend/embed.ts b/backend/embed.ts new file mode 100644 index 0000000..7078e56 --- /dev/null +++ b/backend/embed.ts @@ -0,0 +1,26 @@ +import { EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass" + +export const DEFAULT_EMBED: EmbedTemplate = { + title: "ass - The simple self-hosted ShareX server", + description: "ass is a self-hosted ShareX upload server written in Node.js" +} + +const executeEmbedOperation = (op: EmbedTemplateOperation): string => { + if (typeof op == 'string') { + return op; + } else if (typeof op == 'object') { + switch (op.op) { + case 'random': + if (op.options.length > 0) { + return executeEmbedOperation(op.options[Math.round(Math.random() * (op.options.length - 1))]); + } else throw new Error("Random without child operations"); + } + } else throw new Error("Invalid embed template operation"); +}; + +export const prepareEmbed = (template: EmbedTemplate): PreparedEmbed => { + return { + title: executeEmbedOperation(template.title), + description: executeEmbedOperation(template.description) + }; +}; \ No newline at end of file diff --git a/backend/routers/index.ts b/backend/routers/index.ts index 60948d2..39b815f 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -14,6 +14,7 @@ import { UserConfig } from '../UserConfig'; import { getFileS3, uploadFileS3 } from '../s3'; import { rateLimiterMiddleware } from '../ratelimit'; import { DBManager } from '../sql/database'; +import { DEFAULT_EMBED, prepareEmbed } from '../embed'; const router = Router({ caseSensitive: true }); @@ -121,7 +122,11 @@ router.get('/:fakeId', async (req, res) => { url: `/direct/${fakeId}`, uploader: user?.username ?? 'unknown', size: meta.size, - time: meta.timestamp + time: meta.timestamp, + embed: prepareEmbed({ + title: UserConfig.config.embed?.title ?? DEFAULT_EMBED.title, + description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description + }) }); } }); diff --git a/common/types.d.ts b/common/types.d.ts index fa3a5a5..8881557 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -27,6 +27,25 @@ declare module 'ass' { database?: DatabaseConfiguration; rateLimit?: RateLimitConfiguration; + + // to whoever has to make the config screen + // for this, im very verys sorry + embed?: EmbedTemplate; + } + + /** + * Embed config + */ + interface EmbedConfiguration { + /** + * Title in embed + */ + title?: string, + + /** + * Description(s) in embed + */ + description?: string[] | string, } interface S3Configuration { @@ -255,6 +274,33 @@ declare module 'ass' { cliKey: string; meta: { [key: string]: any }; } + + // Generic embed template operation + type EmbedTemplateOperation = EmbedTemplateRandomOperation | string; + + /** + * Selects one operation and executes it + */ + type EmbedTemplateRandomOperation = { + op: "random"; + options: EmbedTemplateOperation[]; + }; + + /** + * This is so beyond cursed + */ + interface EmbedTemplate { + title: EmbedTemplateOperation; + description: EmbedTemplateOperation; + } + + /** + * + */ + interface PreparedEmbed { + title: string; + description: string; + } } //#region Dummy modules diff --git a/views/viewer.pug b/views/viewer.pug index e466b90..6abbe4f 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -11,8 +11,8 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') title ass 🍑 //- embed data - meta(property='og:title', content=`image uploaded by ${user}`) - meta(property='og:description', content='a one to two sentence description of your objectg') + meta(property='og:title', content=embed.title) + meta(property='og:description', content=embed.description) meta(property='og:type', content='website') meta(property='og:image', content=url) meta(property='og:url', content='.') From c30f45be20e4e5b350e724d4c54a1b4803917a9b Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 16:18:37 -0500 Subject: [PATCH 04/15] oops --- views/viewer.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index 6abbe4f..e86b891 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,7 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:type', content='website') + meta(property='og:type', content='image') meta(property='og:image', content=url) meta(property='og:url', content='.') From 64993533fb6c8d54a6af06b023bbdd3b666b7aa8 Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 16:29:26 -0500 Subject: [PATCH 05/15] uhh --- views/viewer.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index e86b891..214f52c 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,7 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:type', content='image') + meta(property='og:type', content='object') meta(property='og:image', content=url) meta(property='og:url', content='.') From cb0d3ebde3125b1776b24d6f7d75f751f37775a5 Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 16:34:35 -0500 Subject: [PATCH 06/15] uhh --- views/viewer.pug | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index 214f52c..ee535bb 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,9 +13,10 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:type', content='object') + meta(property='og:type', content='image') meta(property='og:image', content=url) meta(property='og:url', content='.') + meta(property='twitter:card', content='summary_large_image') //- mixins include ../node_modules/shoelace-fontawesome-pug/sl-fa-mixin.pug From 83bfd21955cc10b0e5675fee68f76f9394919df2 Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 10 Nov 2023 08:54:29 -0500 Subject: [PATCH 07/15] more embed operations --- backend/embed.ts | 45 +++++++++++++++++++++++++++++++++------- backend/routers/index.ts | 10 ++++++++- common/types.d.ts | 42 ++++++++++++++++++++++++++++++++++--- views/viewer.pug | 1 + 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/backend/embed.ts b/backend/embed.ts index 7078e56..9f5aa5c 100644 --- a/backend/embed.ts +++ b/backend/embed.ts @@ -1,26 +1,55 @@ -import { EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass" +import { AssFile, AssUser, EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass" export const DEFAULT_EMBED: EmbedTemplate = { title: "ass - The simple self-hosted ShareX server", description: "ass is a self-hosted ShareX upload server written in Node.js" } -const executeEmbedOperation = (op: EmbedTemplateOperation): string => { +class EmbedContext { + public uploader: AssUser; + public file: AssFile; + + constructor(uploader: AssUser, file: AssFile) { + this.uploader = uploader; + this.file = file; + } +} + +const executeEmbedOperation = (op: EmbedTemplateOperation, ctx: EmbedContext): string | number => { if (typeof op == 'string') { return op; + } else if (typeof op == 'number') { + return op; } else if (typeof op == 'object') { switch (op.op) { case 'random': - if (op.options.length > 0) { - return executeEmbedOperation(op.options[Math.round(Math.random() * (op.options.length - 1))]); - } else throw new Error("Random without child operations"); + if (op.values.length > 0) { + return executeEmbedOperation(op.values[Math.round(Math.random() * (op.values.length - 1))], ctx); + } else throw new Error('Random without child operations'); + case 'fileSize': + return ctx.file.size; + case 'uploader': + return ctx.uploader.username; + case 'formatBytes': + // calculate the value + let value = executeEmbedOperation(op.value, ctx); + + // calculate the exponent + let exponent = (op.unit != null && { 'b': 0, 'kb': 1, 'mb': 2, 'gb': 3, 'tb': 4 }[executeEmbedOperation(op.unit, ctx)]) + || Math.max(Math.min(Math.floor(Math.log10(Number(value))/3), 4), 0); + + return `${(Number(value) / 1000 ** exponent).toFixed(2)}${['b', 'kb', 'mb', 'gb', 'tb'][exponent]}`; + case 'concat': + return op.values.reduce((prev, op) => prev.concat(executeEmbedOperation(op, ctx).toString()), ""); } } else throw new Error("Invalid embed template operation"); }; -export const prepareEmbed = (template: EmbedTemplate): PreparedEmbed => { +export const prepareEmbed = (template: EmbedTemplate, user: AssUser, file: AssFile): PreparedEmbed => { + let ctx = new EmbedContext(user, file); + return { - title: executeEmbedOperation(template.title), - description: executeEmbedOperation(template.description) + title: executeEmbedOperation(template.title, ctx).toString(), + description: executeEmbedOperation(template.description, ctx).toString() }; }; \ No newline at end of file diff --git a/backend/routers/index.ts b/backend/routers/index.ts index 39b815f..b5288f0 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -126,7 +126,15 @@ router.get('/:fakeId', async (req, res) => { embed: prepareEmbed({ title: UserConfig.config.embed?.title ?? DEFAULT_EMBED.title, description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description - }) + }, user ?? { + admin: false, + files: [], + id: "", + meta: {}, + password: "", + tokens: [], + username: "unknown" + }, meta) }); } }); diff --git a/common/types.d.ts b/common/types.d.ts index 8881557..b5c68b7 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -276,16 +276,52 @@ declare module 'ass' { } // Generic embed template operation - type EmbedTemplateOperation = EmbedTemplateRandomOperation | string; + type EmbedTemplateOperation = + EmbedTemplateRandomOperation | + EmbedTemplateFileSizeOperation | + EmbedTemplateFormatBytesOperation | + EmbedTemplateUploaderOperation | + EmbedTemplateConcatOperation | + string | number; /** * Selects one operation and executes it */ type EmbedTemplateRandomOperation = { - op: "random"; - options: EmbedTemplateOperation[]; + op: 'random'; + values: EmbedTemplateOperation[]; }; + /** + * Returns the file size in bytes + * @returns number + */ + type EmbedTemplateFileSizeOperation = { op: 'fileSize' }; + + /** + * Formats the file size in in {value} + */ + type EmbedTemplateFormatBytesOperation = { + op: 'formatBytes'; + unit?: EmbedTemplateOperation; // ready for ios! + value: EmbedTemplateOperation; + }; + + /** + * Returns the user who uploaded this file + */ + type EmbedTemplateUploaderOperation = { + op: 'uploader' + }; + + /** + * Joins strings + */ + type EmbedTemplateConcatOperation = { + op: 'concat', + values: EmbedTemplateOperation[] + } + /** * This is so beyond cursed */ diff --git a/views/viewer.pug b/views/viewer.pug index ee535bb..e517dd4 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,6 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) + meta(property='og:author', content='ass') meta(property='og:type', content='image') meta(property='og:image', content=url) meta(property='og:url', content='.') From a1a0975a3f320a5b4a421a84c17e7a6cb89c64f9 Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 10 Nov 2023 09:07:24 -0500 Subject: [PATCH 08/15] oops --- views/viewer.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index e517dd4..a05ac28 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,7 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:author', content='ass') + meta(property='og:site_name', content='ass') meta(property='og:type', content='image') meta(property='og:image', content=url) meta(property='og:url', content='.') From db32bb1c2974192a2999bbe2f53fc1fa8f8ad9f4 Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 10 Nov 2023 09:17:29 -0500 Subject: [PATCH 09/15] sitename --- backend/embed.ts | 10 ++++++---- backend/routers/index.ts | 3 ++- common/types.d.ts | 2 ++ views/viewer.pug | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/embed.ts b/backend/embed.ts index 9f5aa5c..27e70a7 100644 --- a/backend/embed.ts +++ b/backend/embed.ts @@ -1,9 +1,10 @@ import { AssFile, AssUser, EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass" export const DEFAULT_EMBED: EmbedTemplate = { - title: "ass - The simple self-hosted ShareX server", - description: "ass is a self-hosted ShareX upload server written in Node.js" -} + sitename: "ass", + title: "", + description: "" +}; class EmbedContext { public uploader: AssUser; @@ -50,6 +51,7 @@ export const prepareEmbed = (template: EmbedTemplate, user: AssUser, file: AssFi return { title: executeEmbedOperation(template.title, ctx).toString(), - description: executeEmbedOperation(template.description, ctx).toString() + description: executeEmbedOperation(template.description, ctx).toString(), + sitename: executeEmbedOperation(template.sitename, ctx).toString() }; }; \ No newline at end of file diff --git a/backend/routers/index.ts b/backend/routers/index.ts index b5288f0..93235ef 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -125,7 +125,8 @@ router.get('/:fakeId', async (req, res) => { time: meta.timestamp, embed: prepareEmbed({ title: UserConfig.config.embed?.title ?? DEFAULT_EMBED.title, - description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description + description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description, + sitename: UserConfig.config.embed?.sitename ?? DEFAULT_EMBED.sitename }, user ?? { admin: false, files: [], diff --git a/common/types.d.ts b/common/types.d.ts index b5c68b7..322ae5a 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -328,6 +328,7 @@ declare module 'ass' { interface EmbedTemplate { title: EmbedTemplateOperation; description: EmbedTemplateOperation; + sitename: EmbedTemplateOperation; } /** @@ -336,6 +337,7 @@ declare module 'ass' { interface PreparedEmbed { title: string; description: string; + sitename: string; } } diff --git a/views/viewer.pug b/views/viewer.pug index a05ac28..84bf35d 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,7 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:site_name', content='ass') + meta(property='og:site_name', content=embed.sitename) meta(property='og:type', content='image') meta(property='og:image', content=url) meta(property='og:url', content='.') From 3660b6cc8b81989ee93c7329363f91addac1bc8d Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 27 Nov 2023 09:29:49 -0500 Subject: [PATCH 10/15] meow --- backend/UserConfig.ts | 25 ++++ backend/embed.ts | 58 ++------ backend/templates/command.ts | 9 ++ backend/templates/error.ts | 28 ++++ backend/templates/executor.ts | 121 +++++++++++++++ backend/templates/parser.ts | 270 ++++++++++++++++++++++++++++++++++ common/types.d.ts | 72 ++++----- 7 files changed, 508 insertions(+), 75 deletions(-) create mode 100644 backend/templates/command.ts create mode 100644 backend/templates/error.ts create mode 100644 backend/templates/executor.ts create mode 100644 backend/templates/parser.ts diff --git a/backend/UserConfig.ts b/backend/UserConfig.ts index a6b90ef..ed74a70 100644 --- a/backend/UserConfig.ts +++ b/backend/UserConfig.ts @@ -3,6 +3,9 @@ import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration, MongoD import fs from 'fs-extra'; import { path } from '@tycrek/joint'; import { log } from './log'; +import { prepareTemplate } from './templates/parser'; +import { TemplateError } from './templates/error'; +import { DEFAULT_EMBED, validateEmbed } from './embed'; const FILEPATH = path.join('.ass-data/userconfig.json'); @@ -130,6 +133,28 @@ export class UserConfig { if (!Checkers.rateLimit.endpoint(config.rateLimit.api)) throw new Error('Invalid API rate limit configuration'); } + // * the embed + if (config.embed != null) { + try { + for (let part of ['title', 'description', 'sitename'] as ('title' | 'description' | 'sitename')[]) { + if (config.embed[part] != null) { + if (typeof config.embed[part] == 'string') { + config.embed[part] = prepareTemplate(config.embed[part] as string); + } else throw new Error(`Template string for embed ${part} is not a string`); + } else config.embed[part] = DEFAULT_EMBED[part]; + } + + validateEmbed(config.embed); + } catch (err) { + if (err instanceof TemplateError) { + // tlog messes up the formatting + console.error(err.format()); + + throw new Error('Template error'); + } else throw err; + } + } else config.embed = DEFAULT_EMBED; + // All is fine, carry on! return config; } diff --git a/backend/embed.ts b/backend/embed.ts index 27e70a7..9828586 100644 --- a/backend/embed.ts +++ b/backend/embed.ts @@ -1,4 +1,7 @@ -import { AssFile, AssUser, EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass" +import { AssFile, AssUser, EmbedTemplate, PreparedEmbed } from "ass" +import { TemplateExecutor } from "./templates/executor"; + +let executor = TemplateExecutor.createExecutor(); export const DEFAULT_EMBED: EmbedTemplate = { sitename: "ass", @@ -6,52 +9,23 @@ export const DEFAULT_EMBED: EmbedTemplate = { description: "" }; -class EmbedContext { - public uploader: AssUser; - public file: AssFile; +// ensures a template is valid +export const validateEmbed = (template: EmbedTemplate) => { + // lets hope this works + let context = executor.createContext(null!, null!); - constructor(uploader: AssUser, file: AssFile) { - this.uploader = uploader; - this.file = file; - } + executor.validateTemplate(template.title, context); + executor.validateTemplate(template.description, context); + executor.validateTemplate(template.sitename, context); } -const executeEmbedOperation = (op: EmbedTemplateOperation, ctx: EmbedContext): string | number => { - if (typeof op == 'string') { - return op; - } else if (typeof op == 'number') { - return op; - } else if (typeof op == 'object') { - switch (op.op) { - case 'random': - if (op.values.length > 0) { - return executeEmbedOperation(op.values[Math.round(Math.random() * (op.values.length - 1))], ctx); - } else throw new Error('Random without child operations'); - case 'fileSize': - return ctx.file.size; - case 'uploader': - return ctx.uploader.username; - case 'formatBytes': - // calculate the value - let value = executeEmbedOperation(op.value, ctx); - - // calculate the exponent - let exponent = (op.unit != null && { 'b': 0, 'kb': 1, 'mb': 2, 'gb': 3, 'tb': 4 }[executeEmbedOperation(op.unit, ctx)]) - || Math.max(Math.min(Math.floor(Math.log10(Number(value))/3), 4), 0); - - return `${(Number(value) / 1000 ** exponent).toFixed(2)}${['b', 'kb', 'mb', 'gb', 'tb'][exponent]}`; - case 'concat': - return op.values.reduce((prev, op) => prev.concat(executeEmbedOperation(op, ctx).toString()), ""); - } - } else throw new Error("Invalid embed template operation"); -}; - +// cooks up the embed export const prepareEmbed = (template: EmbedTemplate, user: AssUser, file: AssFile): PreparedEmbed => { - let ctx = new EmbedContext(user, file); + let context = executor.createContext(user, file); return { - title: executeEmbedOperation(template.title, ctx).toString(), - description: executeEmbedOperation(template.description, ctx).toString(), - sitename: executeEmbedOperation(template.sitename, ctx).toString() + title: executor.executeTemplate(template.title, context), + description: executor.executeTemplate(template.description, context), + sitename: executor.executeTemplate(template.sitename, context) }; }; \ No newline at end of file diff --git a/backend/templates/command.ts b/backend/templates/command.ts new file mode 100644 index 0000000..8e44a1b --- /dev/null +++ b/backend/templates/command.ts @@ -0,0 +1,9 @@ +import { TemplateCommandOp, TemplateCommandSchema } from 'ass'; +import { TemplateContext } from './executor'; + +export type TemplateCommand = { + readonly name: N; + readonly schema: S; + + exec: (op: TemplateCommandOp, ctx: TemplateContext) => string; +}; \ No newline at end of file diff --git a/backend/templates/error.ts b/backend/templates/error.ts new file mode 100644 index 0000000..28c7c4b --- /dev/null +++ b/backend/templates/error.ts @@ -0,0 +1,28 @@ +import { TemplateSourceRange } from "ass"; + +export class TemplateError extends Error { + range?: TemplateSourceRange; + + constructor(msg: string, range?: TemplateSourceRange) { + super(msg); + + this.range = range; + } + + public format(): string { + let format = ''; + + if (this.range) { + format += this.range.file.code + '\n'; + format += ' '.repeat(this.range.from) + '^' + '~'.repeat(Math.max(this.range.to - this.range.from, 0)) + '\n'; + } + + format += `${this.name}: ${this.message}`; + + return format; + } +} + +// template syntax error with token range, token range converted to source position +// outside of prepareTemplate +export class TemplateSyntaxError extends TemplateError {}; \ No newline at end of file diff --git a/backend/templates/executor.ts b/backend/templates/executor.ts new file mode 100644 index 0000000..314b12c --- /dev/null +++ b/backend/templates/executor.ts @@ -0,0 +1,121 @@ +import { AssFile, AssUser, TemplateCommandOp, TemplateCommandSchema, TemplateOp } from 'ass'; +import { TemplateCommand } from './command'; +import { TemplateError } from './error'; + +export class TemplateContext { + public readonly owner: TemplateExecutor; + + public uploader: AssUser; + public file: AssFile; + + constructor(owner: TemplateExecutor, uploader: AssUser, file: AssFile) { + this.owner = owner; + this.uploader = uploader; + this.file = file; + } +} + +export class TemplateExecutor { + private commands: { [index: string]: TemplateCommand } = {}; + + // register a template command globally + public registerCommand(name: N, attrs: S, cmd: (op: TemplateCommandOp, ctx: TemplateContext) => string) { + if (this.commands[name] == null) { + this.commands[name] = { + name: name, + schema: attrs, + exec: cmd + }; + } else throw new Error(`Template command "${name}" already exists`); + } + + public createContext(uploader: AssUser, file: AssFile) { + return new TemplateContext(this, uploader, file); + } + + // expects template to be valid and does not preform runtime checks. + // run validateTemplate first + public executeTemplate(op: TemplateOp, ctx: TemplateContext): string { + switch (typeof op) { + case 'string': return op; + case 'object': return this.commands[op.op].exec(op, ctx); + } + } + + public validateTemplate(op: TemplateOp, ctx: TemplateContext): void { + if (typeof op == 'string') return; + + if (this.commands[op.op] != null) { + let cmd = this.commands[op.op].schema as TemplateCommandSchema; + + if (cmd.named) { + for (let name in cmd.named) { + let arg = cmd.named[name]; + + // @ts-ignore + if (arg.required && op.named[name] == null) { + throw new TemplateError(`Required template argument "${name}" is missing.`, op.srcRange); + } + } + } + + for (let arg of op.args) this.validateTemplate(arg, ctx); + for (let name in op.named) { + if (!cmd.named || cmd.named[name] == null) { + let arg = (op.named as {[index:string]:TemplateOp})[name] as TemplateOp | undefined; + + // @ts-ignore + throw new TemplateError(`Unknown template argument "${name}".`, { + file: op.srcRange.file, + from: (typeof arg == 'object' && arg.srcRange.from - 1 - name.length) || op.srcRange.from, + to: (typeof arg == 'object' && arg.srcRange.to) || op.srcRange.to + }); + } + + // @ts-ignore + this.validateTemplate(op.named[name]!, ctx); + } + } else throw new TemplateError(`Template command "${op.op}" does not exist.`, op.srcRange); + } + + // creates an executor with the default commands. + public static createExecutor(): TemplateExecutor { + let ex = new TemplateExecutor(); + + // joins two strings + ex.registerCommand('concat', {}, (op, ctx) => { + return op.args.reduce((a: string, b): string => a + ctx.owner.executeTemplate(b, ctx), ""); + }); + + // converts a number to a file size + ex.registerCommand('formatbytes', { + named: { unit: {} } + }, (op, ctx) => { + let value = ctx.owner.executeTemplate(op.args[0], ctx); + + let exponent = (op.named.unit != null && { 'b': 0, 'kb': 1, 'mb': 2, 'gb': 3, 'tb': 4 }[ctx.owner.executeTemplate(op.named.unit, ctx)]) + || Math.max(Math.min(Math.floor(Math.log10(Number(value))/3), 4), 0); + + return `${(Number(value) / 1000 ** exponent).toFixed(2)}${['b', 'kb', 'mb', 'gb', 'tb'][exponent]}`; + }); + + // gets the size of the active file + ex.registerCommand('filesize', {}, (op, ctx) => { + return ctx.file.size.toString(); + }); + + // gets the uploader of the active file + ex.registerCommand('uploader', {}, (op, ctx) => { + return ctx.uploader.username; + }); + + // selects a random argument + ex.registerCommand('random', {}, (op, ctx) => { + if (op.args.length > 0) { + return ctx.owner.executeTemplate(op.args[Math.round(Math.random() * (op.args.length - 1))], ctx); + } else throw new TemplateError('Random without arguments'); + }); + + return ex; + } +} \ No newline at end of file diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts new file mode 100644 index 0000000..50a714b --- /dev/null +++ b/backend/templates/parser.ts @@ -0,0 +1,270 @@ +import { TemplateOp, TemplateSource } from 'ass'; +import { TemplateSyntaxError } from './error'; + +enum TokenType { + T_OPEN, T_CLOSE, + PIPE, EQUALS, + TEXT, +}; + +type TemplateToken = { + type : TokenType; + data?: string; + from: number; + to: number; +}; + +// tree used by findReplacement to select the best amp-substitution +type TemplateAmpNode = { [index: string]: TemplateAmpNode | string | undefined; } +const TEMPLATE_AMP_SUBSTITUTIONS: TemplateAmpNode = { + e: { q: { $: '=' } }, + p: { i: { p: { e: { $: '|' } } } }, + t: { + c: { l: { o: { s: { e: { $: '}}' } } } } }, + o: { p: { e: { n: { $: '{{' } } } } + } +}; + +function getTemplateTokens(src: string): TemplateToken[] { + let tokens: TemplateToken[] = []; + let buf : string = ''; + let pos : number = 0; + + // digs through TEMPLATE_AMP_SUBSTITUTIONS to find + // longest possible string to replace + function findReplacement() { + let raw = ""; + let bestpos: number | null = null; + let best: string | null = null; + let node = TEMPLATE_AMP_SUBSTITUTIONS; + + while (true) { + if (pos >= src.length) break; + if (!/[a-z]/.test(src[pos])) break; + + if (node[src[pos]] != null) { // enter the thing + node = node[src[pos]] as TemplateAmpNode; + } else { + break; + } + + if (node.$ != null) { + best = node.$ as string; + bestpos = pos; + } + + raw += src[pos++]; + } + + if (best != null) { + pos = bestpos! + 1; + return best; + } + + return `&${raw}`; + } + + for (; pos < src.length; pos++) { + let lp = pos; + + if (pos + 1 < src.length && src[pos] == '{' && src[pos + 1] == '{') { + tokens.push({ + type: TokenType.T_OPEN, + from: pos, + to: pos + 1 + }); + pos++; + } else if (pos + 1 < src.length && src[pos] == '}' && src[pos + 1] == '}') { + tokens.push({ + type: TokenType.T_CLOSE, + from: pos, + to: pos + 1 + }); + pos++; + } else if (src[pos] == '|') { + tokens.push({ + type: TokenType.PIPE, + from: pos, + to: pos + }); + } else if (src[pos] == '=') { + tokens.push({ + type: TokenType.EQUALS, + from: pos, + to: pos + }); + } else if (src[pos] == '&') { + pos++; + buf += findReplacement(); + pos--; continue; + } else { + buf += src[pos]; + continue; + } + + if (buf.length) { + tokens.splice(-1, 0, { + type: TokenType.TEXT, + data: buf, + from: lp - buf.length, + to: lp - 1 + }); + buf = ''; + } + } + + if (buf.length) tokens.push({ + type: TokenType.TEXT, + data: buf, + from: src.length - buf.length, + to: src.length + }); + + return tokens; +} + +export function prepareTemplate(src: string): TemplateOp { + type ParserStackEntry = { + pos: number + }; + + let tokens = getTemplateTokens(src); + let pos = 0; + + let stack: ParserStackEntry[] = []; + + function stackPush() { + stack.push({ pos: pos }); + } + + function stackDrop() { + stack.pop(); + } + + let file: TemplateSource = { code: src }; + + // parse the text part of stuff. like uh + // you know uh like uh this part + // V---V V V-V V + // Hello {Concat|W|ORL|D} + function parseConcat(root: boolean = false): TemplateOp { + let joined: TemplateOp[] = []; + + let start = pos; + + stackPush(); + + out: while (pos < tokens.length) { + switch (tokens[pos].type) { + case TokenType.TEXT: + joined.push(tokens[pos++].data!); + continue out; + case TokenType.EQUALS: + if (root == true) throw new TemplateSyntaxError('Unexpected equals', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + case TokenType.PIPE: + if (root == true) throw new TemplateSyntaxError('Unexpected pipe', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + case TokenType.T_CLOSE: + if (root == true) throw new TemplateSyntaxError('Unexpected closing tag', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + break out; + case TokenType.T_OPEN: + joined.push(parseTemplate()); + } + } + + stackDrop(); + + return joined.length == 1 ? joined[0] : { + op: "concat", + named: {}, + args: joined, + srcRange: { + file: file, + from: tokens[start].from, + to: tokens[pos - 1].to + } + }; + } + + // parse templates + function parseTemplate(): TemplateOp { + let name: string; + let args: TemplateOp[] = []; + let nargs: {[index: string]: TemplateOp} = {}; + let start = pos; + + stackPush(); + + if (pos < tokens.length && tokens[pos].type == TokenType.T_OPEN) { + pos++; + } else throw new Error('Catastrophic failure'); + + if (pos < tokens.length && tokens[pos].type == TokenType.TEXT) { + name = tokens[pos++].data!; + } else if (pos < tokens.length) { + if (tokens[pos].type == TokenType.T_CLOSE) { + throw new TemplateSyntaxError('Template name missing', { file: file, from: tokens[pos - 1].from, to: tokens[pos].to }); + } else throw new TemplateSyntaxError('Expected template name', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + } else throw new TemplateSyntaxError('Unexpected end of file'); + + if (pos < tokens.length && tokens[pos].type == TokenType.PIPE) { + pos++; + + out: while (pos < tokens.length) { + let argStart = pos; + + let arg = parseConcat(); + + // this is some really nasty control flow im so sorry + if (pos < tokens.length) { + switch (tokens[pos].type) { + case TokenType.EQUALS: // named arguments + if (typeof arg != 'string') { + throw new TemplateSyntaxError('Argument name must be a plain string', { file: file, from: tokens[argStart].from, to: tokens[pos - 1].to }); + } + + pos++; + if (pos < tokens.length) { + let arg2 = parseConcat(); + nargs[arg] = arg2; + if (pos < tokens.length) { + switch (tokens[pos].type) { + case TokenType.T_CLOSE: break out; + case TokenType.PIPE: pos++; + } + } else throw new TemplateSyntaxError('syntax error'); + } else throw new TemplateSyntaxError('syntax error'); + break; + case TokenType.T_CLOSE: + args.push(arg); + break out; + case TokenType.PIPE: + args.push(arg); + pos++; + } + } + } + } else if (pos < tokens.length && tokens[pos].type != TokenType.T_CLOSE) { + throw new TemplateSyntaxError('Expected arguments or closing tag', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + } + + if (pos < tokens.length && tokens[pos].type == TokenType.T_CLOSE) { + pos++; + } else throw new TemplateSyntaxError('Template closing tag missing'); + + stackDrop(); + + return { + op: name.toLocaleLowerCase(), + named: nargs, + args: args, + srcRange: { + file: file, + from: tokens[start].from, + to: tokens[pos - 1].to + } + }; + } + + let result = parseConcat(true); + return result; +} \ No newline at end of file diff --git a/common/types.d.ts b/common/types.d.ts index 322ae5a..1e7fbe2 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -275,60 +275,66 @@ declare module 'ass' { meta: { [key: string]: any }; } - // Generic embed template operation - type EmbedTemplateOperation = - EmbedTemplateRandomOperation | - EmbedTemplateFileSizeOperation | - EmbedTemplateFormatBytesOperation | - EmbedTemplateUploaderOperation | - EmbedTemplateConcatOperation | - string | number; - /** - * Selects one operation and executes it + * Template operation */ - type EmbedTemplateRandomOperation = { - op: 'random'; - values: EmbedTemplateOperation[]; - }; + type TemplateOp = TemplateCommandOp | string; /** - * Returns the file size in bytes - * @returns number + * Please don't waste your time trying to make this look + * nice, it's not possible. */ - type EmbedTemplateFileSizeOperation = { op: 'fileSize' }; + type TemplateCommandOp = { + op: N; + args: TemplateOp[]; + named: { + +readonly [name in keyof T['named']]: ( + TemplateOp | (T['named'] extends object + ? T['named'][name] extends { required?: boolean } + ? T['named'][name]['required'] extends true + ? TemplateOp + : undefined + : undefined + : undefined) + ) + }; + srcRange: TemplateSourceRange; + }; /** - * Formats the file size in in {value} + * Basically a declaration */ - type EmbedTemplateFormatBytesOperation = { - op: 'formatBytes'; - unit?: EmbedTemplateOperation; // ready for ios! - value: EmbedTemplateOperation; + type TemplateCommandSchema = { + named?: { + [index: string]: { + required?: boolean + } + }; }; /** - * Returns the user who uploaded this file + * Template source code */ - type EmbedTemplateUploaderOperation = { - op: 'uploader' + type TemplateSource = { + code: string; }; /** - * Joins strings + * Range in template source code */ - type EmbedTemplateConcatOperation = { - op: 'concat', - values: EmbedTemplateOperation[] - } + type TemplateSourceRange = { + file: TemplateSource; + from: number; + to: number; + }; /** * This is so beyond cursed */ interface EmbedTemplate { - title: EmbedTemplateOperation; - description: EmbedTemplateOperation; - sitename: EmbedTemplateOperation; + title: TemplateOp; + description: TemplateOp; + sitename: TemplateOp; } /** From 8a5684c4ec3c23a894a7be06b5e20a3522c23026 Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 27 Nov 2023 10:18:15 -0500 Subject: [PATCH 11/15] template inclusion --- backend/templates/error.ts | 52 +++++++++++++++++++++++++++++++++++-- backend/templates/parser.ts | 29 ++++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/backend/templates/error.ts b/backend/templates/error.ts index 28c7c4b..8297780 100644 --- a/backend/templates/error.ts +++ b/backend/templates/error.ts @@ -13,8 +13,56 @@ export class TemplateError extends Error { let format = ''; if (this.range) { - format += this.range.file.code + '\n'; - format += ' '.repeat(this.range.from) + '^' + '~'.repeat(Math.max(this.range.to - this.range.from, 0)) + '\n'; + let fcol = 0; + let fline = 1; + let pstart = 0; + + for (let i = 0; i < this.range.from; i++) { + fcol++; + if (this.range.file.code[i] == '\n') { + fline++; + fcol = 0; + pstart = i + 1; + } + } + + let tcol = fcol; + let tline = fline; + let pend = pstart; + + for (let i = this.range.from; i < this.range.to; i++) { + tcol++; + if (this.range.file.code[i] == '\n') { + tline++; + tcol = 0; + pend = i + 1; + } + } + + pend = Math.max(this.range.file.code.indexOf('\n', pend), pend); + + if (fline == tline) { + format += `${fline.toString().padStart(5, ' ')} | ${this.range.file.code.substring(pstart, pend)}\n`; + format += `${fline.toString().padStart(5, ' ')} | ${' '.repeat(fcol)}^${'~'.repeat(Math.max(tcol - fcol, 0))}\n`; + } else { + let lines = this.range.file.code.substring(pstart, pend).split('\n'); + + format += ` | /${'~'.repeat(lines[0].length)}v\n`; + + for (let i = fline; i < fline + 5 && i <= tline; i++) { + format += `${i.toString().padStart(5, ' ')} | | ${lines[i - fline]}\n`; + } + + if (fline + 5 < tline) { + format += ` | | ...\n`; + + for (let i = tline - 4; i <= tline; i++) { + format += `${i.toString().padStart(5, ' ')} | | ${lines[i - fline]}\n`; + } + } + + format += ` | \\${'~'.repeat(tcol + 1)}^\n`; + } } format += `${this.name}: ${this.message}`; diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index 50a714b..e8c3734 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -1,4 +1,7 @@ import { TemplateOp, TemplateSource } from 'ass'; + +import fs from 'fs'; + import { TemplateSyntaxError } from './error'; enum TokenType { @@ -97,6 +100,10 @@ function getTemplateTokens(src: string): TemplateToken[] { pos++; buf += findReplacement(); pos--; continue; + } else if (src[pos] == '\n') { + pos++; + for (; pos < src.length && (src[pos] == ' ' || src[pos] == '\t'); pos++); + pos--; continue; } else { buf += src[pos]; continue; @@ -203,7 +210,7 @@ export function prepareTemplate(src: string): TemplateOp { } else if (pos < tokens.length) { if (tokens[pos].type == TokenType.T_CLOSE) { throw new TemplateSyntaxError('Template name missing', { file: file, from: tokens[pos - 1].from, to: tokens[pos].to }); - } else throw new TemplateSyntaxError('Expected template name', { file: file, from: tokens[pos].from, to: tokens[pos].to }); + } else throw new TemplateSyntaxError('Expected template name', { file: file, from: tokens[pos].from, to: tokens[pos].to }); } else throw new TemplateSyntaxError('Unexpected end of file'); if (pos < tokens.length && tokens[pos].type == TokenType.PIPE) { @@ -253,6 +260,26 @@ export function prepareTemplate(src: string): TemplateOp { stackDrop(); + // include is executed early + if (name.toLowerCase() == 'include') { + if (nargs['file'] != null) { + // TODO: this NEEDS to be restricted before ass 0.15.0 is released + // its extremely insecure and should be restricted to things + // set by operators, users can have their own template inclusion + // thing that doesnt depend on reading files + + if (typeof nargs['file'] == 'string') { + if (fs.existsSync(nargs['file'])) { + let template = fs.readFileSync(nargs['file'], { encoding: 'utf-8' }); + + let tl = prepareTemplate(template); + + return tl; + } else throw new TemplateSyntaxError('File does not exist', { file: file, from: tokens[start].from, to: tokens[pos - 1].to}); + } else throw new TemplateSyntaxError('Include directive can not contain templates', { file: file, from: tokens[start].from, to: tokens[pos - 1].to}); + } else throw new TemplateSyntaxError(`Bad include directive`, { file: file, from: tokens[start].from, to: tokens[pos - 1].to}); + } + return { op: name.toLocaleLowerCase(), named: nargs, From ab94343cb47122f28174d2e5af08609a3ef3919a Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 27 Nov 2023 10:41:13 -0500 Subject: [PATCH 12/15] fuck --- backend/templates/parser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index e8c3734..0a94b43 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -186,8 +186,8 @@ export function prepareTemplate(src: string): TemplateOp { args: joined, srcRange: { file: file, - from: tokens[start].from, - to: tokens[pos - 1].to + from: tokens[Math.min(tokens.length - 1, start)].from, + to: tokens[Math.min(tokens.length, pos) - 1].to } }; } From e1a49821e64c98519738100430b7b84784fca6de Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 27 Nov 2023 10:43:06 -0500 Subject: [PATCH 13/15] fuck --- backend/templates/parser.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index 0a94b43..70d7400 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -186,8 +186,8 @@ export function prepareTemplate(src: string): TemplateOp { args: joined, srcRange: { file: file, - from: tokens[Math.min(tokens.length - 1, start)].from, - to: tokens[Math.min(tokens.length, pos) - 1].to + from: tokens[start]?.from ?? 0, + to: tokens[pos - 1]?.to ?? src.length - 1 } }; } @@ -286,8 +286,8 @@ export function prepareTemplate(src: string): TemplateOp { args: args, srcRange: { file: file, - from: tokens[start].from, - to: tokens[pos - 1].to + from: tokens[start]?.from ?? 0, + to: tokens[pos - 1]?.to ?? src.length - 1 } }; } From 841556ea58c649f08a34162fef8acb36e950422f Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 1 Dec 2023 08:20:01 -0500 Subject: [PATCH 14/15] restrict include to admin defined templates --- backend/UserConfig.ts | 4 +++- backend/templates/error.ts | 11 ++++++++--- backend/templates/parser.ts | 20 ++++++++++++++------ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/backend/UserConfig.ts b/backend/UserConfig.ts index ed74a70..510c47e 100644 --- a/backend/UserConfig.ts +++ b/backend/UserConfig.ts @@ -139,7 +139,9 @@ export class UserConfig { for (let part of ['title', 'description', 'sitename'] as ('title' | 'description' | 'sitename')[]) { if (config.embed[part] != null) { if (typeof config.embed[part] == 'string') { - config.embed[part] = prepareTemplate(config.embed[part] as string); + config.embed[part] = prepareTemplate(config.embed[part] as string, { + allowIncludeFile: true + }); } else throw new Error(`Template string for embed ${part} is not a string`); } else config.embed[part] = DEFAULT_EMBED[part]; } diff --git a/backend/templates/error.ts b/backend/templates/error.ts index 8297780..4485503 100644 --- a/backend/templates/error.ts +++ b/backend/templates/error.ts @@ -9,6 +9,9 @@ export class TemplateError extends Error { this.range = range; } + /** + * Formats the error. + */ public format(): string { let format = ''; @@ -39,13 +42,15 @@ export class TemplateError extends Error { } } - pend = Math.max(this.range.file.code.indexOf('\n', pend), pend); + if ((pend = this.range.file.code.indexOf('\n', pend)) == -1) { + pend = this.range.file.code.length - 1; + } if (fline == tline) { - format += `${fline.toString().padStart(5, ' ')} | ${this.range.file.code.substring(pstart, pend)}\n`; + format += `${fline.toString().padStart(5, ' ')} | ${this.range.file.code.substring(pstart, pend + 1)}\n`; format += `${fline.toString().padStart(5, ' ')} | ${' '.repeat(fcol)}^${'~'.repeat(Math.max(tcol - fcol, 0))}\n`; } else { - let lines = this.range.file.code.substring(pstart, pend).split('\n'); + let lines = this.range.file.code.substring(pstart, pend + 1).split('\n'); format += ` | /${'~'.repeat(lines[0].length)}v\n`; diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index 70d7400..f0ef0f0 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -130,7 +130,15 @@ function getTemplateTokens(src: string): TemplateToken[] { return tokens; } -export function prepareTemplate(src: string): TemplateOp { +export type PrepareTemplateOptions = { + allowIncludeFile?: boolean; +}; + +export function prepareTemplate(src: string, config?: PrepareTemplateOptions): TemplateOp { + let options = { + includeFiles: config?.allowIncludeFile ?? false + }; + type ParserStackEntry = { pos: number }; @@ -263,16 +271,16 @@ export function prepareTemplate(src: string): TemplateOp { // include is executed early if (name.toLowerCase() == 'include') { if (nargs['file'] != null) { - // TODO: this NEEDS to be restricted before ass 0.15.0 is released - // its extremely insecure and should be restricted to things - // set by operators, users can have their own template inclusion - // thing that doesnt depend on reading files + // security check! + if (!options.includeFiles) { + throw new TemplateSyntaxError('You are not allowed to include files', { file: file, from: tokens[start].from, to: tokens[pos - 1].to }); + } if (typeof nargs['file'] == 'string') { if (fs.existsSync(nargs['file'])) { let template = fs.readFileSync(nargs['file'], { encoding: 'utf-8' }); - let tl = prepareTemplate(template); + let tl = prepareTemplate(template, config); return tl; } else throw new TemplateSyntaxError('File does not exist', { file: file, from: tokens[start].from, to: tokens[pos - 1].to}); From 22569a5da72f7c341c8370fbb0c813e2245a870d Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 1 Dec 2023 08:39:50 -0500 Subject: [PATCH 15/15] fix a lot of broken stuff: --- backend/embed.ts | 11 ++++++----- backend/templates/command.ts | 3 ++- backend/templates/error.ts | 2 +- backend/templates/executor.ts | 5 +++-- backend/templates/parser.ts | 2 +- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/backend/embed.ts b/backend/embed.ts index 9828586..ead029f 100644 --- a/backend/embed.ts +++ b/backend/embed.ts @@ -1,12 +1,13 @@ -import { AssFile, AssUser, EmbedTemplate, PreparedEmbed } from "ass" -import { TemplateExecutor } from "./templates/executor"; +import { AssFile, AssUser, EmbedTemplate, PreparedEmbed } from 'ass' + +import { TemplateExecutor } from './templates/executor.js'; let executor = TemplateExecutor.createExecutor(); export const DEFAULT_EMBED: EmbedTemplate = { - sitename: "ass", - title: "", - description: "" + sitename: 'ass', + title: '', + description: '' }; // ensures a template is valid diff --git a/backend/templates/command.ts b/backend/templates/command.ts index 8e44a1b..eb2f651 100644 --- a/backend/templates/command.ts +++ b/backend/templates/command.ts @@ -1,5 +1,6 @@ import { TemplateCommandOp, TemplateCommandSchema } from 'ass'; -import { TemplateContext } from './executor'; + +import { TemplateContext } from './executor.js'; export type TemplateCommand = { readonly name: N; diff --git a/backend/templates/error.ts b/backend/templates/error.ts index 4485503..9956af6 100644 --- a/backend/templates/error.ts +++ b/backend/templates/error.ts @@ -1,4 +1,4 @@ -import { TemplateSourceRange } from "ass"; +import { TemplateSourceRange } from 'ass'; export class TemplateError extends Error { range?: TemplateSourceRange; diff --git a/backend/templates/executor.ts b/backend/templates/executor.ts index 314b12c..de32160 100644 --- a/backend/templates/executor.ts +++ b/backend/templates/executor.ts @@ -1,6 +1,7 @@ import { AssFile, AssUser, TemplateCommandOp, TemplateCommandSchema, TemplateOp } from 'ass'; -import { TemplateCommand } from './command'; -import { TemplateError } from './error'; + +import { TemplateCommand } from './command.js'; +import { TemplateError } from './error.js'; export class TemplateContext { public readonly owner: TemplateExecutor; diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index f0ef0f0..cd4d5e4 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -2,7 +2,7 @@ import { TemplateOp, TemplateSource } from 'ass'; import fs from 'fs'; -import { TemplateSyntaxError } from './error'; +import { TemplateSyntaxError } from './error.js'; enum TokenType { T_OPEN, T_CLOSE,