import * as express from "express"; import { getRepo, handleError } from "./route-utils"; import * as path from "path"; import AnonymizedFile from "../../core/AnonymizedFile"; import AnonymousError from "../../core/AnonymousError"; import * as marked from "marked"; import * as sanitizeHtml from "sanitize-html"; import { streamToString } from "../../core/anonymize-utils"; import { IFile } from "../../core/model/files/files.types"; function escapeHtml(str: string): string { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } const sanitizeOptions: sanitizeHtml.IOptions = { allowedTags: sanitizeHtml.defaults.allowedTags.concat([ "img", "video", "input", "details", "summary", "del", "ins", "sup", "sub", ]), allowedAttributes: { ...sanitizeHtml.defaults.allowedAttributes, img: ["src", "srcset", "alt", "title", "width", "height", "loading"], video: ["src", "controls", "title"], input: ["type", "checked", "disabled"], code: ["class"], span: ["class"], div: ["class"], pre: ["class"], td: ["align"], th: ["align"], }, allowedSchemes: ["http", "https", "mailto"], }; const router = express.Router(); const indexPriority = [ "index.html", "index.htm", "index.md", "index.txt", "index.org", "index.1st", "index", "readme.md", "readme.txt", "readme.org", "readme.1st", "readme", ]; async function webView(req: express.Request, res: express.Response) { const repo = await getRepo(req, res); if (!repo) return; try { if (!repo.options.page || !repo.options.pageSource) { throw new AnonymousError("page_not_activated", { httpStatus: 400, object: repo, }); } if (repo.options.pageSource.branch != repo.model.source.branch) { throw new AnonymousError("page_not_supported_on_different_branch", { httpStatus: 400, object: repo, }); } const wRoot = repo.options.pageSource.path; const indexRepoId = req.path.indexOf(req.params.repoId); const filePath = req.path.substring( indexRepoId + req.params.repoId.length + 1 ); let requestPath = path.join(wRoot, filePath); if (requestPath.at(0) == "/" || requestPath.at(0) == ".") { requestPath = requestPath.substring(1); } let f = new AnonymizedFile({ repository: repo, anonymizedPath: requestPath, }); let info: IFile | null = null; try { info = await f.getFileInfo(); } catch { /* ignored */ } if ( req.headers.accept?.includes("text/html") && (filePath == "" || (info && info.size == null)) ) { const folderPath = info ? path.join(info.path, info.name) : wRoot.substring(1); // look for index file const candidates = await repo.files({ recursive: false, // look for file at the root of the page source path: folderPath == "." ? "" : folderPath, }); let bestMatch = null; indexSelector: for (const p of indexPriority) { for (const file of candidates) { if (file.name.toLowerCase() == p) { bestMatch = file; break indexSelector; } } } if (bestMatch) { requestPath = path.join(bestMatch.path, bestMatch.name); f = new AnonymizedFile({ repository: repo, anonymizedPath: requestPath, }); } else { // print list of files in the root repository const body = `

Content of ${escapeHtml(filePath)}

${candidates .map( (c) => `${escapeHtml(c.name) + (c.size == null ? "/" : "")}` ) .join("")}
`; const html = `Content${body}`; return res.contentType("text/html").send(html); } } if (!f.isFileSupported()) { throw new AnonymousError("file_not_supported", { httpStatus: 400, object: f, }); } if (f.extension() == "md") { const content = await streamToString(await f.anonymizedContent()); const body = sanitizeHtml(marked.marked(content, { headerIds: false, mangle: false }), sanitizeOptions); const html = `Content
${body}
`; res.contentType("text/html").send(html); } else { f.send(res); } } catch (error) { handleError(error, res, req); } } router.get("/:repoId/*", webView); router.get("/:repoId", (req: express.Request, res: express.Response) => { res.redirect("/w" + req.url + "/"); }); export default router;