SECURITY: Query @usernames in bulk

Otherwise you could add many requests at once while composing.
This commit is contained in:
Sam Saffron 2015-06-12 00:31:43 +10:00 committed by Robin Ward
parent 9572b28986
commit c58b495e15
5 changed files with 67 additions and 67 deletions

View File

@ -0,0 +1,56 @@
function replaceSpan($e, username) {
$e.replaceWith("<a href='" +
Discourse.getURL("/users/") + username.toLowerCase() +
"' class='mention'>@" + username + "</a>");
}
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 });
}

View File

@ -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("<a href='" + Discourse.getURL("/users/") + username.toLowerCase() + "' class='mention'>@" + username + "</a>");
} else {
$elem.removeClass('mention-loading').addClass('mention-tested');
}
});
}
};

View File

@ -3,6 +3,7 @@ import afterTransition from 'discourse/lib/after-transition';
import loadScript from 'discourse/lib/load-script'; import loadScript from 'discourse/lib/load-script';
import avatarTemplate from 'discourse/lib/avatar-template'; import avatarTemplate from 'discourse/lib/avatar-template';
import positioningWorkaround from 'discourse/lib/safari-hacks'; import positioningWorkaround from 'discourse/lib/safari-hacks';
import linkMentions from 'discourse/lib/link-mentions';
const ComposerView = Discourse.View.extend(Ember.Evented, { const ComposerView = Discourse.View.extend(Ember.Evented, {
_lastKeyTimeout: null, _lastKeyTimeout: null,
@ -175,11 +176,10 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
$('a.onebox', $wmdPreview).each(function(i, e) { $('a.onebox', $wmdPreview).each(function(i, e) {
Discourse.Onebox.load(e, refresh); Discourse.Onebox.load(e, refresh);
}); });
$('span.mention', $wmdPreview).each(function(i, e) {
Discourse.Mention.paint(e);
});
linkMentions($wmdPreview).then(() => {
this.trigger('previewRefreshed', $wmdPreview); this.trigger('previewRefreshed', $wmdPreview);
});
}, },
_applyEmojiAutocomplete() { _applyEmojiAutocomplete() {

View File

@ -48,6 +48,7 @@
//= require ./discourse/components/dropdown-button //= require ./discourse/components/dropdown-button
//= require ./discourse/components/notifications-button //= require ./discourse/components/notifications-button
//= require ./discourse/components/topic-notifications-button //= require ./discourse/components/topic-notifications-button
//= require ./discourse/lib/link-mentions
//= require ./discourse/views/composer //= require ./discourse/views/composer
//= require ./discourse/lib/show-modal //= require ./discourse/lib/show-modal
//= require ./discourse/routes/discourse //= require ./discourse/routes/discourse

View File

@ -175,10 +175,12 @@ class UsersController < ApplicationController
end end
def is_local_username def is_local_username
params.require(:username) users = params[:usernames]
u = params[:username].downcase users = [params[:username]] if users.blank?
r = User.exec_sql('select 1 from users where username_lower = ?', u).values users.each(&:downcase!)
render json: {valid: r.length == 1}
result = User.where(username_lower: users).pluck(:username_lower)
render json: {valid: result}
end end
def render_available_true def render_available_true