"use strict"; const bent = require("bent"); const getJSON = bent("json"); const { encode } = require("html-entities"); const cleanBaseURL = require("clean-base-url"); const path = require("path"); const fs = require("fs"); // via https://stackoverflow.com/a/6248722/165668 function generateUID() { let firstPart = (Math.random() * 46656) | 0; // eslint-disable-line no-bitwise let secondPart = (Math.random() * 46656) | 0; // eslint-disable-line no-bitwise firstPart = ("000" + firstPart.toString(36)).slice(-3); secondPart = ("000" + secondPart.toString(36)).slice(-3); return firstPart + secondPart; } const IGNORE_PATHS = [ /\/ember-cli-live-reload\.js$/, /\/session\/[^\/]+\/become$/, ]; function htmlTag(buffer, bootstrap) { let classList = ""; if (bootstrap.html_classes) { classList = ` class="${bootstrap.html_classes}"`; } buffer.push(``); } function head(buffer, bootstrap) { if (bootstrap.csrf_token) { buffer.push(``); buffer.push(``); } if (bootstrap.theme_id) { buffer.push( `` ); } if (bootstrap.theme_color) { buffer.push(``); } let setupData = ""; Object.keys(bootstrap.setup_data).forEach((sd) => { let val = bootstrap.setup_data[sd]; if (val) { if (Array.isArray(val)) { val = JSON.stringify(val); } else { val = val.toString(); } setupData += ` data-${sd.replace(/\_/g, "-")}="${encode(val)}"`; } }); buffer.push(``); (bootstrap.stylesheets || []).forEach((s) => { let attrs = []; if (s.media) { attrs.push(`media="${s.media}"`); } if (s.target) { attrs.push(`data-target="${s.target}"`); } if (s.theme_id) { attrs.push(`data-theme-id="${s.theme_id}"`); } if (s.class) { attrs.push(`class="${s.class}"`); } let link = ``; buffer.push(link); }); bootstrap.plugin_js.forEach((src) => buffer.push(``) ); buffer.push(bootstrap.theme_html.translations); buffer.push(bootstrap.theme_html.js); buffer.push(bootstrap.theme_html.head_tag); buffer.push(bootstrap.html.before_head_close); } function localeScript(buffer, bootstrap) { buffer.push(``); } function beforeScriptLoad(buffer, bootstrap) { buffer.push(bootstrap.html.before_script_load); localeScript(buffer, bootstrap); (bootstrap.extra_locales || []).forEach((l) => buffer.push(``) ); } function body(buffer, bootstrap) { buffer.push(bootstrap.theme_html.header); buffer.push(bootstrap.html.header); } function bodyFooter(buffer, bootstrap, headers) { buffer.push(bootstrap.theme_html.body_tag); buffer.push(bootstrap.html.before_body_close); let v = generateUID(); buffer.push(` `); } function hiddenLoginForm(buffer, bootstrap) { if (!bootstrap.preloaded.currentUser) { buffer.push(` `); } } function preloaded(buffer, bootstrap) { buffer.push( `` ); } const BUILDERS = { "html-tag": htmlTag, "before-script-load": beforeScriptLoad, head, body, "hidden-login-form": hiddenLoginForm, preloaded, "body-footer": bodyFooter, "locale-script": localeScript, }; function replaceIn(bootstrap, template, id, headers) { let buffer = []; BUILDERS[id](buffer, bootstrap, headers); let contents = buffer.filter((b) => b && b.length > 0).join("\n"); return template.replace(``, contents); } async function applyBootstrap(bootstrap, template, response) { // If our initial page added some preload data let's not lose that. let json = await response.json(); if (json && json.preloaded) { bootstrap.preloaded = Object.assign(json.preloaded, bootstrap.preloaded); } Object.keys(BUILDERS).forEach((id) => { template = replaceIn(bootstrap, template, id, response); }); return template; } function buildFromBootstrap(assetPath, proxy, baseURL, req, response) { // eslint-disable-next-line return new Promise((resolve, reject) => { fs.readFile( path.join(process.cwd(), "dist", assetPath), "utf8", (err, template) => { let url = `${proxy}${baseURL}bootstrap.json`; let queryLoc = req.url.indexOf("?"); if (queryLoc !== -1) { url += req.url.substr(queryLoc); } getJSON(url, null, req.headers) .then((json) => { return applyBootstrap(json.bootstrap, template, response); }) .then(resolve) .catch((e) => { reject( `Could not get ${proxy}${baseURL}bootstrap.json\n\n${e.toString()}` ); }); } ); }); } async function handleRequest(assetPath, proxy, baseURL, req, res) { if (assetPath.endsWith("tests/index.html")) { return; } if (assetPath.endsWith("index.html")) { try { // Avoid Ember CLI's proxy if doing a GET, since Discourse depends on some non-XHR // GET requests to work. if (req.method === "GET") { let url = `${proxy}${req.path}`; let queryLoc = req.url.indexOf("?"); if (queryLoc !== -1) { url += req.url.substr(queryLoc); } req.headers["X-Discourse-Ember-CLI"] = "true"; let get = bent("GET", [200, 301, 302, 303, 307, 308, 404, 403, 500]); let response = await get(url, null, req.headers); res.set(response.headers); res.set("content-type", "text/html"); if (response.headers["x-discourse-bootstrap-required"] === "true") { req.headers["X-Discourse-Asset-Path"] = req.path; let html = await buildFromBootstrap( assetPath, proxy, baseURL, req, response ); return res.send(html); } res.status(response.status); res.send(await response.text()); } } catch (e) { res.send(`

Discourse Build Error

${e.toString()}
`); } } } module.exports = { name: require("./package").name, isDevelopingAddon() { return true; }, serverMiddleware(config) { let proxy = config.options.proxy; let app = config.app; let options = config.options; if (!proxy) { // eslint-disable-next-line console.error(` Discourse can't be run without a \`--proxy\` setting, because it needs a Rails application to serve API requests. For example: yarn run ember serve --proxy "http://localhost:3000"\n`); throw "--proxy argument is required"; } let watcher = options.watcher; let baseURL = options.rootURL === "" ? "/" : cleanBaseURL(options.rootURL || options.baseURL); app.use(async (req, res, next) => { try { const results = await watcher; if (this.shouldHandleRequest(req, options)) { let assetPath = req.path.slice(baseURL.length); let isFile = false; try { isFile = fs .statSync(path.join(results.directory, assetPath)) .isFile(); } catch (err) {} if (!isFile) { assetPath = "index.html"; } await handleRequest(assetPath, proxy, baseURL, req, res); } } finally { if (!res.headersSent) { return next(); } } }); }, shouldHandleRequest(req) { let acceptHeaders = req.headers.accept || []; let hasHTMLHeader = acceptHeaders.indexOf("text/html") !== -1; if (req.method !== "GET") { return false; } if (!hasHTMLHeader) { return false; } if (IGNORE_PATHS.some((ip) => ip.test(req.path))) { return false; } if (req.path.endsWith(".json")) { return false; } let baseURLRegexp = new RegExp(`^/`); return baseURLRegexp.test(req.path); }, };