class SiteCustomization < ActiveRecord::Base CACHE_PATH = 'stylesheet-cache' @lock = Mutex.new before_create do self.position ||= (SiteCustomization.maximum(:position) || 0) + 1 self.enabled ||= false self.key ||= SecureRandom.uuid true end before_save do if self.stylesheet_changed? begin self.stylesheet_baked = Sass.compile self.stylesheet rescue Sass::SyntaxError => e error = e.sass_backtrace_str("custom stylesheet") error.gsub!("\n", '\A ') error.gsub!("'", '\27 ') self.stylesheet_baked = "#main {display: none;} footer {white-space: pre; margin-left: 100px;} footer:after{ content: '#{error}' }" end end end after_save do if self.stylesheet_changed? if File.exists?(self.stylesheet_fullpath) File.delete self.stylesheet_fullpath end end self.remove_from_cache! if self.stylesheet_changed? self.ensure_stylesheet_on_disk! MessageBus.publish "/file-change/#{self.key}", self.stylesheet_hash end MessageBus.publish "/header-change/#{self.key}", self.header if self.header_changed? end after_destroy do if File.exists?(self.stylesheet_fullpath) File.delete self.stylesheet_fullpath end self.remove_from_cache! end def self.custom_stylesheet(preview_style) style = lookup_style(preview_style) style.stylesheet_link_tag.html_safe if style end def self.custom_header(preview_style) style = lookup_style(preview_style) style.header.html_safe if style end def self.override_default_style(preview_style) style = lookup_style(preview_style) style.override_default_style if style end def self.lookup_style(key) return if key.blank? # cache is cross site resiliant cause key is secure random @cache ||= {} ensure_cache_listener style = @cache[key] return style if style @lock.synchronize do style = self.where(key: key).first @cache[key] = style end end def self.ensure_cache_listener unless @subscribed klass = self MessageBus.subscribe("/site_customization") do |msg| message = msg.data klass.remove_from_cache!(message["key"], false) end @subscribed = true end end def self.remove_from_cache!(key, broadcast=true) MessageBus.publish('/site_customization', {key: key}) if broadcast if @cache @lock.synchronize do @cache[key] = nil end end end def remove_from_cache! self.class.remove_from_cache!(self.key) end def stylesheet_hash Digest::MD5.hexdigest(self.stylesheet) end def ensure_stylesheet_on_disk! path = stylesheet_fullpath dir = "#{Rails.root}/public/#{CACHE_PATH}" FileUtils.mkdir_p(dir) unless File.exists?(path) File.open(path, "w") do |f| f.puts self.stylesheet_baked end end end def stylesheet_filename file = "" dir = "#{Rails.root}/public/#{CACHE_PATH}" path = dir + file "/#{CACHE_PATH}/#{self.key}.css" end def stylesheet_fullpath "#{Rails.root}/public#{self.stylesheet_filename}" end def stylesheet_link_tag return "" unless self.stylesheet.present? return @stylesheet_link_tag if @stylesheet_link_tag ensure_stylesheet_on_disk! @stylesheet_link_tag = "" end end