From c58b495e154079e6afc944e29339e2961a87a090 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 12 Jun 2015 00:31:43 +1000 Subject: [PATCH] SECURITY: Query @usernames in bulk Otherwise you could add many requests at once while composing. --- .../discourse/lib/link-mentions.js.es6 | 56 ++++++++++++++++++ .../javascripts/discourse/lib/mention.js | 59 ------------------- .../discourse/views/composer.js.es6 | 8 +-- app/assets/javascripts/main_include.js | 1 + app/controllers/users_controller.rb | 10 ++-- 5 files changed, 67 insertions(+), 67 deletions(-) create mode 100644 app/assets/javascripts/discourse/lib/link-mentions.js.es6 delete mode 100644 app/assets/javascripts/discourse/lib/mention.js diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 new file mode 100644 index 00000000000..084564291f7 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 @@ -0,0 +1,56 @@ +function replaceSpan($e, username) { + $e.replaceWith("@" + username + ""); +} + +const found = []; +const checked = []; + +function updateFound($mentions, usernames) { + Ember.run.scheduleOnce('afterRender', function() { + $mentions.each((i, e) => { + const $e = $(e); + const username = usernames[i]; + if (found.indexOf(username) !== -1) { + replaceSpan($e, username); + } else { + $e.removeClass('mention-loading').addClass('mention-tested'); + } + }); + }); +} + + +let linking = false; +export default function linkMentions($elem) { + if (linking) { return Ember.RSVP.Promise.resolve(); } + linking = true; + + return new Ember.RSVP.Promise(function(resolve) { + const $mentions = $('span.mention:not(.mention-tested):not(.mention-loading)', $elem); + if ($mentions.length) { + const usernames = $mentions.map((_, e) => $(e).text().substr(1).toLowerCase()); + + if (usernames.length) { + $mentions.addClass('mention-loading'); + const uncached = _.uniq(usernames).filter((u) => { return checked.indexOf(u) === -1; }); + + if (uncached.length) { + return Discourse.ajax("/users/is_local_username", { + data: { usernames: uncached} + }).then(function(r) { + found.push.apply(found, r.valid); + checked.push.apply(checked, uncached); + updateFound($mentions, usernames); + resolve(); + }); + } else { + updateFound($mentions, usernames); + } + } + } + + resolve(); + }).finally(() => { linking = false }); +} diff --git a/app/assets/javascripts/discourse/lib/mention.js b/app/assets/javascripts/discourse/lib/mention.js deleted file mode 100644 index 7668d7bbc93..00000000000 --- a/app/assets/javascripts/discourse/lib/mention.js +++ /dev/null @@ -1,59 +0,0 @@ - -// A local cache lookup -var localCache = []; - - -/** - Lookup a username and return whether it is exists or not. - - @function lookup - @param {String} username to look up - @return {Promise} promise that results to whether the name was found or not -**/ -function lookup(username) { - return new Em.RSVP.Promise(function (resolve) { - var cached = localCache[username]; - - // If we have a cached answer, return it - if (typeof cached !== "undefined") { - resolve(cached); - } else { - Discourse.ajax("/users/is_local_username", { data: { username: username } }).then(function(r) { - localCache[username] = r.valid; - resolve(r.valid); - }); - } - }); -} - -/** - Help us link directly to a mentioned user's profile if the username exists. - - @class Mention - @namespace Discourse - @module Discourse -**/ -Discourse.Mention = { - - /** - Paints an element in the DOM with the appropriate classes and markup if the username - it is mentioning exists. - - @method paint - @param {Element} the element in the DOM to decorate - **/ - paint: function(e) { - var $elem = $(e); - if ($elem.data('mention-tested')) return; - var username = $elem.text().substr(1); - - $elem.addClass('mention-loading'); - lookup(username).then(function(found) { - if (found) { - $elem.replaceWith("@" + username + ""); - } else { - $elem.removeClass('mention-loading').addClass('mention-tested'); - } - }); - } -}; diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 6c6ab4ea65b..03b5c50eb60 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -3,6 +3,7 @@ import afterTransition from 'discourse/lib/after-transition'; import loadScript from 'discourse/lib/load-script'; import avatarTemplate from 'discourse/lib/avatar-template'; import positioningWorkaround from 'discourse/lib/safari-hacks'; +import linkMentions from 'discourse/lib/link-mentions'; const ComposerView = Discourse.View.extend(Ember.Evented, { _lastKeyTimeout: null, @@ -175,11 +176,10 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { $('a.onebox', $wmdPreview).each(function(i, e) { Discourse.Onebox.load(e, refresh); }); - $('span.mention', $wmdPreview).each(function(i, e) { - Discourse.Mention.paint(e); - }); - this.trigger('previewRefreshed', $wmdPreview); + linkMentions($wmdPreview).then(() => { + this.trigger('previewRefreshed', $wmdPreview); + }); }, _applyEmojiAutocomplete() { diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index cf5bbda7b19..9a3b8c092d3 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -48,6 +48,7 @@ //= require ./discourse/components/dropdown-button //= require ./discourse/components/notifications-button //= require ./discourse/components/topic-notifications-button +//= require ./discourse/lib/link-mentions //= require ./discourse/views/composer //= require ./discourse/lib/show-modal //= require ./discourse/routes/discourse diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 13bf9b2821c..3a10834b85c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -175,10 +175,12 @@ class UsersController < ApplicationController end def is_local_username - params.require(:username) - u = params[:username].downcase - r = User.exec_sql('select 1 from users where username_lower = ?', u).values - render json: {valid: r.length == 1} + users = params[:usernames] + users = [params[:username]] if users.blank? + users.each(&:downcase!) + + result = User.where(username_lower: users).pluck(:username_lower) + render json: {valid: result} end def render_available_true