diff --git a/app/models/theme.rb b/app/models/theme.rb index 923415ecf1d..e1cc0419be7 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -151,9 +151,6 @@ class Theme < ActiveRecord::Base end Theme.expire_site_cache! - ColorScheme.hex_cache.clear - CSP::Extension.clear_theme_extensions_cache! - SvgSprite.expire_cache end def self.compiler_version @@ -217,6 +214,8 @@ class Theme < ActiveRecord::Base clear_cache! ApplicationSerializer.expire_cache_fragment!("user_themes") ColorScheme.hex_cache.clear + CSP::Extension.clear_theme_extensions_cache! + SvgSprite.expire_cache end def self.clear_default! diff --git a/lib/discourse.rb b/lib/discourse.rb index 757eeabf325..4616b887d2d 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -1028,4 +1028,14 @@ module Discourse def self.allow_dev_populate? Rails.env.development? || ENV["ALLOW_DEV_POPULATE"] == "1" end + + # warning: this method is very expensive and shouldn't be called in places + # where performance matters. it's meant to be called manually (e.g. in the + # rails console) when dealing with an emergency that requires invalidating + # theme cache + def self.clear_all_theme_cache! + ThemeField.force_recompilation! + Theme.all.each(&:update_javascript_cache!) + Theme.expire_site_cache! + end end diff --git a/spec/lib/discourse_spec.rb b/spec/lib/discourse_spec.rb index b419814331c..cd3a85e6520 100644 --- a/spec/lib/discourse_spec.rb +++ b/spec/lib/discourse_spec.rb @@ -472,4 +472,128 @@ describe Discourse do end end + context ".clear_all_theme_cache!" do + before do + setup_s3 + SiteSetting.s3_cdn_url = "https://s3.cdn.com/gg" + stub_s3_store + end + + let!(:theme) { Fabricate(:theme) } + let!(:upload) { Fabricate(:s3_image_upload) } + let!(:upload_theme_field) do + Fabricate( + :theme_field, + theme: theme, + upload: upload, + type_id: ThemeField.types[:theme_upload_var], + target_id: Theme.targets[:common], + name: "imajee", + value: "", + ) + end + let!(:basic_html_field) do + Fabricate( + :theme_field, + theme: theme, + type_id: ThemeField.types[:html], + target_id: Theme.targets[:common], + name: "head_tag", + value: <<~HTML + <script type="text/discourse-plugin" version="0.1"> + console.log(settings.uploads.imajee); + </script> + HTML + ) + end + let!(:js_field) do + Fabricate( + :theme_field, + theme: theme, + type_id: ThemeField.types[:js], + target_id: Theme.targets[:extra_js], + name: "somefile.js", + value: <<~JS + console.log(settings.uploads.imajee); + JS + ) + end + let!(:scss_field) do + Fabricate( + :theme_field, + theme: theme, + type_id: ThemeField.types[:scss], + target_id: Theme.targets[:common], + name: "scss", + value: <<~SCSS + .something { background: url($imajee); } + SCSS + ) + end + + it "invalidates all JS and CSS caches" do + Stylesheet::Manager.clear_theme_cache! + + old_upload_url = Discourse.store.cdn_url(upload.url) + + head_tag_script = Nokogiri::HTML5.fragment( + Theme.lookup_field(theme.id, :desktop, "head_tag") + ).css('script').first + head_tag_js = JavascriptCache.find_by(digest: head_tag_script[:src][/\h{40}/]).content + expect(head_tag_js).to include(old_upload_url) + + js_file_script = Nokogiri::HTML5.fragment( + Theme.lookup_field(theme.id, :extra_js, nil) + ).css('script').first + file_js = JavascriptCache.find_by(digest: js_file_script[:src][/\h{40}/]).content + expect(file_js).to include(old_upload_url) + + css_link_tag = Nokogiri::HTML5.fragment( + Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, 'all') + ).css('link').first + css = StylesheetCache.find_by(digest: css_link_tag[:href][/\h{40}/]).content + expect(css).to include("url(#{old_upload_url})") + + SiteSetting.s3_cdn_url = "https://new.s3.cdn.com/gg" + new_upload_url = Discourse.store.cdn_url(upload.url) + + head_tag_script = Nokogiri::HTML5.fragment( + Theme.lookup_field(theme.id, :desktop, "head_tag") + ).css('script').first + head_tag_js = JavascriptCache.find_by(digest: head_tag_script[:src][/\h{40}/]).content + expect(head_tag_js).to include(old_upload_url) + + js_file_script = Nokogiri::HTML5.fragment( + Theme.lookup_field(theme.id, :extra_js, nil) + ).css('script').first + file_js = JavascriptCache.find_by(digest: js_file_script[:src][/\h{40}/]).content + expect(file_js).to include(old_upload_url) + + css_link_tag = Nokogiri::HTML5.fragment( + Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, 'all') + ).css('link').first + css = StylesheetCache.find_by(digest: css_link_tag[:href][/\h{40}/]).content + expect(css).to include("url(#{old_upload_url})") + + Discourse.clear_all_theme_cache! + + head_tag_script = Nokogiri::HTML5.fragment( + Theme.lookup_field(theme.id, :desktop, "head_tag") + ).css('script').first + head_tag_js = JavascriptCache.find_by(digest: head_tag_script[:src][/\h{40}/]).content + expect(head_tag_js).to include(new_upload_url) + + js_file_script = Nokogiri::HTML5.fragment( + Theme.lookup_field(theme.id, :extra_js, nil) + ).css('script').first + file_js = JavascriptCache.find_by(digest: js_file_script[:src][/\h{40}/]).content + expect(file_js).to include(new_upload_url) + + css_link_tag = Nokogiri::HTML5.fragment( + Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, 'all') + ).css('link').first + css = StylesheetCache.find_by(digest: css_link_tag[:href][/\h{40}/]).content + expect(css).to include("url(#{new_upload_url})") + end + end end