sourceapp.js

import { readFile } from "fs";
import * as path from "path";

import Database from "better-sqlite3";
import getSqliteStore from "better-sqlite3-session-store";
import { watch } from "chokidar";
import { load as loadYaml } from "js-yaml";
import passport from "passport";
import * as pug from "pug";
import express from "express";
import session from "express-session";
import { rateLimit, ipKeyGenerator } from "express-rate-limit";
import cookieParser from "cookie-parser";
import helmet from "helmet";

import morgan from "morgan";
import { createStream } from "rotating-file-stream";

import grimm from "./app/grimm.js";
import router from "./app/router.js";
import { rootFolder } from "./app/helpers.js";
import { cookieSecret, sessionSecret } from "./shush/shush.js";

const db = new Database("./db/site.db");
db.pragma("journal_mode = DELETE");

const SqliteStore = getSqliteStore(session);

const sessions = new Database("./db/sessions.db");

let app = express();

// view engine setup
app.engine("pug", pug.__express);
app.set("view engine", "pug");
app.set("views", `${rootFolder}/hypertext/public`);

const speedLimiter = rateLimit({
	windowMs: 30000, // Thirty seconds
	limit: 10, // Ten distinct page views per minute aught to be enough for anyone, no?
	skip: (req, res) => req.path.includes("."), // Exempt static files, so that loading /linkroll doesn’t take 3000 years lol
	keyGenerator: (req, res) => ipKeyGenerator(req.headers["x-real-ip"])
});

app.use(speedLimiter);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser(cookieSecret));
app.set("trust proxy", true);

app.use(
	session({
		secret: sessionSecret,
		resave: false,
		saveUninitialized: false,
		store: new SqliteStore({
			client: sessions,
			expired: {
				clear: true,
				intervalMs: 60 * 60 * 1000 // 1 hour
			}
		})
	})
);

app.use(passport.initialize());
app.use(passport.session());
app.use(passport.authenticate("remember-me"));

app.use((req, res, next) => {
	helmet({
		referrerPolicy: {
			policy: "no-referrer-when-downgrade"
		},
		contentSecurityPolicy: {
			directives: {
				"connect-src": ["'self'", "blob:"],
				"script-src": ["'self'", "'unsafe-inline'"],
				"script-src-attr": "'unsafe-inline'",
				"frame-src": [
					"'self'",
					"youtube-nocookie.com",
					"www.youtube-nocookie.com"
				]
			}
		},
		crossOriginResourcePolicy: {
			policy: ["/maps", "maps/", "files/", "/files"].some(x =>
				req.path.startsWith(x)
			)
				? "cross-origin"
				: "same-site"
		}
	})(req, res, next);
});

const generator = (time, index) => {
	if (!time) return "access.log";

	return `access-${time.getFullYear()}-${(time.getMonth() + 1)
		.toString()
		.padStart(2, "0")}.log`;
};

const accessLogStream = createStream(generator, {
	interval: "1M",
	path: `${rootFolder}/analytics`
});

const logger = morgan(":date[iso] :url :referrer /// :user-agent", {
	skip: (req, res) =>
		(req.path.includes(".") && !req.path.includes(".xml")) ||
		res.statusCode >= 400,
	stream: accessLogStream
});

app.use(logger);

app.use(router);

const publisher = watch(["./hypertext"], {
	awaitWriteFinish: {
		stabilityThreshold: 250
	}
});

function getParsed(parsed) {
	return {
		editions: {
			editionID: parsed?.slug,
			lang: parsed?.lang ?? "en",
			translates: parsed?.translates ?? parsed?.slug,
			title: parsed?.title,
			pageAdded: parsed?.pageAdded,
			pageTranslated: parsed?.pageTranslated,
			pageUpdated: parsed?.pageUpdated,
			description: parsed?.description,
			thumbnail: parsed?.thumbnail,
			thumbnailAlt: parsed?.thumbnailAlt,
			subtitle: parsed?.subtitle,
			supertitle: parsed?.supertitle
		},
		pagesets: {
			pagesetID: parsed?.translates ?? parsed?.slug,
			original: parsed?.slug,
			subsite: parsed?.subsite,
			category: parsed?.category,
			alexandrine: parsed?.alexandrine,
			pageCreated: parsed?.pageCreated
		},
		tags:
			typeof parsed?.tags == "object"
				? parsed.tags.map(x => {
						return { tagID: grimm.slugify(x), defaultName: x };
				  })
				: null
	};
}

const insertKeys = db.transaction((row, table) => {
	const rowKeys = Object.keys(row);
	db.prepare(
		`INSERT INTO ${table} (${rowKeys
			.map(x => ` ${x}`)
			.toString()
			.trim()}) VALUES (${rowKeys
			.map(x => ` @${x}`)
			.toString()
			.trim()})`
	).run(row);
});

const updateKeys = db.transaction((newRow, oldRow, table) => {
	const tableID = table.replace(/s$/, "ID");
	const constKeys = ["original"];
	for (let key in newRow) {
		if (
			newRow[key] != oldRow[key] &&
			!constKeys.includes(key) &&
			newRow[key] !== undefined
		) {
			db.prepare(
				`UPDATE ${table} SET ${key} = ? WHERE ${tableID} = ?`
			).run(newRow[key], newRow[tableID]);
		}
	}
});

const insertTag = db.transaction(tag => {
	db.prepare(
		`INSERT INTO tags (tagID, defaultName) VALUES (@tagID, @defaultName)`
	).run(tag);
});

const insertTaggings = db.transaction((pageset, tags) => {
	for (t of tags) {
		db.prepare(`INSERT INTO taggings (pageset, tag) VALUES (?, ?)`).run(
			pageset,
			t
		);
	}
});

const deleteTaggings = db.transaction((pageset, tags) => {
	for (t of tags) {
		db.prepare(`DELETE FROM taggings WHERE pageset = ? AND tag = ?`).run(
			pageset,
			t
		);
	}
});

publisher.on("change", filePath => {
	const fullPath = path.join(rootFolder, filePath);
	if (fullPath.endsWith(".pug") && pug.cache[fullPath]) {
		delete pug.cache[fullPath];
	}
	readFile(filePath, "utf-8", (err, input) => {
		let rawData = input.match(/meta[\r\n]+((\t.*?[\r\n]+)+)/);

		if (typeof rawData != "object" || rawData === null) return;
		if (rawData.length < 2) return;

		rawData = rawData[1].replace(/[\r\n]+$/, "").replace(/^\t/gm, "");

		let parsed;
		try {
			parsed = getParsed(loadYaml(rawData));
		} catch {
			return;
		}

		const records = {
			editions: db
				.prepare("SELECT * FROM editions WHERE editionID = ?")
				.get(parsed.editions.editionID),
			pagesets: db
				.prepare("SELECT * FROM pagesets WHERE pagesetID = ?")
				.get(parsed.pagesets.pagesetID),
			tags: db
				.prepare("SELECT tag FROM taggings WHERE pageset = ?")
				.all(parsed.pagesets.pagesetID)
				.map(x => x?.tag)
		};

		if (records.pagesets === undefined) {
			insertKeys(parsed.pagesets, "pagesets");
		} else {
			updateKeys(parsed.pagesets, records.pagesets, "pagesets");
		}

		if (records.editions === undefined) {
			insertKeys(parsed.editions, "editions");
		} else {
			updateKeys(parsed.editions, records.editions, "editions");
		}

		if (parsed.tags !== null) {
			const parsedTagIDs = parsed.tags.map(x => x.tagID);

			for (const tag of parsed.tags) {
				if (
					db
						.prepare("SELECT * FROM tags WHERE tagID = ?")
						.get(tag.tagID) === undefined
				) {
					insertTag(tag);
				}
			}

			const tagActions = {
				base: new Set(records.tags.concat(parsedTagIDs)),
				inserted: [],
				deleted: []
			};

			for (const tag of tagActions.base) {
				const parsedHas = parsedTagIDs.includes(tag);
				const recordsHas = records.tags.includes(tag);

				if (parsedHas && !recordsHas) {
					tagActions.inserted.push(tag);
				}
				if (!parsedHas && recordsHas) {
					tagActions.deleted.push(tag);
				}
			}

			insertTaggings(parsed.pagesets.pagesetID, tagActions.inserted);
			deleteTaggings(parsed.pagesets.pagesetID, tagActions.deleted);
		}
	});
});

export default app;