PERF: Eager load Theme associations in Stylesheet Manager.

Before this change, calling `StyleSheet::Manager.stylesheet_details`
for the first time resulted in multiple queries to the database. This is
because the code was modelled in a way where each `Theme` was loaded
from the database one at a time.

This PR restructures the code such that it allows us to load all the
theme records in a single query. It also allows us to eager load the
required associations upfront. In order to achieve this, I removed the
support of loading multiple themes per request. It was initially added
to support user selectable theme components but the feature was never
completed and abandoned because it wasn't a feature that we thought was
worth building.
This commit is contained in:
Alan Guo Xiang Tan 2021-06-15 14:57:17 +08:00
parent 53dab8cf1e
commit 8e3691d537
35 changed files with 983 additions and 668 deletions

View File

@ -10,7 +10,7 @@ class ApplicationController < ActionController::Base
include Hijack include Hijack
include ReadOnlyHeader include ReadOnlyHeader
attr_reader :theme_ids attr_reader :theme_id
serialization_scope :guardian serialization_scope :guardian
@ -448,35 +448,34 @@ class ApplicationController < ActionController::Base
resolve_safe_mode resolve_safe_mode
return if request.env[NO_CUSTOM] return if request.env[NO_CUSTOM]
theme_ids = [] theme_id = nil
if preview_theme_id = request[:preview_theme_id]&.to_i if (preview_theme_id = request[:preview_theme_id]&.to_i) &&
ids = [preview_theme_id] guardian.allow_themes?([preview_theme_id], include_preview: true)
theme_ids = ids if guardian.allow_themes?(ids, include_preview: true)
theme_id = preview_theme_id
end end
user_option = current_user&.user_option user_option = current_user&.user_option
if theme_ids.blank? if theme_id.blank?
ids, seq = cookies[:theme_ids]&.split("|") ids, seq = cookies[:theme_ids]&.split("|")
ids = ids&.split(",")&.map(&:to_i) id = ids&.split(",")&.map(&:to_i)&.first
if ids.present? && seq && seq.to_i == user_option&.theme_key_seq.to_i if id.present? && seq && seq.to_i == user_option&.theme_key_seq.to_i
theme_ids = ids if guardian.allow_themes?(ids) theme_id = id if guardian.allow_themes?([id])
end end
end end
if theme_ids.blank? if theme_id.blank?
ids = user_option&.theme_ids || [] ids = user_option&.theme_ids || []
theme_ids = ids if guardian.allow_themes?(ids) theme_id = ids.first if guardian.allow_themes?(ids)
end end
if theme_ids.blank? && SiteSetting.default_theme_id != -1 if theme_id.blank? && SiteSetting.default_theme_id != -1 && guardian.allow_themes?([SiteSetting.default_theme_id])
if guardian.allow_themes?([SiteSetting.default_theme_id]) theme_id = SiteSetting.default_theme_id
theme_ids << SiteSetting.default_theme_id
end
end end
@theme_ids = request.env[:resolved_theme_ids] = theme_ids @theme_id = request.env[:resolved_theme_id] = theme_id
end end
def guardian def guardian
@ -635,10 +634,10 @@ class ApplicationController < ActionController::Base
target = view_context.mobile_view? ? :mobile : :desktop target = view_context.mobile_view? ? :mobile : :desktop
data = data =
if @theme_ids.present? if @theme_id.present?
{ {
top: Theme.lookup_field(@theme_ids, target, "after_header"), top: Theme.lookup_field(@theme_id, target, "after_header"),
footer: Theme.lookup_field(@theme_ids, target, "footer") footer: Theme.lookup_field(@theme_id, target, "footer")
} }
else else
{} {}
@ -943,9 +942,9 @@ class ApplicationController < ActionController::Base
end end
def activated_themes_json def activated_themes_json
ids = @theme_ids&.compact id = @theme_id
return "{}" if ids.blank? return "{}" if id.blank?
ids = Theme.transform_ids(ids) ids = Theme.transform_ids(id)
Theme.where(id: ids).pluck(:id, :name).to_h.to_json Theme.where(id: ids).pluck(:id, :name).to_h.to_json
end end
end end

View File

@ -34,7 +34,7 @@ class BootstrapController < ApplicationController
).each do |file| ).each do |file|
add_style(file, plugin: true) add_style(file, plugin: true)
end 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 = [] extra_locales = []
if ExtraLocalesController.client_overrides_exist? if ExtraLocalesController.client_overrides_exist?
@ -51,7 +51,7 @@ class BootstrapController < ApplicationController
).map { |f| script_asset_path(f) } ).map { |f| script_asset_path(f) }
bootstrap = { bootstrap = {
theme_ids: theme_ids, theme_ids: [theme_id],
title: SiteSetting.title, title: SiteSetting.title,
current_homepage: current_homepage, current_homepage: current_homepage,
locale_script: locale, locale_script: locale,
@ -75,15 +75,14 @@ class BootstrapController < ApplicationController
private private
def add_scheme(scheme_id, media) def add_scheme(scheme_id, media)
return if scheme_id.to_i == -1 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 } @stylesheets << { href: style[:new_href], media: media }
end end
end end
def add_style(target, opts = nil) 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| styles.each do |style|
@stylesheets << { @stylesheets << {
href: style[:new_href], href: style[:new_href],
@ -117,11 +116,11 @@ private
theme_view = mobile_view? ? :mobile : :desktop 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, :body_tag, Theme.lookup_field(theme_id, 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, :head_tag, Theme.lookup_field(theme_id, theme_view, 'head_tag'))
add_if_present(theme_html, :header, Theme.lookup_field(theme_ids, theme_view, 'header')) add_if_present(theme_html, :header, Theme.lookup_field(theme_id, theme_view, 'header'))
add_if_present(theme_html, :translations, Theme.lookup_field(theme_ids, :translations, I18n.locale)) add_if_present(theme_html, :translations, Theme.lookup_field(theme_id, :translations, I18n.locale))
add_if_present(theme_html, :js, Theme.lookup_field(theme_ids, :extra_js, nil)) add_if_present(theme_html, :js, Theme.lookup_field(theme_id, :extra_js, nil))
theme_html theme_html
end end

View File

@ -43,7 +43,7 @@ class QunitController < ApplicationController
return return
end end
request.env[:resolved_theme_ids] = [theme.id] request.env[:resolved_theme_id] = theme.id
request.env[:skip_theme_ids_transformation] = true request.env[:skip_theme_ids_transformation] = true
end end

View File

@ -19,7 +19,8 @@ class StylesheetsController < ApplicationController
params.require("id") params.require("id")
params.permit("theme_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 render json: stylesheet
end end
protected protected
@ -40,16 +41,19 @@ class StylesheetsController < ApplicationController
# we hold off re-compilation till someone asks for asset # we hold off re-compilation till someone asks for asset
if target.include?("color_definitions") if target.include?("color_definitions")
split_target, color_scheme_id = target.split(/_(-?[0-9]+)/) 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 else
if target.include?("theme") theme_id =
split_target, theme_id = target.split(/_(-?[0-9]+)/) if target.include?("theme")
theme = Theme.find_by(id: theme_id) if theme_id.present? split_target, theme_id = target.split(/_(-?[0-9]+)/)
else Theme.where(id: theme_id).pluck_first(:id) if theme_id.present?
split_target, color_scheme_id = target.split(/_(-?[0-9]+)/) else
theme = Theme.find_by(color_scheme_id: color_scheme_id) split_target, color_scheme_id = target.split(/_(-?[0-9]+)/)
end Theme.where(color_scheme_id: color_scheme_id).pluck_first(:id)
Stylesheet::Manager.stylesheet_link_tag(split_target, nil, theme&.id) end
Stylesheet::Manager.new(theme_id: theme_id).stylesheet_link_tag(split_target, nil)
end end
end end

View File

@ -12,13 +12,13 @@ class SvgSpriteController < ApplicationController
no_cookies no_cookies
RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do 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] if SvgSprite.version(theme_id) != params[:version]
return redirect_to path(SvgSprite.path(theme_ids)) return redirect_to path(SvgSprite.path(theme_id))
end 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["Last-Modified"] = 10.years.ago.httpdate
response.headers["Content-Length"] = svg_sprite.bytesize.to_s response.headers["Content-Length"] = svg_sprite.bytesize.to_s

View File

@ -408,14 +408,19 @@ module ApplicationHelper
end end
end end
def theme_ids def theme_id
if customization_disabled? if customization_disabled?
[nil] nil
else else
request.env[:resolved_theme_ids] request.env[:resolved_theme_id]
end end
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 def scheme_id
return @scheme_id if defined?(@scheme_id) return @scheme_id if defined?(@scheme_id)
@ -424,12 +429,9 @@ module ApplicationHelper
return custom_user_scheme_id return custom_user_scheme_id
end end
return if theme_ids.blank? return if theme_id.blank?
@scheme_id = Theme @scheme_id = Theme.where(id: theme_id).pluck_first(:color_scheme_id)
.where(id: theme_ids.first)
.pluck(:color_scheme_id)
.first
end end
def dark_scheme_id def dark_scheme_id
@ -457,7 +459,7 @@ module ApplicationHelper
def theme_lookup(name) def theme_lookup(name)
Theme.lookup_field( Theme.lookup_field(
theme_ids, theme_id,
mobile_view? ? :mobile : :desktop, mobile_view? ? :mobile : :desktop,
name, name,
skip_transformation: request.env[:skip_theme_ids_transformation].present? skip_transformation: request.env[:skip_theme_ids_transformation].present?
@ -466,7 +468,7 @@ module ApplicationHelper
def theme_translations_lookup def theme_translations_lookup
Theme.lookup_field( Theme.lookup_field(
theme_ids, theme_id,
:translations, :translations,
I18n.locale, I18n.locale,
skip_transformation: request.env[:skip_theme_ids_transformation].present? skip_transformation: request.env[:skip_theme_ids_transformation].present?
@ -475,7 +477,7 @@ module ApplicationHelper
def theme_js_lookup def theme_js_lookup
Theme.lookup_field( Theme.lookup_field(
theme_ids, theme_id,
:extra_js, :extra_js,
nil, nil,
skip_transformation: request.env[:skip_theme_ids_transformation].present? skip_transformation: request.env[:skip_theme_ids_transformation].present?
@ -483,22 +485,26 @@ module ApplicationHelper
end end
def discourse_stylesheet_link_tag(name, opts = {}) def discourse_stylesheet_link_tag(name, opts = {})
if opts.key?(:theme_ids) manager =
ids = opts[:theme_ids] unless customization_disabled? if opts.key?(:theme_id)
else Stylesheet::Manager.new(
ids = theme_ids theme_id: customization_disabled? ? nil : opts[:theme_id]
end )
else
stylesheet_manager
end
Stylesheet::Manager.stylesheet_link_tag(name, 'all', ids) manager.stylesheet_link_tag(name, 'all')
end end
def discourse_color_scheme_stylesheets def discourse_color_scheme_stylesheets
result = +"" 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 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 end
result.html_safe result.html_safe
end end
@ -525,7 +531,7 @@ module ApplicationHelper
asset_version: Discourse.assets_digest, asset_version: Discourse.assets_digest,
disable_custom_css: loading_admin?, disable_custom_css: loading_admin?,
highlight_js_path: HighlightJs.path, 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, enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
color_scheme_is_dark: dark_color_scheme?, color_scheme_is_dark: dark_color_scheme?,
user_color_scheme_id: scheme_id, user_color_scheme_id: scheme_id,
@ -533,7 +539,7 @@ module ApplicationHelper
} }
if Rails.env.development? 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'] if ENV['DEBUG_PRELOADED_APP_DATA']
setup_data[:debug_preloaded_app_data] = true setup_data[:debug_preloaded_app_data] = true

View File

@ -2,7 +2,7 @@
module QunitHelper module QunitHelper
def theme_tests 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? return "" if theme.blank?
_, digest = theme.baked_js_tests_with_digest _, digest = theme.baked_js_tests_with_digest

View File

@ -320,6 +320,7 @@ class ColorScheme < ActiveRecord::Base
end end
if theme_ids.present? if theme_ids.present?
Stylesheet::Manager.cache.clear Stylesheet::Manager.cache.clear
Theme.notify_theme_change( Theme.notify_theme_change(
theme_ids, theme_ids,
with_scheme: true, with_scheme: true,

View File

@ -29,6 +29,9 @@ class Theme < ActiveRecord::Base
has_many :locale_fields, -> { filter_locale_fields(I18n.fallbacks[I18n.locale]) }, class_name: 'ThemeField' 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 :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 :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 validate :component_validations
@ -164,6 +167,16 @@ class Theme < ActiveRecord::Base
end end
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 def self.user_theme_ids
get_set_cache "user_theme_ids" do get_set_cache "user_theme_ids" do
Theme.user_selectable.pluck(:id) Theme.user_selectable.pluck(:id)
@ -188,25 +201,22 @@ class Theme < ActiveRecord::Base
expire_site_cache! expire_site_cache!
end end
def self.transform_ids(ids, extend: true) def self.transform_ids(id)
return [] if ids.nil? return [] if id.blank?
get_set_cache "#{extend ? "extended_" : ""}transformed_ids_#{ids.join("_")}" do
next [] if ids.blank?
ids = ids.dup get_set_cache "transformed_ids_#{id}" do
ids.uniq! all_ids =
parent = ids.shift if self.is_parent_theme?(id)
components = components_for(id).tap { |c| c.sort!.uniq! }
components = ids [id, *components]
components.push(*components_for(parent)) if extend else
components.sort!.uniq! [id]
end
all_ids = [parent, *components]
disabled_ids = Theme.where(id: all_ids) disabled_ids = Theme.where(id: all_ids)
.includes(:remote_theme) .includes(:remote_theme)
.select { |t| !t.supported? || !t.enabled? } .select { |t| !t.supported? || !t.enabled? }
.pluck(:id) .map(&:id)
all_ids - disabled_ids all_ids - disabled_ids
end end
@ -272,11 +282,10 @@ class Theme < ActiveRecord::Base
end end
end end
def self.lookup_field(theme_ids, target, field, skip_transformation: false) def self.lookup_field(theme_id, target, field, skip_transformation: false)
return if theme_ids.blank? return "" if theme_id.blank?
theme_ids = [theme_ids] unless Array === theme_ids
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}" cache_key = "#{theme_ids.join(",")}:#{target}:#{field}:#{Theme.compiler_version}"
lookup = @cache[cache_key] lookup = @cache[cache_key]
return lookup.html_safe if lookup return lookup.html_safe if lookup
@ -289,8 +298,8 @@ class Theme < ActiveRecord::Base
def self.lookup_modifier(theme_ids, modifier_name) def self.lookup_modifier(theme_ids, modifier_name)
theme_ids = [theme_ids] unless Array === theme_ids theme_ids = [theme_ids] unless Array === theme_ids
theme_ids = transform_ids(theme_ids) theme_ids = transform_ids(theme_ids)
get_set_cache("#{theme_ids.join(",")}:modifier:#{modifier_name}:#{Theme.compiler_version}") do get_set_cache("#{theme_ids.join(",")}:modifier:#{modifier_name}:#{Theme.compiler_version}") do
ThemeModifierSet.resolve_modifier_for_themes(theme_ids, modifier_name) ThemeModifierSet.resolve_modifier_for_themes(theme_ids, modifier_name)
end end
@ -335,14 +344,18 @@ class Theme < ActiveRecord::Base
def notify_theme_change(with_scheme: false) def notify_theme_change(with_scheme: false)
DB.after_commit do 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) self.class.notify_theme_change(theme_ids, with_scheme: with_scheme)
end end
end end
def self.refresh_message_for_targets(targets, theme_ids) def self.refresh_message_for_targets(targets, theme_ids)
targets.map do |target| theme_ids = [theme_ids] unless theme_ids === Array
Stylesheet::Manager.stylesheet_data(target.to_sym, theme_ids)
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
end end
@ -385,7 +398,8 @@ class Theme < ActiveRecord::Base
end end
def list_baked_fields(target, name) 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) self.class.list_baked_fields(theme_ids, target, name)
end end
@ -435,7 +449,7 @@ class Theme < ActiveRecord::Base
def all_theme_variables def all_theme_variables
fields = {} 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| ThemeField.find_by_theme_ids(ids).where(type_id: ThemeField.theme_var_type_ids).each do |field|
next if fields.key?(field.name) next if fields.key?(field.name)
fields[field.name] = field fields[field.name] = field
@ -530,7 +544,7 @@ class Theme < ActiveRecord::Base
def included_settings def included_settings
hash = {} 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) hash.merge!(theme.cached_settings)
end end
@ -641,11 +655,6 @@ class Theme < ActiveRecord::Base
contents contents
end end
def has_scss(target)
name = target == :embedded_theme ? :embedded_scss : :scss
list_baked_fields(target, name).count > 0
end
def convert_settings def convert_settings
settings.each do |setting| settings.each do |setting|
setting_row = ThemeSetting.where(theme_id: self.id, name: setting.name.to_s).first setting_row = ThemeSetting.where(theme_id: self.id, name: setting.name.to_s).first

View File

@ -10,6 +10,6 @@
<%= discourse_stylesheet_link_tag(file) %> <%= discourse_stylesheet_link_tag(file) %>
<%- end %> <%- end %>
<%- if theme_ids.present? %> <%- if theme_id.present? %>
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %> <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
<%- end %> <%- end %>

View File

@ -14,7 +14,6 @@
<%= discourse_stylesheet_link_tag(file) %> <%= discourse_stylesheet_link_tag(file) %>
<%- end %> <%- end %>
<%- if theme_ids.present? %> <%- if theme_id.present? %>
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %> <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
<%- end %> <%- end %>

View File

@ -5,7 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title><%= content_for?(:title) ? yield(:title) : SiteSetting.title %></title> <title><%= content_for?(:title) ? yield(:title) : SiteSetting.title %></title>
<meta name="description" content="<%= @description_meta || SiteSetting.site_description %>"> <meta name="description" content="<%= @description_meta || SiteSetting.site_description %>">
<meta name="discourse_theme_ids" content="<%= theme_ids&.join(",") %>"> <meta name="discourse_theme_ids" content="<%= theme_id %>">
<meta name="discourse_current_homepage" content="<%= current_homepage %>"> <meta name="discourse_current_homepage" content="<%= current_homepage %>">
<%= render partial: "layouts/head" %> <%= render partial: "layouts/head" %>
<%= discourse_csrf_tags %> <%= discourse_csrf_tags %>

View File

@ -10,7 +10,7 @@
<%- else %> <%- else %>
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %> <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %>
<%- end %> <%- end %>
<%- if theme_ids.present? %> <%- if theme_id.present? %>
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %> <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
<%- end %> <%- end %>
<%= theme_lookup("head_tag") %> <%= theme_lookup("head_tag") %>

View File

@ -13,8 +13,11 @@
<%= build_plugin_html 'server:before-head-close' %> <%= build_plugin_html 'server:before-head-close' %>
</head> </head>
<body class="no-ember <%= @custom_body_class %>"> <body class="no-ember <%= @custom_body_class %>">
<%= theme_lookup("header") %> <%- unless customization_disabled? %>
<%= build_plugin_html 'server:header' %> <%= theme_lookup("header") %>
<%= build_plugin_html 'server:header' %>
<%- end %>
<section id='main'> <section id='main'>
<%= render partial: 'header', locals: { hide_auth_buttons: local_assigns[:hide_auth_buttons] } %> <%= render partial: 'header', locals: { hide_auth_buttons: local_assigns[:hide_auth_buttons] } %>
<div id="main-outlet" class="<%= @container_class ? @container_class : 'wrap' %>"> <div id="main-outlet" class="<%= @container_class ? @container_class : 'wrap' %>">

View File

@ -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 "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/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/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 } 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 }

View File

@ -4,15 +4,15 @@ require 'content_security_policy/extension'
class ContentSecurityPolicy class ContentSecurityPolicy
class << self class << self
def policy(theme_ids = [], base_url: Discourse.base_url, path_info: "/") def policy(theme_id = nil, base_url: Discourse.base_url, path_info: "/")
new.build(theme_ids, base_url: base_url, path_info: path_info) new.build(theme_id, base_url: base_url, path_info: path_info)
end end
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) 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 } Extension.plugin_extensions.each { |extension| builder << extension }
builder << Extension.site_setting_extension builder << Extension.site_setting_extension
builder << Extension.path_specific_extension(path_info) builder << Extension.path_specific_extension(path_info)

View File

@ -25,9 +25,9 @@ class ContentSecurityPolicy
THEME_SETTING = 'extend_content_security_policy' THEME_SETTING = 'extend_content_security_policy'
def theme_extensions(theme_ids) def theme_extensions(theme_id)
key = "theme_extensions_#{Theme.transform_ids(theme_ids).join(',')}" key = "theme_extensions_#{theme_id}"
cache[key] ||= find_theme_extensions(theme_ids) cache[key] ||= find_theme_extensions(theme_id)
end end
def clear_theme_extensions_cache! def clear_theme_extensions_cache!
@ -40,12 +40,11 @@ class ContentSecurityPolicy
@cache ||= DistributedCache.new('csp_extensions') @cache ||= DistributedCache.new('csp_extensions')
end end
def find_theme_extensions(theme_ids) def find_theme_extensions(theme_id)
extensions = [] extensions = []
theme_ids = Theme.transform_ids(theme_id)
resolved_ids = Theme.transform_ids(theme_ids) Theme.where(id: theme_ids).find_each do |theme|
Theme.where(id: resolved_ids).find_each do |theme|
theme.cached_settings.each do |setting, value| theme.cached_settings.each do |setting, value|
extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING
end end
@ -54,7 +53,7 @@ class ContentSecurityPolicy
extensions << build_theme_extension(ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions) extensions << build_theme_extension(ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions)
html_fields = ThemeField.where( html_fields = ThemeField.where(
theme_id: resolved_ids, theme_id: theme_ids,
target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] }, target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] },
name: ThemeField.html_fields name: ThemeField.html_fields
) )

View File

@ -17,10 +17,10 @@ class ContentSecurityPolicy
protocol = (SiteSetting.force_https || request.ssl?) ? "https://" : "http://" protocol = (SiteSetting.force_https || request.ssl?) ? "https://" : "http://"
base_url = protocol + request.host_with_port + Discourse.base_path 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'] = 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_ids, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy_report_only 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 response
end end

View File

@ -132,9 +132,9 @@ module Middleware
def theme_ids def theme_ids
ids, _ = @request.cookies['theme_ids']&.split('|') ids, _ = @request.cookies['theme_ids']&.split('|')
ids = ids&.split(",")&.map(&:to_i) id = ids&.split(",")&.map(&:to_i)&.first
if ids && Guardian.new.allow_themes?(ids) if id && Guardian.new.allow_themes?([id])
Theme.transform_ids(ids) Theme.transform_ids(id)
else else
[] []
end end

View File

@ -101,7 +101,7 @@ module Stylesheet
end end
theme_id = @theme_id || SiteSetting.default_theme_id 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 if resolved_ids
theme = Theme.find_by_id(theme_id) theme = Theme.find_by_id(theme_id)

View File

@ -13,7 +13,7 @@ class Stylesheet::Manager
THEME_REGEX ||= /_theme$/ THEME_REGEX ||= /_theme$/
COLOR_SCHEME_STYLESHEET ||= "color_definitions" COLOR_SCHEME_STYLESHEET ||= "color_definitions"
@lock = Mutex.new @@lock = Mutex.new
def self.cache def self.cache
@cache ||= DistributedCache.new("discourse_stylesheet") @cache ||= DistributedCache.new("discourse_stylesheet")
@ -35,117 +35,6 @@ class Stylesheet::Manager
cache.hash.keys.select { |k| k =~ /#{plugin}/ }.each { |k| cache.delete(k) } cache.hash.keys.select { |k| k =~ /#{plugin}/ }.each { |k| cache.delete(k) }
end 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}\"" : ""
%[<link href="#{href}" media="#{media}" rel="stylesheet" data-target="#{target}" #{data_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"
%[<link href="#{href}" media="#{media}" rel="stylesheet" class="#{css_class}"/>].html_safe
end
def self.color_scheme_cache_key(color_scheme, theme_id = nil) def self.color_scheme_cache_key(color_scheme, theme_id = nil)
color_scheme_name = Slug.for(color_scheme.name) + color_scheme&.id.to_s color_scheme_name = Slug.for(color_scheme.name) + color_scheme&.id.to_s
theme_string = theme_id ? "_theme#{theme_id}" : "" 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) targets += Discourse.find_plugin_css_assets(include_disabled: true, mobile_view: true, desktop_view: true)
themes.each do |id, name, color_scheme_id| 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 if target =~ THEME_REGEX
next if theme_id == -1 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}" 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) builder.compile(force: true)
end end
else else
STDERR.puts "precompile target: #{target} #{name}" 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
end end
@ -190,8 +85,12 @@ class Stylesheet::Manager
[theme_color_scheme, *color_schemes].uniq.each do |scheme| [theme_color_scheme, *color_schemes].uniq.each do |scheme|
STDERR.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{name} (#{scheme.name})" STDERR.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{name} (#{scheme.name})"
builder = self.new(COLOR_SCHEME_STYLESHEET, id, scheme) Stylesheet::Manager::Builder.new(
builder.compile(force: true) target: COLOR_SCHEME_STYLESHEET,
theme: manager.get_theme(theme_id),
color_scheme: scheme,
manager: manager
).compile(force: true)
end end
clear_color_scheme_cache! clear_color_scheme_cache!
end end
@ -232,245 +131,165 @@ class Stylesheet::Manager
"#{Rails.root}/#{CACHE_PATH}" "#{Rails.root}/#{CACHE_PATH}"
end end
def initialize(target = :desktop, theme_id = nil, color_scheme = nil) attr_reader :theme_ids
@target = target
@theme_id = theme_id def initialize(theme_id: nil)
@color_scheme = color_scheme @theme_id = theme_id || SiteSetting.default_theme_id
@theme_ids = Theme.transform_ids(@theme_id)
@themes_cache = {}
end end
def compile(opts = {}) def cache
unless opts[:force] self.class.cache
if File.exists?(stylesheet_fullpath) end
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) def get_theme(theme_id)
rescue => e if theme = @themes_cache[theme_id]
Rails.logger.warn "Completely unexpected error adding contents of '#{stylesheet_fullpath}' to cache #{e}" theme
end 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}\"" : ""
%[<link href="#{href}" media="#{media}" rel="stylesheet" data-target="#{target}" #{data_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 end
return true
end end
end
rtl = @target.to_s =~ /_rtl$/ scss_checker = ScssChecker.new(target, stale_theme_ids)
css, source_map = with_load_paths do |load_paths|
Stylesheet::Compiler.compile_asset( load_themes(stale_theme_ids).each do |theme|
@target, theme_id = theme.id
rtl: rtl, data = { target: target, theme_id: theme_id }
theme_id: theme&.id, builder = Builder.new(target: target, theme: theme, manager: self)
theme_variables: theme&.scss_variables.to_s,
source_map_file: source_map_filename, next if builder.theme.component && !scss_checker.has_scss(theme_id)
color_scheme_id: @color_scheme&.id, builder.compile unless File.exists?(builder.stylesheet_fullpath)
load_paths: load_paths href = builder.stylesheet_path(current_hostname)
)
rescue SassC::SyntaxError => e cache.defer_set("path_#{target}_#{theme_id}_#{current_hostname}", href)
if Stylesheet::Importer::THEME_TARGETS.include?(@target.to_s)
# no special errors for theme, handled in theme editor data[:new_href] = href
["", nil] stylesheets << data
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
end end
end
FileUtils.mkdir_p(cache_fullpath) cache.defer_set(array_cache_key, stylesheets.freeze)
stylesheets
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}"
end end
end end
def stylesheet_filename(with_digest = true) def color_scheme_stylesheet_details(color_scheme_id = nil, media)
digest_string = "_#{self.digest}" if with_digest theme_id = @theme_ids.first
"#{qualified_target}#{digest_string}.css"
end
def stylesheet_filename_no_digest color_scheme = begin
stylesheet_filename(_with_digest = false) ColorScheme.find(color_scheme_id)
end rescue
# don't load fallback when requesting dark color scheme
return false if media != "all"
def is_theme? get_theme(theme_id)&.color_scheme || ColorScheme.base
!!(@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
end end
end
def theme return false if !color_scheme
@theme ||= Theme.find_by(id: @theme_id) || :nil
@theme == :nil ? nil : @theme target = COLOR_SCHEME_STYLESHEET.to_sym
end 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 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 else
yield nil {}
end end
end end
def theme_digest def color_scheme_stylesheet_link_tag(color_scheme_id = nil, media = 'all')
if [:mobile_theme, :desktop_theme].include?(@target) stylesheet = color_scheme_stylesheet_details(color_scheme_id, media)
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
Digest::SHA1.hexdigest(scss_digest.to_s + color_scheme_digest.to_s + settings_digest + plugins_digest + uploads_digest) return '' if !stylesheet
end
# this protects us from situations where new versions of a plugin removed a file href = stylesheet[:new_href]
# 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 css_class = media == 'all' ? "light-scheme" : "dark-scheme"
theme_ids = Theme.components_for(@theme_id).dup
theme_ids << @theme_id
fields = ThemeField.where( %[<link href="#{href}" media="#{media}" rel="stylesheet" class="#{css_class}"/>].html_safe
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
end end
end end

View File

@ -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

View File

@ -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

View File

@ -228,12 +228,12 @@ module SvgSprite
badge_icons badge_icons
end end
def self.custom_svg_sprites(theme_ids = []) def self.custom_svg_sprites(theme_id)
get_set_cache("custom_svg_sprites_#{Theme.transform_ids(theme_ids).join(',')}") do get_set_cache("custom_svg_sprites_#{Theme.transform_ids(theme_id).join(',')}") do
custom_sprite_paths = Dir.glob("#{Rails.root}/plugins/*/svg-icons/*.svg") custom_sprite_paths = Dir.glob("#{Rails.root}/plugins/*/svg-icons/*.svg")
if theme_ids.present? if theme_id.present?
ThemeField.where(type_id: ThemeField.types[:theme_upload_var], name: THEME_SPRITE_VAR_NAME, theme_id: Theme.transform_ids(theme_ids)) 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| .pluck(:upload_id).each do |upload_id|
upload = Upload.find(upload_id) rescue nil upload = Upload.find(upload_id) rescue nil
@ -253,15 +253,15 @@ module SvgSprite
end end
end end
def self.all_icons(theme_ids = []) def self.all_icons(theme_id = nil)
get_set_cache("icons_#{Theme.transform_ids(theme_ids).join(',')}") do get_set_cache("icons_#{Theme.transform_ids(theme_id).join(',')}") do
Set.new() Set.new()
.merge(settings_icons) .merge(settings_icons)
.merge(plugin_icons) .merge(plugin_icons)
.merge(badge_icons) .merge(badge_icons)
.merge(group_icons) .merge(group_icons)
.merge(theme_icons(theme_ids)) .merge(theme_icons(theme_id))
.merge(custom_icons(theme_ids)) .merge(custom_icons(theme_id))
.delete_if { |i| i.blank? || i.include?("/") } .delete_if { |i| i.blank? || i.include?("/") }
.map! { |i| process(i.dup) } .map! { |i| process(i.dup) }
.merge(SVG_ICONS) .merge(SVG_ICONS)
@ -269,25 +269,25 @@ module SvgSprite
end end
end end
def self.version(theme_ids = []) def self.version(theme_id = nil)
get_set_cache("version_#{Theme.transform_ids(theme_ids).join(',')}") do get_set_cache("version_#{Theme.transform_ids(theme_id).join(',')}") do
Digest::SHA1.hexdigest(bundle(theme_ids)) Digest::SHA1.hexdigest(bundle(theme_id))
end end
end end
def self.path(theme_ids = []) def self.path(theme_id = nil)
"/svg-sprite/#{Discourse.current_hostname}/svg-#{theme_ids&.join(",")}-#{version(theme_ids)}.js" "/svg-sprite/#{Discourse.current_hostname}/svg-#{theme_id}-#{version(theme_id)}.js"
end end
def self.expire_cache def self.expire_cache
cache&.clear cache&.clear
end end
def self.sprite_sources(theme_ids) def self.sprite_sources(theme_id)
sources = CORE_SVG_SPRITES sources = CORE_SVG_SPRITES
if theme_ids.present? if theme_id.present?
sources = sources + custom_svg_sprites(theme_ids) sources = sources + custom_svg_sprites(theme_id)
end end
sources sources
@ -313,8 +313,8 @@ module SvgSprite
end end
end end
def self.bundle(theme_ids = []) def self.bundle(theme_id = nil)
icons = all_icons(theme_ids) icons = all_icons(theme_id)
svg_subset = """<!-- svg_subset = """<!--
Discourse SVG subset of Font Awesome Free by @fontawesome - https://fontawesome.com Discourse SVG subset of Font Awesome Free by @fontawesome - https://fontawesome.com
@ -329,9 +329,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
end end
end end
custom_svg_sprites(theme_ids).each do |fname| custom_svg_sprites(theme_id).each do |fname|
if !File.exist?(fname) if !File.exist?(fname)
cache.delete("custom_svg_sprites_#{Theme.transform_ids(theme_ids).join(',')}") cache.delete("custom_svg_sprites_#{Theme.transform_ids(theme_id).join(',')}")
next next
end end
@ -461,13 +461,13 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
end end
end end
def self.theme_icons(theme_ids) def self.theme_icons(theme_id)
return [] if theme_ids.blank? return [] if theme_id.blank?
theme_icon_settings = [] theme_icon_settings = []
# Need to load full records for default values # Need to load full records for default values
Theme.where(id: Theme.transform_ids(theme_ids)).each do |theme| Theme.where(id: Theme.transform_ids(theme_id)).each do |theme|
settings = theme.cached_settings.each do |key, value| settings = theme.cached_settings.each do |key, value|
if key.to_s.include?("_icon") && String === value if key.to_s.include?("_icon") && String === value
theme_icon_settings |= value.split('|') theme_icon_settings |= value.split('|')
@ -475,15 +475,15 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
end end
end end
theme_icon_settings |= ThemeModifierHelper.new(theme_ids: theme_ids).svg_icons theme_icon_settings |= ThemeModifierHelper.new(theme_ids: [theme_id]).svg_icons
theme_icon_settings theme_icon_settings
end end
def self.custom_icons(theme_ids) def self.custom_icons(theme_id)
# Automatically register icons in sprites added via themes or plugins # Automatically register icons in sprites added via themes or plugins
icons = [] icons = []
custom_svg_sprites(theme_ids).each do |fname| custom_svg_sprites(theme_id).each do |fname|
next if !File.exist?(fname) next if !File.exist?(fname)
svg_file = Nokogiri::XML(File.open(fname)) svg_file = Nokogiri::XML(File.open(fname))

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ThemeModifierHelper class ThemeModifierHelper
def initialize(request: nil, theme_ids: nil) def initialize(request: nil, theme_ids: nil)
@theme_ids = theme_ids || request&.env&.[](:resolved_theme_ids) @theme_ids = theme_ids || [request&.env&.[](:resolved_theme_id)]
end end
ThemeModifierSet.modifiers.keys.each do |modifier| ThemeModifierSet.modifiers.keys.each do |modifier|

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'rails_helper'
describe Stylesheet::Manager::ScssChecker do
fab!(:theme) { Fabricate(:theme) }
describe '#has_scss' do
it 'should return true when theme has scss' do
scss_theme = Fabricate(:theme, component: true)
scss_theme.set_field(target: :common, name: "scss", value: ".scss{color: red;}")
scss_theme.save!
embedded_scss_theme = Fabricate(:theme, component: true)
embedded_scss_theme.set_field(target: :common, name: "embedded_scss", value: ".scss{color: red;}")
embedded_scss_theme.save!
theme_ids = [scss_theme.id, embedded_scss_theme.id]
desktop_theme_checker = described_class.new(:desktop_theme, theme_ids)
expect(desktop_theme_checker.has_scss(scss_theme.id)).to eq(true)
expect(desktop_theme_checker.has_scss(embedded_scss_theme.id)).to eq(false)
embedded_theme_checker = described_class.new(:embedded_theme, theme_ids)
expect(embedded_theme_checker.has_scss(scss_theme.id)).to eq(false)
expect(embedded_theme_checker.has_scss(embedded_scss_theme.id)).to eq(true)
end
it 'should return false when theme does not have scss' do
expect(described_class.new(:desktop_theme, [theme.id]).has_scss(theme.id))
.to eq(false)
end
end
end

View File

@ -4,21 +4,24 @@ require 'rails_helper'
require 'stylesheet/compiler' require 'stylesheet/compiler'
describe Stylesheet::Manager do describe Stylesheet::Manager do
def manager(theme_id = nil)
Stylesheet::Manager.new(theme_id: theme_id)
end
it 'does not crash for missing theme' do it 'does not crash for missing theme' do
Theme.clear_default! Theme.clear_default!
link = Stylesheet::Manager.stylesheet_link_tag(:embedded_theme) link = manager.stylesheet_link_tag(:embedded_theme)
expect(link).to eq("") expect(link).to eq("")
theme = Fabricate(:theme) theme = Fabricate(:theme)
SiteSetting.default_theme_id = theme.id SiteSetting.default_theme_id = theme.id
link = Stylesheet::Manager.stylesheet_link_tag(:embedded_theme) link = manager.stylesheet_link_tag(:embedded_theme)
expect(link).not_to eq("") expect(link).not_to eq("")
end end
it "still returns something for no themes" do it "still returns something for no themes" do
link = Stylesheet::Manager.stylesheet_link_tag(:desktop, 'all', []) link = manager.stylesheet_link_tag(:desktop, 'all')
expect(link).not_to eq("") expect(link).not_to eq("")
end end
@ -42,13 +45,17 @@ describe Stylesheet::Manager do
}} }}
it 'can correctly compile theme css' do it 'can correctly compile theme css' do
old_links = Stylesheet::Manager.stylesheet_link_tag(:desktop_theme, 'all', theme.id) manager = manager(theme.id)
old_links = manager.stylesheet_link_tag(:desktop_theme, 'all')
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) builder = Stylesheet::Manager::Builder.new(
manager.compile(force: true) target: :desktop_theme, theme: theme, manager: manager
)
css = File.read(manager.stylesheet_fullpath) builder.compile(force: true)
_source_map = File.read(manager.source_map_fullpath)
css = File.read(builder.stylesheet_fullpath)
_source_map = File.read(builder.source_map_fullpath)
expect(css).to match(/\.common/) expect(css).to match(/\.common/)
expect(css).to match(/\.desktop/) expect(css).to match(/\.desktop/)
@ -57,11 +64,14 @@ describe Stylesheet::Manager do
expect(css).not_to match(/child_common/) expect(css).not_to match(/child_common/)
expect(css).not_to match(/child_desktop/) expect(css).not_to match(/child_desktop/)
child_theme_manager = Stylesheet::Manager.new(:desktop_theme, child_theme.id) child_theme_builder = Stylesheet::Manager::Builder.new(
child_theme_manager.compile(force: true) target: :desktop_theme, theme: child_theme, manager: manager
)
child_css = File.read(child_theme_manager.stylesheet_fullpath) child_theme_builder.compile(force: true)
_child_source_map = File.read(child_theme_manager.source_map_fullpath)
child_css = File.read(child_theme_builder.stylesheet_fullpath)
_child_source_map = File.read(child_theme_builder.source_map_fullpath)
expect(child_css).to match(/child_common/) expect(child_css).to match(/child_common/)
expect(child_css).to match(/child_desktop/) expect(child_css).to match(/child_desktop/)
@ -69,7 +79,7 @@ describe Stylesheet::Manager do
child_theme.set_field(target: :desktop, name: :scss, value: ".nothing{color: green;}") child_theme.set_field(target: :desktop, name: :scss, value: ".nothing{color: green;}")
child_theme.save! child_theme.save!
new_links = Stylesheet::Manager.stylesheet_link_tag(:desktop_theme, 'all', theme.id) new_links = manager(theme.id).stylesheet_link_tag(:desktop_theme, 'all')
expect(new_links).not_to eq(old_links) expect(new_links).not_to eq(old_links)
@ -79,30 +89,48 @@ describe Stylesheet::Manager do
end end
it 'can correctly compile embedded theme css' do it 'can correctly compile embedded theme css' do
manager = Stylesheet::Manager.new(:embedded_theme, theme.id) manager = manager(theme.id)
manager.compile(force: true)
css = File.read(manager.stylesheet_fullpath) builder = Stylesheet::Manager::Builder.new(
target: :embedded_theme, theme: theme, manager: manager
)
builder.compile(force: true)
css = File.read(builder.stylesheet_fullpath)
expect(css).to match(/\.embedded/) expect(css).to match(/\.embedded/)
expect(css).not_to match(/\.child_embedded/) expect(css).not_to match(/\.child_embedded/)
child_theme_manager = Stylesheet::Manager.new(:embedded_theme, child_theme.id) child_theme_builder = Stylesheet::Manager::Builder.new(
child_theme_manager.compile(force: true) target: :embedded_theme,
theme: child_theme,
manager: manager
)
css = File.read(child_theme_manager.stylesheet_fullpath) child_theme_builder.compile(force: true)
css = File.read(child_theme_builder.stylesheet_fullpath)
expect(css).to match(/\.child_embedded/) expect(css).to match(/\.child_embedded/)
end end
it 'includes both parent and child theme assets' do it 'includes both parent and child theme assets' do
hrefs = Stylesheet::Manager.stylesheet_details(:desktop_theme, 'all', [theme.id]) manager = manager(theme.id)
expect(hrefs.count).to eq(2)
expect(hrefs[0][:theme_id]).to eq(theme.id) hrefs = manager.stylesheet_details(:desktop_theme, 'all')
expect(hrefs[1][:theme_id]).to eq(child_theme.id)
hrefs = Stylesheet::Manager.stylesheet_details(:embedded_theme, 'all', [theme.id])
expect(hrefs.count).to eq(2) expect(hrefs.count).to eq(2)
expect(hrefs[0][:theme_id]).to eq(theme.id)
expect(hrefs[1][:theme_id]).to eq(child_theme.id) expect(hrefs.map { |href| href[:theme_id] }).to contain_exactly(
theme.id, child_theme.id
)
hrefs = manager.stylesheet_details(:embedded_theme, 'all')
expect(hrefs.count).to eq(2)
expect(hrefs.map { |href| href[:theme_id] }).to contain_exactly(
theme.id, child_theme.id
)
end end
it 'does not output tags for component targets with no styles' do it 'does not output tags for component targets with no styles' do
@ -112,10 +140,12 @@ describe Stylesheet::Manager do
theme.add_relative_theme!(:child, embedded_scss_child) theme.add_relative_theme!(:child, embedded_scss_child)
hrefs = Stylesheet::Manager.stylesheet_details(:desktop_theme, 'all', [theme.id]) manager = manager(theme.id)
hrefs = manager.stylesheet_details(:desktop_theme, 'all')
expect(hrefs.count).to eq(2) # theme + child_theme expect(hrefs.count).to eq(2) # theme + child_theme
hrefs = Stylesheet::Manager.stylesheet_details(:embedded_theme, 'all', [theme.id]) hrefs = manager.stylesheet_details(:embedded_theme, 'all')
expect(hrefs.count).to eq(3) # theme + child_theme + embedded_scss_child expect(hrefs.count).to eq(3) # theme + child_theme + embedded_scss_child
end end
@ -125,16 +155,20 @@ describe Stylesheet::Manager do
child_with_mobile_scss.save! child_with_mobile_scss.save!
theme.add_relative_theme!(:child, child_with_mobile_scss) theme.add_relative_theme!(:child, child_with_mobile_scss)
hrefs = Stylesheet::Manager.stylesheet_details(:mobile_theme, 'all', [theme.id]) manager = manager(theme.id)
expect(hrefs.find { |h| h[:theme_id] == child_with_mobile_scss.id }).to be_present hrefs = manager.stylesheet_details(:mobile_theme, 'all')
expect(hrefs.count).to eq(3) expect(hrefs.count).to eq(3)
expect(hrefs.find { |h| h[:theme_id] == child_with_mobile_scss.id }).to be_present
end end
it 'does not output multiple assets for non-theme targets' do it 'does not output multiple assets for non-theme targets' do
hrefs = Stylesheet::Manager.stylesheet_details(:admin, 'all', [theme.id]) manager = manager()
hrefs = manager.stylesheet_details(:admin, 'all')
expect(hrefs.count).to eq(1) expect(hrefs.count).to eq(1)
hrefs = Stylesheet::Manager.stylesheet_details(:mobile, 'all', [theme.id]) hrefs = manager.stylesheet_details(:mobile, 'all')
expect(hrefs.count).to eq(1) expect(hrefs.count).to eq(1)
end end
end end
@ -146,14 +180,21 @@ describe Stylesheet::Manager do
it 'can correctly account for plugins in digest' do it 'can correctly account for plugins in digest' do
theme = Fabricate(:theme) theme = Fabricate(:theme)
manager = manager(theme.id)
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) builder = Stylesheet::Manager::Builder.new(
digest1 = manager.digest target: :desktop_theme, theme: theme, manager: manager
)
digest1 = builder.digest
DiscoursePluginRegistry.stylesheets["fake"] = Set.new(["fake_file"]) DiscoursePluginRegistry.stylesheets["fake"] = Set.new(["fake_file"])
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) builder = Stylesheet::Manager::Builder.new(
digest2 = manager.digest target: :desktop_theme, theme: theme, manager: manager
)
digest2 = builder.digest
expect(digest1).not_to eq(digest2) expect(digest1).not_to eq(digest2)
end end
@ -167,13 +208,23 @@ describe Stylesheet::Manager do
child.set_field(target: :common, name: :scss, value: "body {background-color: $childcolor}") child.set_field(target: :common, name: :scss, value: "body {background-color: $childcolor}")
child.save! child.save!
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) manager = manager(theme.id)
digest1 = manager.digest
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest1 = builder.digest
child.update_setting(:childcolor, "green") child.update_setting(:childcolor, "green")
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) manager = manager(theme.id)
digest2 = manager.digest
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest2 = builder.digest
expect(digest1).not_to eq(digest2) expect(digest1).not_to eq(digest2)
end end
@ -194,8 +245,13 @@ describe Stylesheet::Manager do
type_id: ThemeField.types[:theme_upload_var] type_id: ThemeField.types[:theme_upload_var]
) )
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) manager = manager(theme.id)
digest1 = manager.digest
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest1 = builder.digest
field.destroy! field.destroy!
upload = UploadCreator.new(image2, "logo.png").create_for(-1) upload = UploadCreator.new(image2, "logo.png").create_for(-1)
@ -208,63 +264,93 @@ describe Stylesheet::Manager do
type_id: ThemeField.types[:theme_upload_var] type_id: ThemeField.types[:theme_upload_var]
) )
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) builder = Stylesheet::Manager::Builder.new(
digest2 = manager.digest target: :desktop_theme, theme: theme.reload, manager: manager
)
digest2 = builder.digest
expect(digest1).not_to eq(digest2) expect(digest1).not_to eq(digest2)
end end
end end
describe 'color_scheme_digest' do describe 'color_scheme_digest' do
let(:theme) { Fabricate(:theme) } fab!(:theme) { Fabricate(:theme) }
it "changes with category background image" do it "changes with category background image" do
category1 = Fabricate(:category, uploaded_background_id: 123, updated_at: 1.week.ago) category1 = Fabricate(:category, uploaded_background_id: 123, updated_at: 1.week.ago)
category2 = Fabricate(:category, uploaded_background_id: 456, updated_at: 2.days.ago) category2 = Fabricate(:category, uploaded_background_id: 456, updated_at: 2.days.ago)
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) manager = manager(theme.id)
digest1 = manager.color_scheme_digest builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
category2.update(uploaded_background_id: 789, updated_at: 1.day.ago) digest1 = builder.color_scheme_digest
digest2 = manager.color_scheme_digest category2.update!(uploaded_background_id: 789, updated_at: 1.day.ago)
digest2 = builder.color_scheme_digest
expect(digest2).to_not eq(digest1) expect(digest2).to_not eq(digest1)
category1.update(uploaded_background_id: nil, updated_at: 5.minutes.ago) category1.update!(uploaded_background_id: nil, updated_at: 5.minutes.ago)
digest3 = manager.color_scheme_digest digest3 = builder.color_scheme_digest
expect(digest3).to_not eq(digest2) expect(digest3).to_not eq(digest2)
expect(digest3).to_not eq(digest1) expect(digest3).to_not eq(digest1)
end end
it "updates digest when updating a color scheme" do it "updates digest when updating a color scheme" do
scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral") scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
manager = Stylesheet::Manager.new(:color_definitions, nil, scheme) manager = manager(theme.id)
digest1 = manager.color_scheme_digest
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest1 = builder.color_scheme_digest
ColorSchemeRevisor.revise(scheme, colors: [{ name: "primary", hex: "CC0000" }]) ColorSchemeRevisor.revise(scheme, colors: [{ name: "primary", hex: "CC0000" }])
digest2 = manager.color_scheme_digest digest2 = builder.color_scheme_digest
expect(digest1).to_not eq(digest2) expect(digest1).to_not eq(digest2)
end end
it "updates digest when updating a theme's color definitions" do it "updates digest when updating a theme's color definitions" do
scheme = ColorScheme.base scheme = ColorScheme.base
manager = Stylesheet::Manager.new(:color_definitions, theme.id, scheme) manager = manager(theme.id)
digest1 = manager.color_scheme_digest
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest1 = builder.color_scheme_digest
theme.set_field(target: :common, name: :color_definitions, value: 'body {color: brown}') theme.set_field(target: :common, name: :color_definitions, value: 'body {color: brown}')
theme.save! theme.save!
digest2 = manager.color_scheme_digest manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest2 = builder.color_scheme_digest
expect(digest1).to_not eq(digest2) expect(digest1).to_not eq(digest2)
end end
it "updates digest when updating a theme component's color definitions" do it "updates digest when updating a theme component's color definitions" do
scheme = ColorScheme.base scheme = ColorScheme.base
manager = Stylesheet::Manager.new(:color_definitions, theme.id, scheme) manager = manager(theme.id)
digest1 = manager.color_scheme_digest
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest1 = builder.color_scheme_digest
child_theme = Fabricate(:theme, component: true) child_theme = Fabricate(:theme, component: true)
child_theme.set_field(target: :common, name: "color_definitions", value: 'body {color: fuchsia}') child_theme.set_field(target: :common, name: "color_definitions", value: 'body {color: fuchsia}')
@ -272,26 +358,41 @@ describe Stylesheet::Manager do
theme.add_relative_theme!(:child, child_theme) theme.add_relative_theme!(:child, child_theme)
theme.save! theme.save!
digest2 = manager.color_scheme_digest manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest2 = builder.color_scheme_digest
expect(digest1).to_not eq(digest2) expect(digest1).to_not eq(digest2)
child_theme.set_field(target: :common, name: "color_definitions", value: 'body {color: blue}') child_theme.set_field(target: :common, name: "color_definitions", value: 'body {color: blue}')
child_theme.save! child_theme.save!
digest3 = manager.color_scheme_digest
expect(digest2).to_not eq(digest3)
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest3 = builder.color_scheme_digest
expect(digest2).to_not eq(digest3)
end end
it "updates digest when setting fonts" do it "updates digest when setting fonts" do
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) manager = manager(theme.id)
digest1 = manager.color_scheme_digest builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest1 = builder.color_scheme_digest
SiteSetting.base_font = DiscourseFonts.fonts[2][:key] SiteSetting.base_font = DiscourseFonts.fonts[2][:key]
digest2 = manager.color_scheme_digest digest2 = builder.color_scheme_digest
expect(digest1).to_not eq(digest2) expect(digest1).to_not eq(digest2)
SiteSetting.heading_font = DiscourseFonts.fonts[4][:key] SiteSetting.heading_font = DiscourseFonts.fonts[4][:key]
digest3 = manager.color_scheme_digest digest3 = builder.color_scheme_digest
expect(digest3).to_not eq(digest2) expect(digest3).to_not eq(digest2)
end end
@ -300,23 +401,23 @@ describe Stylesheet::Manager do
describe 'color_scheme_stylesheets' do describe 'color_scheme_stylesheets' do
it "returns something by default" do it "returns something by default" do
link = Stylesheet::Manager.color_scheme_stylesheet_link_tag() link = manager.color_scheme_stylesheet_link_tag
expect(link).not_to eq("") expect(link).not_to eq("")
end end
it "does not crash when no default theme is set" do it "does not crash when no default theme is set" do
SiteSetting.default_theme_id = -1 SiteSetting.default_theme_id = -1
link = Stylesheet::Manager.color_scheme_stylesheet_link_tag() link = manager.color_scheme_stylesheet_link_tag
expect(link).not_to eq("") expect(link).not_to eq("")
end end
it "loads base scheme when defined scheme id is missing" do it "loads base scheme when defined scheme id is missing" do
link = Stylesheet::Manager.color_scheme_stylesheet_link_tag(125) link = manager.color_scheme_stylesheet_link_tag(125)
expect(link).to include("color_definitions_base") expect(link).to include("color_definitions_base")
end end
it "loads nothing when defined dark scheme id is missing" do it "loads nothing when defined dark scheme id is missing" do
link = Stylesheet::Manager.color_scheme_stylesheet_link_tag(125, "(prefers-color-scheme: dark)") link = manager.color_scheme_stylesheet_link_tag(125, "(prefers-color-scheme: dark)")
expect(link).to eq("") expect(link).to eq("")
end end
@ -325,7 +426,7 @@ describe Stylesheet::Manager do
theme = Fabricate(:theme, color_scheme_id: cs.id) theme = Fabricate(:theme, color_scheme_id: cs.id)
SiteSetting.default_theme_id = theme.id SiteSetting.default_theme_id = theme.id
link = Stylesheet::Manager.color_scheme_stylesheet_link_tag() link = manager.color_scheme_stylesheet_link_tag()
expect(link).to include("/stylesheets/color_definitions_funky_#{cs.id}_") expect(link).to include("/stylesheets/color_definitions_funky_#{cs.id}_")
end end
@ -337,16 +438,19 @@ describe Stylesheet::Manager do
user_theme = Fabricate(:theme, color_scheme_id: nil) user_theme = Fabricate(:theme, color_scheme_id: nil)
link = Stylesheet::Manager.color_scheme_stylesheet_link_tag(nil, "all", [user_theme.id]) link = manager(user_theme.id).color_scheme_stylesheet_link_tag(nil, "all")
expect(link).to include("/stylesheets/color_definitions_base_") expect(link).to include("/stylesheets/color_definitions_base_")
stylesheet = Stylesheet::Manager.new(:color_definitions, user_theme.id, nil).compile(force: true) stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: user_theme, manager: manager
).compile(force: true)
expect(stylesheet).not_to include("--primary: #c00;") expect(stylesheet).not_to include("--primary: #c00;")
expect(stylesheet).to include("--primary: #222;") # from base scheme expect(stylesheet).to include("--primary: #222;") # from base scheme
end end
it "uses the correct scheme when a valid scheme id is used" do it "uses the correct scheme when a valid scheme id is used" do
link = Stylesheet::Manager.color_scheme_stylesheet_link_tag(ColorScheme.first.id) link = manager.color_scheme_stylesheet_link_tag(ColorScheme.first.id)
slug = Slug.for(ColorScheme.first.name) + "_" + ColorScheme.first.id.to_s slug = Slug.for(ColorScheme.first.name) + "_" + ColorScheme.first.id.to_s
expect(link).to include("/stylesheets/color_definitions_#{slug}_") expect(link).to include("/stylesheets/color_definitions_#{slug}_")
end end
@ -356,19 +460,27 @@ describe Stylesheet::Manager do
theme = Fabricate(:theme, color_scheme_id: cs.id) theme = Fabricate(:theme, color_scheme_id: cs.id)
SiteSetting.default_theme_id = theme.id SiteSetting.default_theme_id = theme.id
link = Stylesheet::Manager.color_scheme_stylesheet_link_tag() link = manager.color_scheme_stylesheet_link_tag
expect(link).to include("/stylesheets/color_definitions_funky-bunch_#{cs.id}_") expect(link).to include("/stylesheets/color_definitions_funky-bunch_#{cs.id}_")
end end
it "updates outputted colors when updating a color scheme" do it "updates outputted colors when updating a color scheme" do
scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral") scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
manager = Stylesheet::Manager.new(:color_definitions, nil, scheme) theme = Fabricate(:theme)
stylesheet = manager.compile manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
stylesheet = builder.compile
ColorSchemeRevisor.revise(scheme, colors: [{ name: "primary", hex: "CC0000" }]) ColorSchemeRevisor.revise(scheme, colors: [{ name: "primary", hex: "CC0000" }])
manager2 = Stylesheet::Manager.new(:color_definitions, nil, scheme) builder2 = Stylesheet::Manager::Builder.new(
stylesheet2 = manager2.compile target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
stylesheet2 = builder2.compile
expect(stylesheet).not_to eq(stylesheet2) expect(stylesheet).not_to eq(stylesheet2)
expect(stylesheet2).to include("--primary: #c00;") expect(stylesheet2).to include("--primary: #c00;")
@ -389,14 +501,23 @@ describe Stylesheet::Manager do
let(:dark_scheme) { ColorScheme.create_from_base(name: 'Dark', base_scheme_id: 'Dark') } let(:dark_scheme) { ColorScheme.create_from_base(name: 'Dark', base_scheme_id: 'Dark') }
it "includes theme color definitions in color scheme" do it "includes theme color definitions in color scheme" do
stylesheet = Stylesheet::Manager.new(:color_definitions, theme.id, scheme).compile(force: true) manager = manager(theme.id)
stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
).compile(force: true)
expect(stylesheet).to include("--special: rebeccapurple") expect(stylesheet).to include("--special: rebeccapurple")
end end
it "includes child color definitions in color schemes" do it "includes child color definitions in color schemes" do
theme.add_relative_theme!(:child, child) theme.add_relative_theme!(:child, child)
theme.save! theme.save!
stylesheet = Stylesheet::Manager.new(:color_definitions, theme.id, scheme).compile(force: true) manager = manager(theme.id)
stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
).compile(force: true)
expect(stylesheet).to include("--special: rebeccapurple") expect(stylesheet).to include("--special: rebeccapurple")
expect(stylesheet).to include("--child-definition: #c00") expect(stylesheet).to include("--child-definition: #c00")
@ -406,7 +527,12 @@ describe Stylesheet::Manager do
theme.add_relative_theme!(:child, child) theme.add_relative_theme!(:child, child)
theme.save! theme.save!
stylesheet = Stylesheet::Manager.new(:color_definitions, theme.id, dark_scheme).compile(force: true) manager = manager(theme.id)
stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: dark_scheme, manager: manager
).compile(force: true)
expect(stylesheet).to include("--special: rebeccapurple") expect(stylesheet).to include("--special: rebeccapurple")
expect(stylesheet).to include("--child-definition: #fff") expect(stylesheet).to include("--child-definition: #fff")
end end
@ -416,7 +542,11 @@ describe Stylesheet::Manager do
theme.set_field(target: :common, name: "color_definitions", value: scss) theme.set_field(target: :common, name: "color_definitions", value: scss)
theme.save! theme.save!
stylesheet = Stylesheet::Manager.new(:color_definitions, theme.id, scheme) manager = manager(theme.id)
stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
expect { stylesheet.compile }.not_to raise_error expect { stylesheet.compile }.not_to raise_error
end end
@ -432,7 +562,12 @@ describe Stylesheet::Manager do
child.set_field(target: :common, name: "scss", value: scss) child.set_field(target: :common, name: "scss", value: scss)
child.save! child.save!
child_theme_manager = Stylesheet::Manager.new(:desktop_theme, child.id) manager = manager(theme.id)
child_theme_manager = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: child, manager: manager
)
child_theme_manager.compile(force: true) child_theme_manager.compile(force: true)
child_css = File.read(child_theme_manager.stylesheet_fullpath) child_css = File.read(child_theme_manager.stylesheet_fullpath)
@ -448,9 +583,9 @@ describe Stylesheet::Manager do
cs = Fabricate(:color_scheme, name: 'Grün') cs = Fabricate(:color_scheme, name: 'Grün')
cs2 = Fabricate(:color_scheme, name: '어두운') cs2 = Fabricate(:color_scheme, name: '어두운')
link = Stylesheet::Manager.color_scheme_stylesheet_link_tag(cs.id) link = manager.color_scheme_stylesheet_link_tag(cs.id)
expect(link).to include("/stylesheets/color_definitions_grun_#{cs.id}_") expect(link).to include("/stylesheets/color_definitions_grun_#{cs.id}_")
link2 = Stylesheet::Manager.color_scheme_stylesheet_link_tag(cs2.id) link2 = manager.color_scheme_stylesheet_link_tag(cs2.id)
expect(link2).to include("/stylesheets/color_definitions_scheme_#{cs2.id}_") expect(link2).to include("/stylesheets/color_definitions_scheme_#{cs2.id}_")
end end
end end

View File

@ -17,10 +17,10 @@ describe SvgSprite do
it 'can generate paths' do it 'can generate paths' do
version = SvgSprite.version # Icons won't change for this test version = SvgSprite.version # Icons won't change for this test
expect(SvgSprite.path).to eq("/svg-sprite/#{Discourse.current_hostname}/svg--#{version}.js") expect(SvgSprite.path).to eq("/svg-sprite/#{Discourse.current_hostname}/svg--#{version}.js")
expect(SvgSprite.path([1, 2])).to eq("/svg-sprite/#{Discourse.current_hostname}/svg-1,2-#{version}.js") expect(SvgSprite.path(1)).to eq("/svg-sprite/#{Discourse.current_hostname}/svg-1-#{version}.js")
# Safe mode # Safe mode
expect(SvgSprite.path([nil])).to eq("/svg-sprite/#{Discourse.current_hostname}/svg--#{version}.js") expect(SvgSprite.path(nil)).to eq("/svg-sprite/#{Discourse.current_hostname}/svg--#{version}.js")
end end
it 'can search for a specific FA icon' do it 'can search for a specific FA icon' do
@ -54,13 +54,13 @@ describe SvgSprite do
fname = "custom-theme-icon-sprite.svg" fname = "custom-theme-icon-sprite.svg"
upload = UploadCreator.new(file_from_fixtures(fname), fname, for_theme: true).create_for(-1) upload = UploadCreator.new(file_from_fixtures(fname), fname, for_theme: true).create_for(-1)
version1 = SvgSprite.version([theme.id]) version1 = SvgSprite.version(theme.id)
bundle1 = SvgSprite.bundle([theme.id]) bundle1 = SvgSprite.bundle(theme.id)
SiteSetting.svg_icon_subset = "my-custom-theme-icon" SiteSetting.svg_icon_subset = "my-custom-theme-icon"
version2 = SvgSprite.version([theme.id]) version2 = SvgSprite.version(theme.id)
bundle2 = SvgSprite.bundle([theme.id]) bundle2 = SvgSprite.bundle(theme.id)
# The contents of the bundle should not change, because the icon does not actually exist # The contents of the bundle should not change, because the icon does not actually exist
expect(bundle1).to eq(bundle2) expect(bundle1).to eq(bundle2)
@ -71,8 +71,8 @@ describe SvgSprite do
theme.set_field(target: :common, name: SvgSprite.theme_sprite_variable_name, upload_id: upload.id, type: :theme_upload_var) theme.set_field(target: :common, name: SvgSprite.theme_sprite_variable_name, upload_id: upload.id, type: :theme_upload_var)
theme.save! theme.save!
version3 = SvgSprite.version([theme.id]) version3 = SvgSprite.version(theme.id)
bundle3 = SvgSprite.bundle([theme.id]) bundle3 = SvgSprite.bundle(theme.id)
# The version/bundle should be updated # The version/bundle should be updated
expect(bundle3).not_to match(bundle2) expect(bundle3).not_to match(bundle2)
@ -97,34 +97,34 @@ describe SvgSprite do
# Works for default settings: # Works for default settings:
theme.set_field(target: :settings, name: :yaml, value: "custom_icon: dragon") theme.set_field(target: :settings, name: :yaml, value: "custom_icon: dragon")
theme.save! theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("dragon") expect(SvgSprite.all_icons(theme.id)).to include("dragon")
# Automatically purges cache when default changes: # Automatically purges cache when default changes:
theme.set_field(target: :settings, name: :yaml, value: "custom_icon: gamepad") theme.set_field(target: :settings, name: :yaml, value: "custom_icon: gamepad")
theme.save! theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("gamepad") expect(SvgSprite.all_icons(theme.id)).to include("gamepad")
# Works when applying override # Works when applying override
theme.update_setting(:custom_icon, "gas-pump") theme.update_setting(:custom_icon, "gas-pump")
theme.save! theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("gas-pump") expect(SvgSprite.all_icons(theme.id)).to include("gas-pump")
# Works when changing override # Works when changing override
theme.update_setting(:custom_icon, "gamepad") theme.update_setting(:custom_icon, "gamepad")
theme.save! theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("gamepad") expect(SvgSprite.all_icons(theme.id)).to include("gamepad")
expect(SvgSprite.all_icons([theme.id])).not_to include("gas-pump") expect(SvgSprite.all_icons(theme.id)).not_to include("gas-pump")
# FA5 syntax # FA5 syntax
theme.update_setting(:custom_icon, "fab fa-bandcamp") theme.update_setting(:custom_icon, "fab fa-bandcamp")
theme.save! theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("fab-bandcamp") expect(SvgSprite.all_icons(theme.id)).to include("fab-bandcamp")
# Internal Discourse syntax + multiple icons # Internal Discourse syntax + multiple icons
theme.update_setting(:custom_icon, "fab-android|dragon") theme.update_setting(:custom_icon, "fab-android|dragon")
theme.save! theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("fab-android") expect(SvgSprite.all_icons(theme.id)).to include("fab-android")
expect(SvgSprite.all_icons([theme.id])).to include("dragon") expect(SvgSprite.all_icons(theme.id)).to include("dragon")
# Check themes don't leak into non-theme sprite sheet # Check themes don't leak into non-theme sprite sheet
expect(SvgSprite.all_icons).not_to include("dragon") expect(SvgSprite.all_icons).not_to include("dragon")
@ -134,17 +134,17 @@ describe SvgSprite do
theme.save! theme.save!
parent_theme = Fabricate(:theme) parent_theme = Fabricate(:theme)
parent_theme.add_relative_theme!(:child, theme) parent_theme.add_relative_theme!(:child, theme)
expect(SvgSprite.all_icons([parent_theme.id])).to include("dragon") expect(SvgSprite.all_icons(parent_theme.id)).to include("dragon")
end end
it 'includes icons defined in theme modifiers' do it 'includes icons defined in theme modifiers' do
theme = Fabricate(:theme) theme = Fabricate(:theme)
expect(SvgSprite.all_icons([theme.id])).not_to include("dragon") expect(SvgSprite.all_icons(theme.id)).not_to include("dragon")
theme.theme_modifier_set.svg_icons = ["dragon"] theme.theme_modifier_set.svg_icons = ["dragon"]
theme.save! theme.save!
expect(SvgSprite.all_icons([theme.id])).to include("dragon") expect(SvgSprite.all_icons(theme.id)).to include("dragon")
end end
it 'includes custom icons from a sprite in a theme' do it 'includes custom icons from a sprite in a theme' do
@ -157,7 +157,7 @@ describe SvgSprite do
theme.save! theme.save!
expect(Upload.where(id: upload.id)).to be_exist expect(Upload.where(id: upload.id)).to be_exist
expect(SvgSprite.bundle([theme.id])).to match(/my-custom-theme-icon/) expect(SvgSprite.bundle(theme.id)).to match(/my-custom-theme-icon/)
end end
context "s3" do context "s3" do
@ -181,17 +181,17 @@ describe SvgSprite do
theme.set_field(target: :common, name: SvgSprite.theme_sprite_variable_name, upload_id: upload_s3.id, type: :theme_upload_var) theme.set_field(target: :common, name: SvgSprite.theme_sprite_variable_name, upload_id: upload_s3.id, type: :theme_upload_var)
theme.save! theme.save!
sprite_files = SvgSprite.custom_svg_sprites([theme.id]).join("|") sprite_files = SvgSprite.custom_svg_sprites(theme.id).join("|")
expect(sprite_files).to match(/#{upload_s3.sha1}/) expect(sprite_files).to match(/#{upload_s3.sha1}/)
expect(sprite_files).not_to match(/amazonaws/) expect(sprite_files).not_to match(/amazonaws/)
SvgSprite.bundle([theme.id]) SvgSprite.bundle(theme.id)
expect(SvgSprite.cache.hash.keys).to include("custom_svg_sprites_#{theme.id}") expect(SvgSprite.cache.hash.keys).to include("custom_svg_sprites_#{theme.id}")
external_copy = Discourse.store.download(upload_s3) external_copy = Discourse.store.download(upload_s3)
File.delete external_copy.try(:path) File.delete external_copy.try(:path)
SvgSprite.bundle([theme.id]) SvgSprite.bundle(theme.id)
# when a file is missing, ensure that cache entry is cleared # when a file is missing, ensure that cache entry is cleared
expect(SvgSprite.cache.hash.keys).to_not include("custom_svg_sprites_#{theme.id}") expect(SvgSprite.cache.hash.keys).to_not include("custom_svg_sprites_#{theme.id}")

View File

@ -89,7 +89,7 @@ describe ApplicationHelper do
user_id: -1, user_id: -1,
color_scheme_id: ColorScheme.find_by(base_scheme_id: "Dark").id color_scheme_id: ColorScheme.find_by(base_scheme_id: "Dark").id
) )
helper.request.env[:resolved_theme_ids] = [dark_theme.id] helper.request.env[:resolved_theme_id] = dark_theme.id
end end
context "on desktop" do context "on desktop" do
before do before do
@ -509,7 +509,7 @@ describe ApplicationHelper do
user_id: -1, user_id: -1,
color_scheme_id: ColorScheme.find_by(base_scheme_id: "Dark").id color_scheme_id: ColorScheme.find_by(base_scheme_id: "Dark").id
) )
helper.request.env[:resolved_theme_ids] = [dark_theme.id] helper.request.env[:resolved_theme_id] = dark_theme.id
expect(helper.dark_color_scheme?).to eq(true) expect(helper.dark_color_scheme?).to eq(true)
end end

View File

@ -10,7 +10,7 @@ describe ThemeModifierHelper do
end end
it "can extract theme ids from a request object" do it "can extract theme ids from a request object" do
request = Rack::Request.new({ resolved_theme_ids: [theme.id] }) request = Rack::Request.new({ resolved_theme_id: theme.id })
tmh = ThemeModifierHelper.new(request: request) tmh = ThemeModifierHelper.new(request: request)
expect(tmh.serialize_topic_excerpts).to eq(true) expect(tmh.serialize_topic_excerpts).to eq(true)
end end

View File

@ -20,13 +20,14 @@ describe ColorScheme do
theme.set_field(name: :scss, target: :desktop, value: '.bob {color: $primary;}') theme.set_field(name: :scss, target: :desktop, value: '.bob {color: $primary;}')
theme.save! theme.save!
href = Stylesheet::Manager.stylesheet_data(:desktop_theme, theme.id)[0][:new_href] manager = Stylesheet::Manager.new(theme_id: theme.id)
colors_href = Stylesheet::Manager.color_scheme_stylesheet_details(scheme.id, "all", nil) href = manager.stylesheet_data(:desktop_theme)[0][:new_href]
colors_href = manager.color_scheme_stylesheet_details(scheme.id, "all")
ColorSchemeRevisor.revise(scheme, colors: [{ name: 'primary', hex: 'bbb' }]) ColorSchemeRevisor.revise(scheme, colors: [{ name: 'primary', hex: 'bbb' }])
href2 = Stylesheet::Manager.stylesheet_data(:desktop_theme, theme.id)[0][:new_href] href2 = manager.stylesheet_data(:desktop_theme)[0][:new_href]
colors_href2 = Stylesheet::Manager.color_scheme_stylesheet_details(scheme.id, "all", nil) colors_href2 = manager.color_scheme_stylesheet_details(scheme.id, "all")
expect(href).not_to eq(href2) expect(href).not_to eq(href2)
expect(colors_href).not_to eq(colors_href2) expect(colors_href).not_to eq(colors_href2)

View File

@ -66,24 +66,22 @@ describe Theme do
end end
it "can automatically disable for mismatching version" do it "can automatically disable for mismatching version" do
expect(theme.supported?).to eq(true)
theme.create_remote_theme!(remote_url: "", minimum_discourse_version: "99.99.99") theme.create_remote_theme!(remote_url: "", minimum_discourse_version: "99.99.99")
theme.save! theme.save!
expect(theme.supported?).to eq(false)
expect(Theme.transform_ids([theme.id])).to be_empty expect(Theme.transform_ids(theme.id)).to eq([])
end end
xit "#transform_ids works with nil values" do it "#transform_ids works with nil values" do
# Used in safe mode # Used in safe mode
expect(Theme.transform_ids([nil])).to eq([nil]) expect(Theme.transform_ids(nil)).to eq([])
end end
it '#transform_ids filters out disabled components' do it '#transform_ids filters out disabled components' do
theme.add_relative_theme!(:child, child) theme.add_relative_theme!(:child, child)
expect(Theme.transform_ids([theme.id], extend: true)).to eq([theme.id, child.id]) expect(Theme.transform_ids(theme.id)).to eq([theme.id, child.id])
child.update!(enabled: false) child.update!(enabled: false)
expect(Theme.transform_ids([theme.id], extend: true)).to eq([theme.id]) expect(Theme.transform_ids(theme.id)).to eq([theme.id])
end end
it "doesn't allow multi-level theme components" do it "doesn't allow multi-level theme components" do
@ -171,19 +169,6 @@ HTML
expect(Theme.lookup_field(theme.id, :desktop, :body_tag)).to match(/<b>test<\/b>/) expect(Theme.lookup_field(theme.id, :desktop, :body_tag)).to match(/<b>test<\/b>/)
end end
it 'can find fields for multiple themes' do
theme2 = Fabricate(:theme)
theme.set_field(target: :common, name: :body_tag, value: "<b>testtheme1</b>")
theme2.set_field(target: :common, name: :body_tag, value: "<b>theme2test</b>")
theme.save!
theme2.save!
field = Theme.lookup_field([theme.id, theme2.id], :desktop, :body_tag)
expect(field).to match(/<b>testtheme1<\/b>/)
expect(field).to match(/<b>theme2test<\/b>/)
end
describe "#switch_to_component!" do describe "#switch_to_component!" do
it "correctly converts a theme to component" do it "correctly converts a theme to component" do
theme.add_relative_theme!(:child, child) theme.add_relative_theme!(:child, child)
@ -229,25 +214,13 @@ HTML
end end
it "returns an empty array if no ids are passed" do it "returns an empty array if no ids are passed" do
expect(Theme.transform_ids([])).to eq([]) expect(Theme.transform_ids(nil)).to eq([])
end end
it "adds the child themes of the parent" do it "adds the child themes of the parent" do
sorted = [child.id, child2.id].sort sorted = [child.id, child2.id].sort
expect(Theme.transform_ids([theme.id])).to eq([theme.id, *sorted]) expect(Theme.transform_ids(theme.id)).to eq([theme.id, *sorted])
expect(Theme.transform_ids([theme.id, orphan1.id, orphan2.id])).to eq([theme.id, orphan1.id, *sorted, orphan2.id])
end
it "doesn't insert children when extend is false" do
fake_id = orphan2.id
fake_id2 = orphan3.id
fake_id3 = orphan4.id
expect(Theme.transform_ids([theme.id], extend: false)).to eq([theme.id])
expect(Theme.transform_ids([theme.id, fake_id3, fake_id, fake_id2, fake_id2], extend: false))
.to eq([theme.id, fake_id, fake_id2, fake_id3])
end end
end end
@ -317,7 +290,12 @@ HTML
theme.reload theme.reload
expect(theme.theme_fields.find_by(name: :scss).error).to eq(nil) expect(theme.theme_fields.find_by(name: :scss).error).to eq(nil)
scss, _map = Stylesheet::Manager.new(:desktop_theme, theme.id).compile(force: true) manager = Stylesheet::Manager.new(theme_id: theme.id)
scss, _map = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
).compile(force: true)
expect(scss).to include(upload.url) expect(scss).to include(upload.url)
end end
end end
@ -328,7 +306,12 @@ HTML
theme.set_field(target: :common, name: :scss, value: 'body {background-color: $background_color; font-size: $font-size}') theme.set_field(target: :common, name: :scss, value: 'body {background-color: $background_color; font-size: $font-size}')
theme.save! theme.save!
scss, _map = Stylesheet::Manager.new(:desktop_theme, theme.id).compile(force: true) manager = Stylesheet::Manager.new(theme_id: theme.id)
scss, _map = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
).compile(force: true)
expect(scss).to include("background-color:red") expect(scss).to include("background-color:red")
expect(scss).to include("font-size:25px") expect(scss).to include("font-size:25px")
@ -336,7 +319,10 @@ HTML
setting.value = '30px' setting.value = '30px'
theme.save! theme.save!
scss, _map = Stylesheet::Manager.new(:desktop_theme, theme.id).compile(force: true) scss, _map = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
).compile(force: true)
expect(scss).to include("font-size:30px") expect(scss).to include("font-size:30px")
# Escapes correctly. If not, compiling this would throw an exception # Escapes correctly. If not, compiling this would throw an exception
@ -348,7 +334,10 @@ HTML
theme.set_field(target: :common, name: :scss, value: 'body {font-size: quote($font-size)}') theme.set_field(target: :common, name: :scss, value: 'body {font-size: quote($font-size)}')
theme.save! theme.save!
scss, _map = Stylesheet::Manager.new(:desktop_theme, theme.id).compile(force: true) scss, _map = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
).compile(force: true)
expect(scss).to include('font-size:"#{$fakeinterpolatedvariable}\a andanothervalue \'withquotes\'; margin: 0;\a"') expect(scss).to include('font-size:"#{$fakeinterpolatedvariable}\a andanothervalue \'withquotes\'; margin: 0;\a"')
end end
@ -804,8 +793,13 @@ HTML
}} }}
let(:compiler) { let(:compiler) {
manager = Stylesheet::Manager.new(:desktop_theme, theme.id) manager = Stylesheet::Manager.new(theme_id: theme.id)
manager.compile(force: true)
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
builder.compile(force: true)
} }
it "works when importing file by path" do it "works when importing file by path" do
@ -829,8 +823,13 @@ HTML
child_theme.set_field(target: :common, name: :scss, value: '@import "my_files/moremagic"') child_theme.set_field(target: :common, name: :scss, value: '@import "my_files/moremagic"')
child_theme.save! child_theme.save!
manager = Stylesheet::Manager.new(:desktop_theme, child_theme.id) manager = Stylesheet::Manager.new(theme_id: child_theme.id)
css, _map = manager.compile(force: true)
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: child_theme, manager: manager
)
css, _map = builder.compile(force: true)
expect(css).to include("body{background:green}") expect(css).to include("body{background:green}")
end end
end end

View File

@ -442,13 +442,13 @@ RSpec.describe ApplicationController do
get "/" get "/"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme.id]) expect(controller.theme_id).to eq(theme.id)
theme.update_attribute(:user_selectable, false) theme.update_attribute(:user_selectable, false)
get "/" get "/"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([SiteSetting.default_theme_id]) expect(controller.theme_id).to eq(SiteSetting.default_theme_id)
end end
it "can be overridden with a cookie" do it "can be overridden with a cookie" do
@ -458,15 +458,7 @@ RSpec.describe ApplicationController do
get "/" get "/"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme2.id]) expect(controller.theme_id).to eq(theme2.id)
theme2.update!(user_selectable: false, component: true)
theme.add_relative_theme!(:child, theme2)
cookies['theme_ids'] = "#{theme.id},#{theme2.id}|#{user.user_option.theme_key_seq}"
get "/"
expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme.id, theme2.id])
end end
it "falls back to the default theme when the user has no cookies or preferences" do it "falls back to the default theme when the user has no cookies or preferences" do
@ -476,25 +468,25 @@ RSpec.describe ApplicationController do
get "/" get "/"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme2.id]) expect(controller.theme_id).to eq(theme2.id)
end end
it "can be overridden with preview_theme_id param" do it "can be overridden with preview_theme_id param" do
sign_in(admin) sign_in(admin)
cookies['theme_ids'] = "#{theme.id},#{theme2.id}|#{admin.user_option.theme_key_seq}" cookies['theme_ids'] = "#{theme.id}|#{admin.user_option.theme_key_seq}"
get "/", params: { preview_theme_id: theme2.id } get "/", params: { preview_theme_id: theme2.id }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme2.id]) expect(controller.theme_id).to eq(theme2.id)
get "/", params: { preview_theme_id: non_selectable_theme.id } get "/", params: { preview_theme_id: non_selectable_theme.id }
expect(controller.theme_ids).to eq([non_selectable_theme.id]) expect(controller.theme_id).to eq(non_selectable_theme.id)
end end
it "does not allow non privileged user to preview themes" do it "does not allow non privileged user to preview themes" do
sign_in(user) sign_in(user)
get "/", params: { preview_theme_id: non_selectable_theme.id } get "/", params: { preview_theme_id: non_selectable_theme.id }
expect(controller.theme_ids).to eq([SiteSetting.default_theme_id]) expect(controller.theme_id).to eq(SiteSetting.default_theme_id)
end end
it "cookie can fail back to user if out of sync" do it "cookie can fail back to user if out of sync" do
@ -503,7 +495,7 @@ RSpec.describe ApplicationController do
get "/" get "/"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme.id]) expect(controller.theme_id).to eq(theme.id)
end end
end end

View File

@ -11,6 +11,8 @@ RSpec.describe SafeModeController do
theme.set_default! theme.set_default!
get '/safe-mode' get '/safe-mode'
expect(response.status).to eq(200)
expect(response.body).not_to include("My Custom Header") expect(response.body).not_to include("My Custom Header")
end end
end end

View File

@ -5,7 +5,8 @@ require 'rails_helper'
describe StylesheetsController do describe StylesheetsController do
it 'can survive cache miss' do it 'can survive cache miss' do
StylesheetCache.destroy_all StylesheetCache.destroy_all
builder = Stylesheet::Manager.new('desktop_rtl', nil) manager = Stylesheet::Manager.new(theme_id: nil)
builder = Stylesheet::Manager::Builder.new(target: 'desktop_rtl', manager: manager, theme: nil)
builder.compile builder.compile
digest = StylesheetCache.first.digest digest = StylesheetCache.first.digest
@ -31,7 +32,9 @@ describe StylesheetsController do
scheme = ColorScheme.create_from_base(name: "testing", colors: []) scheme = ColorScheme.create_from_base(name: "testing", colors: [])
theme = Fabricate(:theme, color_scheme_id: scheme.id) theme = Fabricate(:theme, color_scheme_id: scheme.id)
builder = Stylesheet::Manager.new(:desktop, theme.id) manager = Stylesheet::Manager.new(theme_id: theme.id)
builder = Stylesheet::Manager::Builder.new(target: :desktop, theme: theme, manager: manager)
builder.compile builder.compile
`rm -rf #{Stylesheet::Manager.cache_fullpath}` `rm -rf #{Stylesheet::Manager.cache_fullpath}`
@ -44,7 +47,7 @@ describe StylesheetsController do
expect(response.status).to eq(200) expect(response.status).to eq(200)
builder = Stylesheet::Manager.new(:desktop_theme, theme.id) builder = Stylesheet::Manager::Builder.new(target: :desktop_theme, theme: theme, manager: manager)
builder.compile builder.compile
`rm -rf #{Stylesheet::Manager.cache_fullpath}` `rm -rf #{Stylesheet::Manager.cache_fullpath}`