diff --git a/package.json b/package.json index a279819..9449a1f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "clean-css": "^5.3.3", + "ejs": "^3.1.10", "express": "^4.19.2", "helmet": "^7.1.0", "uglify-js": "^3.17.4", diff --git a/server.js b/server.js index 8035890..0c42116 100644 --- a/server.js +++ b/server.js @@ -4,6 +4,7 @@ const path = require('path'); const express = require('express'); const helmet = require('helmet'); const winston = require('winston'); +const ejs = require('ejs'); const DocumentHandler = require('./lib/document_handler.js'); const FileStorage = require('./lib/file_storage.js'); @@ -37,25 +38,6 @@ logger.info('Welcome to unknownBIN!'); // init file-storage const fileStorage = new FileStorage({ path: config.dataPath, logger: logger }); -// load static documents into file-storage -if (config.documents) { - for (const name in config.documents) { - const docPath = config.documents[name]; - try { - const data = fs.readFileSync(docPath, 'utf8'); - fileStorage.set(name, data, (success) => { - if (success) { - logger.verbose(`Loaded document: ${name} from ${docPath}`); - } else { - logger.warn(`Failed to store document: ${name}`); - } - }); - } catch (err) { - logger.warn(`Unable to find or read document: ${name} at ${docPath}`); - } - } -} - // configure the document handler const documentHandler = new DocumentHandler({ store: fileStorage, @@ -68,9 +50,14 @@ const documentHandler = new DocumentHandler({ // setup routes and request-handling const app = express(); +// Configure EJS as the view engine +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + // Use helmet for basic security headers app.use(helmet()); +// API routes app.get('/raw/:id', (req, res) => { return documentHandler.handleRawGet(req.params.id, res); }); @@ -81,17 +68,47 @@ app.get('/documents/:id', (req, res) => { return documentHandler.handleGet(req.params.id, res); }); +// Static files app.use(express.static(path.join(__dirname, 'static'))); -app.get('/:id', (req, res, next) => { - res.sendFile(path.join(__dirname, '/static/index.html')); +// Frontend routes (SSR) +app.get('/:id', (req, res) => { + const key = req.params.id; + + // Don't treat static files as paste IDs + if (key.includes('.')) { + return res.status(404).send('Not Found'); + } + + fileStorage.get(key, (data) => { + if (data) { + // Paste found, render it with dynamic SEO tags + const title = `Paste ${key} - unknownBIN`; + const description = data.substring(0, 160).replace(/\n/g, ' '); + res.render('index', { + paste: { + key: key, + data: data, + title: title, + description: description + } + }); + } else { + // Paste not found, render the homepage + res.render('index', { + paste: null + }); + } + }); }); -app.get('/', (req, res, next) => { - res.sendFile(path.join(__dirname, '/static/index.html')); +app.get('/', (req, res) => { + // Render the homepage + res.render('index', { + paste: null + }); }); - app.listen(config.port, config.host, () => { logger.info(`Listening on ${config.host}:${config.port}`); }); \ No newline at end of file diff --git a/static/application.js b/static/application.js index 8a1b40a..593e6a1 100644 --- a/static/application.js +++ b/static/application.js @@ -53,20 +53,40 @@ Haste.prototype.newDocument = function(hideHistory) { // load an existing document Haste.prototype.loadDocument = function(key) { - const _this = this; - _this.doc = new HasteDocument(); - _this.doc.load(key, function(ret) { - if (ret) { - _this.$code.html(ret.value); - _this.setTitle(ret.key); - window.history.pushState(null, `${_this.appName}-${ret.key}`, `/${ret.key}`); - _this.fullKey(); - _this.$textarea.val('').hide(); - _this.$box.show(); - } else { - _this.newDocument(); - } - }); + // Check if data is pre-rendered by the server + const $preloaded = $('#preloaded-data'); + if ($preloaded.length > 0 && $preloaded.text().trim().length > 0) { + this.doc = new HasteDocument(); + this.doc.locked = true; + this.doc.key = key; + this.doc.data = $preloaded.text(); + const high = hljs.highlightAuto(this.doc.data).value; + const ret = { + value: high.replace(/.+/g, "$&").replace(/^\s*[\r\n]/gm, "\n"), + key: key, + }; + this.$code.html(ret.value); + this.setTitle(ret.key); + this.fullKey(); + this.$textarea.val('').hide(); + this.$box.show(); + } else { + // Data is not pre-rendered, fetch from API + const _this = this; + _this.doc = new HasteDocument(); + _this.doc.load(key, function(ret) { + if (ret) { + _this.$code.html(ret.value); + _this.setTitle(ret.key); + window.history.pushState(null, `${_this.appName}-${ret.key}`, `/${ret.key}`); + _this.fullKey(); + _this.$textarea.val('').hide(); + _this.$box.show(); + } else { + _this.newDocument(); + } + }); + } }; // duplicate the current document @@ -83,7 +103,10 @@ Haste.prototype.lockDocument = function() { const _this = this; this.doc.save(this.$textarea.val(), function(err, ret) { if (!err && ret) { - _this.$code.html(ret.value.trim().replace(/.+/g, "$&").replace(/^\s*[\r\n]/gm, "\n")); + // Manually highlight and display after saving + const high = hljs.highlightAuto(ret.data).value; + const formatted = high.replace(/.+/g, "$&").replace(/^\s*[\r\n]/gm, "\n"); + _this.$code.html(formatted); _this.setTitle(ret.key); window.history.pushState(null, `${_this.appName}-${ret.key}`, `/${ret.key}`); _this.fullKey(); @@ -114,7 +137,7 @@ Haste.prototype.configureButtons = function() { return evt.ctrlKey && evt.keyCode === 32; }, action: function() { - _this.newDocument(!_this.doc.key); + window.location.href = '/'; } }, { @@ -172,15 +195,6 @@ const HasteDocument = function() { this.locked = false; }; -// escape HTML-characters -HasteDocument.prototype.htmlEscape = function(s) { - return s - .replace(/&/g, '&') - .replace(/>/g, '>') - .replace(/ + +
+ + <% if (paste) { %> +