diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js
index 27b12c53ab..feb375d90c 100644
--- a/app/assets/javascripts/discourse.js
+++ b/app/assets/javascripts/discourse.js
@@ -24,6 +24,12 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
return u + url;
},
+ getURLWithCDN: function(url) {
+ url = this.getURL(url);
+ if (Discourse.CDN) { url = Discourse.CDN + url; }
+ return url;
+ },
+
Resolver: DiscourseResolver,
_titleChanged: function() {
diff --git a/app/assets/javascripts/discourse/dialects/quote_dialect.js b/app/assets/javascripts/discourse/dialects/quote_dialect.js
index b0b8f5bbb8..3c50598cfc 100644
--- a/app/assets/javascripts/discourse/dialects/quote_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/quote_dialect.js
@@ -1,4 +1,5 @@
var esc = Handlebars.Utils.escapeExpression;
+
Discourse.BBCode.register('quote', {noWrap: true, singlePara: true}, function(contents, bbParams, options) {
var params = {'class': 'quote'},
username = null;
diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js
index 7a5bd5c37c..fde7692255 100644
--- a/app/assets/javascripts/discourse/models/user.js
+++ b/app/assets/javascripts/discourse/models/user.js
@@ -72,10 +72,9 @@ Discourse.User = Discourse.Model.extend({
@type {String}
**/
profileBackground: function() {
- var background = this.get('profile_background');
- if(Em.isEmpty(background) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; }
-
- return 'background-image: url(' + background + ')';
+ var url = this.get('profile_background');
+ if (Em.isEmpty(url) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; }
+ return 'background-image: url(' + Discourse.getURLWithCDN(url) + ')';
}.property('profile_background'),
/**
@@ -442,6 +441,7 @@ Discourse.User.reopenClass(Discourse.Singleton, {
avatarTemplate: function(username, uploadedAvatarId) {
var url;
+
if (uploadedAvatarId) {
url = "/user_avatar/" +
Discourse.BaseUrl +
@@ -456,11 +456,7 @@ Discourse.User.reopenClass(Discourse.Singleton, {
Discourse.LetterAvatarVersion + ".png";
}
- url = Discourse.getURL(url);
- if (Discourse.CDN) {
- url = Discourse.CDN + url;
- }
- return url;
+ return Discourse.getURLWithCDN(url);
},
/**
diff --git a/app/assets/javascripts/discourse/views/user-card.js.es6 b/app/assets/javascripts/discourse/views/user-card.js.es6
index 1239420297..66e62b88d5 100644
--- a/app/assets/javascripts/discourse/views/user-card.js.es6
+++ b/app/assets/javascripts/discourse/views/user-card.js.es6
@@ -11,6 +11,7 @@ export default Discourse.View.extend(CleansUp, {
addBackground: function() {
var url = this.get('controller.user.card_background');
+
if (!this.get('allowBackgrounds')) { return; }
var $this = this.$();
@@ -19,7 +20,7 @@ export default Discourse.View.extend(CleansUp, {
if (Ember.isEmpty(url)) {
$this.css('background-image', '').addClass('no-bg');
} else {
- $this.css('background-image', "url(" + url + ")").removeClass('no-bg');
+ $this.css('background-image', "url(" + Discourse.getURLWithCDN(url) + ")").removeClass('no-bg');
}
}.observes('controller.user.card_background'),
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b3158c509f..ff805687b4 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -155,7 +155,7 @@ class ApplicationController < ActionController::Base
# If we are rendering HTML, preload the session data
def preload_json
# We don't preload JSON on xhr or JSON request
- return if request.xhr?
+ return if request.xhr? || request.format.json?
preload_anonymous_data
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 7cd44ce553..34a5a8c353 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -5,7 +5,7 @@ require_dependency 'distributed_memoizer'
class PostsController < ApplicationController
# Need to be logged in for all actions here
- before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :reply_history, :revisions, :latest_revision, :expand_embed, :markdown, :raw, :cooked]
+ before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :reply_history, :revisions, :latest_revision, :expand_embed, :markdown_id, :markdown_num, :cooked, :latest]
skip_before_filter :check_xhr, only: [:markdown_id, :markdown_num, :short_link]
@@ -25,6 +25,33 @@ class PostsController < ApplicationController
end
end
+ def latest
+ params.permit(:before)
+ last_post_id = params[:before].to_i
+ last_post_id = Post.last.id if last_post_id <= 0
+
+ # last 50 post IDs only, to avoid counting deleted posts in security check
+ posts = Post.order(created_at: :desc)
+ .where('posts.id <= ?', last_post_id)
+ .where('posts.id > ?', last_post_id - 50)
+ .includes(topic: :category)
+ .includes(:user)
+ .limit(50)
+ # Remove posts the user doesn't have permission to see
+ # This isn't leaking any information we weren't already through the post ID numbers
+ posts = posts.reject { |post| !guardian.can_see?(post) }
+
+ counts = PostAction.counts_for(posts, current_user)
+
+ render_json_dump(serialize_data(posts,
+ PostSerializer,
+ scope: guardian,
+ root: 'latest_posts',
+ add_raw: true,
+ all_post_actions: counts)
+ )
+ end
+
def cooked
post = find_post_from_params
render json: {cooked: post.cooked}
diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb
index 854dee20ef..7b68761902 100644
--- a/app/models/concerns/has_custom_fields.rb
+++ b/app/models/concerns/has_custom_fields.rb
@@ -78,17 +78,6 @@ module HasCustomFields
!@custom_fields || @custom_fields_orig == @custom_fields
end
- protected
-
- def refresh_custom_fields_from_db
- target = Hash.new
- _custom_fields.pluck(:name,:value).each do |key, value|
- self.class.append_custom_field(target, key, value)
- end
- @custom_fields_orig = target
- @custom_fields = @custom_fields_orig.dup
- end
-
def save_custom_fields
if !custom_fields_clean?
dup = @custom_fields.dup
@@ -134,4 +123,16 @@ module HasCustomFields
refresh_custom_fields_from_db
end
end
+
+ protected
+
+ def refresh_custom_fields_from_db
+ target = Hash.new
+ _custom_fields.pluck(:name,:value).each do |key, value|
+ self.class.append_custom_field(target, key, value)
+ end
+ @custom_fields_orig = target
+ @custom_fields = @custom_fields_orig.dup
+ end
+
end
diff --git a/app/models/user.rb b/app/models/user.rb
index b90c5544d2..4a8cd72b09 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -62,7 +62,7 @@ class User < ActiveRecord::Base
delegate :last_sent_email_address, :to => :email_logs
- before_validation :downcase_email
+ before_validation :strip_downcase_email
validates_presence_of :username
validate :username_validator
@@ -764,8 +764,11 @@ class User < ActiveRecord::Base
self.username_lower = username.downcase
end
- def downcase_email
- self.email = self.email.downcase if self.email
+ def strip_downcase_email
+ if self.email
+ self.email = self.email.strip
+ self.email = self.email.downcase
+ end
end
def username_validator
diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb
index 2a1b31d88b..3496159a52 100644
--- a/app/serializers/post_serializer.rb
+++ b/app/serializers/post_serializer.rb
@@ -1,12 +1,17 @@
class PostSerializer < BasicPostSerializer
# To pass in additional information we might need
- attr_accessor :topic_view,
+ INSTANCE_VARS = [:topic_view,
:parent_post,
:add_raw,
:single_post_link_counts,
:draft_sequence,
- :post_actions
+ :post_actions,
+ :all_post_actions]
+
+ INSTANCE_VARS.each do |v|
+ self.send(:attr_accessor, v)
+ end
attributes :post_number,
:post_type,
@@ -54,6 +59,15 @@ class PostSerializer < BasicPostSerializer
:static_doc,
:via_email
+ def initialize(object, opts)
+ super(object, opts)
+ PostSerializer::INSTANCE_VARS.each do |name|
+ if opts.include? name
+ self.send("#{name}=", opts[name])
+ end
+ end
+ end
+
def topic_slug
object.try(:topic).try(:slug)
end
@@ -155,6 +169,13 @@ class PostSerializer < BasicPostSerializer
scope.is_staff? && object.deleted_by.present?
end
+ # Helper function to decide between #post_actions and @all_post_actions
+ def actions
+ return post_actions if post_actions.present?
+ return all_post_actions[object.id] if all_post_actions.present?
+ nil
+ end
+
# Summary of the actions taken on this post
def actions_summary
result = []
@@ -168,7 +189,7 @@ class PostSerializer < BasicPostSerializer
id: id,
count: count,
hidden: (sym == :vote),
- can_act: scope.post_can_act?(object, sym, taken_actions: post_actions)
+ can_act: scope.post_can_act?(object, sym, taken_actions: actions)
}
if sym == :notify_user && scope.current_user.present? && scope.current_user == object.user
@@ -183,9 +204,9 @@ class PostSerializer < BasicPostSerializer
active_flags[id].count > 0
end
- if post_actions.present? && post_actions.has_key?(id)
+ if actions.present? && actions.has_key?(id)
action_summary[:acted] = true
- action_summary[:can_undo] = scope.can_delete?(post_actions[id])
+ action_summary[:can_undo] = scope.can_delete?(actions[id])
end
# only show public data
@@ -226,7 +247,7 @@ class PostSerializer < BasicPostSerializer
end
def include_bookmarked?
- post_actions.present? && post_actions.keys.include?(PostActionType.types[:bookmark])
+ actions.present? && actions.keys.include?(PostActionType.types[:bookmark])
end
def include_display_username?
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index b8470eb786..a75be34e8d 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1013,7 +1013,7 @@ en:
disable_edit_notifications: "Disables edit notifications by the system user when 'download_remote_images_to_local' is active."
- enable_names: "Allow showing user full names. Disable to hide full names."
+ enable_names: "Show the user's full name on their profile, user card, and emails. Disable to hide full name everywhere."
display_name_on_posts: "Show a user's full name on their posts in addition to their @username."
invites_per_page: "Default invites shown on the user page."
short_progress_text_threshold: "After the number of posts in a topic goes above this number, the progress bar will only show the current post number. If you change the progress bar's width, you may need to change this value."
diff --git a/config/routes.rb b/config/routes.rb
index 3c288b57d0..8a41b8be10 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -269,6 +269,7 @@ Discourse::Application.routes.draw do
get "uploads/:site/:sha" => "uploads#show", constraints: { site: /\w+/, sha: /[a-z0-9]{40}/}
post "uploads" => "uploads#create"
+ get "posts" => "posts#latest"
get "posts/by_number/:topic_id/:post_number" => "posts#by_number"
get "posts/:id/reply-history" => "posts#reply_history"
get "posts/:username/deleted" => "posts#deleted_posts", constraints: {username: USERNAME_ROUTE_FORMAT}
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index ee5aa57833..d095bd0c6a 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -1,11 +1,13 @@
require 'v8'
require 'nokogiri'
+require_dependency 'url_helper'
require_dependency 'excerpt_parser'
require_dependency 'post'
module PrettyText
class Helpers
+ include UrlHelper
def t(key, opts)
key = "js." + key
@@ -21,15 +23,15 @@ module PrettyText
# function here are available to v8
def avatar_template(username)
return "" unless username
-
user = User.find_by(username_lower: username.downcase)
- user.avatar_template if user.present?
+ return "" unless user.present?
+ schemaless absolute user.avatar_template
end
def is_username_valid(username)
return false unless username
username = username.downcase
- return User.exec_sql('SELECT 1 FROM users WHERE username_lower = ?', username).values.length == 1
+ User.exec_sql('SELECT 1 FROM users WHERE username_lower = ?', username).values.length == 1
end
end
@@ -128,7 +130,9 @@ module PrettyText
context.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
context.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';")
context.eval("Discourse.BaseUrl = 'http://#{RailsMultisite::ConnectionManagement.current_hostname}';")
- context.eval("Discourse.getURL = function(url) {return '#{Discourse::base_uri}' + url};")
+
+ context.eval("Discourse.getURL = function(url) { return '#{Discourse::base_uri}' + url };")
+ context.eval("Discourse.getURLWithCDN = function(url) { url = Discourse.getURL(url); if (Discourse.CDN) { url = Discourse.CDN + url; } return url; };")
end
def self.markdown(text, opts=nil)
diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb
index 7dda67ae99..00bb220717 100644
--- a/spec/components/pretty_text_spec.rb
+++ b/spec/components/pretty_text_spec.rb
@@ -12,20 +12,20 @@ describe PrettyText do
before(:each) do
eviltrout = User.new
- eviltrout.stubs(:avatar_template).returns("http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png")
+ eviltrout.stubs(:avatar_template).returns("//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png")
User.expects(:find_by).with(username_lower: "eviltrout").returns(eviltrout)
end
it "produces a quote even with new lines in it" do
- expect(PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]")).to match_html ""
+ expect(PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]")).to match_html ""
end
it "should produce a quote" do
- expect(PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]")).to match_html ""
+ expect(PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]")).to match_html ""
end
it "trims spaces on quote params" do
- expect(PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]")).to match_html ""
+ expect(PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]")).to match_html ""
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a13dc476f3..69bd8cb3a6 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -261,8 +261,14 @@ describe User do
it "downcases email addresses" do
user = Fabricate.build(:user, email: 'Fancy.Caps.4.U@gmail.com')
- user.save
- expect(user.reload.email).to eq('fancy.caps.4.u@gmail.com')
+ user.valid?
+ expect(user.email).to eq('fancy.caps.4.u@gmail.com')
+ end
+
+ it "strips whitespace from email addresses" do
+ user = Fabricate.build(:user, email: ' example@gmail.com ')
+ user.valid?
+ expect(user.email).to eq('example@gmail.com')
end
end