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