mirror of
https://github.com/discourse/discourse.git
synced 2024-11-24 09:53:32 +08:00
695773db1c
This pushes our internal V8 JavaScript engine from Chrome 32 to 50. It also resolves some long standing issues we had with the old wrapper.
273 lines
7.7 KiB
Ruby
273 lines
7.7 KiB
Ruby
require_dependency 'sass/discourse_sass_compiler'
|
|
require_dependency 'sass/discourse_stylesheets'
|
|
require_dependency 'distributed_cache'
|
|
|
|
class SiteCustomization < ActiveRecord::Base
|
|
ENABLED_KEY = '7e202ef2-56d7-47d5-98d8-a9c8d15e57dd'
|
|
@cache = DistributedCache.new('site_customization')
|
|
|
|
def self.css_fields
|
|
%w(stylesheet mobile_stylesheet embedded_css)
|
|
end
|
|
|
|
def self.html_fields
|
|
%w(body_tag head_tag header mobile_header footer mobile_footer)
|
|
end
|
|
|
|
before_create do
|
|
self.enabled ||= false
|
|
self.key ||= SecureRandom.uuid
|
|
true
|
|
end
|
|
|
|
def compile_stylesheet(scss)
|
|
DiscourseSassCompiler.compile("@import \"theme_variables\";\n" << scss, 'custom')
|
|
rescue => e
|
|
puts e.backtrace.join("\n") unless Sass::SyntaxError === e
|
|
raise e
|
|
end
|
|
|
|
def transpile(es6_source, version)
|
|
template = Tilt::ES6ModuleTranspilerTemplate.new {}
|
|
wrapped = <<PLUGIN_API_JS
|
|
Discourse._registerPluginCode('#{version}', api => {
|
|
#{es6_source}
|
|
});
|
|
PLUGIN_API_JS
|
|
|
|
template.babel_transpile(wrapped)
|
|
end
|
|
|
|
def process_html(html)
|
|
doc = Nokogiri::HTML.fragment(html)
|
|
doc.css('script[type="text/x-handlebars"]').each do |node|
|
|
name = node["name"] || node["data-template-name"] || "broken"
|
|
precompiled =
|
|
if name =~ /\.raw$/
|
|
"Discourse.EmberCompatHandlebars.template(#{Barber::Precompiler.compile(node.inner_html)})"
|
|
else
|
|
"Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(node.inner_html)})"
|
|
end
|
|
compiled = <<SCRIPT
|
|
Ember.TEMPLATES[#{name.inspect}] = #{precompiled};
|
|
SCRIPT
|
|
node.replace("<script>#{compiled}</script>")
|
|
end
|
|
|
|
doc.css('script[type="text/discourse-plugin"]').each do |node|
|
|
if node['version'].present?
|
|
begin
|
|
code = transpile(node.inner_html, node['version'])
|
|
node.replace("<script>#{code}</script>")
|
|
rescue MiniRacer::RuntimeError => ex
|
|
node.replace("<script type='text/discourse-js-error'>#{ex.message}</script>")
|
|
end
|
|
end
|
|
end
|
|
|
|
doc.to_s
|
|
end
|
|
|
|
before_save do
|
|
SiteCustomization.html_fields.each do |html_attr|
|
|
if self.send("#{html_attr}_changed?")
|
|
self.send("#{html_attr}_baked=", process_html(self.send(html_attr)))
|
|
end
|
|
end
|
|
|
|
SiteCustomization.css_fields.each do |stylesheet_attr|
|
|
if self.send("#{stylesheet_attr}_changed?")
|
|
begin
|
|
self.send("#{stylesheet_attr}_baked=", compile_stylesheet(self.send(stylesheet_attr)))
|
|
rescue Sass::SyntaxError => e
|
|
self.send("#{stylesheet_attr}_baked=", DiscourseSassCompiler.error_as_css(e, "custom stylesheet"))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def any_stylesheet_changed?
|
|
SiteCustomization.css_fields.each do |fieldname|
|
|
return true if self.send("#{fieldname}_changed?")
|
|
end
|
|
false
|
|
end
|
|
|
|
after_save do
|
|
remove_from_cache!
|
|
if any_stylesheet_changed?
|
|
MessageBus.publish "/file-change/#{key}", SecureRandom.hex
|
|
MessageBus.publish "/file-change/#{SiteCustomization::ENABLED_KEY}", SecureRandom.hex
|
|
end
|
|
MessageBus.publish "/header-change/#{key}", header if header_changed?
|
|
MessageBus.publish "/footer-change/#{key}", footer if footer_changed?
|
|
DiscourseStylesheets.cache.clear
|
|
end
|
|
|
|
after_destroy do
|
|
remove_from_cache!
|
|
end
|
|
|
|
def self.enabled_key
|
|
ENABLED_KEY.dup << RailsMultisite::ConnectionManagement.current_db
|
|
end
|
|
|
|
def self.field_for_target(target=nil)
|
|
target ||= :desktop
|
|
|
|
case target.to_sym
|
|
when :mobile then :mobile_stylesheet
|
|
when :desktop then :stylesheet
|
|
when :embedded then :embedded_css
|
|
end
|
|
end
|
|
|
|
def self.baked_for_target(target=nil)
|
|
"#{field_for_target(target)}_baked".to_sym
|
|
end
|
|
|
|
def self.enabled_stylesheet_contents(target=:desktop)
|
|
@cache["enabled_stylesheet_#{target}"] ||= where(enabled: true)
|
|
.order(:name)
|
|
.pluck(baked_for_target(target))
|
|
.compact
|
|
.join("\n")
|
|
end
|
|
|
|
def self.stylesheet_contents(key, target)
|
|
if key == ENABLED_KEY
|
|
enabled_stylesheet_contents(target)
|
|
else
|
|
where(key: key)
|
|
.pluck(baked_for_target(target))
|
|
.first
|
|
end
|
|
end
|
|
|
|
def self.custom_stylesheet(preview_style=nil, target=:desktop)
|
|
preview_style ||= ENABLED_KEY
|
|
if preview_style == ENABLED_KEY
|
|
stylesheet_link_tag(ENABLED_KEY, target, enabled_stylesheet_contents(target))
|
|
else
|
|
lookup_field(preview_style, target, :stylesheet_link_tag)
|
|
end
|
|
end
|
|
|
|
%i{header top footer head_tag body_tag}.each do |name|
|
|
define_singleton_method("custom_#{name}") do |preview_style=nil, target=:desktop|
|
|
preview_style ||= ENABLED_KEY
|
|
lookup_field(preview_style, target, name)
|
|
end
|
|
end
|
|
|
|
def self.lookup_field(key, target, field)
|
|
return if key.blank?
|
|
|
|
cache_key = key + target.to_s + field.to_s;
|
|
|
|
lookup = @cache[cache_key]
|
|
return lookup.html_safe if lookup
|
|
|
|
styles = if key == ENABLED_KEY
|
|
order(:name).where(enabled:true).to_a
|
|
else
|
|
[find_by(key: key)].compact
|
|
end
|
|
|
|
val = if styles.present?
|
|
styles.map do |style|
|
|
lookup = target == :mobile ? "mobile_#{field}" : field
|
|
if html_fields.include?(lookup.to_s)
|
|
style.ensure_baked!(lookup)
|
|
style.send("#{lookup}_baked")
|
|
else
|
|
style.send(lookup)
|
|
end
|
|
end.compact.join("\n")
|
|
end
|
|
|
|
(@cache[cache_key] = val || "").html_safe
|
|
end
|
|
|
|
def self.remove_from_cache!(key, broadcast = true)
|
|
MessageBus.publish('/site_customization', key: key) if broadcast
|
|
clear_cache!
|
|
end
|
|
|
|
def self.clear_cache!
|
|
@cache.clear
|
|
end
|
|
|
|
def ensure_baked!(field)
|
|
unless self.send("#{field}_baked")
|
|
if val = self.send(field)
|
|
val = process_html(val) rescue ""
|
|
self.update_columns("#{field}_baked" => val)
|
|
end
|
|
end
|
|
end
|
|
|
|
def remove_from_cache!
|
|
self.class.remove_from_cache!(self.class.enabled_key)
|
|
self.class.remove_from_cache!(key)
|
|
end
|
|
|
|
def mobile_stylesheet_link_tag
|
|
stylesheet_link_tag(:mobile)
|
|
end
|
|
|
|
def stylesheet_link_tag(target=:desktop)
|
|
content = self.send(SiteCustomization.field_for_target(target))
|
|
SiteCustomization.stylesheet_link_tag(key, target, content)
|
|
end
|
|
|
|
def self.stylesheet_link_tag(key, target, content)
|
|
return "" unless content.present?
|
|
|
|
hash = Digest::MD5.hexdigest(content)
|
|
link_css_tag "/site_customizations/#{key}.css?target=#{target}&v=#{hash}"
|
|
end
|
|
|
|
def self.link_css_tag(href)
|
|
href = (GlobalSetting.cdn_url || "") + "#{GlobalSetting.relative_url_root}#{href}&__ws=#{Discourse.current_hostname}"
|
|
%Q{<link class="custom-css" rel="stylesheet" href="#{href}" type="text/css" media="all">}.html_safe
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: site_customizations
|
|
#
|
|
# id :integer not null, primary key
|
|
# name :string not null
|
|
# stylesheet :text
|
|
# header :text
|
|
# user_id :integer not null
|
|
# enabled :boolean not null
|
|
# key :string not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# stylesheet_baked :text default(""), not null
|
|
# mobile_stylesheet :text
|
|
# mobile_header :text
|
|
# mobile_stylesheet_baked :text
|
|
# footer :text
|
|
# mobile_footer :text
|
|
# head_tag :text
|
|
# body_tag :text
|
|
# top :text
|
|
# mobile_top :text
|
|
# embedded_css :text
|
|
# embedded_css_baked :text
|
|
# head_tag_baked :text
|
|
# body_tag_baked :text
|
|
# header_baked :text
|
|
# mobile_header_baked :text
|
|
# footer_baked :text
|
|
# mobile_footer_baked :text
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_site_customizations_on_key (key)
|
|
#
|