2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-04-12 10:52:52 -04:00
|
|
|
class ThemeField < ActiveRecord::Base
|
2017-05-08 11:38:48 -04:00
|
|
|
belongs_to :upload
|
2018-10-15 00:55:23 -04:00
|
|
|
has_one :javascript_cache, dependent: :destroy
|
2022-06-08 19:24:30 -04:00
|
|
|
has_one :upload_reference, as: :target, dependent: :destroy
|
|
|
|
|
|
|
|
after_save do
|
|
|
|
if self.type_id == ThemeField.types[:theme_upload_var] && saved_change_to_upload_id?
|
|
|
|
UploadReference.ensure_exist!(upload_ids: [self.upload_id], target: self)
|
|
|
|
end
|
|
|
|
end
|
2017-05-08 11:38:48 -04:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
scope :find_by_theme_ids,
|
|
|
|
->(theme_ids) {
|
|
|
|
return none unless theme_ids.present?
|
2018-08-08 00:46:34 -04:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
where(theme_id: theme_ids).joins(
|
|
|
|
"JOIN (
|
2019-01-17 06:46:11 -05:00
|
|
|
SELECT #{theme_ids.map.with_index { |id, idx| "#{id.to_i} AS theme_id, #{idx} AS theme_sort_column" }.join(" UNION ALL SELECT ")}
|
2023-01-09 07:20:10 -05:00
|
|
|
) as X ON X.theme_id = theme_fields.theme_id",
|
|
|
|
).order("theme_sort_column")
|
|
|
|
}
|
2019-01-17 06:46:11 -05:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
scope :filter_locale_fields,
|
|
|
|
->(locale_codes) {
|
|
|
|
return none unless locale_codes.present?
|
2019-01-17 06:46:11 -05:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
where(target_id: Theme.targets[:translations], name: locale_codes).joins(
|
|
|
|
DB.sql_fragment(
|
|
|
|
"JOIN (
|
2019-02-26 09:22:02 -05:00
|
|
|
SELECT * FROM (VALUES #{locale_codes.map { "(?)" }.join(",")}) as Y (locale_code, locale_sort_column)
|
|
|
|
) as Y ON Y.locale_code = theme_fields.name",
|
2023-01-09 07:20:10 -05:00
|
|
|
*locale_codes.map.with_index { |code, index| [code, index] },
|
|
|
|
),
|
|
|
|
).order("Y.locale_sort_column")
|
|
|
|
}
|
|
|
|
|
|
|
|
scope :find_first_locale_fields,
|
|
|
|
->(theme_ids, locale_codes) {
|
|
|
|
find_by_theme_ids(theme_ids)
|
|
|
|
.filter_locale_fields(locale_codes)
|
|
|
|
.reorder("X.theme_sort_column", "Y.locale_sort_column")
|
|
|
|
.select("DISTINCT ON (X.theme_sort_column) *")
|
|
|
|
}
|
2018-08-08 00:46:34 -04:00
|
|
|
|
2017-05-02 16:01:01 -04:00
|
|
|
def self.types
|
2023-01-09 07:20:10 -05:00
|
|
|
@types ||=
|
|
|
|
Enum.new(
|
|
|
|
html: 0,
|
|
|
|
scss: 1,
|
|
|
|
theme_upload_var: 2,
|
|
|
|
theme_color_var: 3, # No longer used
|
|
|
|
theme_var: 4, # No longer used
|
|
|
|
yaml: 5,
|
|
|
|
js: 6,
|
|
|
|
)
|
2017-05-02 16:01:01 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.theme_var_type_ids
|
2019-01-18 07:03:55 -05:00
|
|
|
@theme_var_type_ids ||= [2]
|
2017-05-02 16:01:01 -04:00
|
|
|
end
|
|
|
|
|
2021-02-11 15:48:57 -05:00
|
|
|
def self.css_theme_type_ids
|
|
|
|
@css_theme_type_ids ||= [0, 1]
|
|
|
|
end
|
|
|
|
|
2019-01-10 05:06:01 -05:00
|
|
|
def self.force_recompilation!
|
|
|
|
find_each do |field|
|
|
|
|
field.compiler_version = 0
|
|
|
|
field.ensure_baked!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
validates :name,
|
|
|
|
format: {
|
|
|
|
with: /\A[a-z_][a-z0-9_-]*\z/i,
|
|
|
|
},
|
|
|
|
if: Proc.new { |field| ThemeField.theme_var_type_ids.include?(field.type_id) }
|
2017-12-19 10:10:44 -05:00
|
|
|
|
2017-04-12 10:52:52 -04:00
|
|
|
belongs_to :theme
|
|
|
|
|
|
|
|
def process_html(html)
|
2019-01-17 06:46:11 -05:00
|
|
|
errors = []
|
2018-10-15 00:55:23 -04:00
|
|
|
javascript_cache || build_javascript_cache
|
2019-01-17 06:46:11 -05:00
|
|
|
|
2019-11-12 09:30:19 -05:00
|
|
|
errors << I18n.t("themes.errors.optimized_link") if contains_optimized_link?(html)
|
|
|
|
|
2019-05-24 10:25:55 -04:00
|
|
|
js_compiler = ThemeJavascriptCompiler.new(theme_id, self.theme.name)
|
2017-04-19 16:46:28 -04:00
|
|
|
|
2020-05-04 23:46:57 -04:00
|
|
|
doc = Nokogiri::HTML5.fragment(html)
|
2019-01-17 06:46:11 -05:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
doc
|
|
|
|
.css('script[type="text/x-handlebars"]')
|
|
|
|
.each do |node|
|
|
|
|
name = node["name"] || node["data-template-name"] || "broken"
|
|
|
|
is_raw = name =~ /\.(raw|hbr)$/
|
|
|
|
hbs_template = node.inner_html
|
|
|
|
|
|
|
|
begin
|
|
|
|
if is_raw
|
|
|
|
js_compiler.append_raw_template(name, hbs_template)
|
|
|
|
else
|
|
|
|
js_compiler.append_ember_template("discourse/templates/#{name}", hbs_template)
|
|
|
|
end
|
|
|
|
rescue ThemeJavascriptCompiler::CompileError => ex
|
|
|
|
js_compiler.append_js_error("discourse/templates/#{name}", ex.message)
|
|
|
|
errors << ex.message
|
2019-01-17 06:46:11 -05:00
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
node.remove
|
|
|
|
end
|
2021-04-12 08:02:58 -04:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
doc
|
|
|
|
.css('script[type="text/discourse-plugin"]')
|
|
|
|
.each_with_index do |node, index|
|
|
|
|
version = node["version"]
|
|
|
|
next if version.blank?
|
|
|
|
|
|
|
|
initializer_name =
|
|
|
|
"theme-field" + "-#{self.id}" + "-#{Theme.targets[self.target_id]}" +
|
|
|
|
"-#{ThemeField.types[self.type_id]}" + "-script-#{index + 1}"
|
|
|
|
begin
|
|
|
|
js = <<~JS
|
2021-04-12 08:02:58 -04:00
|
|
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
|
|
|
|
|
|
|
export default {
|
|
|
|
name: #{initializer_name.inspect},
|
|
|
|
after: "inject-objects",
|
|
|
|
|
|
|
|
initialize() {
|
|
|
|
withPluginApi(#{version.inspect}, (api) => {
|
|
|
|
#{node.inner_html}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
JS
|
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
js_compiler.append_module(
|
|
|
|
js,
|
|
|
|
"discourse/initializers/#{initializer_name}",
|
|
|
|
include_variables: true,
|
|
|
|
)
|
|
|
|
rescue ThemeJavascriptCompiler::CompileError => ex
|
|
|
|
js_compiler.append_js_error("discourse/initializers/#{initializer_name}", ex.message)
|
|
|
|
errors << ex.message
|
|
|
|
end
|
2019-01-17 06:46:11 -05:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
node.remove
|
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
doc
|
|
|
|
.css("script")
|
|
|
|
.each_with_index do |node, index|
|
|
|
|
next unless inline_javascript?(node)
|
|
|
|
js_compiler.append_raw_script(
|
|
|
|
"_html/#{Theme.targets[self.target_id]}/#{name}_#{index + 1}.js",
|
|
|
|
node.inner_html,
|
|
|
|
)
|
|
|
|
node.remove
|
|
|
|
end
|
2018-10-15 00:55:23 -04:00
|
|
|
|
2020-05-01 12:51:11 -04:00
|
|
|
settings_hash = theme.build_settings_hash
|
2023-01-09 07:20:10 -05:00
|
|
|
if js_compiler.has_content? && settings_hash.present?
|
|
|
|
js_compiler.prepend_settings(settings_hash)
|
|
|
|
end
|
2019-01-17 06:46:11 -05:00
|
|
|
javascript_cache.content = js_compiler.content
|
2022-10-18 13:20:10 -04:00
|
|
|
javascript_cache.source_map = js_compiler.source_map
|
2018-10-15 00:55:23 -04:00
|
|
|
javascript_cache.save!
|
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
doc.add_child(<<~HTML.html_safe) if javascript_cache.content.present?
|
2022-11-30 13:43:01 -05:00
|
|
|
<link rel="preload" href="#{javascript_cache.url}" as="script">
|
|
|
|
<script defer src='#{javascript_cache.url}' data-theme-id='#{theme_id}'></script>
|
|
|
|
HTML
|
2017-04-19 16:46:28 -04:00
|
|
|
[doc.to_s, errors&.join("\n")]
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
2021-07-14 15:18:29 -04:00
|
|
|
def validate_svg_sprite_xml
|
2023-01-09 07:20:10 -05:00
|
|
|
upload =
|
|
|
|
begin
|
|
|
|
Upload.find(self.upload_id)
|
|
|
|
rescue StandardError
|
|
|
|
nil
|
|
|
|
end
|
2021-07-14 15:18:29 -04:00
|
|
|
|
|
|
|
if Discourse.store.external?
|
2023-01-09 07:20:10 -05:00
|
|
|
external_copy =
|
|
|
|
begin
|
|
|
|
Discourse.store.download(upload)
|
|
|
|
rescue StandardError
|
|
|
|
nil
|
|
|
|
end
|
2021-07-14 15:18:29 -04:00
|
|
|
path = external_copy.try(:path)
|
|
|
|
else
|
|
|
|
path = Discourse.store.path_for(upload)
|
|
|
|
end
|
|
|
|
|
|
|
|
error = nil
|
|
|
|
|
|
|
|
begin
|
2021-07-25 22:35:27 -04:00
|
|
|
content = File.read(path)
|
2023-01-09 07:20:10 -05:00
|
|
|
Nokogiri.XML(content) { |config| config.options = Nokogiri::XML::ParseOptions::NOBLANKS }
|
2021-07-14 15:18:29 -04:00
|
|
|
rescue => e
|
|
|
|
error = "Error with #{self.name}: #{e.inspect}"
|
|
|
|
end
|
|
|
|
error
|
|
|
|
end
|
|
|
|
|
2019-01-25 09:19:01 -05:00
|
|
|
def raw_translation_data(internal: false)
|
2019-01-17 06:46:11 -05:00
|
|
|
# Might raise ThemeTranslationParser::InvalidYaml
|
2019-01-25 09:19:01 -05:00
|
|
|
ThemeTranslationParser.new(self, internal: internal).load
|
2019-01-17 06:46:11 -05:00
|
|
|
end
|
|
|
|
|
2019-02-26 09:22:02 -05:00
|
|
|
def translation_data(with_overrides: true, internal: false, fallback_fields: nil)
|
|
|
|
fallback_fields ||= theme.theme_fields.filter_locale_fields(I18n.fallbacks[name])
|
2019-01-17 06:46:11 -05:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
fallback_data =
|
|
|
|
fallback_fields.each_with_index.map do |field, index|
|
|
|
|
begin
|
|
|
|
field.raw_translation_data(internal: internal)
|
|
|
|
rescue ThemeTranslationParser::InvalidYaml
|
|
|
|
# If this is the locale with the error, raise it.
|
|
|
|
# If not, let the other theme_field raise the error when it processes itself
|
|
|
|
raise if field.id == id
|
|
|
|
{}
|
|
|
|
end
|
2019-01-17 06:46:11 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# TODO: Deduplicate the fallback data in the same way as JSLocaleHelper#load_translations_merged
|
|
|
|
# this would reduce the size of the payload, without affecting functionality
|
|
|
|
data = {}
|
|
|
|
fallback_data.each { |hash| data.merge!(hash) }
|
2021-11-11 12:11:23 -05:00
|
|
|
|
|
|
|
if with_overrides
|
|
|
|
overrides = theme.translation_override_hash.deep_symbolize_keys
|
|
|
|
data.deep_merge!(overrides)
|
|
|
|
end
|
|
|
|
|
2019-01-17 06:46:11 -05:00
|
|
|
data
|
|
|
|
end
|
|
|
|
|
|
|
|
def process_translation
|
|
|
|
errors = []
|
|
|
|
javascript_cache || build_javascript_cache
|
2019-05-24 10:25:55 -04:00
|
|
|
js_compiler = ThemeJavascriptCompiler.new(theme_id, self.theme.name)
|
2019-01-17 06:46:11 -05:00
|
|
|
begin
|
|
|
|
data = translation_data
|
|
|
|
|
|
|
|
js = <<~JS
|
2019-11-05 06:54:12 -05:00
|
|
|
export default {
|
|
|
|
name: "theme-#{theme_id}-translations",
|
|
|
|
initialize() {
|
|
|
|
/* Translation data for theme #{self.theme_id} (#{self.name})*/
|
|
|
|
const data = #{data.to_json};
|
|
|
|
|
|
|
|
for (let lang in data){
|
|
|
|
let cursor = I18n.translations;
|
|
|
|
for (let key of [lang, "js", "theme_translations"]){
|
|
|
|
cursor = cursor[key] = cursor[key] || {};
|
|
|
|
}
|
|
|
|
cursor[#{self.theme_id}] = data[lang];
|
|
|
|
}
|
2019-01-17 06:46:11 -05:00
|
|
|
}
|
2019-11-05 06:54:12 -05:00
|
|
|
};
|
2019-01-17 06:46:11 -05:00
|
|
|
JS
|
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
js_compiler.append_module(
|
|
|
|
js,
|
|
|
|
"discourse/pre-initializers/theme-#{theme_id}-translations",
|
|
|
|
include_variables: false,
|
|
|
|
)
|
2019-01-17 06:46:11 -05:00
|
|
|
rescue ThemeTranslationParser::InvalidYaml => e
|
|
|
|
errors << e.message
|
|
|
|
end
|
|
|
|
|
|
|
|
javascript_cache.content = js_compiler.content
|
2022-10-18 13:20:10 -04:00
|
|
|
javascript_cache.source_map = js_compiler.source_map
|
2019-01-17 06:46:11 -05:00
|
|
|
javascript_cache.save!
|
|
|
|
doc = ""
|
2023-01-09 07:20:10 -05:00
|
|
|
doc = <<~HTML.html_safe if javascript_cache.content.present?
|
2022-11-30 13:43:01 -05:00
|
|
|
<link rel="preload" href="#{javascript_cache.url}" as="script">
|
|
|
|
<script defer src='#{javascript_cache.url}' data-theme-id='#{theme_id}'></script>
|
|
|
|
HTML
|
2019-01-17 06:46:11 -05:00
|
|
|
[doc, errors&.join("\n")]
|
|
|
|
end
|
|
|
|
|
2018-03-04 19:04:23 -05:00
|
|
|
def validate_yaml!
|
|
|
|
return unless self.name == "yaml"
|
|
|
|
|
|
|
|
errors = []
|
|
|
|
begin
|
2023-01-09 07:20:10 -05:00
|
|
|
ThemeSettingsParser
|
|
|
|
.new(self)
|
|
|
|
.load do |name, default, type, opts|
|
|
|
|
setting = ThemeSetting.new(name: name, data_type: type, theme: theme)
|
|
|
|
translation_key = "themes.settings_errors"
|
|
|
|
|
|
|
|
if setting.invalid?
|
|
|
|
setting.errors.details.each_pair do |attribute, _errors|
|
|
|
|
_errors.each do |hash|
|
|
|
|
errors << I18n.t("#{translation_key}.#{attribute}_#{hash[:error]}", name: name)
|
|
|
|
end
|
2018-03-04 19:04:23 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
errors << I18n.t("#{translation_key}.default_value_missing", name: name) if default.nil?
|
2018-03-04 19:04:23 -05:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
if (min = opts[:min]) && (max = opts[:max])
|
|
|
|
unless ThemeSetting.value_in_range?(default, (min..max), type)
|
|
|
|
errors << I18n.t("#{translation_key}.default_out_range", name: name)
|
|
|
|
end
|
2018-03-04 19:04:23 -05:00
|
|
|
end
|
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
unless ThemeSetting.acceptable_value_for_type?(default, type)
|
|
|
|
errors << I18n.t("#{translation_key}.default_not_match_type", name: name)
|
|
|
|
end
|
2018-03-04 19:04:23 -05:00
|
|
|
end
|
|
|
|
rescue ThemeSettingsParser::InvalidYaml => e
|
|
|
|
errors << e.message
|
|
|
|
end
|
|
|
|
|
2018-12-19 10:36:31 -05:00
|
|
|
self.error = errors.join("\n").presence
|
2018-03-04 19:04:23 -05:00
|
|
|
end
|
|
|
|
|
2019-01-17 06:46:11 -05:00
|
|
|
def self.guess_type(name:, target:)
|
2019-04-12 06:36:08 -04:00
|
|
|
if basic_targets.include?(target.to_s) && html_fields.include?(name.to_s)
|
2017-05-02 16:01:01 -04:00
|
|
|
types[:html]
|
2019-04-12 06:36:08 -04:00
|
|
|
elsif basic_targets.include?(target.to_s) && scss_fields.include?(name.to_s)
|
2017-05-02 16:01:01 -04:00
|
|
|
types[:scss]
|
2019-04-12 06:36:08 -04:00
|
|
|
elsif target.to_s == "extra_scss"
|
|
|
|
types[:scss]
|
2019-06-03 05:41:00 -04:00
|
|
|
elsif target.to_s == "extra_js"
|
|
|
|
types[:js]
|
2019-04-12 06:36:08 -04:00
|
|
|
elsif target.to_s == "settings" || target.to_s == "translations"
|
2018-03-04 19:04:23 -05:00
|
|
|
types[:yaml]
|
2017-05-02 16:01:01 -04:00
|
|
|
end
|
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
|
|
|
|
def self.html_fields
|
2023-01-09 07:20:10 -05:00
|
|
|
@html_fields ||= %w[body_tag head_tag header footer after_header]
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
2017-04-19 16:46:28 -04:00
|
|
|
def self.scss_fields
|
2023-01-09 07:20:10 -05:00
|
|
|
@scss_fields ||= %w[scss embedded_scss color_definitions]
|
2017-04-19 16:46:28 -04:00
|
|
|
end
|
|
|
|
|
2019-04-12 06:36:08 -04:00
|
|
|
def self.basic_targets
|
2023-01-09 07:20:10 -05:00
|
|
|
@basic_targets ||= %w[common desktop mobile]
|
2019-04-12 06:36:08 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def basic_html_field?
|
|
|
|
ThemeField.basic_targets.include?(Theme.targets[self.target_id].to_s) &&
|
|
|
|
ThemeField.html_fields.include?(self.name)
|
|
|
|
end
|
|
|
|
|
2019-06-03 05:41:00 -04:00
|
|
|
def extra_js_field?
|
|
|
|
Theme.targets[self.target_id] == :extra_js
|
|
|
|
end
|
|
|
|
|
2021-04-12 08:02:58 -04:00
|
|
|
def js_tests_field?
|
|
|
|
Theme.targets[self.target_id] == :tests_js
|
|
|
|
end
|
|
|
|
|
2019-04-12 06:36:08 -04:00
|
|
|
def basic_scss_field?
|
|
|
|
ThemeField.basic_targets.include?(Theme.targets[self.target_id].to_s) &&
|
|
|
|
ThemeField.scss_fields.include?(self.name)
|
|
|
|
end
|
|
|
|
|
|
|
|
def extra_scss_field?
|
|
|
|
Theme.targets[self.target_id] == :extra_scss
|
|
|
|
end
|
|
|
|
|
|
|
|
def settings_field?
|
|
|
|
Theme.targets[:settings] == self.target_id
|
|
|
|
end
|
|
|
|
|
|
|
|
def translation_field?
|
|
|
|
Theme.targets[:translations] == self.target_id
|
|
|
|
end
|
|
|
|
|
|
|
|
def svg_sprite_field?
|
2023-01-09 07:20:10 -05:00
|
|
|
ThemeField.theme_var_type_ids.include?(self.type_id) &&
|
|
|
|
self.name == SvgSprite.theme_sprite_variable_name
|
2019-04-12 06:36:08 -04:00
|
|
|
end
|
|
|
|
|
2017-04-12 10:52:52 -04:00
|
|
|
def ensure_baked!
|
2020-05-29 08:04:51 -04:00
|
|
|
needs_baking = !self.value_baked || compiler_version != Theme.compiler_version
|
2019-04-12 06:36:08 -04:00
|
|
|
return unless needs_baking
|
|
|
|
|
|
|
|
if basic_html_field? || translation_field?
|
2023-01-09 07:20:10 -05:00
|
|
|
self.value_baked, self.error =
|
|
|
|
translation_field? ? process_translation : process_html(self.value)
|
2019-04-12 06:36:08 -04:00
|
|
|
self.error = nil unless self.error.present?
|
2020-05-29 08:04:51 -04:00
|
|
|
self.compiler_version = Theme.compiler_version
|
2020-05-01 11:44:50 -04:00
|
|
|
DB.after_commit { CSP::Extension.clear_theme_extensions_cache! }
|
2021-04-12 08:02:58 -04:00
|
|
|
elsif extra_js_field? || js_tests_field?
|
2022-10-17 10:04:04 -04:00
|
|
|
self.error = nil
|
|
|
|
self.value_baked = "baked"
|
2020-05-29 08:04:51 -04:00
|
|
|
self.compiler_version = Theme.compiler_version
|
2019-04-12 06:36:08 -04:00
|
|
|
elsif basic_scss_field?
|
|
|
|
ensure_scss_compiles!
|
2020-05-01 11:44:50 -04:00
|
|
|
DB.after_commit { Stylesheet::Manager.clear_theme_cache! }
|
2019-04-12 06:36:08 -04:00
|
|
|
elsif settings_field?
|
|
|
|
validate_yaml!
|
2020-05-01 11:44:50 -04:00
|
|
|
DB.after_commit { CSP::Extension.clear_theme_extensions_cache! }
|
|
|
|
DB.after_commit { SvgSprite.expire_cache }
|
2019-04-12 06:36:08 -04:00
|
|
|
self.value_baked = "baked"
|
2020-05-29 08:04:51 -04:00
|
|
|
self.compiler_version = Theme.compiler_version
|
2019-04-12 06:36:08 -04:00
|
|
|
elsif svg_sprite_field?
|
2020-05-01 11:44:50 -04:00
|
|
|
DB.after_commit { SvgSprite.expire_cache }
|
2021-07-14 15:18:29 -04:00
|
|
|
self.error = validate_svg_sprite_xml
|
2019-04-12 06:36:08 -04:00
|
|
|
self.value_baked = "baked"
|
2020-05-29 08:04:51 -04:00
|
|
|
self.compiler_version = Theme.compiler_version
|
2019-04-12 06:36:08 -04:00
|
|
|
end
|
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
if self.will_save_change_to_value_baked? || self.will_save_change_to_compiler_version? ||
|
|
|
|
self.will_save_change_to_error?
|
|
|
|
self.update_columns(
|
|
|
|
value_baked: value_baked,
|
|
|
|
compiler_version: compiler_version,
|
|
|
|
error: error,
|
|
|
|
)
|
2017-04-19 16:46:28 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-02-19 11:22:24 -05:00
|
|
|
def compile_scss(prepended_scss = nil)
|
|
|
|
prepended_scss ||= Stylesheet::Importer.new({}).prepended_scss
|
2021-02-02 13:09:41 -05:00
|
|
|
|
2021-04-27 09:33:43 -04:00
|
|
|
self.theme.with_scss_load_paths do |load_paths|
|
2023-01-09 07:20:10 -05:00
|
|
|
Stylesheet::Compiler.compile(
|
|
|
|
"#{prepended_scss} #{self.theme.scss_variables.to_s} #{self.value}",
|
2021-04-27 09:33:43 -04:00
|
|
|
"#{Theme.targets[self.target_id]}.scss",
|
|
|
|
theme: self.theme,
|
2023-01-09 07:20:10 -05:00
|
|
|
load_paths: load_paths,
|
2021-04-27 09:33:43 -04:00
|
|
|
)
|
|
|
|
end
|
2019-04-12 06:36:08 -04:00
|
|
|
end
|
2017-04-20 16:55:09 -04:00
|
|
|
|
2021-02-19 11:22:24 -05:00
|
|
|
def compiled_css(prepended_scss)
|
2023-01-09 07:20:10 -05:00
|
|
|
css, _source_map =
|
|
|
|
begin
|
|
|
|
compile_scss(prepended_scss)
|
|
|
|
rescue SassC::SyntaxError => e
|
|
|
|
# We don't want to raise a blocking error here
|
|
|
|
# admin theme editor or discourse_theme CLI will show it nonetheless
|
|
|
|
Rails.logger.error "SCSS compilation error: #{e.message}"
|
|
|
|
["", nil]
|
|
|
|
end
|
2021-02-04 08:51:18 -05:00
|
|
|
css
|
|
|
|
end
|
|
|
|
|
2019-04-12 06:36:08 -04:00
|
|
|
def ensure_scss_compiles!
|
|
|
|
result = ["failed"]
|
|
|
|
begin
|
|
|
|
result = compile_scss
|
2019-11-12 09:30:19 -05:00
|
|
|
if contains_optimized_link?(self.value)
|
|
|
|
self.error = I18n.t("themes.errors.optimized_link")
|
2021-02-11 15:48:57 -05:00
|
|
|
elsif contains_ember_css_selector?(self.value)
|
|
|
|
self.error = I18n.t("themes.ember_selector_error")
|
2019-11-12 09:30:19 -05:00
|
|
|
else
|
|
|
|
self.error = nil unless error.nil?
|
|
|
|
end
|
2019-04-12 06:36:08 -04:00
|
|
|
rescue SassC::SyntaxError => e
|
|
|
|
self.error = e.message unless self.destroyed?
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
2020-05-29 08:04:51 -04:00
|
|
|
self.compiler_version = Theme.compiler_version
|
2019-04-12 06:36:08 -04:00
|
|
|
self.value_baked = Digest::SHA1.hexdigest(result.join(",")) # We don't use the compiled CSS here, we just use it to invalidate the stylesheet cache
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def target_name
|
2019-04-12 06:36:08 -04:00
|
|
|
Theme.targets[target_id].to_s
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
2019-11-12 09:30:19 -05:00
|
|
|
def contains_optimized_link?(text)
|
|
|
|
OptimizedImage::URL_REGEX.match?(text)
|
|
|
|
end
|
|
|
|
|
2021-02-11 15:48:57 -05:00
|
|
|
def contains_ember_css_selector?(text)
|
|
|
|
text.match(/#ember\d+|[.]ember-view/)
|
|
|
|
end
|
|
|
|
|
2019-01-23 09:40:21 -05:00
|
|
|
class ThemeFileMatcher
|
2023-01-09 07:20:10 -05:00
|
|
|
OPTIONS = %i[name type target]
|
2019-01-23 09:40:21 -05:00
|
|
|
# regex: used to match file names to fields (import).
|
|
|
|
# can contain named capture groups for name/type/target
|
|
|
|
# canonical: a lambda which converts name/type/target
|
|
|
|
# to filename (export)
|
|
|
|
# targets/names/types: can be nil if any value is allowed
|
|
|
|
# single value
|
|
|
|
# array of allowed values
|
|
|
|
def initialize(regex:, canonical:, targets:, names:, types:)
|
|
|
|
@allowed_values = {}
|
|
|
|
@allowed_values[:names] = Array(names) if names
|
|
|
|
@allowed_values[:targets] = Array(targets) if targets
|
|
|
|
@allowed_values[:types] = Array(types) if types
|
|
|
|
@canonical = canonical
|
|
|
|
@regex = regex
|
|
|
|
end
|
|
|
|
|
|
|
|
def opts_from_filename(filename)
|
|
|
|
match = @regex.match(filename)
|
|
|
|
return false unless match
|
|
|
|
hash = {}
|
|
|
|
OPTIONS.each do |option|
|
|
|
|
plural = :"#{option}s"
|
2021-04-12 08:02:58 -04:00
|
|
|
hash[option] = @allowed_values[plural][0] if @allowed_values[plural]&.length == 1
|
2019-01-23 09:40:21 -05:00
|
|
|
hash[option] = match[option] if hash[option].nil?
|
|
|
|
end
|
|
|
|
hash
|
|
|
|
end
|
|
|
|
|
|
|
|
def filename_from_opts(opts)
|
2023-01-09 07:20:10 -05:00
|
|
|
is_match =
|
|
|
|
OPTIONS.all? do |option|
|
|
|
|
plural = :"#{option}s"
|
|
|
|
next true if @allowed_values[plural] == nil # Allows any value
|
|
|
|
next true if @allowed_values[plural].include?(opts[option]) # Value is allowed
|
|
|
|
end
|
2019-01-23 09:40:21 -05:00
|
|
|
is_match ? @canonical.call(opts) : nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
FILE_MATCHERS = [
|
2023-01-09 07:20:10 -05:00
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex:
|
|
|
|
%r{^(?<target>(?:mobile|desktop|common))/(?<name>(?:head_tag|header|after_header|body_tag|footer))\.html$},
|
|
|
|
targets: %i[mobile desktop common],
|
|
|
|
names: %w[head_tag header after_header body_tag footer],
|
|
|
|
types: :html,
|
|
|
|
canonical: ->(h) { "#{h[:target]}/#{h[:name]}.html" },
|
|
|
|
),
|
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex: %r{^(?<target>(?:mobile|desktop|common))/(?:\k<target>)\.scss$},
|
|
|
|
targets: %i[mobile desktop common],
|
|
|
|
names: "scss",
|
|
|
|
types: :scss,
|
|
|
|
canonical: ->(h) { "#{h[:target]}/#{h[:target]}.scss" },
|
|
|
|
),
|
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex: %r{^common/embedded\.scss$},
|
|
|
|
targets: :common,
|
|
|
|
names: "embedded_scss",
|
|
|
|
types: :scss,
|
|
|
|
canonical: ->(h) { "common/embedded.scss" },
|
|
|
|
),
|
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex: %r{^common/color_definitions\.scss$},
|
|
|
|
targets: :common,
|
|
|
|
names: "color_definitions",
|
|
|
|
types: :scss,
|
|
|
|
canonical: ->(h) { "common/color_definitions.scss" },
|
|
|
|
),
|
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex: %r{^(?:scss|stylesheets)/(?<name>.+)\.scss$},
|
|
|
|
targets: :extra_scss,
|
|
|
|
names: nil,
|
|
|
|
types: :scss,
|
|
|
|
canonical: ->(h) { "stylesheets/#{h[:name]}.scss" },
|
|
|
|
),
|
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex: %r{^javascripts/(?<name>.+)$},
|
|
|
|
targets: :extra_js,
|
|
|
|
names: nil,
|
|
|
|
types: :js,
|
|
|
|
canonical: ->(h) { "javascripts/#{h[:name]}" },
|
|
|
|
),
|
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex: %r{^test/(?<name>.+)$},
|
|
|
|
targets: :tests_js,
|
|
|
|
names: nil,
|
|
|
|
types: :js,
|
|
|
|
canonical: ->(h) { "test/#{h[:name]}" },
|
|
|
|
),
|
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex: /^settings\.ya?ml$/,
|
|
|
|
names: "yaml",
|
|
|
|
types: :yaml,
|
|
|
|
targets: :settings,
|
|
|
|
canonical: ->(h) { "settings.yml" },
|
|
|
|
),
|
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex: %r{^locales/(?<name>(?:#{I18n.available_locales.join("|")}))\.yml$},
|
|
|
|
names: I18n.available_locales.map(&:to_s),
|
|
|
|
types: :yaml,
|
|
|
|
targets: :translations,
|
|
|
|
canonical: ->(h) { "locales/#{h[:name]}.yml" },
|
|
|
|
),
|
|
|
|
ThemeFileMatcher.new(
|
|
|
|
regex: /(?!)/, # Never match uploads by filename, they must be named in about.json
|
|
|
|
names: nil,
|
|
|
|
types: :theme_upload_var,
|
|
|
|
targets: :common,
|
|
|
|
canonical: ->(h) { "assets/#{h[:name]}#{File.extname(h[:filename])}" },
|
|
|
|
),
|
2019-01-23 09:40:21 -05:00
|
|
|
]
|
|
|
|
|
|
|
|
# For now just work for standard fields
|
|
|
|
def file_path
|
|
|
|
FILE_MATCHERS.each do |matcher|
|
2023-01-09 07:20:10 -05:00
|
|
|
if filename =
|
|
|
|
matcher.filename_from_opts(
|
|
|
|
target: target_name.to_sym,
|
|
|
|
name: name,
|
|
|
|
type: ThemeField.types[type_id],
|
|
|
|
filename: upload&.original_filename,
|
|
|
|
)
|
2019-01-23 09:40:21 -05:00
|
|
|
return filename
|
|
|
|
end
|
|
|
|
end
|
|
|
|
nil # Not a file (e.g. a theme variable/color)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.opts_from_file_path(filename)
|
|
|
|
FILE_MATCHERS.each do |matcher|
|
|
|
|
if opts = matcher.opts_from_filename(filename)
|
|
|
|
return opts
|
|
|
|
end
|
|
|
|
end
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
2019-04-12 06:36:08 -04:00
|
|
|
def dependent_fields
|
|
|
|
if extra_scss_field?
|
2023-01-09 07:20:10 -05:00
|
|
|
return(
|
|
|
|
theme.theme_fields.where(
|
|
|
|
target_id: ThemeField.basic_targets.map { |t| Theme.targets[t.to_sym] },
|
|
|
|
name: ThemeField.scss_fields,
|
|
|
|
)
|
|
|
|
)
|
2019-04-12 06:36:08 -04:00
|
|
|
elsif settings_field?
|
2023-01-09 07:20:10 -05:00
|
|
|
return(
|
|
|
|
theme.theme_fields.where(
|
|
|
|
target_id: ThemeField.basic_targets.map { |t| Theme.targets[t.to_sym] },
|
|
|
|
name: ThemeField.scss_fields + ThemeField.html_fields,
|
|
|
|
)
|
|
|
|
)
|
2019-04-12 06:36:08 -04:00
|
|
|
end
|
|
|
|
ThemeField.none
|
|
|
|
end
|
|
|
|
|
|
|
|
def invalidate_baked!
|
|
|
|
update_column(:value_baked, nil)
|
|
|
|
dependent_fields.update_all(value_baked: nil)
|
|
|
|
end
|
2018-12-19 10:36:31 -05:00
|
|
|
|
2019-04-12 06:36:08 -04:00
|
|
|
before_save do
|
2023-01-09 07:20:10 -05:00
|
|
|
if (will_save_change_to_value? || will_save_change_to_upload_id?) &&
|
|
|
|
!will_save_change_to_value_baked?
|
2017-04-12 10:52:52 -04:00
|
|
|
self.value_baked = nil
|
|
|
|
end
|
2022-04-06 17:58:10 -04:00
|
|
|
if upload && upload.extension == "js"
|
|
|
|
if will_save_change_to_upload_id? || !javascript_cache
|
|
|
|
javascript_cache ||= build_javascript_cache
|
|
|
|
javascript_cache.content = upload.content
|
|
|
|
end
|
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
after_save { dependent_fields.each(&:invalidate_baked!) }
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
after_destroy { DB.after_commit { SvgSprite.expire_cache } if svg_sprite_field? }
|
2021-07-14 15:18:29 -04:00
|
|
|
|
2018-11-01 16:01:46 -04:00
|
|
|
private
|
|
|
|
|
2023-01-09 07:20:10 -05:00
|
|
|
JAVASCRIPT_TYPES = %w[text/javascript application/javascript application/ecmascript]
|
2018-11-01 16:01:46 -04:00
|
|
|
|
|
|
|
def inline_javascript?(node)
|
2023-01-09 07:20:10 -05:00
|
|
|
if node["src"].present?
|
2018-11-01 16:01:46 -04:00
|
|
|
false
|
2023-01-09 07:20:10 -05:00
|
|
|
elsif node["type"].present?
|
|
|
|
JAVASCRIPT_TYPES.include?(node["type"].downcase)
|
2018-11-01 16:01:46 -04:00
|
|
|
else
|
|
|
|
true
|
|
|
|
end
|
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: theme_fields
|
|
|
|
#
|
|
|
|
# id :integer not null, primary key
|
|
|
|
# theme_id :integer not null
|
2017-05-02 16:01:01 -04:00
|
|
|
# target_id :integer not null
|
2019-04-23 07:34:32 -04:00
|
|
|
# name :string(255) not null
|
2017-04-12 10:52:52 -04:00
|
|
|
# value :text not null
|
|
|
|
# value_baked :text
|
2019-01-11 14:29:56 -05:00
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
2019-06-03 11:38:02 -04:00
|
|
|
# compiler_version :string(50) default("0"), not null
|
2017-04-20 15:47:25 -04:00
|
|
|
# error :string
|
2017-05-02 16:01:01 -04:00
|
|
|
# upload_id :integer
|
|
|
|
# type_id :integer default(0), not null
|
2017-04-12 10:52:52 -04:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
2017-05-02 16:01:01 -04:00
|
|
|
# theme_field_unique_index (theme_id,target_id,type_id,name) UNIQUE
|
2017-04-12 10:52:52 -04:00
|
|
|
#
|