diff --git a/Gemfile b/Gemfile index 2332689189a..7e0fb0fc015 100644 --- a/Gemfile +++ b/Gemfile @@ -41,7 +41,6 @@ gem 'onebox' gem 'ember-rails' gem 'ember-source', '1.12.1' -gem 'handlebars-source', '2.0.0' gem 'barber' gem 'babel-transpiler' diff --git a/Gemfile.lock b/Gemfile.lock index 46f0d552f39..74bf1e6b0f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -119,7 +119,6 @@ GEM given_core (3.5.4) sorcerer (>= 0.3.7) guess_html_encoding (0.0.11) - handlebars-source (2.0.0) hashie (3.4.0) highline (1.7.1) hike (1.2.3) @@ -418,7 +417,6 @@ DEPENDENCIES flamegraph foreman gctools - handlebars-source (= 2.0.0) highline hiredis htmlentities diff --git a/app/assets/javascripts/admin/components/ace-editor.js.es6 b/app/assets/javascripts/admin/components/ace-editor.js.es6 index b660d488591..755dc574b24 100644 --- a/app/assets/javascripts/admin/components/ace-editor.js.es6 +++ b/app/assets/javascripts/admin/components/ace-editor.js.es6 @@ -16,7 +16,7 @@ export default Ember.Component.extend({ render(buffer) { buffer.push("
"); if (this.get('content')) { - buffer.push(Handlebars.Utils.escapeExpression(this.get('content'))); + buffer.push(Discourse.Utilities.escapeExpression(this.get('content'))); } buffer.push("
"); }, diff --git a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 index 70973e27813..d26585f3481 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 @@ -22,7 +22,7 @@ export default Ember.Controller.extend({ returned = "
";
 
     _.each(raw, function(linehash) {
-      returned += Handlebars.Utils.escapeExpression(linehash["QUERY PLAN"]);
+      returned += Discourse.Utilities.escapeExpression(linehash["QUERY PLAN"]);
       returned += "
"; }); @@ -32,7 +32,7 @@ export default Ember.Controller.extend({ processed_sample: Ember.computed.map('model.sample', function(grant) { var i18nKey = 'admin.badges.preview.grant.with', - i18nParams = { username: Handlebars.Utils.escapeExpression(grant.username) }; + i18nParams = { username: Discourse.Utilities.escapeExpression(grant.username) }; if (grant.post_id) { i18nKey += "_post"; @@ -41,7 +41,7 @@ export default Ember.Controller.extend({ if (grant.granted_at) { i18nKey += "_time"; - i18nParams.time = Handlebars.Utils.escapeExpression(moment(grant.granted_at).format(I18n.t('dates.long_with_year'))); + i18nParams.time = Discourse.Utilities.escapeExpression(moment(grant.granted_at).format(I18n.t('dates.long_with_year'))); } return I18n.t(i18nKey, i18nParams); diff --git a/app/assets/javascripts/admin/models/staff_action_log.js b/app/assets/javascripts/admin/models/staff_action_log.js index dc52a7b170a..393ec7d484c 100644 --- a/app/assets/javascripts/admin/models/staff_action_log.js +++ b/app/assets/javascripts/admin/models/staff_action_log.js @@ -16,14 +16,14 @@ Discourse.StaffActionLog = Discourse.Model.extend({ formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value'); } if (!this.get('useModalForDetails')) { - if (this.get('details')) formatted += Handlebars.Utils.escapeExpression(this.get('details')) + '
'; + if (this.get('details')) formatted += Discourse.Utilities.escapeExpression(this.get('details')) + '
'; } return formatted; }.property('ip_address', 'email', 'topic_id', 'post_id'), format: function(label, propertyName) { if (this.get(propertyName)) { - return ('' + I18n.t(label) + ': ' + Handlebars.Utils.escapeExpression(this.get(propertyName)) + '
'); + return ('' + I18n.t(label) + ': ' + Discourse.Utilities.escapeExpression(this.get(propertyName)) + '
'); } else { return ''; } diff --git a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 index 9d164142aa3..c5ba44e27e7 100644 --- a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 +++ b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 @@ -19,7 +19,7 @@ export default Ember.View.extend({ let formattedLogs = this.get("formattedLogs"); for (let i = this.get("index"), length = logs.length; i < length; i++) { const date = logs[i].get("timestamp"), - message = Handlebars.Utils.escapeExpression(logs[i].get("message")); + message = Discourse.Utilities.escapeExpression(logs[i].get("message")); formattedLogs += "[" + date + "] " + message + "\n"; } // update the formatted logs & cache index diff --git a/app/assets/javascripts/discourse/components/notification-item.js.es6 b/app/assets/javascripts/discourse/components/notification-item.js.es6 index 2aa93e141f4..affad9ad638 100644 --- a/app/assets/javascripts/discourse/components/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/components/notification-item.js.es6 @@ -38,10 +38,10 @@ export default Ember.Component.extend({ description: function() { const badgeName = this.get("notification.data.badge_name"); - if (badgeName) { return Handlebars.Utils.escapeExpression(badgeName); } + if (badgeName) { return Discourse.Utilities.escapeExpression(badgeName); } const title = this.get('notification.data.topic_title'); - return Ember.isEmpty(title) ? "" : Handlebars.Utils.escapeExpression(title); + return Ember.isEmpty(title) ? "" : Discourse.Utilities.escapeExpression(title); }.property("notification.data.{badge_name,topic_title}"), _markRead: function(){ diff --git a/app/assets/javascripts/discourse/components/post-gutter.js.es6 b/app/assets/javascripts/discourse/components/post-gutter.js.es6 index 5b923febe33..9ed4776498d 100644 --- a/app/assets/javascripts/discourse/components/post-gutter.js.es6 +++ b/app/assets/javascripts/discourse/components/post-gutter.js.es6 @@ -45,7 +45,7 @@ export default Em.Component.extend(StringBuffer, { var title = Em.get(l, 'title'); if (!Em.isEmpty(title)) { - title = Handlebars.Utils.escapeExpression(title); + title = Discourse.Utilities.escapeExpression(title); buffer.push(Discourse.Emoji.unescape(title)); } if (clicks) { diff --git a/app/assets/javascripts/discourse/components/poster-name.js.es6 b/app/assets/javascripts/discourse/components/poster-name.js.es6 index 5a2a7bd8eab..7f213f7c00e 100644 --- a/app/assets/javascripts/discourse/components/poster-name.js.es6 +++ b/app/assets/javascripts/discourse/components/poster-name.js.es6 @@ -40,7 +40,7 @@ const PosterNameComponent = Em.Component.extend({ // Are we showing full names? if (name && this.get('displayNameOnPosts') && (this.sanitizeName(name) !== this.sanitizeName(username))) { - name = Handlebars.Utils.escapeExpression(name); + name = Discourse.Utilities.escapeExpression(name); buffer.push("" + name + ""); } @@ -48,7 +48,7 @@ const PosterNameComponent = Em.Component.extend({ let title = post.get('user_title'); if (!Em.isEmpty(title)) { - title = Handlebars.Utils.escapeExpression(title); + title = Discourse.Utilities.escapeExpression(title); buffer.push(''); if (Em.isEmpty(primaryGroupName)) { buffer.push(title); diff --git a/app/assets/javascripts/discourse/components/topic-status.js.es6 b/app/assets/javascripts/discourse/components/topic-status.js.es6 index 9b8c6445612..9576164b1bc 100644 --- a/app/assets/javascripts/discourse/components/topic-status.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-status.js.es6 @@ -29,7 +29,7 @@ export default Ember.Component.extend(StringBuffer, { const self = this; const renderIcon = function(name, key, actionable) { - const title = Handlebars.Utils.escapeExpression(I18n.t(`topic_statuses.${key}.help`)), + const title = Discourse.Utilities.escapeExpression(I18n.t(`topic_statuses.${key}.help`)), startTag = actionable ? "a href" : "span", endTag = actionable ? "a" : "span", iconArgs = key === 'unpinned' ? { 'class': 'unpinned' } : null, diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index b8328a87f3a..a91f019761b 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -265,7 +265,7 @@ export default Ember.Controller.extend({ if (currentTopic) { buttons.push({ - "label": I18n.t("composer.reply_here") + "
" + Handlebars.Utils.escapeExpression(currentTopic.get('title')) + "
", + "label": I18n.t("composer.reply_here") + "
" + Discourse.Utilities.escapeExpression(currentTopic.get('title')) + "
", "class": "btn btn-reply-here", "callback": function() { composer.set('topic', currentTopic); @@ -276,7 +276,7 @@ export default Ember.Controller.extend({ } buttons.push({ - "label": I18n.t("composer.reply_original") + "
" + Handlebars.Utils.escapeExpression(this.get('model.topic.title')) + "
", + "label": I18n.t("composer.reply_original") + "
" + Discourse.Utilities.escapeExpression(this.get('model.topic.title')) + "
", "class": "btn-primary btn-reply-on-original", "callback": function() { self.save(true); diff --git a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 index 64da750cfe0..afecb74ef9c 100644 --- a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 +++ b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 @@ -17,7 +17,7 @@ export default Ember.Controller.extend(ModalFunctionality, { var success = function(data) { // don't tell people what happened, this keeps it more secure (ensure same on server) - var escaped = Handlebars.Utils.escapeExpression(self.get('accountEmailOrUsername')); + var escaped = Discourse.Utilities.escapeExpression(self.get('accountEmailOrUsername')); var isEmail = self.get('accountEmailOrUsername').match(/@/); var key = 'forgot_password.complete_' + (isEmail ? 'email' : 'username'); diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 index 173b14cb502..967ef2df534 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -75,7 +75,7 @@ export default Ember.Controller.extend({ } }); } - return Handlebars.Utils.escapeExpression(q); + return Discourse.Utilities.escapeExpression(q); }, _searchOnSortChange: true, diff --git a/app/assets/javascripts/discourse/dialects/code_dialect.js b/app/assets/javascripts/discourse/dialects/code_dialect.js index 2594cd8316d..8e2b5bddf24 100644 --- a/app/assets/javascripts/discourse/dialects/code_dialect.js +++ b/app/assets/javascripts/discourse/dialects/code_dialect.js @@ -76,7 +76,7 @@ Discourse.Dialect.on('parseNode', function (event) { } else { regexp = /^ +| +$/g; } - node[node.length-1] = Handlebars.Utils.escapeExpression(contents.replace(regexp,'')); + node[node.length-1] = Discourse.Utilities.escapeExpression(contents.replace(regexp,'')); } }); diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js index 1614c1c0f19..bf9dd3f34dc 100644 --- a/app/assets/javascripts/discourse/dialects/dialect.js +++ b/app/assets/javascripts/discourse/dialects/dialect.js @@ -13,7 +13,7 @@ var parser = window.BetterMarkdown, emitters = [], hoisted, preProcessors = [], - escape = Handlebars.Utils.escapeExpression; + escape = Discourse.Utilities.escapeExpression; /** Initialize our dialects for processing. diff --git a/app/assets/javascripts/discourse/helpers/user-status.js.es6 b/app/assets/javascripts/discourse/helpers/user-status.js.es6 index 7a5ddbd78f0..e40197b7b51 100644 --- a/app/assets/javascripts/discourse/helpers/user-status.js.es6 +++ b/app/assets/javascripts/discourse/helpers/user-status.js.es6 @@ -5,7 +5,7 @@ const Safe = Handlebars.SafeString; export default Ember.Handlebars.makeBoundHelper(function(user, args) { if (!user) { return; } - const name = Handlebars.Utils.escapeExpression(user.get('name')); + const name = Discourse.Utilities.escapeExpression(user.get('name')); const currentUser = args.hash.currentUser; if (currentUser && user.get('admin') && currentUser.get('staff')) { diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index f398e791033..c37fba12978 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -1,3 +1,18 @@ + +var discourseEscape = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + '`': '`' +}; +var discourseBadChars = /[&<>"'`]/g; +var discoursePossible = /[&<>"'`]/; + +function discourseEscapeChar(chr) { + return discourseEscape[chr]; +} Discourse.Utilities = { translateSize: function(size) { @@ -24,6 +39,28 @@ Discourse.Utilities = { } }, + // Handlebars no longer allows spaces in its `escapeExpression` code which makes it + // unsuitable for many of Discourse's uses. Use `Handlebars.Utils.escapeExpression` + // when escaping an attribute in HTML, otherwise this one will do. + escapeExpression: function(string) { + // don't escape SafeStrings, since they're already safe + if (string instanceof Handlebars.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(!discoursePossible.test(string)) { return string; } + return string.replace(discourseBadChars, discourseEscapeChar); + }, + avatarUrl: function(template, size) { if (!template) { return ""; } var rawSize = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize(size)); diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 366a55e73e9..e9bed9f90ee 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -138,7 +138,7 @@ const Composer = RestModel.extend({ const postNumber = this.get('post.post_number'); postLink = "" + I18n.t("post.post_number", { number: postNumber }) + ""; - topicLink = " " + (Handlebars.Utils.escapeExpression(topic.get('title'))) + ""; + topicLink = " " + Discourse.Utilities.escapeExpression(topic.get('title')) + ""; usernameLink = "" + this.get('post.username') + ""; } diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 757f7308ec3..4991279ece8 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -73,6 +73,8 @@ //= require ./discourse/components/topic-notifications-button //= require ./discourse/lib/link-mentions //= require ./discourse/views/header +//= require ./discourse/lib/utilities +//= require ./discourse/dialects/dialect //= require ./discourse/views/composer //= require ./discourse/lib/show-modal //= require ./discourse/lib/screen-track diff --git a/lib/freedom_patches/ember_compat_handlebars.rb b/lib/freedom_patches/ember_compat_handlebars.rb index b3cdd4285ef..bfffaeddead 100644 --- a/lib/freedom_patches/ember_compat_handlebars.rb +++ b/lib/freedom_patches/ember_compat_handlebars.rb @@ -5,7 +5,7 @@ module Barber class EmberCompatPrecompiler < Barber::Precompiler def sources - [handlebars, precompiler] + [File.open("#{Rails.root}/vendor/assets/javascripts/handlebars.js"), precompiler] end def precompiler diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 1b43959482c..d2a9850c928 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -78,9 +78,9 @@ module PrettyText ctx_load(ctx, "vendor/assets/javascripts/better_markdown.js", "app/assets/javascripts/defer/html-sanitizer-bundle.js", + "app/assets/javascripts/discourse/lib/utilities.js", "app/assets/javascripts/discourse/dialects/dialect.js", "app/assets/javascripts/discourse/lib/censored-words.js", - "app/assets/javascripts/discourse/lib/utilities.js", "app/assets/javascripts/discourse/lib/markdown.js", ) diff --git a/vendor/assets/javascripts/handlebars.js b/vendor/assets/javascripts/handlebars.js index f826bbfd387..04bb23566fc 100644 --- a/vendor/assets/javascripts/handlebars.js +++ b/vendor/assets/javascripts/handlebars.js @@ -64,11 +64,16 @@ var __module3__ = (function(__dependency1__) { ">": ">", '"': """, "'": "'", - "`": "`" + '`': '`', + '\n' : '\\n', // NewLine + '\r' : '\\n', // Return + '\b' : '\\b', // Backspace + '\f' : '\\f', // Form fee + '\t' : '\\t', // Tab + '\v' : '\\v' // Vertical Tab }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; + var badChars = /[&<>"'`\b\f\n\r\t\v]/g; + var possible = /[&<>"'`\b\f\n\r\t\v]/; function escapeChar(chr) { return escape[chr];