diff --git a/app/assets/javascripts/discourse/initializers/live-development.js.es6 b/app/assets/javascripts/discourse/initializers/live-development.js.es6 index 701789cf7..a7318b1a7 100644 --- a/app/assets/javascripts/discourse/initializers/live-development.js.es6 +++ b/app/assets/javascripts/discourse/initializers/live-development.js.es6 @@ -32,7 +32,10 @@ export default { // Observe file changes messageBus.subscribe("/file-change", function(data) { - Ember.TEMPLATES.empty = Handlebars.compile("
"); + if (Handlebars.compile && !Ember.TEMPLATES.empty) { + // hbs notifications only happen in dev + Ember.TEMPLATES.empty = Handlebars.compile("
"); + } _.each(data,function(me) { if (me === "refresh") { diff --git a/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js b/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js index d6e85b19a..a6666cc6c 100644 --- a/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js +++ b/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js @@ -57,57 +57,59 @@ stringCompatHelper("with"); - RawHandlebars.Compiler = function() {}; - RawHandlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); - RawHandlebars.Compiler.prototype.compiler = RawHandlebars.Compiler; + if (Handlebars.Compiler) { + RawHandlebars.Compiler = function() {}; + RawHandlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); + RawHandlebars.Compiler.prototype.compiler = RawHandlebars.Compiler; - RawHandlebars.JavaScriptCompiler = function() {}; + RawHandlebars.JavaScriptCompiler = function() {}; - RawHandlebars.JavaScriptCompiler.prototype = objectCreate(Handlebars.JavaScriptCompiler.prototype); - RawHandlebars.JavaScriptCompiler.prototype.compiler = RawHandlebars.JavaScriptCompiler; - RawHandlebars.JavaScriptCompiler.prototype.namespace = "Discourse.EmberCompatHandlebars"; + RawHandlebars.JavaScriptCompiler.prototype = objectCreate(Handlebars.JavaScriptCompiler.prototype); + RawHandlebars.JavaScriptCompiler.prototype.compiler = RawHandlebars.JavaScriptCompiler; + RawHandlebars.JavaScriptCompiler.prototype.namespace = "Discourse.EmberCompatHandlebars"; - RawHandlebars.Compiler.prototype.mustache = function(mustache) { - if ( !(mustache.params.length || mustache.hash)) { + RawHandlebars.Compiler.prototype.mustache = function(mustache) { + if ( !(mustache.params.length || mustache.hash)) { - var id = new Handlebars.AST.IdNode([{ part: 'get' }]); - mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, mustache.escaped); - } + var id = new Handlebars.AST.IdNode([{ part: 'get' }]); + mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, mustache.escaped); + } - return Handlebars.Compiler.prototype.mustache.call(this, mustache); - }; - - RawHandlebars.precompile = function(value, asObject) { - var ast = Handlebars.parse(value); - - var options = { - knownHelpers: { - get: true - }, - data: true, - stringParams: true + return Handlebars.Compiler.prototype.mustache.call(this, mustache); }; - asObject = asObject === undefined ? true : asObject; + RawHandlebars.precompile = function(value, asObject) { + var ast = Handlebars.parse(value); - var environment = new RawHandlebars.Compiler().compile(ast, options); - return new RawHandlebars.JavaScriptCompiler().compile(environment, options, undefined, asObject); - }; + var options = { + knownHelpers: { + get: true + }, + data: true, + stringParams: true + }; + + asObject = asObject === undefined ? true : asObject; + + var environment = new RawHandlebars.Compiler().compile(ast, options); + return new RawHandlebars.JavaScriptCompiler().compile(environment, options, undefined, asObject); + }; - RawHandlebars.compile = function(string) { - var ast = Handlebars.parse(string); - // this forces us to rewrite helpers - var options = { data: true, stringParams: true }; - var environment = new RawHandlebars.Compiler().compile(ast, options); - var templateSpec = new RawHandlebars.JavaScriptCompiler().compile(environment, options, undefined, true); + RawHandlebars.compile = function(string) { + var ast = Handlebars.parse(string); + // this forces us to rewrite helpers + var options = { data: true, stringParams: true }; + var environment = new RawHandlebars.Compiler().compile(ast, options); + var templateSpec = new RawHandlebars.JavaScriptCompiler().compile(environment, options, undefined, true); - var template = RawHandlebars.template(templateSpec); - template.isMethod = false; + var template = RawHandlebars.template(templateSpec); + template.isMethod = false; - return template; - }; + return template; + }; + } RawHandlebars.get = function(ctx, property, options){ if (options.types && options.data.view) { diff --git a/app/assets/javascripts/discourse/pre-initializers/register-dom-templates.js.es6 b/app/assets/javascripts/discourse/pre-initializers/register-dom-templates.js.es6 index 2ceaf46a5..41e72df39 100644 --- a/app/assets/javascripts/discourse/pre-initializers/register-dom-templates.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/register-dom-templates.js.es6 @@ -2,16 +2,14 @@ export default { name: "register-discourse-dom-templates", before: 'domTemplates', - // a bit smarter than the default one (domTemplates) - // figures out raw vs non-raw automatically - // allows overriding initialize: function() { $('script[type="text/x-handlebars"]').each(function(){ var $this = $(this); var name = $this.attr("name") || $this.data("template-name"); - Ember.TEMPLATES[name] = name.match(/\.raw$/) ? - Discourse.EmberCompatHandlebars.compile($this.text()) : - Ember.Handlebars.compile($this.text()); + + if (window.console) { + window.console.log("WARNING: you have a handlebars template named " + name + " this is an unsupported setup, precompile your templates"); + } $this.remove(); }); } diff --git a/app/assets/javascripts/template_include.js.erb b/app/assets/javascripts/template_include.js.erb new file mode 100644 index 000000000..ff7c323ae --- /dev/null +++ b/app/assets/javascripts/template_include.js.erb @@ -0,0 +1,8 @@ +<% +if Rails.env.development? || Rails.env.test? + require_asset ("handlebars.js") + require_asset ("ember-template-compiler.js") +else + require_asset ("handlebars.runtime.js") +end +%> diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index ae33b711b..29ee2c532 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -2,8 +2,7 @@ //= require ./env //= require probes.js -//= require ember-template-compiler -//= require handlebars.js +//= require template_include.js //= require i18n-patches //= require loader diff --git a/app/models/site_customization.rb b/app/models/site_customization.rb index 45397f6ab..3cb786229 100644 --- a/app/models/site_customization.rb +++ b/app/models/site_customization.rb @@ -10,6 +10,10 @@ class SiteCustomization < ActiveRecord::Base %w(stylesheet mobile_stylesheet embedded_css) end + def self.html_fields + %w(body_tag head_tag) + end + before_create do self.enabled ||= false self.key ||= SecureRandom.uuid @@ -23,7 +27,32 @@ class SiteCustomization < ActiveRecord::Base raise e 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::EmberCompatPrecompiler.compile(node.inner_html)})" + else + "Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(node.inner_html)})" + end + compiled = <") + 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 @@ -126,7 +155,12 @@ class SiteCustomization < ActiveRecord::Base val = if styles.present? styles.map do |style| lookup = target == :mobile ? "mobile_#{field}" : field - style.send(lookup) + 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 @@ -142,6 +176,15 @@ class SiteCustomization < ActiveRecord::Base @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) @@ -190,6 +233,8 @@ end # mobile_footer :text # head_tag :text # body_tag :text +# head_tag_baked :text +# body_tag_baked :text # top :text # mobile_top :text # embedded_css :text diff --git a/db/migrate/20151126233623_add_baked_head_and_body_to_site_customizations.rb b/db/migrate/20151126233623_add_baked_head_and_body_to_site_customizations.rb new file mode 100644 index 000000000..c7f8ed417 --- /dev/null +++ b/db/migrate/20151126233623_add_baked_head_and_body_to_site_customizations.rb @@ -0,0 +1,6 @@ +class AddBakedHeadAndBodyToSiteCustomizations < ActiveRecord::Migration + def change + add_column :site_customizations, :head_tag_baked, :text + add_column :site_customizations, :body_tag_baked, :text + end +end diff --git a/spec/models/site_customization_spec.rb b/spec/models/site_customization_spec.rb index b3ab1a845..8831ea5d6 100644 --- a/spec/models/site_customization_spec.rb +++ b/spec/models/site_customization_spec.rb @@ -91,5 +91,32 @@ describe SiteCustomization do expect(c.stylesheet_baked).not_to be_present end + it 'should correct bad html in body_tag_baked and head_tag_baked' do + c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: "I am bold", body_tag: "I am bold") + expect(c.head_tag_baked).to eq("I am bold") + expect(c.body_tag_baked).to eq("I am bold") + end + + it 'should precompile fragments in body and head tags' do + with_template = < + {{hello}} + + +HTML + c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: with_template, body_tag: with_template) + expect(c.head_tag_baked).to match(/HTMLBars/) + expect(c.body_tag_baked).to match(/HTMLBars/) + expect(c.body_tag_baked).to match(/EmberCompatHandlebars/) + expect(c.head_tag_baked).to match(/EmberCompatHandlebars/) + end + + it 'should create body_tag_baked on demand if needed' do + c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: "test", enabled: true) + c.update_columns(head_tag_baked: nil) + expect(SiteCustomization.custom_head_tag).to match(/test<\/b>/) + end end diff --git a/vendor/assets/javascripts/handlebars.runtime.js b/vendor/assets/javascripts/handlebars.runtime.js new file mode 100644 index 000000000..acd734440 --- /dev/null +++ b/vendor/assets/javascripts/handlebars.runtime.js @@ -0,0 +1,666 @@ +/*! + + handlebars v2.0.0 + +Copyright (C) 2011-2014 by Yehuda Katz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +@license +*/ +/* exported Handlebars */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.Handlebars = root.Handlebars || factory(); + } +}(this, function () { +// handlebars/safe-string.js +var __module3__ = (function() { + "use strict"; + var __exports__; + // Build out our basic SafeString type + function SafeString(string) { + this.string = string; + } + + SafeString.prototype.toString = function() { + return "" + this.string; + }; + + __exports__ = SafeString; + return __exports__; +})(); + +// handlebars/utils.js +var __module2__ = (function(__dependency1__) { + "use strict"; + var __exports__ = {}; + /*jshint -W004 */ + var SafeString = __dependency1__; + + var escape = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`", + '\n' : '\\n', // NewLine + '\r' : '\\n', // Return + '\b' : '\\b', // Backspace + '\f' : '\\f', // Form fee + '\t' : '\\t', // Tab + '\v' : '\\v' // Vertical Tab + }; + var badChars = /[&<>"'`\b\f\n\r\t\v]/g; + var possible = /[&<>"'`\b\f\n\r\t\v]/; + + + function escapeChar(chr) { + return escape[chr]; + } + + function extend(obj /* , ...source */) { + for (var i = 1; i < arguments.length; i++) { + for (var key in arguments[i]) { + if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { + obj[key] = arguments[i][key]; + } + } + } + + return obj; + } + + __exports__.extend = extend;var toString = Object.prototype.toString; + __exports__.toString = toString; + // Sourced from lodash + // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt + var isFunction = function(value) { + return typeof value === 'function'; + }; + // fallback for older versions of Chrome and Safari + /* istanbul ignore next */ + if (isFunction(/x/)) { + isFunction = function(value) { + return typeof value === 'function' && toString.call(value) === '[object Function]'; + }; + } + var isFunction; + __exports__.isFunction = isFunction; + /* istanbul ignore next */ + var isArray = Array.isArray || function(value) { + return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; + }; + __exports__.isArray = isArray; + + function escapeExpression(string) { + // don't escape SafeStrings, since they're already safe + if (string instanceof SafeString) { + return string.toString(); + } else if (string == null) { + return ""; + } else if (!string) { + return string + ''; + } + + // Force a string conversion as this will be done by the append regardless and + // the regex test will do this transparently behind the scenes, causing issues if + // an object's to string has escaped characters in it. + string = "" + string; + + if(!possible.test(string)) { return string; } + return string.replace(badChars, escapeChar); + } + + __exports__.escapeExpression = escapeExpression;function isEmpty(value) { + if (!value && value !== 0) { + return true; + } else if (isArray(value) && value.length === 0) { + return true; + } else { + return false; + } + } + + __exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) { + return (contextPath ? contextPath + '.' : '') + id; + } + + __exports__.appendContextPath = appendContextPath; + return __exports__; +})(__module3__); + +// handlebars/exception.js +var __module4__ = (function() { + "use strict"; + var __exports__; + + var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + + function Exception(message, node) { + var line; + if (node && node.firstLine) { + line = node.firstLine; + + message += ' - ' + line + ':' + node.firstColumn; + } + + var tmp = Error.prototype.constructor.call(this, message); + + // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. + for (var idx = 0; idx < errorProps.length; idx++) { + this[errorProps[idx]] = tmp[errorProps[idx]]; + } + + if (line) { + this.lineNumber = line; + this.column = node.firstColumn; + } + } + + Exception.prototype = new Error(); + + __exports__ = Exception; + return __exports__; +})(); + +// handlebars/base.js +var __module1__ = (function(__dependency1__, __dependency2__) { + "use strict"; + var __exports__ = {}; + var Utils = __dependency1__; + var Exception = __dependency2__; + + var VERSION = "2.0.0"; + __exports__.VERSION = VERSION;var COMPILER_REVISION = 6; + __exports__.COMPILER_REVISION = COMPILER_REVISION; + var REVISION_CHANGES = { + 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it + 2: '== 1.0.0-rc.3', + 3: '== 1.0.0-rc.4', + 4: '== 1.x.x', + 5: '== 2.0.0-alpha.x', + 6: '>= 2.0.0-beta.1' + }; + __exports__.REVISION_CHANGES = REVISION_CHANGES; + var isArray = Utils.isArray, + isFunction = Utils.isFunction, + toString = Utils.toString, + objectType = '[object Object]'; + + function HandlebarsEnvironment(helpers, partials) { + this.helpers = helpers || {}; + this.partials = partials || {}; + + registerDefaultHelpers(this); + } + + __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { + constructor: HandlebarsEnvironment, + + logger: logger, + log: log, + + registerHelper: function(name, fn) { + if (toString.call(name) === objectType) { + if (fn) { throw new Exception('Arg not supported with multiple helpers'); } + Utils.extend(this.helpers, name); + } else { + this.helpers[name] = fn; + } + }, + unregisterHelper: function(name) { + delete this.helpers[name]; + }, + + registerPartial: function(name, partial) { + if (toString.call(name) === objectType) { + Utils.extend(this.partials, name); + } else { + this.partials[name] = partial; + } + }, + unregisterPartial: function(name) { + delete this.partials[name]; + } + }; + + function registerDefaultHelpers(instance) { + instance.registerHelper('helperMissing', function(/* [args, ]options */) { + if(arguments.length === 1) { + // A missing field in a {{foo}} constuct. + return undefined; + } else { + // Someone is actually trying to call something, blow up. + throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); + } + }); + + instance.registerHelper('blockHelperMissing', function(context, options) { + var inverse = options.inverse, + fn = options.fn; + + if(context === true) { + return fn(this); + } else if(context === false || context == null) { + return inverse(this); + } else if (isArray(context)) { + if(context.length > 0) { + if (options.ids) { + options.ids = [options.name]; + } + + return instance.helpers.each(context, options); + } else { + return inverse(this); + } + } else { + if (options.data && options.ids) { + var data = createFrame(options.data); + data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); + options = {data: data}; + } + + return fn(context, options); + } + }); + + instance.registerHelper('each', function(context, options) { + if (!options) { + throw new Exception('Must pass iterator to #each'); + } + + var fn = options.fn, inverse = options.inverse; + var i = 0, ret = "", data; + + var contextPath; + if (options.data && options.ids) { + contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; + } + + if (isFunction(context)) { context = context.call(this); } + + if (options.data) { + data = createFrame(options.data); + } + + if(context && typeof context === 'object') { + if (isArray(context)) { + for(var j = context.length; i