diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 7ec30918698..e7fcbdcd25b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -10,7 +10,7 @@ class ApplicationController < ActionController::Base
include Hijack
include ReadOnlyHeader
- attr_reader :theme_ids
+ attr_reader :theme_id
serialization_scope :guardian
@@ -448,35 +448,34 @@ class ApplicationController < ActionController::Base
resolve_safe_mode
return if request.env[NO_CUSTOM]
- theme_ids = []
+ theme_id = nil
- if preview_theme_id = request[:preview_theme_id]&.to_i
- ids = [preview_theme_id]
- theme_ids = ids if guardian.allow_themes?(ids, include_preview: true)
+ if (preview_theme_id = request[:preview_theme_id]&.to_i) &&
+ guardian.allow_themes?([preview_theme_id], include_preview: true)
+
+ theme_id = preview_theme_id
end
user_option = current_user&.user_option
- if theme_ids.blank?
+ if theme_id.blank?
ids, seq = cookies[:theme_ids]&.split("|")
- ids = ids&.split(",")&.map(&:to_i)
- if ids.present? && seq && seq.to_i == user_option&.theme_key_seq.to_i
- theme_ids = ids if guardian.allow_themes?(ids)
+ id = ids&.split(",")&.map(&:to_i)&.first
+ if id.present? && seq && seq.to_i == user_option&.theme_key_seq.to_i
+ theme_id = id if guardian.allow_themes?([id])
end
end
- if theme_ids.blank?
+ if theme_id.blank?
ids = user_option&.theme_ids || []
- theme_ids = ids if guardian.allow_themes?(ids)
+ theme_id = ids.first if guardian.allow_themes?(ids)
end
- if theme_ids.blank? && SiteSetting.default_theme_id != -1
- if guardian.allow_themes?([SiteSetting.default_theme_id])
- theme_ids << SiteSetting.default_theme_id
- end
+ if theme_id.blank? && SiteSetting.default_theme_id != -1 && guardian.allow_themes?([SiteSetting.default_theme_id])
+ theme_id = SiteSetting.default_theme_id
end
- @theme_ids = request.env[:resolved_theme_ids] = theme_ids
+ @theme_id = request.env[:resolved_theme_id] = theme_id
end
def guardian
@@ -635,10 +634,10 @@ class ApplicationController < ActionController::Base
target = view_context.mobile_view? ? :mobile : :desktop
data =
- if @theme_ids.present?
+ if @theme_id.present?
{
- top: Theme.lookup_field(@theme_ids, target, "after_header"),
- footer: Theme.lookup_field(@theme_ids, target, "footer")
+ top: Theme.lookup_field(@theme_id, target, "after_header"),
+ footer: Theme.lookup_field(@theme_id, target, "footer")
}
else
{}
@@ -943,9 +942,9 @@ class ApplicationController < ActionController::Base
end
def activated_themes_json
- ids = @theme_ids&.compact
- return "{}" if ids.blank?
- ids = Theme.transform_ids(ids)
+ id = @theme_id
+ return "{}" if id.blank?
+ ids = Theme.transform_ids(id)
Theme.where(id: ids).pluck(:id, :name).to_h.to_json
end
end
diff --git a/app/controllers/bootstrap_controller.rb b/app/controllers/bootstrap_controller.rb
index 0921e0aab81..5bb4dc6ddb3 100644
--- a/app/controllers/bootstrap_controller.rb
+++ b/app/controllers/bootstrap_controller.rb
@@ -34,7 +34,7 @@ class BootstrapController < ApplicationController
).each do |file|
add_style(file, plugin: true)
end
- add_style(mobile_view? ? :mobile_theme : :desktop_theme) if theme_ids.present?
+ add_style(mobile_view? ? :mobile_theme : :desktop_theme) if theme_id.present?
extra_locales = []
if ExtraLocalesController.client_overrides_exist?
@@ -51,7 +51,7 @@ class BootstrapController < ApplicationController
).map { |f| script_asset_path(f) }
bootstrap = {
- theme_ids: theme_ids,
+ theme_ids: [theme_id],
title: SiteSetting.title,
current_homepage: current_homepage,
locale_script: locale,
@@ -75,15 +75,14 @@ class BootstrapController < ApplicationController
private
def add_scheme(scheme_id, media)
return if scheme_id.to_i == -1
- theme_id = theme_ids&.first
- if style = Stylesheet::Manager.color_scheme_stylesheet_details(scheme_id, media, theme_id)
+ if style = Stylesheet::Manager.new(theme_id: theme_id).color_scheme_stylesheet_details(scheme_id, media)
@stylesheets << { href: style[:new_href], media: media }
end
end
def add_style(target, opts = nil)
- if styles = Stylesheet::Manager.stylesheet_details(target, 'all', theme_ids)
+ if styles = Stylesheet::Manager.new(theme_id: theme_id).stylesheet_details(target, 'all')
styles.each do |style|
@stylesheets << {
href: style[:new_href],
@@ -117,11 +116,11 @@ private
theme_view = mobile_view? ? :mobile : :desktop
- add_if_present(theme_html, :body_tag, Theme.lookup_field(theme_ids, theme_view, 'body_tag'))
- add_if_present(theme_html, :head_tag, Theme.lookup_field(theme_ids, theme_view, 'head_tag'))
- add_if_present(theme_html, :header, Theme.lookup_field(theme_ids, theme_view, 'header'))
- add_if_present(theme_html, :translations, Theme.lookup_field(theme_ids, :translations, I18n.locale))
- add_if_present(theme_html, :js, Theme.lookup_field(theme_ids, :extra_js, nil))
+ add_if_present(theme_html, :body_tag, Theme.lookup_field(theme_id, theme_view, 'body_tag'))
+ add_if_present(theme_html, :head_tag, Theme.lookup_field(theme_id, theme_view, 'head_tag'))
+ add_if_present(theme_html, :header, Theme.lookup_field(theme_id, theme_view, 'header'))
+ add_if_present(theme_html, :translations, Theme.lookup_field(theme_id, :translations, I18n.locale))
+ add_if_present(theme_html, :js, Theme.lookup_field(theme_id, :extra_js, nil))
theme_html
end
diff --git a/app/controllers/qunit_controller.rb b/app/controllers/qunit_controller.rb
index 8bb951b0fa0..98f7d70dac9 100644
--- a/app/controllers/qunit_controller.rb
+++ b/app/controllers/qunit_controller.rb
@@ -43,7 +43,7 @@ class QunitController < ApplicationController
return
end
- request.env[:resolved_theme_ids] = [theme.id]
+ request.env[:resolved_theme_id] = theme.id
request.env[:skip_theme_ids_transformation] = true
end
diff --git a/app/controllers/stylesheets_controller.rb b/app/controllers/stylesheets_controller.rb
index d9446882a2e..427f2b9203a 100644
--- a/app/controllers/stylesheets_controller.rb
+++ b/app/controllers/stylesheets_controller.rb
@@ -19,7 +19,8 @@ class StylesheetsController < ApplicationController
params.require("id")
params.permit("theme_id")
- stylesheet = Stylesheet::Manager.color_scheme_stylesheet_details(params[:id], 'all', params[:theme_id])
+ manager = Stylesheet::Manager.new(theme_id: params[:theme_id])
+ stylesheet = manager.color_scheme_stylesheet_details(params[:id], 'all')
render json: stylesheet
end
protected
@@ -40,16 +41,19 @@ class StylesheetsController < ApplicationController
# we hold off re-compilation till someone asks for asset
if target.include?("color_definitions")
split_target, color_scheme_id = target.split(/_(-?[0-9]+)/)
- Stylesheet::Manager.color_scheme_stylesheet_link_tag(color_scheme_id)
+
+ Stylesheet::Manager.new.color_scheme_stylesheet_link_tag(color_scheme_id)
else
- if target.include?("theme")
- split_target, theme_id = target.split(/_(-?[0-9]+)/)
- theme = Theme.find_by(id: theme_id) if theme_id.present?
- else
- split_target, color_scheme_id = target.split(/_(-?[0-9]+)/)
- theme = Theme.find_by(color_scheme_id: color_scheme_id)
- end
- Stylesheet::Manager.stylesheet_link_tag(split_target, nil, theme&.id)
+ theme_id =
+ if target.include?("theme")
+ split_target, theme_id = target.split(/_(-?[0-9]+)/)
+ Theme.where(id: theme_id).pluck_first(:id) if theme_id.present?
+ else
+ split_target, color_scheme_id = target.split(/_(-?[0-9]+)/)
+ Theme.where(color_scheme_id: color_scheme_id).pluck_first(:id)
+ end
+
+ Stylesheet::Manager.new(theme_id: theme_id).stylesheet_link_tag(split_target, nil)
end
end
diff --git a/app/controllers/svg_sprite_controller.rb b/app/controllers/svg_sprite_controller.rb
index 5851e4b5645..259fb11c326 100644
--- a/app/controllers/svg_sprite_controller.rb
+++ b/app/controllers/svg_sprite_controller.rb
@@ -12,13 +12,13 @@ class SvgSpriteController < ApplicationController
no_cookies
RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do
- theme_ids = params[:theme_ids].split(",").map(&:to_i)
+ theme_id = params[:theme_id].to_i
- if SvgSprite.version(theme_ids) != params[:version]
- return redirect_to path(SvgSprite.path(theme_ids))
+ if SvgSprite.version(theme_id) != params[:version]
+ return redirect_to path(SvgSprite.path(theme_id))
end
- svg_sprite = "window.__svg_sprite = #{SvgSprite.bundle(theme_ids).inspect};"
+ svg_sprite = "window.__svg_sprite = #{SvgSprite.bundle(theme_id).inspect};"
response.headers["Last-Modified"] = 10.years.ago.httpdate
response.headers["Content-Length"] = svg_sprite.bytesize.to_s
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 702f83ca5af..480b242969d 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -408,14 +408,19 @@ module ApplicationHelper
end
end
- def theme_ids
+ def theme_id
if customization_disabled?
- [nil]
+ nil
else
- request.env[:resolved_theme_ids]
+ request.env[:resolved_theme_id]
end
end
+ def stylesheet_manager
+ return @stylesheet_manager if defined?(@stylesheet_manager)
+ @stylesheet_manager = Stylesheet::Manager.new(theme_id: theme_id)
+ end
+
def scheme_id
return @scheme_id if defined?(@scheme_id)
@@ -424,12 +429,9 @@ module ApplicationHelper
return custom_user_scheme_id
end
- return if theme_ids.blank?
+ return if theme_id.blank?
- @scheme_id = Theme
- .where(id: theme_ids.first)
- .pluck(:color_scheme_id)
- .first
+ @scheme_id = Theme.where(id: theme_id).pluck_first(:color_scheme_id)
end
def dark_scheme_id
@@ -457,7 +459,7 @@ module ApplicationHelper
def theme_lookup(name)
Theme.lookup_field(
- theme_ids,
+ theme_id,
mobile_view? ? :mobile : :desktop,
name,
skip_transformation: request.env[:skip_theme_ids_transformation].present?
@@ -466,7 +468,7 @@ module ApplicationHelper
def theme_translations_lookup
Theme.lookup_field(
- theme_ids,
+ theme_id,
:translations,
I18n.locale,
skip_transformation: request.env[:skip_theme_ids_transformation].present?
@@ -475,7 +477,7 @@ module ApplicationHelper
def theme_js_lookup
Theme.lookup_field(
- theme_ids,
+ theme_id,
:extra_js,
nil,
skip_transformation: request.env[:skip_theme_ids_transformation].present?
@@ -483,22 +485,26 @@ module ApplicationHelper
end
def discourse_stylesheet_link_tag(name, opts = {})
- if opts.key?(:theme_ids)
- ids = opts[:theme_ids] unless customization_disabled?
- else
- ids = theme_ids
- end
+ manager =
+ if opts.key?(:theme_id)
+ Stylesheet::Manager.new(
+ theme_id: customization_disabled? ? nil : opts[:theme_id]
+ )
+ else
+ stylesheet_manager
+ end
- Stylesheet::Manager.stylesheet_link_tag(name, 'all', ids)
+ manager.stylesheet_link_tag(name, 'all')
end
def discourse_color_scheme_stylesheets
result = +""
- result << Stylesheet::Manager.color_scheme_stylesheet_link_tag(scheme_id, 'all', theme_ids)
+ result << stylesheet_manager.color_scheme_stylesheet_link_tag(scheme_id, 'all')
if dark_scheme_id != -1
- result << Stylesheet::Manager.color_scheme_stylesheet_link_tag(dark_scheme_id, '(prefers-color-scheme: dark)', theme_ids)
+ result << stylesheet_manager.color_scheme_stylesheet_link_tag(dark_scheme_id, '(prefers-color-scheme: dark)')
end
+
result.html_safe
end
@@ -525,7 +531,7 @@ module ApplicationHelper
asset_version: Discourse.assets_digest,
disable_custom_css: loading_admin?,
highlight_js_path: HighlightJs.path,
- svg_sprite_path: SvgSprite.path(theme_ids),
+ svg_sprite_path: SvgSprite.path(theme_id),
enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
color_scheme_is_dark: dark_color_scheme?,
user_color_scheme_id: scheme_id,
@@ -533,7 +539,7 @@ module ApplicationHelper
}
if Rails.env.development?
- setup_data[:svg_icon_list] = SvgSprite.all_icons(theme_ids)
+ setup_data[:svg_icon_list] = SvgSprite.all_icons(theme_id)
if ENV['DEBUG_PRELOADED_APP_DATA']
setup_data[:debug_preloaded_app_data] = true
diff --git a/app/helpers/qunit_helper.rb b/app/helpers/qunit_helper.rb
index 98f509c6644..e0376a1ad26 100644
--- a/app/helpers/qunit_helper.rb
+++ b/app/helpers/qunit_helper.rb
@@ -2,7 +2,7 @@
module QunitHelper
def theme_tests
- theme = Theme.find_by(id: request.env[:resolved_theme_ids]&.first)
+ theme = Theme.find_by(id: request.env[:resolved_theme_id])
return "" if theme.blank?
_, digest = theme.baked_js_tests_with_digest
diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb
index dd2cf021cc8..22682be18dd 100644
--- a/app/models/color_scheme.rb
+++ b/app/models/color_scheme.rb
@@ -320,6 +320,7 @@ class ColorScheme < ActiveRecord::Base
end
if theme_ids.present?
Stylesheet::Manager.cache.clear
+
Theme.notify_theme_change(
theme_ids,
with_scheme: true,
diff --git a/app/models/theme.rb b/app/models/theme.rb
index 8d3cd01c9ba..ad5f63be4c5 100644
--- a/app/models/theme.rb
+++ b/app/models/theme.rb
@@ -29,6 +29,9 @@ class Theme < ActiveRecord::Base
has_many :locale_fields, -> { filter_locale_fields(I18n.fallbacks[I18n.locale]) }, class_name: 'ThemeField'
has_many :upload_fields, -> { where(type_id: ThemeField.types[:theme_upload_var]).preload(:upload) }, class_name: 'ThemeField'
has_many :extra_scss_fields, -> { where(target_id: Theme.targets[:extra_scss]) }, class_name: 'ThemeField'
+ has_many :yaml_theme_fields, -> { where("name = 'yaml' AND type_id = ?", ThemeField.types[:yaml]) }, class_name: 'ThemeField'
+ has_many :var_theme_fields, -> { where("type_id IN (?)", ThemeField.theme_var_type_ids) }, class_name: 'ThemeField'
+ has_many :builder_theme_fields, -> { where("name IN (?)", ThemeField.scss_fields) }, class_name: 'ThemeField'
validate :component_validations
@@ -164,6 +167,16 @@ class Theme < ActiveRecord::Base
end
end
+ def self.parent_theme_ids
+ get_set_cache "parent_theme_ids" do
+ Theme.where(component: false).pluck(:id)
+ end
+ end
+
+ def self.is_parent_theme?(id)
+ self.parent_theme_ids.include?(id)
+ end
+
def self.user_theme_ids
get_set_cache "user_theme_ids" do
Theme.user_selectable.pluck(:id)
@@ -188,25 +201,22 @@ class Theme < ActiveRecord::Base
expire_site_cache!
end
- def self.transform_ids(ids, extend: true)
- return [] if ids.nil?
- get_set_cache "#{extend ? "extended_" : ""}transformed_ids_#{ids.join("_")}" do
- next [] if ids.blank?
+ def self.transform_ids(id)
+ return [] if id.blank?
- ids = ids.dup
- ids.uniq!
- parent = ids.shift
-
- components = ids
- components.push(*components_for(parent)) if extend
- components.sort!.uniq!
-
- all_ids = [parent, *components]
+ get_set_cache "transformed_ids_#{id}" do
+ all_ids =
+ if self.is_parent_theme?(id)
+ components = components_for(id).tap { |c| c.sort!.uniq! }
+ [id, *components]
+ else
+ [id]
+ end
disabled_ids = Theme.where(id: all_ids)
.includes(:remote_theme)
.select { |t| !t.supported? || !t.enabled? }
- .pluck(:id)
+ .map(&:id)
all_ids - disabled_ids
end
@@ -272,11 +282,10 @@ class Theme < ActiveRecord::Base
end
end
- def self.lookup_field(theme_ids, target, field, skip_transformation: false)
- return if theme_ids.blank?
- theme_ids = [theme_ids] unless Array === theme_ids
+ def self.lookup_field(theme_id, target, field, skip_transformation: false)
+ return "" if theme_id.blank?
- theme_ids = transform_ids(theme_ids) if !skip_transformation
+ theme_ids = !skip_transformation ? transform_ids(theme_id) : [theme_id]
cache_key = "#{theme_ids.join(",")}:#{target}:#{field}:#{Theme.compiler_version}"
lookup = @cache[cache_key]
return lookup.html_safe if lookup
@@ -289,8 +298,8 @@ class Theme < ActiveRecord::Base
def self.lookup_modifier(theme_ids, modifier_name)
theme_ids = [theme_ids] unless Array === theme_ids
-
theme_ids = transform_ids(theme_ids)
+
get_set_cache("#{theme_ids.join(",")}:modifier:#{modifier_name}:#{Theme.compiler_version}") do
ThemeModifierSet.resolve_modifier_for_themes(theme_ids, modifier_name)
end
@@ -335,14 +344,18 @@ class Theme < ActiveRecord::Base
def notify_theme_change(with_scheme: false)
DB.after_commit do
- theme_ids = Theme.transform_ids([id])
+ theme_ids = Theme.transform_ids(id)
self.class.notify_theme_change(theme_ids, with_scheme: with_scheme)
end
end
def self.refresh_message_for_targets(targets, theme_ids)
- targets.map do |target|
- Stylesheet::Manager.stylesheet_data(target.to_sym, theme_ids)
+ theme_ids = [theme_ids] unless theme_ids === Array
+
+ targets.each_with_object([]) do |target, data|
+ theme_ids.each do |theme_id|
+ data << Stylesheet::Manager.new(theme_id: theme_id).stylesheet_data(target.to_sym)
+ end
end
end
@@ -385,7 +398,8 @@ class Theme < ActiveRecord::Base
end
def list_baked_fields(target, name)
- theme_ids = Theme.transform_ids([id], extend: name == :color_definitions)
+ theme_ids = Theme.transform_ids(id)
+ theme_ids = [theme_ids.first] if name != :color_definitions
self.class.list_baked_fields(theme_ids, target, name)
end
@@ -435,7 +449,7 @@ class Theme < ActiveRecord::Base
def all_theme_variables
fields = {}
- ids = Theme.transform_ids([id])
+ ids = Theme.transform_ids(id)
ThemeField.find_by_theme_ids(ids).where(type_id: ThemeField.theme_var_type_ids).each do |field|
next if fields.key?(field.name)
fields[field.name] = field
@@ -530,7 +544,7 @@ class Theme < ActiveRecord::Base
def included_settings
hash = {}
- Theme.where(id: Theme.transform_ids([id])).each do |theme|
+ Theme.where(id: Theme.transform_ids(id)).each do |theme|
hash.merge!(theme.cached_settings)
end
@@ -641,11 +655,6 @@ class Theme < ActiveRecord::Base
contents
end
- def has_scss(target)
- name = target == :embedded_theme ? :embedded_scss : :scss
- list_baked_fields(target, name).count > 0
- end
-
def convert_settings
settings.each do |setting|
setting_row = ThemeSetting.where(theme_id: self.id, name: setting.name.to_s).first
diff --git a/app/views/common/_discourse_publish_stylesheet.html.erb b/app/views/common/_discourse_publish_stylesheet.html.erb
index ddd5b9e40b5..90fe9a9f9e2 100644
--- a/app/views/common/_discourse_publish_stylesheet.html.erb
+++ b/app/views/common/_discourse_publish_stylesheet.html.erb
@@ -10,6 +10,6 @@
<%= discourse_stylesheet_link_tag(file) %>
<%- end %>
-<%- if theme_ids.present? %>
+<%- if theme_id.present? %>
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
<%- end %>
diff --git a/app/views/common/_discourse_stylesheet.html.erb b/app/views/common/_discourse_stylesheet.html.erb
index 700e28cf005..3ef9c170c36 100644
--- a/app/views/common/_discourse_stylesheet.html.erb
+++ b/app/views/common/_discourse_stylesheet.html.erb
@@ -14,7 +14,6 @@
<%= discourse_stylesheet_link_tag(file) %>
<%- end %>
-<%- if theme_ids.present? %>
+<%- if theme_id.present? %>
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
<%- end %>
-
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 2d9445a3276..47159a614a5 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -5,7 +5,7 @@
<%= content_for?(:title) ? yield(:title) : SiteSetting.title %>
- ">
+
<%= render partial: "layouts/head" %>
<%= discourse_csrf_tags %>
diff --git a/app/views/layouts/crawler.html.erb b/app/views/layouts/crawler.html.erb
index 4e0a7e7bdd4..ec1f753d6ef 100644
--- a/app/views/layouts/crawler.html.erb
+++ b/app/views/layouts/crawler.html.erb
@@ -10,7 +10,7 @@
<%- else %>
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %>
<%- end %>
- <%- if theme_ids.present? %>
+ <%- if theme_id.present? %>
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
<%- end %>
<%= theme_lookup("head_tag") %>
diff --git a/app/views/layouts/no_ember.html.erb b/app/views/layouts/no_ember.html.erb
index ca08de88c74..b36b9205b5d 100644
--- a/app/views/layouts/no_ember.html.erb
+++ b/app/views/layouts/no_ember.html.erb
@@ -13,8 +13,11 @@
<%= build_plugin_html 'server:before-head-close' %>
- <%= theme_lookup("header") %>
- <%= build_plugin_html 'server:header' %>
+ <%- unless customization_disabled? %>
+ <%= theme_lookup("header") %>
+ <%= build_plugin_html 'server:header' %>
+ <%- end %>
+
<%= render partial: 'header', locals: { hide_auth_buttons: local_assigns[:hide_auth_buttons] } %>
diff --git a/config/routes.rb b/config/routes.rb
index ee1b2644dde..052480ef8ab 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -519,7 +519,7 @@ Discourse::Application.routes.draw do
get "letter_avatar_proxy/:version/letter/:letter/:color/:size.png" => "user_avatars#show_proxy_letter", constraints: { format: :png }
- get "svg-sprite/:hostname/svg-:theme_ids-:version.js" => "svg_sprite#show", constraints: { hostname: /[\w\.-]+/, version: /\h{40}/, theme_ids: /([0-9]+(,[0-9]+)*)?/, format: :js }
+ get "svg-sprite/:hostname/svg-:theme_id-:version.js" => "svg_sprite#show", constraints: { hostname: /[\w\.-]+/, version: /\h{40}/, theme_id: /([0-9]+)?/, format: :js }
get "svg-sprite/search/:keyword" => "svg_sprite#search", format: false, constraints: { keyword: /[-a-z0-9\s\%]+/ }
get "svg-sprite/picker-search" => "svg_sprite#icon_picker_search", defaults: { format: :json }
get "svg-sprite/:hostname/icon(/:color)/:name.svg" => "svg_sprite#svg_icon", constraints: { hostname: /[\w\.-]+/, name: /[-a-z0-9\s\%]+/, color: /(\h{3}{1,2})/, format: :svg }
diff --git a/lib/content_security_policy.rb b/lib/content_security_policy.rb
index 76d2246e5f8..0cfd309a4bc 100644
--- a/lib/content_security_policy.rb
+++ b/lib/content_security_policy.rb
@@ -4,15 +4,15 @@ require 'content_security_policy/extension'
class ContentSecurityPolicy
class << self
- def policy(theme_ids = [], base_url: Discourse.base_url, path_info: "/")
- new.build(theme_ids, base_url: base_url, path_info: path_info)
+ def policy(theme_id = nil, base_url: Discourse.base_url, path_info: "/")
+ new.build(theme_id, base_url: base_url, path_info: path_info)
end
end
- def build(theme_ids, base_url:, path_info: "/")
+ def build(theme_id, base_url:, path_info: "/")
builder = Builder.new(base_url: base_url)
- Extension.theme_extensions(theme_ids).each { |extension| builder << extension }
+ Extension.theme_extensions(theme_id).each { |extension| builder << extension }
Extension.plugin_extensions.each { |extension| builder << extension }
builder << Extension.site_setting_extension
builder << Extension.path_specific_extension(path_info)
diff --git a/lib/content_security_policy/extension.rb b/lib/content_security_policy/extension.rb
index 751e907fc59..51b59acda31 100644
--- a/lib/content_security_policy/extension.rb
+++ b/lib/content_security_policy/extension.rb
@@ -25,9 +25,9 @@ class ContentSecurityPolicy
THEME_SETTING = 'extend_content_security_policy'
- def theme_extensions(theme_ids)
- key = "theme_extensions_#{Theme.transform_ids(theme_ids).join(',')}"
- cache[key] ||= find_theme_extensions(theme_ids)
+ def theme_extensions(theme_id)
+ key = "theme_extensions_#{theme_id}"
+ cache[key] ||= find_theme_extensions(theme_id)
end
def clear_theme_extensions_cache!
@@ -40,12 +40,11 @@ class ContentSecurityPolicy
@cache ||= DistributedCache.new('csp_extensions')
end
- def find_theme_extensions(theme_ids)
+ def find_theme_extensions(theme_id)
extensions = []
+ theme_ids = Theme.transform_ids(theme_id)
- resolved_ids = Theme.transform_ids(theme_ids)
-
- Theme.where(id: resolved_ids).find_each do |theme|
+ Theme.where(id: theme_ids).find_each do |theme|
theme.cached_settings.each do |setting, value|
extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING
end
@@ -54,7 +53,7 @@ class ContentSecurityPolicy
extensions << build_theme_extension(ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions)
html_fields = ThemeField.where(
- theme_id: resolved_ids,
+ theme_id: theme_ids,
target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] },
name: ThemeField.html_fields
)
diff --git a/lib/content_security_policy/middleware.rb b/lib/content_security_policy/middleware.rb
index d587f5994c6..0435529bff7 100644
--- a/lib/content_security_policy/middleware.rb
+++ b/lib/content_security_policy/middleware.rb
@@ -17,10 +17,10 @@ class ContentSecurityPolicy
protocol = (SiteSetting.force_https || request.ssl?) ? "https://" : "http://"
base_url = protocol + request.host_with_port + Discourse.base_path
- theme_ids = env[:resolved_theme_ids]
+ theme_id = env[:resolved_theme_id]
- headers['Content-Security-Policy'] = policy(theme_ids, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy
- headers['Content-Security-Policy-Report-Only'] = policy(theme_ids, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy_report_only
+ headers['Content-Security-Policy'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy
+ headers['Content-Security-Policy-Report-Only'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy_report_only
response
end
diff --git a/lib/middleware/anonymous_cache.rb b/lib/middleware/anonymous_cache.rb
index ca869abb977..9fe8c8cd5b4 100644
--- a/lib/middleware/anonymous_cache.rb
+++ b/lib/middleware/anonymous_cache.rb
@@ -132,9 +132,9 @@ module Middleware
def theme_ids
ids, _ = @request.cookies['theme_ids']&.split('|')
- ids = ids&.split(",")&.map(&:to_i)
- if ids && Guardian.new.allow_themes?(ids)
- Theme.transform_ids(ids)
+ id = ids&.split(",")&.map(&:to_i)&.first
+ if id && Guardian.new.allow_themes?([id])
+ Theme.transform_ids(id)
else
[]
end
diff --git a/lib/stylesheet/importer.rb b/lib/stylesheet/importer.rb
index 8b02e809c97..d9282465976 100644
--- a/lib/stylesheet/importer.rb
+++ b/lib/stylesheet/importer.rb
@@ -101,7 +101,7 @@ module Stylesheet
end
theme_id = @theme_id || SiteSetting.default_theme_id
- resolved_ids = Theme.transform_ids([theme_id])
+ resolved_ids = Theme.transform_ids(theme_id)
if resolved_ids
theme = Theme.find_by_id(theme_id)
diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb
index 4461cfbacee..2f4b6791619 100644
--- a/lib/stylesheet/manager.rb
+++ b/lib/stylesheet/manager.rb
@@ -13,7 +13,7 @@ class Stylesheet::Manager
THEME_REGEX ||= /_theme$/
COLOR_SCHEME_STYLESHEET ||= "color_definitions"
- @lock = Mutex.new
+ @@lock = Mutex.new
def self.cache
@cache ||= DistributedCache.new("discourse_stylesheet")
@@ -35,117 +35,6 @@ class Stylesheet::Manager
cache.hash.keys.select { |k| k =~ /#{plugin}/ }.each { |k| cache.delete(k) }
end
- def self.stylesheet_data(target = :desktop, theme_ids = :missing)
- stylesheet_details(target, "all", theme_ids)
- end
-
- def self.stylesheet_link_tag(target = :desktop, media = 'all', theme_ids = :missing)
- stylesheets = stylesheet_details(target, media, theme_ids)
- stylesheets.map do |stylesheet|
- href = stylesheet[:new_href]
- theme_id = stylesheet[:theme_id]
- data_theme_id = theme_id ? "data-theme-id=\"#{theme_id}\"" : ""
- %[]
- end.join("\n").html_safe
- end
-
- def self.stylesheet_details(target = :desktop, media = 'all', theme_ids = :missing)
- if theme_ids == :missing
- theme_ids = [SiteSetting.default_theme_id]
- end
-
- target = target.to_sym
-
- theme_ids = [theme_ids] unless Array === theme_ids
- theme_ids = [theme_ids.first] unless target =~ THEME_REGEX
- include_components = !!(target =~ THEME_REGEX)
-
- theme_ids = Theme.transform_ids(theme_ids, extend: include_components)
-
- current_hostname = Discourse.current_hostname
-
- array_cache_key = "array_themes_#{theme_ids.join(",")}_#{target}_#{current_hostname}"
- stylesheets = cache[array_cache_key]
- return stylesheets if stylesheets.present?
-
- @lock.synchronize do
- stylesheets = []
- theme_ids.each do |theme_id|
- data = { target: target }
- cache_key = "path_#{target}_#{theme_id}_#{current_hostname}"
- href = cache[cache_key]
-
- unless href
- builder = self.new(target, theme_id)
- is_theme = builder.is_theme?
- has_theme = builder.theme.present?
-
- if is_theme && !has_theme
- next
- else
- next if builder.theme&.component && !builder.theme&.has_scss(target)
- data[:theme_id] = builder.theme.id if has_theme && is_theme
- builder.compile unless File.exists?(builder.stylesheet_fullpath)
- href = builder.stylesheet_path(current_hostname)
- end
-
- cache.defer_set(cache_key, href)
- end
-
- data[:theme_id] = theme_id if theme_id.present? && data[:theme_id].blank?
- data[:new_href] = href
- stylesheets << data
- end
-
- cache.defer_set(array_cache_key, stylesheets.freeze)
- stylesheets
- end
- end
-
- def self.color_scheme_stylesheet_details(color_scheme_id = nil, media, theme_id)
- theme_id = theme_id || SiteSetting.default_theme_id
-
- color_scheme = begin
- ColorScheme.find(color_scheme_id)
- rescue
- # don't load fallback when requesting dark color scheme
- return false if media != "all"
-
- Theme.find_by_id(theme_id)&.color_scheme || ColorScheme.base
- end
-
- return false if !color_scheme
-
- target = COLOR_SCHEME_STYLESHEET.to_sym
- current_hostname = Discourse.current_hostname
- cache_key = color_scheme_cache_key(color_scheme, theme_id)
- stylesheets = cache[cache_key]
- return stylesheets if stylesheets.present?
-
- stylesheet = { color_scheme_id: color_scheme&.id }
-
- builder = self.new(target, theme_id, color_scheme)
-
- builder.compile unless File.exists?(builder.stylesheet_fullpath)
-
- href = builder.stylesheet_path(current_hostname)
- stylesheet[:new_href] = href
- cache.defer_set(cache_key, stylesheet.freeze)
- stylesheet
- end
-
- def self.color_scheme_stylesheet_link_tag(color_scheme_id = nil, media = 'all', theme_ids = nil)
- theme_id = theme_ids&.first
- stylesheet = color_scheme_stylesheet_details(color_scheme_id, media, theme_id)
- return '' if !stylesheet
-
- href = stylesheet[:new_href]
-
- css_class = media == 'all' ? "light-scheme" : "dark-scheme"
-
- %[].html_safe
- end
-
def self.color_scheme_cache_key(color_scheme, theme_id = nil)
color_scheme_name = Slug.for(color_scheme.name) + color_scheme&.id.to_s
theme_string = theme_id ? "_theme#{theme_id}" : ""
@@ -164,24 +53,30 @@ class Stylesheet::Manager
targets += Discourse.find_plugin_css_assets(include_disabled: true, mobile_view: true, desktop_view: true)
themes.each do |id, name, color_scheme_id|
- targets.each do |target|
- theme_id = id || SiteSetting.default_theme_id
+ theme_id = id || SiteSetting.default_theme_id
+ manager = self.new(theme_id: theme_id)
+ targets.each do |target|
if target =~ THEME_REGEX
next if theme_id == -1
- theme_ids = Theme.transform_ids([theme_id], extend: true)
+ scss_checker = ScssChecker.new(target, manager.theme_ids)
+
+ manager.load_themes(manager.theme_ids).each do |theme|
+ builder = Stylesheet::Manager::Builder.new(
+ target: target, theme: theme, manager: manager
+ )
- theme_ids.each do |t_id|
- builder = self.new(target, t_id)
STDERR.puts "precompile target: #{target} #{builder.theme.name}"
- next if builder.theme.component && !builder.theme.has_scss(target)
+ next if theme.component && !scss_checker.has_scss(theme.id)
builder.compile(force: true)
end
else
STDERR.puts "precompile target: #{target} #{name}"
- builder = self.new(target, theme_id)
- builder.compile(force: true)
+
+ Stylesheet::Manager::Builder.new(
+ target: target, theme: manager.get_theme(theme_id), manager: manager
+ ).compile(force: true)
end
end
@@ -190,8 +85,12 @@ class Stylesheet::Manager
[theme_color_scheme, *color_schemes].uniq.each do |scheme|
STDERR.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{name} (#{scheme.name})"
- builder = self.new(COLOR_SCHEME_STYLESHEET, id, scheme)
- builder.compile(force: true)
+ Stylesheet::Manager::Builder.new(
+ target: COLOR_SCHEME_STYLESHEET,
+ theme: manager.get_theme(theme_id),
+ color_scheme: scheme,
+ manager: manager
+ ).compile(force: true)
end
clear_color_scheme_cache!
end
@@ -232,245 +131,165 @@ class Stylesheet::Manager
"#{Rails.root}/#{CACHE_PATH}"
end
- def initialize(target = :desktop, theme_id = nil, color_scheme = nil)
- @target = target
- @theme_id = theme_id
- @color_scheme = color_scheme
+ attr_reader :theme_ids
+
+ def initialize(theme_id: nil)
+ @theme_id = theme_id || SiteSetting.default_theme_id
+ @theme_ids = Theme.transform_ids(@theme_id)
+ @themes_cache = {}
end
- def compile(opts = {})
- unless opts[:force]
- if File.exists?(stylesheet_fullpath)
- unless StylesheetCache.where(target: qualified_target, digest: digest).exists?
- begin
- source_map = begin
- File.read(source_map_fullpath)
- rescue Errno::ENOENT
- end
+ def cache
+ self.class.cache
+ end
- StylesheetCache.add(qualified_target, digest, File.read(stylesheet_fullpath), source_map)
- rescue => e
- Rails.logger.warn "Completely unexpected error adding contents of '#{stylesheet_fullpath}' to cache #{e}"
- end
+ def get_theme(theme_id)
+ if theme = @themes_cache[theme_id]
+ theme
+ else
+ load_themes([theme_id]).first
+ end
+ end
+
+ def load_themes(theme_ids)
+ themes = []
+ to_load_theme_ids = []
+
+ theme_ids.each do |theme_id|
+ if @themes_cache[theme_id]
+ themes << @themes_cache[theme_id]
+ else
+ to_load_theme_ids << theme_id
+ end
+ end
+
+ Theme
+ .where(id: to_load_theme_ids)
+ .includes(:yaml_theme_fields, :theme_settings, :upload_fields, :builder_theme_fields)
+ .each do |theme|
+
+ @themes_cache[theme.id] = theme
+ themes << theme
+ end
+
+ themes
+ end
+
+ def stylesheet_data(target = :desktop)
+ stylesheet_details(target, "all")
+ end
+
+ def stylesheet_link_tag(target = :desktop, media = 'all')
+ stylesheets = stylesheet_details(target, media)
+
+ stylesheets.map do |stylesheet|
+ href = stylesheet[:new_href]
+ theme_id = stylesheet[:theme_id]
+ data_theme_id = theme_id ? "data-theme-id=\"#{theme_id}\"" : ""
+ %[]
+ end.join("\n").html_safe
+ end
+
+ def stylesheet_details(target = :desktop, media = 'all')
+ target = target.to_sym
+ current_hostname = Discourse.current_hostname
+
+ array_cache_key = "array_themes_#{@theme_ids.join(",")}_#{target}_#{current_hostname}"
+ stylesheets = cache[array_cache_key]
+ return stylesheets if stylesheets.present?
+
+ @@lock.synchronize do
+ stylesheets = []
+ stale_theme_ids = []
+
+ @theme_ids.each do |theme_id|
+ cache_key = "path_#{target}_#{theme_id}_#{current_hostname}"
+
+ if href = cache[cache_key]
+ stylesheets << {
+ target: target,
+ theme_id: theme_id,
+ new_href: href
+ }
+ else
+ stale_theme_ids << theme_id
end
- return true
end
- end
- rtl = @target.to_s =~ /_rtl$/
- css, source_map = with_load_paths do |load_paths|
- Stylesheet::Compiler.compile_asset(
- @target,
- rtl: rtl,
- theme_id: theme&.id,
- theme_variables: theme&.scss_variables.to_s,
- source_map_file: source_map_filename,
- color_scheme_id: @color_scheme&.id,
- load_paths: load_paths
- )
- rescue SassC::SyntaxError => e
- if Stylesheet::Importer::THEME_TARGETS.include?(@target.to_s)
- # no special errors for theme, handled in theme editor
- ["", nil]
- elsif @target.to_s == COLOR_SCHEME_STYLESHEET
- # log error but do not crash for errors in color definitions SCSS
- Rails.logger.error "SCSS compilation error: #{e.message}"
- ["", nil]
- else
- raise Discourse::ScssError, e.message
+ scss_checker = ScssChecker.new(target, stale_theme_ids)
+
+ load_themes(stale_theme_ids).each do |theme|
+ theme_id = theme.id
+ data = { target: target, theme_id: theme_id }
+ builder = Builder.new(target: target, theme: theme, manager: self)
+
+ next if builder.theme.component && !scss_checker.has_scss(theme_id)
+ builder.compile unless File.exists?(builder.stylesheet_fullpath)
+ href = builder.stylesheet_path(current_hostname)
+
+ cache.defer_set("path_#{target}_#{theme_id}_#{current_hostname}", href)
+
+ data[:new_href] = href
+ stylesheets << data
end
- end
- FileUtils.mkdir_p(cache_fullpath)
-
- File.open(stylesheet_fullpath, "w") do |f|
- f.puts css
- end
-
- if source_map.present?
- File.open(source_map_fullpath, "w") do |f|
- f.puts source_map
- end
- end
-
- begin
- StylesheetCache.add(qualified_target, digest, css, source_map)
- rescue => e
- Rails.logger.warn "Completely unexpected error adding item to cache #{e}"
- end
- css
- end
-
- def cache_fullpath
- self.class.cache_fullpath
- end
-
- def stylesheet_fullpath
- "#{cache_fullpath}/#{stylesheet_filename}"
- end
-
- def source_map_fullpath
- "#{cache_fullpath}/#{source_map_filename}"
- end
-
- def source_map_filename
- "#{stylesheet_filename}.map"
- end
-
- def stylesheet_fullpath_no_digest
- "#{cache_fullpath}/#{stylesheet_filename_no_digest}"
- end
-
- def stylesheet_cdnpath(hostname)
- "#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{hostname}"
- end
-
- def stylesheet_path(hostname)
- stylesheet_cdnpath(hostname)
- end
-
- def root_path
- "#{GlobalSetting.relative_url_root}/"
- end
-
- def stylesheet_relpath
- "#{root_path}stylesheets/#{stylesheet_filename}"
- end
-
- def stylesheet_relpath_no_digest
- "#{root_path}stylesheets/#{stylesheet_filename_no_digest}"
- end
-
- def qualified_target
- if is_theme?
- "#{@target}_#{theme.id}"
- elsif @color_scheme
- "#{@target}_#{scheme_slug}_#{@color_scheme&.id.to_s}"
- else
- scheme_string = theme && theme.color_scheme ? "_#{theme.color_scheme.id}" : ""
- "#{@target}#{scheme_string}"
+ cache.defer_set(array_cache_key, stylesheets.freeze)
+ stylesheets
end
end
- def stylesheet_filename(with_digest = true)
- digest_string = "_#{self.digest}" if with_digest
- "#{qualified_target}#{digest_string}.css"
- end
+ def color_scheme_stylesheet_details(color_scheme_id = nil, media)
+ theme_id = @theme_ids.first
- def stylesheet_filename_no_digest
- stylesheet_filename(_with_digest = false)
- end
+ color_scheme = begin
+ ColorScheme.find(color_scheme_id)
+ rescue
+ # don't load fallback when requesting dark color scheme
+ return false if media != "all"
- def is_theme?
- !!(@target.to_s =~ THEME_REGEX)
- end
-
- def scheme_slug
- Slug.for(ActiveSupport::Inflector.transliterate(@color_scheme.name), 'scheme')
- end
-
- # digest encodes the things that trigger a recompile
- def digest
- @digest ||= begin
- if is_theme?
- theme_digest
- else
- color_scheme_digest
- end
+ get_theme(theme_id)&.color_scheme || ColorScheme.base
end
- end
- def theme
- @theme ||= Theme.find_by(id: @theme_id) || :nil
- @theme == :nil ? nil : @theme
- end
+ return false if !color_scheme
+
+ target = COLOR_SCHEME_STYLESHEET.to_sym
+ current_hostname = Discourse.current_hostname
+ cache_key = self.class.color_scheme_cache_key(color_scheme, theme_id)
+ stylesheets = cache[cache_key]
+ return stylesheets if stylesheets.present?
+
+ stylesheet = { color_scheme_id: color_scheme.id }
+
+ theme = get_theme(theme_id)
- def with_load_paths
if theme
- theme.with_scss_load_paths { |p| yield p }
+ builder = Builder.new(
+ target: target,
+ theme: get_theme(theme_id),
+ color_scheme: color_scheme,
+ manager: self
+ )
+
+ builder.compile unless File.exists?(builder.stylesheet_fullpath)
+
+ href = builder.stylesheet_path(current_hostname)
+ stylesheet[:new_href] = href
+ cache.defer_set(cache_key, stylesheet.freeze)
+ stylesheet
else
- yield nil
+ {}
end
end
- def theme_digest
- if [:mobile_theme, :desktop_theme].include?(@target)
- scss_digest = theme.resolve_baked_field(@target.to_s.sub("_theme", ""), :scss)
- elsif @target == :embedded_theme
- scss_digest = theme.resolve_baked_field(:common, :embedded_scss)
- else
- raise "attempting to look up theme digest for invalid field"
- end
+ def color_scheme_stylesheet_link_tag(color_scheme_id = nil, media = 'all')
+ stylesheet = color_scheme_stylesheet_details(color_scheme_id, media)
- Digest::SHA1.hexdigest(scss_digest.to_s + color_scheme_digest.to_s + settings_digest + plugins_digest + uploads_digest)
- end
+ return '' if !stylesheet
- # this protects us from situations where new versions of a plugin removed a file
- # old instances may still be serving CSS and not aware of the change
- # so we could end up poisoning the cache with a bad file that can not be removed
- def plugins_digest
- assets = []
- DiscoursePluginRegistry.stylesheets.each { |_, paths| assets += paths.to_a }
- DiscoursePluginRegistry.mobile_stylesheets.each { |_, paths| assets += paths.to_a }
- DiscoursePluginRegistry.desktop_stylesheets.each { |_, paths| assets += paths.to_a }
- Digest::SHA1.hexdigest(assets.sort.join)
- end
+ href = stylesheet[:new_href]
- def settings_digest
- theme_ids = Theme.components_for(@theme_id).dup
- theme_ids << @theme_id
+ css_class = media == 'all' ? "light-scheme" : "dark-scheme"
- fields = ThemeField.where(
- name: "yaml",
- type_id: ThemeField.types[:yaml],
- theme_id: theme_ids
- ).pluck(:updated_at)
-
- settings = ThemeSetting.where(theme_id: theme_ids).pluck(:updated_at)
- timestamps = fields.concat(settings).map!(&:to_f).sort!.join(",")
-
- Digest::SHA1.hexdigest(timestamps)
- end
-
- def uploads_digest
- sha1s =
- if (theme_ids = theme&.all_theme_variables).present?
- ThemeField
- .joins(:upload)
- .where(id: theme_ids)
- .pluck(:sha1)
- .join(",")
- else
- ""
- end
-
- Digest::SHA1.hexdigest(sha1s)
- end
-
- def color_scheme_digest
- cs = @color_scheme || theme&.color_scheme
-
- categories_updated = self.class.cache.defer_get_set("categories_updated") do
- Category
- .where("uploaded_background_id IS NOT NULL")
- .pluck(:updated_at)
- .map(&:to_i)
- .sum
- end
-
- fonts = "#{SiteSetting.base_font}-#{SiteSetting.heading_font}"
-
- if cs || categories_updated > 0
- theme_color_defs = theme&.resolve_baked_field(:common, :color_definitions)
- Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.last_file_updated}-#{categories_updated}-#{fonts}"
- else
- digest_string = "defaults-#{Stylesheet::Manager.last_file_updated}-#{fonts}"
-
- if cdn_url = GlobalSetting.cdn_url
- digest_string = "#{digest_string}-#{cdn_url}"
- end
-
- Digest::SHA1.hexdigest digest_string
- end
+ %[].html_safe
end
end
diff --git a/lib/stylesheet/manager/builder.rb b/lib/stylesheet/manager/builder.rb
new file mode 100644
index 00000000000..a04de17edea
--- /dev/null
+++ b/lib/stylesheet/manager/builder.rb
@@ -0,0 +1,274 @@
+# frozen_string_literal: true
+
+class Stylesheet::Manager::Builder
+ attr_reader :theme
+
+ def initialize(target: :desktop, theme:, color_scheme: nil, manager:)
+ @target = target
+ @theme = theme
+ @color_scheme = color_scheme
+ @manager = manager
+ end
+
+ def compile(opts = {})
+ if !opts[:force]
+ if File.exists?(stylesheet_fullpath)
+ unless StylesheetCache.where(target: qualified_target, digest: digest).exists?
+ begin
+ source_map = begin
+ File.read(source_map_fullpath)
+ rescue Errno::ENOENT
+ end
+
+ StylesheetCache.add(qualified_target, digest, File.read(stylesheet_fullpath), source_map)
+ rescue => e
+ Rails.logger.warn "Completely unexpected error adding contents of '#{stylesheet_fullpath}' to cache #{e}"
+ end
+ end
+ return true
+ end
+ end
+
+ rtl = @target.to_s =~ /_rtl$/
+ css, source_map = with_load_paths do |load_paths|
+ Stylesheet::Compiler.compile_asset(
+ @target,
+ rtl: rtl,
+ theme_id: theme&.id,
+ theme_variables: theme&.scss_variables.to_s,
+ source_map_file: source_map_filename,
+ color_scheme_id: @color_scheme&.id,
+ load_paths: load_paths
+ )
+ rescue SassC::SyntaxError => e
+ if Stylesheet::Importer::THEME_TARGETS.include?(@target.to_s)
+ # no special errors for theme, handled in theme editor
+ ["", nil]
+ elsif @target.to_s == Stylesheet::Manager::COLOR_SCHEME_STYLESHEET
+ # log error but do not crash for errors in color definitions SCSS
+ Rails.logger.error "SCSS compilation error: #{e.message}"
+ ["", nil]
+ else
+ raise Discourse::ScssError, e.message
+ end
+ end
+
+ FileUtils.mkdir_p(cache_fullpath)
+
+ File.open(stylesheet_fullpath, "w") do |f|
+ f.puts css
+ end
+
+ if source_map.present?
+ File.open(source_map_fullpath, "w") do |f|
+ f.puts source_map
+ end
+ end
+
+ begin
+ StylesheetCache.add(qualified_target, digest, css, source_map)
+ rescue => e
+ Rails.logger.warn "Completely unexpected error adding item to cache #{e}"
+ end
+ css
+ end
+
+ def cache_fullpath
+ Stylesheet::Manager.cache_fullpath
+ end
+
+ def stylesheet_fullpath
+ "#{cache_fullpath}/#{stylesheet_filename}"
+ end
+
+ def source_map_fullpath
+ "#{cache_fullpath}/#{source_map_filename}"
+ end
+
+ def source_map_filename
+ "#{stylesheet_filename}.map"
+ end
+
+ def stylesheet_fullpath_no_digest
+ "#{cache_fullpath}/#{stylesheet_filename_no_digest}"
+ end
+
+ def stylesheet_cdnpath(hostname)
+ "#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{hostname}"
+ end
+
+ def stylesheet_path(hostname)
+ stylesheet_cdnpath(hostname)
+ end
+
+ def root_path
+ "#{GlobalSetting.relative_url_root}/"
+ end
+
+ def stylesheet_relpath
+ "#{root_path}stylesheets/#{stylesheet_filename}"
+ end
+
+ def stylesheet_relpath_no_digest
+ "#{root_path}stylesheets/#{stylesheet_filename_no_digest}"
+ end
+
+ def qualified_target
+ if is_theme?
+ "#{@target}_#{theme.id}"
+ elsif @color_scheme
+ "#{@target}_#{scheme_slug}_#{@color_scheme&.id.to_s}"
+ else
+ scheme_string = theme && theme.color_scheme ? "_#{theme.color_scheme.id}" : ""
+ "#{@target}#{scheme_string}"
+ end
+ end
+
+ def stylesheet_filename(with_digest = true)
+ digest_string = "_#{self.digest}" if with_digest
+ "#{qualified_target}#{digest_string}.css"
+ end
+
+ def stylesheet_filename_no_digest
+ stylesheet_filename(_with_digest = false)
+ end
+
+ def is_theme?
+ !!(@target.to_s =~ Stylesheet::Manager::THEME_REGEX)
+ end
+
+ def scheme_slug
+ Slug.for(ActiveSupport::Inflector.transliterate(@color_scheme.name), 'scheme')
+ end
+
+ # digest encodes the things that trigger a recompile
+ def digest
+ @digest ||= begin
+ if is_theme?
+ theme_digest
+ else
+ color_scheme_digest
+ end
+ end
+ end
+
+ def with_load_paths
+ if theme
+ theme.with_scss_load_paths { |p| yield p }
+ else
+ yield nil
+ end
+ end
+
+ def scss_digest
+ if [:mobile_theme, :desktop_theme].include?(@target)
+ resolve_baked_field(@target.to_s.sub("_theme", ""), :scss)
+ elsif @target == :embedded_theme
+ resolve_baked_field(:common, :embedded_scss)
+ else
+ raise "attempting to look up theme digest for invalid field"
+ end
+ end
+
+ def theme_digest
+ Digest::SHA1.hexdigest(scss_digest.to_s + color_scheme_digest.to_s + settings_digest + plugins_digest + uploads_digest)
+ end
+
+ # this protects us from situations where new versions of a plugin removed a file
+ # old instances may still be serving CSS and not aware of the change
+ # so we could end up poisoning the cache with a bad file that can not be removed
+ def plugins_digest
+ assets = []
+ DiscoursePluginRegistry.stylesheets.each { |_, paths| assets += paths.to_a }
+ DiscoursePluginRegistry.mobile_stylesheets.each { |_, paths| assets += paths.to_a }
+ DiscoursePluginRegistry.desktop_stylesheets.each { |_, paths| assets += paths.to_a }
+ Digest::SHA1.hexdigest(assets.sort.join)
+ end
+
+ def settings_digest
+ theme_ids = Theme.is_parent_theme?(theme.id) ? @manager.theme_ids : [theme.id]
+
+ themes =
+ if Theme.is_parent_theme?(theme.id)
+ @manager.load_themes(@manager.theme_ids)
+ else
+ [@manager.get_theme(theme.id)]
+ end
+
+ fields = themes.each_with_object([]) do |theme, array|
+ array.concat(theme.yaml_theme_fields.map(&:updated_at))
+ end
+
+ settings = themes.each_with_object([]) do |theme, array|
+ array.concat(theme.theme_settings.map(&:updated_at))
+ end
+
+ timestamps = fields.concat(settings).map!(&:to_f).sort!.join(",")
+
+ Digest::SHA1.hexdigest(timestamps)
+ end
+
+ def uploads_digest
+ sha1s = []
+
+ theme.upload_fields.map do |upload_field|
+ sha1s << upload_field.upload.sha1
+ end
+
+ Digest::SHA1.hexdigest(sha1s.sort!.join("\n"))
+ end
+
+ def color_scheme_digest
+ cs = @color_scheme || theme&.color_scheme
+
+ categories_updated = Stylesheet::Manager.cache.defer_get_set("categories_updated") do
+ Category
+ .where("uploaded_background_id IS NOT NULL")
+ .pluck(:updated_at)
+ .map(&:to_i)
+ .sum
+ end
+
+ fonts = "#{SiteSetting.base_font}-#{SiteSetting.heading_font}"
+
+ if cs || categories_updated > 0
+ theme_color_defs = resolve_baked_field(:common, :color_definitions)
+ Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.last_file_updated}-#{categories_updated}-#{fonts}"
+ else
+ digest_string = "defaults-#{Stylesheet::Manager.last_file_updated}-#{fonts}"
+
+ if cdn_url = GlobalSetting.cdn_url
+ digest_string = "#{digest_string}-#{cdn_url}"
+ end
+
+ Digest::SHA1.hexdigest digest_string
+ end
+ end
+
+ def resolve_baked_field(target, name)
+ theme_ids =
+ if Theme.is_parent_theme?(theme.id)
+ @manager.theme_ids
+ else
+ [theme.id]
+ end
+
+ theme_ids = [theme_ids.first] if name != :color_definitions
+
+ baked_fields = []
+ targets = [Theme.targets[target.to_sym], Theme.targets[:common]]
+
+ @manager.load_themes(theme_ids).each do |theme|
+ theme.builder_theme_fields.each do |theme_field|
+ if theme_field.name == name.to_s && targets.include?(theme_field.target_id)
+ baked_fields << theme_field
+ end
+ end
+ end
+
+ baked_fields.map do |f|
+ f.ensure_baked!
+ f.value_baked || f.value
+ end.join("\n")
+ end
+end
diff --git a/lib/stylesheet/manager/scss_checker.rb b/lib/stylesheet/manager/scss_checker.rb
new file mode 100644
index 00000000000..117b4ccfe1e
--- /dev/null
+++ b/lib/stylesheet/manager/scss_checker.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class Stylesheet::Manager::ScssChecker
+ def initialize(target, theme_ids)
+ @target = target.to_sym
+ @theme_ids = theme_ids
+ end
+
+ def has_scss(theme_id)
+ !!get_themes_with_scss[theme_id]
+ end
+
+ private
+
+ def get_themes_with_scss
+ @themes_with_scss ||= begin
+ theme_target = @target.to_sym
+ theme_target = :mobile if theme_target == :mobile_theme
+ theme_target = :desktop if theme_target == :desktop_theme
+ name = @target == :embedded_theme ? :embedded_scss : :scss
+
+ results = Theme
+ .where(id: @theme_ids)
+ .left_joins(:theme_fields)
+ .where(theme_fields: {
+ target_id: [Theme.targets[theme_target], Theme.targets[:common]],
+ name: name
+ })
+ .group(:id)
+ .size
+
+ results
+ end
+ end
+end
diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb
index b2010fe4105..0754ab7b2a4 100644
--- a/lib/svg_sprite/svg_sprite.rb
+++ b/lib/svg_sprite/svg_sprite.rb
@@ -228,12 +228,12 @@ module SvgSprite
badge_icons
end
- def self.custom_svg_sprites(theme_ids = [])
- get_set_cache("custom_svg_sprites_#{Theme.transform_ids(theme_ids).join(',')}") do
+ def self.custom_svg_sprites(theme_id)
+ get_set_cache("custom_svg_sprites_#{Theme.transform_ids(theme_id).join(',')}") do
custom_sprite_paths = Dir.glob("#{Rails.root}/plugins/*/svg-icons/*.svg")
- if theme_ids.present?
- ThemeField.where(type_id: ThemeField.types[:theme_upload_var], name: THEME_SPRITE_VAR_NAME, theme_id: Theme.transform_ids(theme_ids))
+ if theme_id.present?
+ ThemeField.where(type_id: ThemeField.types[:theme_upload_var], name: THEME_SPRITE_VAR_NAME, theme_id: Theme.transform_ids(theme_id))
.pluck(:upload_id).each do |upload_id|
upload = Upload.find(upload_id) rescue nil
@@ -253,15 +253,15 @@ module SvgSprite
end
end
- def self.all_icons(theme_ids = [])
- get_set_cache("icons_#{Theme.transform_ids(theme_ids).join(',')}") do
+ def self.all_icons(theme_id = nil)
+ get_set_cache("icons_#{Theme.transform_ids(theme_id).join(',')}") do
Set.new()
.merge(settings_icons)
.merge(plugin_icons)
.merge(badge_icons)
.merge(group_icons)
- .merge(theme_icons(theme_ids))
- .merge(custom_icons(theme_ids))
+ .merge(theme_icons(theme_id))
+ .merge(custom_icons(theme_id))
.delete_if { |i| i.blank? || i.include?("/") }
.map! { |i| process(i.dup) }
.merge(SVG_ICONS)
@@ -269,25 +269,25 @@ module SvgSprite
end
end
- def self.version(theme_ids = [])
- get_set_cache("version_#{Theme.transform_ids(theme_ids).join(',')}") do
- Digest::SHA1.hexdigest(bundle(theme_ids))
+ def self.version(theme_id = nil)
+ get_set_cache("version_#{Theme.transform_ids(theme_id).join(',')}") do
+ Digest::SHA1.hexdigest(bundle(theme_id))
end
end
- def self.path(theme_ids = [])
- "/svg-sprite/#{Discourse.current_hostname}/svg-#{theme_ids&.join(",")}-#{version(theme_ids)}.js"
+ def self.path(theme_id = nil)
+ "/svg-sprite/#{Discourse.current_hostname}/svg-#{theme_id}-#{version(theme_id)}.js"
end
def self.expire_cache
cache&.clear
end
- def self.sprite_sources(theme_ids)
+ def self.sprite_sources(theme_id)
sources = CORE_SVG_SPRITES
- if theme_ids.present?
- sources = sources + custom_svg_sprites(theme_ids)
+ if theme_id.present?
+ sources = sources + custom_svg_sprites(theme_id)
end
sources
@@ -313,8 +313,8 @@ module SvgSprite
end
end
- def self.bundle(theme_ids = [])
- icons = all_icons(theme_ids)
+ def self.bundle(theme_id = nil)
+ icons = all_icons(theme_id)
svg_subset = """