2021-03-09 10:09:35 -05:00
|
|
|
"use strict";
|
|
|
|
|
2021-11-23 17:31:54 -05:00
|
|
|
const express = require("express");
|
2021-11-30 11:40:32 -05:00
|
|
|
const fetch = require("node-fetch");
|
2021-03-09 10:09:35 -05:00
|
|
|
const { encode } = require("html-entities");
|
|
|
|
const cleanBaseURL = require("clean-base-url");
|
|
|
|
const path = require("path");
|
2021-11-24 06:52:25 -05:00
|
|
|
const { promises: fs } = require("fs");
|
2021-12-01 11:10:40 -05:00
|
|
|
const { JSDOM } = require("jsdom");
|
2022-01-11 16:06:48 -05:00
|
|
|
const { shouldLoadPluginTestJs } = require("discourse/lib/plugin-js");
|
2021-05-07 09:59:45 -04:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
2021-03-09 10:09:35 -05:00
|
|
|
|
|
|
|
function htmlTag(buffer, bootstrap) {
|
|
|
|
let classList = "";
|
|
|
|
if (bootstrap.html_classes) {
|
|
|
|
classList = ` class="${bootstrap.html_classes}"`;
|
|
|
|
}
|
|
|
|
buffer.push(`<html lang="${bootstrap.html_lang}"${classList}>`);
|
|
|
|
}
|
|
|
|
|
2021-10-04 16:13:01 -04:00
|
|
|
function head(buffer, bootstrap, headers, baseURL) {
|
2021-03-09 10:09:35 -05:00
|
|
|
if (bootstrap.csrf_token) {
|
2021-04-30 06:26:48 -04:00
|
|
|
buffer.push(`<meta name="csrf-param" content="authenticity_token">`);
|
|
|
|
buffer.push(`<meta name="csrf-token" content="${bootstrap.csrf_token}">`);
|
2021-03-09 10:09:35 -05:00
|
|
|
}
|
2021-07-02 10:43:10 -04:00
|
|
|
|
2021-06-17 22:16:26 -04:00
|
|
|
if (bootstrap.theme_id) {
|
2021-03-09 10:09:35 -05:00
|
|
|
buffer.push(
|
2021-06-17 22:16:26 -04:00
|
|
|
`<meta name="discourse_theme_id" content="${bootstrap.theme_id}">`
|
2021-03-09 10:09:35 -05:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-07-02 10:43:10 -04:00
|
|
|
if (bootstrap.theme_color) {
|
|
|
|
buffer.push(`<meta name="theme-color" content="${bootstrap.theme_color}">`);
|
|
|
|
}
|
|
|
|
|
2021-07-29 09:01:11 -04:00
|
|
|
if (bootstrap.authentication_data) {
|
|
|
|
buffer.push(
|
|
|
|
`<meta id="data-authentication" data-authentication-data="${encode(
|
|
|
|
bootstrap.authentication_data
|
|
|
|
)}">`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-09 10:09:35 -05:00
|
|
|
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(`<meta id="data-discourse-setup"${setupData} />`);
|
|
|
|
|
|
|
|
(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}"`);
|
|
|
|
}
|
2021-07-01 04:58:26 -04:00
|
|
|
if (s.class) {
|
|
|
|
attrs.push(`class="${s.class}"`);
|
|
|
|
}
|
2021-03-09 10:09:35 -05:00
|
|
|
let link = `<link rel="stylesheet" type="text/css" href="${
|
|
|
|
s.href
|
2021-06-30 17:40:19 -04:00
|
|
|
}" ${attrs.join(" ")}>`;
|
2021-03-09 10:09:35 -05:00
|
|
|
buffer.push(link);
|
|
|
|
});
|
|
|
|
|
2021-10-04 16:13:01 -04:00
|
|
|
if (bootstrap.preloaded.currentUser) {
|
2022-06-17 08:50:21 -04:00
|
|
|
const user = JSON.parse(bootstrap.preloaded.currentUser);
|
|
|
|
let { admin, staff } = user;
|
|
|
|
|
2021-10-04 16:13:01 -04:00
|
|
|
if (staff) {
|
|
|
|
buffer.push(`<script src="${baseURL}assets/admin.js"></script>`);
|
|
|
|
}
|
2022-06-17 08:50:21 -04:00
|
|
|
|
|
|
|
if (admin) {
|
|
|
|
buffer.push(`<script src="${baseURL}assets/wizard.js"></script>`);
|
|
|
|
}
|
2021-10-04 16:13:01 -04:00
|
|
|
}
|
|
|
|
|
2021-03-09 10:09:35 -05:00
|
|
|
bootstrap.plugin_js.forEach((src) =>
|
|
|
|
buffer.push(`<script src="${src}"></script>`)
|
|
|
|
);
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-03-11 14:40:25 -05:00
|
|
|
function localeScript(buffer, bootstrap) {
|
|
|
|
buffer.push(`<script src="${bootstrap.locale_script}"></script>`);
|
|
|
|
}
|
|
|
|
|
2021-03-09 10:09:35 -05:00
|
|
|
function beforeScriptLoad(buffer, bootstrap) {
|
|
|
|
buffer.push(bootstrap.html.before_script_load);
|
2021-03-11 14:40:25 -05:00
|
|
|
localeScript(buffer, bootstrap);
|
2021-03-09 10:09:35 -05:00
|
|
|
(bootstrap.extra_locales || []).forEach((l) =>
|
|
|
|
buffer.push(`<script src="${l}"></script>`)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function body(buffer, bootstrap) {
|
|
|
|
buffer.push(bootstrap.theme_html.header);
|
|
|
|
buffer.push(bootstrap.html.header);
|
|
|
|
}
|
|
|
|
|
2021-05-07 08:49:47 -04:00
|
|
|
function bodyFooter(buffer, bootstrap, headers) {
|
2021-03-09 10:09:35 -05:00
|
|
|
buffer.push(bootstrap.theme_html.body_tag);
|
|
|
|
buffer.push(bootstrap.html.before_body_close);
|
2021-05-06 11:54:17 -04:00
|
|
|
|
2021-05-07 09:59:45 -04:00
|
|
|
let v = generateUID();
|
2021-05-06 11:54:17 -04:00
|
|
|
buffer.push(`
|
2021-11-30 11:40:32 -05:00
|
|
|
<script async type="text/javascript" id="mini-profiler" src="/mini-profiler-resources/includes.js?v=${v}" data-css-url="/mini-profiler-resources/includes.css?v=${v}" data-version="${v}" data-path="/mini-profiler-resources/" data-horizontal-position="left" data-vertical-position="top" data-trivial="false" data-children="false" data-max-traces="20" data-controls="false" data-total-sql-count="false" data-authorized="true" data-toggle-shortcut="alt+p" data-start-hidden="false" data-collapse-results="true" data-html-container="body" data-hidden-custom-fields="x" data-ids="${headers.get(
|
|
|
|
"x-miniprofiler-ids"
|
|
|
|
)}"></script>
|
2021-05-06 11:54:17 -04:00
|
|
|
`);
|
2021-03-09 10:09:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function hiddenLoginForm(buffer, bootstrap) {
|
|
|
|
if (!bootstrap.preloaded.currentUser) {
|
|
|
|
buffer.push(`
|
|
|
|
<form id='hidden-login-form' method="post" action="${bootstrap.login_path}" style="display: none;">
|
|
|
|
<input name="username" type="text" id="signin_username">
|
|
|
|
<input name="password" type="password" id="signin_password">
|
|
|
|
<input name="redirect" type="hidden">
|
|
|
|
<input type="submit" id="signin-button">
|
|
|
|
</form>
|
|
|
|
`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function preloaded(buffer, bootstrap) {
|
|
|
|
buffer.push(
|
|
|
|
`<div class="hidden" id="data-preloaded" data-preloaded="${encode(
|
|
|
|
JSON.stringify(bootstrap.preloaded)
|
|
|
|
)}"></div>`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const BUILDERS = {
|
|
|
|
"html-tag": htmlTag,
|
|
|
|
"before-script-load": beforeScriptLoad,
|
2021-06-16 13:45:02 -04:00
|
|
|
head,
|
|
|
|
body,
|
2021-03-09 10:09:35 -05:00
|
|
|
"hidden-login-form": hiddenLoginForm,
|
2021-06-16 13:45:02 -04:00
|
|
|
preloaded,
|
2021-03-09 10:09:35 -05:00
|
|
|
"body-footer": bodyFooter,
|
2021-03-11 14:40:25 -05:00
|
|
|
"locale-script": localeScript,
|
2021-03-09 10:09:35 -05:00
|
|
|
};
|
|
|
|
|
2021-10-04 16:13:01 -04:00
|
|
|
function replaceIn(bootstrap, template, id, headers, baseURL) {
|
2021-03-09 10:09:35 -05:00
|
|
|
let buffer = [];
|
2021-10-04 16:13:01 -04:00
|
|
|
BUILDERS[id](buffer, bootstrap, headers, baseURL);
|
2021-03-09 10:09:35 -05:00
|
|
|
let contents = buffer.filter((b) => b && b.length > 0).join("\n");
|
|
|
|
|
2021-03-19 09:32:46 -04:00
|
|
|
return template.replace(`<bootstrap-content key="${id}">`, contents);
|
2021-03-09 10:09:35 -05:00
|
|
|
}
|
|
|
|
|
2021-12-01 11:10:40 -05:00
|
|
|
function extractPreloadJson(html) {
|
|
|
|
const dom = new JSDOM(html);
|
2021-12-01 16:04:56 -05:00
|
|
|
const dataElement = dom.window.document.querySelector("#data-preloaded");
|
|
|
|
|
|
|
|
if (!dataElement || !dataElement.dataset) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return dataElement.dataset.preloaded;
|
2021-12-01 11:10:40 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
async function applyBootstrap(bootstrap, template, response, baseURL, preload) {
|
|
|
|
bootstrap.preloaded = Object.assign(JSON.parse(preload), bootstrap.preloaded);
|
2021-06-16 13:45:02 -04:00
|
|
|
|
2021-03-09 10:09:35 -05:00
|
|
|
Object.keys(BUILDERS).forEach((id) => {
|
2021-11-30 11:40:32 -05:00
|
|
|
template = replaceIn(bootstrap, template, id, response.headers, baseURL);
|
2021-03-09 10:09:35 -05:00
|
|
|
});
|
|
|
|
return template;
|
|
|
|
}
|
|
|
|
|
2021-12-01 11:10:40 -05:00
|
|
|
async function buildFromBootstrap(proxy, baseURL, req, response, preload) {
|
2021-11-23 17:31:54 -05:00
|
|
|
try {
|
|
|
|
const template = await fs.readFile(
|
|
|
|
path.join(process.cwd(), "dist", "index.html"),
|
|
|
|
"utf8"
|
|
|
|
);
|
2021-05-17 14:51:36 -04:00
|
|
|
|
2021-12-01 11:10:40 -05:00
|
|
|
let url = new URL(`${proxy}${baseURL}bootstrap.json`);
|
|
|
|
url.searchParams.append("for_url", req.url);
|
2021-11-23 17:31:54 -05:00
|
|
|
|
2021-11-30 11:40:32 -05:00
|
|
|
const res = await fetch(url, { headers: req.headers });
|
|
|
|
const json = await res.json();
|
2021-11-23 17:31:54 -05:00
|
|
|
|
2021-12-01 11:10:40 -05:00
|
|
|
return applyBootstrap(json.bootstrap, template, response, baseURL, preload);
|
2021-11-23 17:31:54 -05:00
|
|
|
} catch (error) {
|
|
|
|
throw new Error(
|
|
|
|
`Could not get ${proxy}${baseURL}bootstrap.json\n\n${error}`
|
2021-03-09 10:09:35 -05:00
|
|
|
);
|
2021-11-23 17:31:54 -05:00
|
|
|
}
|
2021-03-09 10:09:35 -05:00
|
|
|
}
|
|
|
|
|
2021-11-23 17:31:54 -05:00
|
|
|
async function handleRequest(proxy, baseURL, req, res) {
|
2022-04-28 08:46:59 -04:00
|
|
|
const originalHost = req.headers["x-forwarded-host"] || req.headers.host;
|
2021-11-23 17:31:54 -05:00
|
|
|
req.headers.host = new URL(proxy).host;
|
|
|
|
|
|
|
|
if (req.headers["Origin"]) {
|
|
|
|
req.headers["Origin"] = req.headers["Origin"]
|
|
|
|
.replace(req.headers.host, originalHost)
|
|
|
|
.replace(/^https/, "http");
|
2021-04-27 14:40:17 -04:00
|
|
|
}
|
|
|
|
|
2021-11-23 17:31:54 -05:00
|
|
|
if (req.headers["Referer"]) {
|
|
|
|
req.headers["Referer"] = req.headers["Referer"]
|
|
|
|
.replace(req.headers.host, originalHost)
|
|
|
|
.replace(/^https/, "http");
|
|
|
|
}
|
2021-04-23 10:24:42 -04:00
|
|
|
|
2021-11-23 17:31:54 -05:00
|
|
|
let url = `${proxy}${req.path}`;
|
|
|
|
const queryLoc = req.url.indexOf("?");
|
|
|
|
if (queryLoc !== -1) {
|
2022-04-01 11:35:17 -04:00
|
|
|
url += req.url.slice(queryLoc);
|
2021-11-23 17:31:54 -05:00
|
|
|
}
|
2021-04-23 10:24:42 -04:00
|
|
|
|
2021-11-23 17:31:54 -05:00
|
|
|
if (req.method === "GET") {
|
|
|
|
req.headers["X-Discourse-Ember-CLI"] = "true";
|
|
|
|
}
|
|
|
|
|
2021-11-30 11:40:32 -05:00
|
|
|
const response = await fetch(url, {
|
|
|
|
method: req.method,
|
|
|
|
body: /GET|HEAD/.test(req.method) ? null : req.body,
|
|
|
|
headers: req.headers,
|
2021-12-02 10:03:45 -05:00
|
|
|
redirect: "manual",
|
2021-11-30 11:40:32 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
response.headers.forEach((value, header) => {
|
|
|
|
res.set(header, value);
|
|
|
|
});
|
2021-11-23 17:31:54 -05:00
|
|
|
res.set("content-encoding", null);
|
|
|
|
|
2021-11-30 11:40:32 -05:00
|
|
|
const location = response.headers.get("location");
|
2021-11-23 17:31:54 -05:00
|
|
|
if (location) {
|
2021-11-24 06:52:25 -05:00
|
|
|
const newLocation = location.replace(proxy, `http://${originalHost}`);
|
2021-11-23 17:31:54 -05:00
|
|
|
res.set("location", newLocation);
|
|
|
|
}
|
|
|
|
|
2021-11-30 11:40:32 -05:00
|
|
|
const csp = response.headers.get("content-security-policy");
|
2021-11-24 06:52:25 -05:00
|
|
|
if (csp) {
|
2021-12-01 11:10:40 -05:00
|
|
|
const emberCliAdditions = [
|
|
|
|
`http://${originalHost}/assets/`,
|
|
|
|
`http://${originalHost}/ember-cli-live-reload.js`,
|
|
|
|
`http://${originalHost}/_lr/`,
|
|
|
|
];
|
|
|
|
const newCSP = csp
|
|
|
|
.replace(new RegExp(proxy, "g"), `http://${originalHost}`)
|
|
|
|
.replace(
|
|
|
|
new RegExp("script-src ", "g"),
|
|
|
|
`script-src ${emberCliAdditions.join(" ")} `
|
|
|
|
);
|
2021-11-24 06:52:25 -05:00
|
|
|
res.set("content-security-policy", newCSP);
|
|
|
|
}
|
|
|
|
|
2021-12-01 16:04:56 -05:00
|
|
|
const contentType = response.headers.get("content-type");
|
2021-12-02 05:58:54 -05:00
|
|
|
const isHTML = contentType && contentType.startsWith("text/html");
|
2021-12-01 11:10:40 -05:00
|
|
|
const responseText = await response.text();
|
2021-12-02 05:58:54 -05:00
|
|
|
const preloadJson = isHTML ? extractPreloadJson(responseText) : null;
|
2021-12-01 11:10:40 -05:00
|
|
|
|
2021-12-02 05:58:54 -05:00
|
|
|
if (preloadJson) {
|
2021-12-01 11:10:40 -05:00
|
|
|
const html = await buildFromBootstrap(
|
|
|
|
proxy,
|
|
|
|
baseURL,
|
|
|
|
req,
|
|
|
|
response,
|
2021-12-01 16:04:56 -05:00
|
|
|
extractPreloadJson(responseText)
|
2021-12-01 11:10:40 -05:00
|
|
|
);
|
2021-11-23 17:31:54 -05:00
|
|
|
res.set("content-type", "text/html");
|
|
|
|
res.send(html);
|
|
|
|
} else {
|
|
|
|
res.status(response.status);
|
2021-12-01 11:10:40 -05:00
|
|
|
res.send(responseText);
|
2021-04-23 10:24:42 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-09 10:09:35 -05:00
|
|
|
module.exports = {
|
|
|
|
name: require("./package").name,
|
|
|
|
|
|
|
|
isDevelopingAddon() {
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
2022-01-11 16:06:48 -05:00
|
|
|
contentFor: function (type, config) {
|
|
|
|
if (shouldLoadPluginTestJs() && type === "test-plugin-js") {
|
2022-03-21 15:46:41 -04:00
|
|
|
return `
|
|
|
|
<script src="${config.rootURL}assets/discourse/tests/active-plugins.js"></script>
|
|
|
|
<script src="${config.rootURL}assets/admin-plugins.js"></script>
|
|
|
|
`;
|
2022-01-11 16:06:48 -05:00
|
|
|
} else if (shouldLoadPluginTestJs() && type === "test-plugin-tests-js") {
|
|
|
|
return `<script id="plugin-test-script" src="${config.rootURL}assets/discourse/tests/plugin-tests.js"></script>`;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2021-03-09 10:09:35 -05:00
|
|
|
serverMiddleware(config) {
|
2021-11-23 17:31:54 -05:00
|
|
|
const app = config.app;
|
|
|
|
let { proxy, rootURL, baseURL } = config.options;
|
2021-03-09 10:09:35 -05:00
|
|
|
|
2021-04-23 10:24:42 -04:00
|
|
|
if (!proxy) {
|
2021-11-23 17:31:54 -05:00
|
|
|
// eslint-disable-next-line no-console
|
2021-04-23 10:24:42 -04:00
|
|
|
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";
|
|
|
|
}
|
|
|
|
|
2021-11-23 17:31:54 -05:00
|
|
|
baseURL = rootURL === "" ? "/" : cleanBaseURL(rootURL || baseURL);
|
2021-03-09 10:09:35 -05:00
|
|
|
|
2021-11-24 18:45:55 -05:00
|
|
|
const rawMiddleware = express.raw({ type: () => true, limit: "100mb" });
|
2021-11-24 06:52:25 -05:00
|
|
|
|
|
|
|
app.use(rawMiddleware, async (req, res, next) => {
|
2021-03-09 10:09:35 -05:00
|
|
|
try {
|
2021-11-23 17:31:54 -05:00
|
|
|
if (this.shouldHandleRequest(req)) {
|
|
|
|
await handleRequest(proxy, baseURL, req, res);
|
2021-03-09 10:09:35 -05:00
|
|
|
}
|
2021-11-23 17:31:54 -05:00
|
|
|
} catch (error) {
|
|
|
|
res.send(`
|
|
|
|
<html>
|
2021-12-02 05:58:03 -05:00
|
|
|
<h1>Discourse Ember CLI Proxy Error</h1>
|
|
|
|
<pre><code>${error.stack}</code></pre>
|
2021-11-23 17:31:54 -05:00
|
|
|
</html>
|
|
|
|
`);
|
2021-03-09 10:09:35 -05:00
|
|
|
} finally {
|
2021-04-23 10:24:42 -04:00
|
|
|
if (!res.headersSent) {
|
2021-04-14 15:25:49 -04:00
|
|
|
return next();
|
|
|
|
}
|
2021-03-09 10:09:35 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2021-11-23 17:31:54 -05:00
|
|
|
shouldHandleRequest(request) {
|
2021-11-24 09:34:04 -05:00
|
|
|
if (request.path === "/tests/index.html") {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-11-24 06:52:25 -05:00
|
|
|
if (request.get("Accept") && request.get("Accept").includes("text/html")) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-11-24 12:54:15 -05:00
|
|
|
const contentType = request.get("Content-Type");
|
|
|
|
if (!contentType) {
|
|
|
|
return false;
|
2021-03-09 10:09:35 -05:00
|
|
|
}
|
|
|
|
|
2021-11-23 17:31:54 -05:00
|
|
|
if (
|
2021-11-24 12:54:15 -05:00
|
|
|
contentType.includes("application/x-www-form-urlencoded") ||
|
|
|
|
contentType.includes("multipart/form-data") ||
|
|
|
|
contentType.includes("application/json")
|
2021-11-23 17:31:54 -05:00
|
|
|
) {
|
|
|
|
return true;
|
2021-04-20 14:26:15 -04:00
|
|
|
}
|
|
|
|
|
2021-11-23 17:31:54 -05:00
|
|
|
return false;
|
2021-03-09 10:09:35 -05:00
|
|
|
},
|
|
|
|
};
|