diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js index e67e996402e..c66e186c445 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -141,25 +141,61 @@ function applyBootstrap(bootstrap, template) { return template; } -function decorateIndex(assetPath, baseUrl, headers) { +function buildFromBootstrap(assetPath, proxy, req) { // eslint-disable-next-line return new Promise((resolve, reject) => { fs.readFile( path.join(process.cwd(), "dist", assetPath), "utf8", (err, template) => { - getJSON(`${baseUrl}/bootstrap.json`, null, headers) + getJSON(`${proxy}/bootstrap.json`, null, req.headers) .then((json) => { resolve(applyBootstrap(json.bootstrap, template)); }) .catch(() => { - reject(`Could not get ${baseUrl}/bootstrap.json`); + reject(`Could not get ${proxy}/bootstrap.json`); }); } ); }); } +async function handleRequest(assetPath, proxy, req, res) { + 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, 404, 403, 500]); + let response = await get(url, null, req.headers); + if (response.headers["x-discourse-bootstrap-required"] === "true") { + req.headers["X-Discourse-Asset-Path"] = req.path; + let json = await buildFromBootstrap(assetPath, proxy, req); + return res.send(json); + } + res.status(response.status); + res.set(response.headers); + res.send(await response.text()); + } + } catch (e) { + res.send(` + +
${e.toString()}
+ + `); + } + } +} + module.exports = { name: require("./package").name, @@ -172,6 +208,16 @@ module.exports = { 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 = @@ -180,7 +226,6 @@ module.exports = { : cleanBaseURL(options.rootURL || options.baseURL); app.use(async (req, res, next) => { - let sentTemplate = false; try { const results = await watcher; if (this.shouldHandleRequest(req, options)) { @@ -191,32 +236,15 @@ module.exports = { isFile = fs .statSync(path.join(results.directory, assetPath)) .isFile(); - } catch (err) { - /* ignore */ - } + } catch (err) {} if (!isFile) { assetPath = "index.html"; } - - if (assetPath.endsWith("index.html")) { - let template; - try { - template = await decorateIndex(assetPath, proxy, req.headers); - } catch (e) { - template = ` - -${e.toString()}
- - `; - } - sentTemplate = true; - return res.send(template); - } + await handleRequest(assetPath, proxy, req, res); } } finally { - if (!sentTemplate) { + if (!res.headersSent) { return next(); } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 885e53e91ad..6903b9df652 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -107,9 +107,23 @@ class ApplicationController < ActionController::Base class RenderEmpty < StandardError; end class PluginDisabled < StandardError; end + class EmberCLIHijacked < StandardError; end + + def catch_ember_cli_hijack + yield + rescue ActionView::Template::Error => ex + raise ex unless ex.cause.is_a?(EmberCLIHijacked) + send_ember_cli_bootstrap + end rescue_from RenderEmpty do - with_resolved_locale { render 'default/empty' } + catch_ember_cli_hijack do + with_resolved_locale { render 'default/empty' } + end + end + + rescue_from EmberCLIHijacked do + send_ember_cli_bootstrap end rescue_from ArgumentError do |e| @@ -286,13 +300,19 @@ class ApplicationController < ActionController::Base rescue Discourse::InvalidAccess return render plain: message, status: status_code end - with_resolved_locale do - error_page_opts[:layout] = opts[:include_ember] ? 'application' : 'no_ember' - render html: build_not_found_page(error_page_opts) + catch_ember_cli_hijack do + with_resolved_locale do + error_page_opts[:layout] = opts[:include_ember] ? 'application' : 'no_ember' + render html: build_not_found_page(error_page_opts) + end end end end + def send_ember_cli_bootstrap + head 200, content_type: "text/html", "X-Discourse-Bootstrap-Required": true + end + # If a controller requires a plugin, it will raise an exception if that plugin is # disabled. This allows plugins to be disabled programatically. def self.requires_plugin(plugin_name) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2df54fc8fd9..624d3322315 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -593,4 +593,10 @@ module ApplicationHelper current_user ? nil : value end end + + def hijack_if_ember_cli! + if request.headers["HTTP_X_DISCOURSE_EMBER_CLI"] == "true" + raise ApplicationController::EmberCLIHijacked.new + end + end end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 796a7be265d..205637c2ae3 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,3 +1,4 @@ +<%- hijack_if_ember_cli! -%> diff --git a/lib/discourse.rb b/lib/discourse.rb index 34aca6dca5e..a04949fe538 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -290,12 +290,30 @@ module Discourse end end - def self.find_plugin_css_assets(args) - plugins = self.find_plugins(args) - - plugins = plugins.select do |plugin| - plugin.asset_filters.all? { |b| b.call(:css, args[:request]) } + def self.apply_asset_filters(plugins, type, request) + filter_opts = asset_filter_options(type, request) + plugins.select do |plugin| + plugin.asset_filters.all? { |b| b.call(type, request, filter_opts) } end + end + + def self.asset_filter_options(type, request) + result = {} + return result if request.blank? + + path = request.fullpath + result[:path] = path if path.present? + + # When we bootstrap using the JSON method, we want to be able to filter assets on + # the path we're bootstrapping for. + asset_path = request.headers["HTTP_X_DISCOURSE_ASSET_PATH"] + result[:path] = asset_path if asset_path.present? + + result + end + + def self.find_plugin_css_assets(args) + plugins = apply_asset_filters(self.find_plugins(args), :css, args[:request]) assets = [] @@ -319,9 +337,7 @@ module Discourse plugin.js_asset_exists? end - plugins = plugins.select do |plugin| - plugin.asset_filters.all? { |b| b.call(:js, args[:request]) } - end + plugins = apply_asset_filters(plugins, :js, args[:request]) plugins.map { |plugin| "plugins/#{plugin.directory_name}" } end diff --git a/plugins/styleguide/plugin.rb b/plugins/styleguide/plugin.rb index 2e0703359cd..24fe4266d66 100644 --- a/plugins/styleguide/plugin.rb +++ b/plugins/styleguide/plugin.rb @@ -15,7 +15,7 @@ Discourse::Application.routes.append do end after_initialize do - register_asset_filter do |type, request| - request&.fullpath&.start_with?('/styleguide') + register_asset_filter do |type, request, opts| + (opts[:path] || '').start_with?('/styleguide') end end diff --git a/spec/components/discourse_spec.rb b/spec/components/discourse_spec.rb index 99c458f2144..701cc399500 100644 --- a/spec/components/discourse_spec.rb +++ b/spec/components/discourse_spec.rb @@ -65,6 +65,25 @@ describe Discourse do end end + context "asset_filter_options" do + it "obmits path if request is missing" do + opts = Discourse.asset_filter_options(:js, nil) + expect(opts[:path]).to be_blank + end + + it "returns a hash with a path from the request" do + req = stub(fullpath: "/hello", headers: {}) + opts = Discourse.asset_filter_options(:js, req) + expect(opts[:path]).to eq("/hello") + end + + it "overwrites the path if the asset path is present" do + req = stub(fullpath: "/bootstrap.json", headers: { "HTTP_X_DISCOURSE_ASSET_PATH" => "/hello" }) + opts = Discourse.asset_filter_options(:js, req) + expect(opts[:path]).to eq("/hello") + end + end + context 'plugins' do let(:plugin_class) do Class.new(Plugin::Instance) do @@ -107,7 +126,7 @@ describe Discourse do expect(Discourse.find_plugin_css_assets({}).length).to eq(2) expect(Discourse.find_plugin_js_assets({}).length).to eq(2) - plugin1.register_asset_filter do |type, request| + plugin1.register_asset_filter do |type, request, opts| false end expect(Discourse.find_plugin_css_assets({}).length).to eq(1)