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:
parent
4ccbb91691
commit
0f8e4d7acc
|
@ -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 }
|
||||
);
|
||||
}
|
|
@ -128,10 +128,6 @@ module ApplicationHelper
|
|||
path
|
||||
end
|
||||
|
||||
def self.splash_screen_nonce
|
||||
@splash_screen_nonce ||= SecureRandom.hex
|
||||
end
|
||||
|
||||
def preload_script(script)
|
||||
scripts = [script]
|
||||
|
||||
|
|
|
@ -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
|
|
@ -246,71 +246,6 @@
|
|||
</style>
|
||||
</noscript>
|
||||
|
||||
<script nonce="<%= ApplicationHelper.splash_screen_nonce %>">
|
||||
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>
|
||||
<%= SplashScreenHelper.inline_splash_screen_script %>
|
||||
</section>
|
||||
<%- end %>
|
||||
|
|
|
@ -75,7 +75,7 @@ class ContentSecurityPolicy
|
|||
end
|
||||
|
||||
if SiteSetting.splash_screen
|
||||
sources << "'nonce-#{ApplicationHelper.splash_screen_nonce}'"
|
||||
sources << "'#{SplashScreenHelper.fingerprint}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -665,17 +665,17 @@ RSpec.describe ApplicationController do
|
|||
expect(response.body).to include(nonce)
|
||||
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.splash_screen = true
|
||||
|
||||
get '/latest'
|
||||
nonce = ApplicationHelper.splash_screen_nonce
|
||||
fingerprint = SplashScreenHelper.fingerprint
|
||||
expect(response.headers).to include('Content-Security-Policy')
|
||||
|
||||
script_src = parse(response.headers['Content-Security-Policy'])['script-src']
|
||||
expect(script_src.to_s).to include(nonce)
|
||||
expect(response.body).to include(nonce)
|
||||
expect(script_src.to_s).to include(fingerprint)
|
||||
expect(response.body).to include(SplashScreenHelper.inline_splash_screen_script)
|
||||
end
|
||||
|
||||
def parse(csp_string)
|
||||
|
|
Loading…
Reference in New Issue