DEV: Compile splash screen JS with ember-cli before inlining (#18150)

This lets us use all our normal JS tooling like prettier, esline and babel on the splash screen JS. At runtime the JS file is read and inlined into the HTML. This commit also switches us to use a CSP hash rather than a nonce for the splash screen.
This commit is contained in:
David Taylor 2022-09-01 09:58:48 +01:00 committed by GitHub
parent 4ccbb91691
commit 0f8e4d7acc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 110 additions and 75 deletions

View File

@ -0,0 +1,66 @@
// This script is inlined in `_discourse_splash.html.erb
const DELAY_TARGET = 2000;
const POLLING_INTERVAL = 50;
const splashSvgTemplate = document.querySelector(".splash-svg-template");
const splashTemplateClone = splashSvgTemplate.content.cloneNode(true);
const svgElement = splashTemplateClone.querySelector("svg");
const svgString = new XMLSerializer().serializeToString(svgElement);
const encodedSvg = btoa(svgString);
const splashWrapper = document.querySelector("#d-splash");
const splashImage =
splashWrapper && splashWrapper.querySelector(".preloader-image");
if (splashImage) {
splashImage.src = `data:image/svg+xml;base64,${encodedSvg}`;
const connectStart = performance.timing.connectStart || 0;
const targetTime = connectStart + DELAY_TARGET;
let splashInterval;
let discourseReady;
const swapSplash = () => {
splashWrapper &&
splashWrapper.style.setProperty("--animation-state", "running");
svgElement && svgElement.style.setProperty("--animation-state", "running");
const newSvgString = new XMLSerializer().serializeToString(svgElement);
const newEncodedSvg = btoa(newSvgString);
splashImage.src = `data:image/svg+xml;base64,${newEncodedSvg}`;
performance.mark("discourse-splash-visible");
clearSplashInterval();
};
const clearSplashInterval = () => {
clearInterval(splashInterval);
splashInterval = null;
};
(() => {
splashInterval = setInterval(() => {
if (discourseReady) {
clearSplashInterval();
}
if (Date.now() > targetTime) {
swapSplash();
}
}, POLLING_INTERVAL);
})();
document.addEventListener(
"discourse-ready",
() => {
discourseReady = true;
splashWrapper && splashWrapper.remove();
performance.mark("discourse-splash-removed");
},
{ once: true }
);
}

View File

@ -128,10 +128,6 @@ module ApplicationHelper
path path
end end
def self.splash_screen_nonce
@splash_screen_nonce ||= SecureRandom.hex
end
def preload_script(script) def preload_script(script)
scripts = [script] scripts = [script]

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module SplashScreenHelper
def self.inline_splash_screen_script
<<~HTML.html_safe
<script>#{raw_js}</script>
HTML
end
def self.fingerprint
if Rails.env.development?
calculate_fingerprint
else
@fingerprint ||= calculate_fingerprint
end
end
private
def self.load_js
File.read("#{Rails.root}/app/assets/javascripts/discourse/dist/assets/splash-screen.js").sub("//# sourceMappingURL=splash-screen.map\n", "")
rescue Errno::ENOENT
Rails.logger.error("Unable to load splash screen JS") if Rails.env.production?
"console.log('Unable to load splash screen JS')"
end
def self.raw_js
if Rails.env.development?
load_js
else
@loaded_js ||= load_js
end
end
def self.calculate_fingerprint
"sha256-#{Digest::SHA256.base64digest(raw_js)}"
end
end

View File

@ -246,71 +246,6 @@
</style> </style>
</noscript> </noscript>
<script nonce="<%= ApplicationHelper.splash_screen_nonce %>"> <%= SplashScreenHelper.inline_splash_screen_script %>
const DELAY_TARGET = 2000;
const POLLING_INTERVAL = 50;
const splashSvgTemplate = document.querySelector(".splash-svg-template");
const splashTemplateClone = splashSvgTemplate.content.cloneNode(true);
const svgElement = splashTemplateClone.querySelector("svg");
const svgString = new XMLSerializer().serializeToString(svgElement);
const encodedSvg = btoa(svgString);
const splashWrapper = document.querySelector("#d-splash");
const splashImage = splashWrapper && splashWrapper.querySelector(".preloader-image");
if (splashImage) {
splashImage.src = `data:image/svg+xml;base64,${encodedSvg}`;
const connectStart = performance.timing.connectStart || 0;
const splashDelay = connectStart ? DELAY_TARGET : 0;
const targetTime = connectStart + DELAY_TARGET;
let splashInterval;
let discourseReady;
const swapSplash = () => {
splashWrapper && splashWrapper.style.setProperty("--animation-state", "running");
svgElement && svgElement.style.setProperty("--animation-state", "running");
const newSvgString = new XMLSerializer().serializeToString(svgElement);
const newEncodedSvg = btoa(newSvgString);
splashImage.src = `data:image/svg+xml;base64,${newEncodedSvg}`;
performance.mark("discourse-splash-visible");
clearSplashInterval();
};
const clearSplashInterval = () => {
clearInterval(splashInterval);
splashInterval = null;
};
(() => {
splashInterval = setInterval(() => {
if (discourseReady) {
clearSplashInterval();
}
if (Date.now() > targetTime) {
swapSplash();
}
}, POLLING_INTERVAL);
})();
document.addEventListener(
"discourse-ready",
() => {
discourseReady = true;
splashWrapper && splashWrapper.remove();
performance.mark("discourse-splash-removed");
},
{ once: true }
);
}
</script>
</section> </section>
<%- end %> <%- end %>

View File

@ -75,7 +75,7 @@ class ContentSecurityPolicy
end end
if SiteSetting.splash_screen if SiteSetting.splash_screen
sources << "'nonce-#{ApplicationHelper.splash_screen_nonce}'" sources << "'#{SplashScreenHelper.fingerprint}'"
end end
end end
end end

View File

@ -665,17 +665,17 @@ RSpec.describe ApplicationController do
expect(response.body).to include(nonce) expect(response.body).to include(nonce)
end end
it 'when splash screen is enabled it adds the same nonce to the policy and the inline splash script' do it 'when splash screen is enabled it adds the fingerprint to the policy' do
SiteSetting.content_security_policy = true SiteSetting.content_security_policy = true
SiteSetting.splash_screen = true SiteSetting.splash_screen = true
get '/latest' get '/latest'
nonce = ApplicationHelper.splash_screen_nonce fingerprint = SplashScreenHelper.fingerprint
expect(response.headers).to include('Content-Security-Policy') expect(response.headers).to include('Content-Security-Policy')
script_src = parse(response.headers['Content-Security-Policy'])['script-src'] script_src = parse(response.headers['Content-Security-Policy'])['script-src']
expect(script_src.to_s).to include(nonce) expect(script_src.to_s).to include(fingerprint)
expect(response.body).to include(nonce) expect(response.body).to include(SplashScreenHelper.inline_splash_screen_script)
end end
def parse(csp_string) def parse(csp_string)