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)