diff --git a/app/assets/javascripts/discourse/scripts/splash-screen.js b/app/assets/javascripts/discourse/scripts/splash-screen.js
new file mode 100644
index 00000000000..a8a56af7eee
--- /dev/null
+++ b/app/assets/javascripts/discourse/scripts/splash-screen.js
@@ -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 }
+ );
+}
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 43c5418baf9..fdc40fc444e 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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]
diff --git a/app/helpers/splash_screen_helper.rb b/app/helpers/splash_screen_helper.rb
new file mode 100644
index 00000000000..7dfc4194565
--- /dev/null
+++ b/app/helpers/splash_screen_helper.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module SplashScreenHelper
+ def self.inline_splash_screen_script
+ <<~HTML.html_safe
+
+ 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
diff --git a/app/views/common/_discourse_splash.html.erb b/app/views/common/_discourse_splash.html.erb
index 3ce1009a694..4ce41e78e8e 100644
--- a/app/views/common/_discourse_splash.html.erb
+++ b/app/views/common/_discourse_splash.html.erb
@@ -246,71 +246,6 @@
-
+ <%= SplashScreenHelper.inline_splash_screen_script %>
<%- end %>
diff --git a/lib/content_security_policy/default.rb b/lib/content_security_policy/default.rb
index 515dde89017..7245bac31d2 100644
--- a/lib/content_security_policy/default.rb
+++ b/lib/content_security_policy/default.rb
@@ -75,7 +75,7 @@ class ContentSecurityPolicy
end
if SiteSetting.splash_screen
- sources << "'nonce-#{ApplicationHelper.splash_screen_nonce}'"
+ sources << "'#{SplashScreenHelper.fingerprint}'"
end
end
end
diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb
index c23aece7e98..adc80945adb 100644
--- a/spec/requests/application_controller_spec.rb
+++ b/spec/requests/application_controller_spec.rb
@@ -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)