- {{#if grandChild.edit_reason}} — {{unbound grandChild.edit_reason}}{{/if}}
{{/each}}
{{/each}}
From 4cb6606e8ca2f617a454d49da5d3f07144ed6f9c Mon Sep 17 00:00:00 2001
From: Jeff Atwood
Date: Mon, 19 Jan 2015 01:19:34 -0800
Subject: [PATCH 052/230] block some more dumb trackback spam from logging
---
config/initializers/logster.rb | 3 +++
1 file changed, 3 insertions(+)
diff --git a/config/initializers/logster.rb b/config/initializers/logster.rb
index a86d0474b7..3e89656075 100644
--- a/config/initializers/logster.rb
+++ b/config/initializers/logster.rb
@@ -25,6 +25,9 @@ if Rails.env.production?
# suppress trackback spam bots
Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { REQUEST_URI: /\/trackback\/$/ }),
+ # suppress trackback spam bots submitting to random URLs
+ # test for the presence of these params: url, title, excerpt, blog_name
+ Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { params: { url: /./, title: /./, excerpt: /./, blog_name: /./} },
# API calls, TODO fix this in rails
Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { REQUEST_URI: /api_key/ })
From dae39b5b71021a42c4b90612f5676b28209894eb Mon Sep 17 00:00:00 2001
From: Jeff Atwood
Date: Mon, 19 Jan 2015 01:29:02 -0800
Subject: [PATCH 053/230] missed closing paren
---
config/initializers/logster.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/initializers/logster.rb b/config/initializers/logster.rb
index 3e89656075..48abc05567 100644
--- a/config/initializers/logster.rb
+++ b/config/initializers/logster.rb
@@ -27,7 +27,7 @@ if Rails.env.production?
Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { REQUEST_URI: /\/trackback\/$/ }),
# suppress trackback spam bots submitting to random URLs
# test for the presence of these params: url, title, excerpt, blog_name
- Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { params: { url: /./, title: /./, excerpt: /./, blog_name: /./} },
+ Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { params: { url: /./, title: /./, excerpt: /./, blog_name: /./} }),
# API calls, TODO fix this in rails
Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { REQUEST_URI: /api_key/ })
From 5287669116f1821bfd99874bf54e4125e5985e40 Mon Sep 17 00:00:00 2001
From: Arpit Jalan
Date: Mon, 19 Jan 2015 15:21:39 +0530
Subject: [PATCH 054/230] :lipstick: simplify utf-8 conversion
---
lib/email/receiver.rb | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb
index 44857065e3..900108c531 100644
--- a/lib/email/receiver.rb
+++ b/lib/email/receiver.rb
@@ -146,9 +146,7 @@ module Email
return nil if object.nil?
if object.charset
- # convert UTF8 charset to UTF-8
- object.charset = object.charset.gsub(/utf8/i, "UTF-8") if object.charset.downcase == "utf8"
- object.body.decoded.force_encoding(object.charset).encode("UTF-8").to_s
+ object.body.decoded.force_encoding(object.charset.gsub(/utf8/i, "UTF-8")).encode("UTF-8").to_s
else
object.body.to_s
end
From 7687c95e7b24fa27a29601a86784ff83185022f9 Mon Sep 17 00:00:00 2001
From: Arpit Jalan
Date: Mon, 19 Jan 2015 19:21:53 +0530
Subject: [PATCH 055/230] UX: add file size in CSV export notification
---
app/jobs/regular/export_csv_file.rb | 4 +++-
config/locales/server.en.yml | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb
index 9b2d319261..913c7593d0 100644
--- a/app/jobs/regular/export_csv_file.rb
+++ b/app/jobs/regular/export_csv_file.rb
@@ -4,6 +4,8 @@ require_dependency 'system_message'
module Jobs
class ExportCsvFile < Jobs::Base
+ include ActionView::Helpers::NumberHelper
+
HEADER_ATTRS_FOR = {}
HEADER_ATTRS_FOR['user_archive'] = ['topic_title','category','sub_category','is_pm','post','like_count','reply_count','url','created_at']
HEADER_ATTRS_FOR['user_list'] = ['id','name','username','email','title','created_at','trust_level','active','admin','moderator','ip_address']
@@ -288,7 +290,7 @@ module Jobs
def notify_user
if @current_user
if @file_name != "" && File.exists?("#{UserExport.base_directory}/#{@file_name}.gz")
- SystemMessage.create_from_system_user(@current_user, :csv_export_succeeded, download_link: "#{Discourse.base_url}/export_csv/#{@file_name}.gz", file_name: "#{@file_name}.gz")
+ SystemMessage.create_from_system_user(@current_user, :csv_export_succeeded, download_link: "#{Discourse.base_url}/export_csv/#{@file_name}.gz", file_name: "#{@file_name}.gz", file_size: number_to_human_size(File.size("#{UserExport.base_directory}/#{@file_name}.gz")))
else
SystemMessage.create_from_system_user(@current_user, :csv_export_failed)
end
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index c225240f29..eb81c14712 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1485,7 +1485,7 @@ en:
text_body_template: |
Your data export was successful! :dvd:
- %{file_name}
+ %{file_name} (%{file_size})
The above download link will be valid for 48 hours.
From 6c4d85201148b73f097f8ea41dc46d8afd6e2efa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?=
Date: Mon, 19 Jan 2015 15:00:55 +0100
Subject: [PATCH 056/230] Improve vBulletin importer
- FEATURE: TopicCreator now supports 'pinned_at' parameter
- FIX: :bug: FIX TopicQuerySQL to support pinned topic older than 2010
- FIX: :bug: Properly remove all HTML Entities from Usernames/Titles/Category Names/Groups in vBulletin importer
- FIX: :bug: Properly handle specific vBulletin BBCode (quotes/mentions)
- FIX: :bug: Make sure we generate a username from the name of the user instead of a fake email
- FEATURE: Allow for custom timezone in vBulletin importer
- FEATURE: Support for profile pictures/background in vBulletin importer
- FIX: :bug: merge the categories tree to only 2 levels in vBulletin importer
---
lib/post_creator.rb | 2 +
lib/topic_creator.rb | 2 +
lib/topic_query_sql.rb | 2 +-
script/import_scripts/base.rb | 2 +-
script/import_scripts/vbulletin.rb | 241 ++++++++++++++++++++++++-----
5 files changed, 208 insertions(+), 41 deletions(-)
diff --git a/lib/post_creator.rb b/lib/post_creator.rb
index bd21db5e9e..d87aa172e9 100644
--- a/lib/post_creator.rb
+++ b/lib/post_creator.rb
@@ -41,6 +41,8 @@ class PostCreator
# target_usernames - comma delimited list of usernames for membership (private message)
# target_group_names - comma delimited list of groups for membership (private message)
# meta_data - Topic meta data hash
+ # created_at - Topic creation time (optional)
+ # pinned_at - Topic pinned time (optional)
#
def initialize(user, opts)
# TODO: we should reload user in case it is tainted, should take in a user_id as opposed to user
diff --git a/lib/topic_creator.rb b/lib/topic_creator.rb
index 035af60887..179e8d2333 100644
--- a/lib/topic_creator.rb
+++ b/lib/topic_creator.rb
@@ -86,6 +86,8 @@ class TopicCreator
topic_params[:created_at] = Time.zone.parse(@opts[:created_at].to_s) if @opts[:created_at].present?
+ topic_params[:pinned_at] = Time.zone.parse(@opts[:pinned_at].to_s) if @opts[:pinned_at].present?
+
topic_params
end
diff --git a/lib/topic_query_sql.rb b/lib/topic_query_sql.rb
index 97d4598209..9decf581a6 100644
--- a/lib/topic_query_sql.rb
+++ b/lib/topic_query_sql.rb
@@ -6,7 +6,7 @@ module TopicQuerySQL
class << self
def lowest_date
- "2010-01-01"
+ "1900-01-01"
end
def order_by_category_sql(dir)
diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb
index 09d0c805b6..664a3088f6 100644
--- a/script/import_scripts/base.rb
+++ b/script/import_scripts/base.rb
@@ -229,7 +229,7 @@ class ImportScripts::Base
results.each do |result|
u = yield(result)
- # block returns nil to skip a post
+ # block returns nil to skip a user
if u.nil?
users_skipped += 1
else
diff --git a/script/import_scripts/vbulletin.rb b/script/import_scripts/vbulletin.rb
index e847fc083d..dda4f29671 100644
--- a/script/import_scripts/vbulletin.rb
+++ b/script/import_scripts/vbulletin.rb
@@ -1,14 +1,21 @@
require File.expand_path(File.dirname(__FILE__) + "/base.rb")
require 'mysql2'
+require 'htmlentities'
class ImportScripts::VBulletin < ImportScripts::Base
-
- DATABASE = "iref"
BATCH_SIZE = 1000
+ # CHANGE THESE BEFORE RUNNING THE IMPORTER
+ DATABASE = "iref"
+ TIMEZONE = "Asia/Kolkata"
+
def initialize
super
+ @tz = TZInfo::Timezone.get(TIMEZONE)
+
+ @htmlentities = HTMLEntities.new
+
@client = Mysql2::Client.new(
host: "localhost",
username: "root",
@@ -24,6 +31,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
import_posts
close_topics
+ post_process_posts
end
def import_groups
@@ -37,8 +45,8 @@ class ImportScripts::VBulletin < ImportScripts::Base
create_groups(groups) do |group|
{
- id: group["usergroupid"].to_i,
- name: group["title"]
+ id: group["usergroupid"],
+ name: @htmlentities.decode(group["title"]).strip
}
end
end
@@ -50,6 +58,8 @@ class ImportScripts::VBulletin < ImportScripts::Base
user_count = mysql_query("SELECT COUNT(userid) count FROM user").first["count"]
+ # TODO: add email back in when using real data
+
batches(BATCH_SIZE) do |offset|
users = mysql_query <<-SQL
SELECT userid, username, homepage, usertitle, usergroupid, joindate
@@ -62,59 +72,118 @@ class ImportScripts::VBulletin < ImportScripts::Base
break if users.size < 1
create_users(users, total: user_count, offset: offset) do |user|
+ username = @htmlentities.decode(user["username"]).strip
+
{
- id: user["userid"].to_i,
- username: user["username"],
+ id: user["userid"],
+ name: username,
+ username: username,
email: user["email"].presence || fake_email,
- website: user["homepage"],
- title: user["usertitle"],
+ website: user["homepage"].strip,
+ title: @htmlentities.decode(user["usertitle"]).strip,
primary_group_id: group_id_from_imported_group_id(user["usergroupid"]),
- created_at: Time.at(user["joindate"].to_i),
+ created_at: parse_timestamp(user["joindate"]),
post_create_action: proc do |u|
@old_username_to_new_usernames[user["username"]] = u.username
+ import_profile_picture(user, u)
+ import_profile_background(user, u)
end
}
end
end
end
+ def import_profile_picture(old_user, imported_user)
+ query = mysql_query <<-SQL
+ SELECT filedata, filename
+ FROM customavatar
+ WHERE userid = #{old_user["userid"]}
+ ORDER BY dateline DESC
+ LIMIT 1
+ SQL
+
+ picture = query.first
+
+ return if picture.nil?
+
+ file = Tempfile.new("profile-picture")
+ file.write(picture["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
+ file.rewind
+
+ upload = Upload.create_for(imported_user.id, file, picture["filename"], file.size)
+
+ return if !upload.persisted?
+
+ imported_user.create_user_avatar
+ imported_user.user_avatar.update(custom_upload_id: upload.id)
+ imported_user.update(uploaded_avatar_id: upload.id)
+ ensure
+ file.close rescue nil
+ file.unlind rescue nil
+ end
+
+ def import_profile_background(old_user, imported_user)
+ query = mysql_query <<-SQL
+ SELECT filedata, filename
+ FROM customprofilepic
+ WHERE userid = #{old_user["userid"]}
+ ORDER BY dateline DESC
+ LIMIT 1
+ SQL
+
+ background = query.first
+
+ return if background.nil?
+
+ file = Tempfile.new("profile-background")
+ file.write(background["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
+ file.rewind
+
+ upload = Upload.create_for(imported_user.id, file, background["filename"], file.size)
+
+ return if !upload.persisted?
+
+ imported_user.user_profile.update(profile_background: upload.url)
+ ensure
+ file.close rescue nil
+ file.unlink rescue nil
+ end
+
def import_categories
puts "", "importing top level categories..."
- # TODO: deal with permissions
+ categories = mysql_query("SELECT forumid, title, description, displayorder, parentid FROM forum ORDER BY forumid").to_a
- top_level_categories = mysql_query <<-SQL
- SELECT forumid, title, description, displayorder
- FROM forum
- WHERE parentid = -1
- ORDER BY forumid
- SQL
+ top_level_categories = categories.select { |c| c["parentid"] == -1 }
create_categories(top_level_categories) do |category|
{
- id: category["forumid"].to_i,
- name: category["title"],
- position: category["displayorder"].to_i,
- description: category["description"]
+ id: category["forumid"],
+ name: @htmlentities.decode(category["title"]).strip,
+ position: category["displayorder"],
+ description: @htmlentities.decode(category["description"]).strip
}
end
puts "", "importing children categories..."
- childen_categories = mysql_query <<-SQL
- SELECT forumid, title, description, displayorder, parentid
- FROM forum
- WHERE parentid <> -1
- ORDER BY forumid
- SQL
+ children_categories = categories.select { |c| c["parentid"] != -1 }
+ top_level_category_ids = Set.new(top_level_categories.map { |c| c["forumid"] })
- create_categories(childen_categories) do |category|
+ # cut down the tree to only 2 levels of categories
+ children_categories.each do |cc|
+ while !top_level_category_ids.include?(cc["parentid"])
+ cc["parentid"] = categories.detect { |c| c["forumid"] == cc["parentid"] }["parentid"]
+ end
+ end
+
+ create_categories(children_categories) do |category|
{
- id: category["forumid"].to_i,
- name: category["title"],
- position: category["displayorder"].to_i,
- description: category["description"].strip!,
- parent_category_id: category_from_imported_category_id(category["parentid"].to_i).try(:[], "id")
+ id: category["forumid"],
+ name: @htmlentities.decode(category["title"]).strip,
+ position: category["displayorder"],
+ description: @htmlentities.decode(category["description"]).strip,
+ parent_category_id: category_from_imported_category_id(category["parentid"]).try(:[], "id")
}
end
end
@@ -145,13 +214,13 @@ class ImportScripts::VBulletin < ImportScripts::Base
@closed_topic_ids << topic_id if topic["open"] == "0"
t = {
id: topic_id,
- user_id: user_id_from_imported_user_id(topic["postuserid"].to_i) || Discourse::SYSTEM_USER_ID,
- title: CGI.unescapeHTML(topic["title"]).strip[0...255],
- category: category_from_imported_category_id(topic["forumid"].to_i).try(:name),
+ user_id: user_id_from_imported_user_id(topic["postuserid"]) || Discourse::SYSTEM_USER_ID,
+ title: @htmlentities.decode(topic["title"]).strip[0...255],
+ category: category_from_imported_category_id(topic["forumid"]).try(:name),
raw: preprocess_post_raw(topic["raw"]),
- created_at: Time.at(topic["dateline"].to_i),
+ created_at: parse_timestamp(topic["dateline"]),
visible: topic["visible"].to_i == 1,
- views: topic["views"].to_i,
+ views: topic["views"],
}
t[:pinned_at] = t[:created_at] if topic["sticky"].to_i == 1
t
@@ -179,11 +248,11 @@ class ImportScripts::VBulletin < ImportScripts::Base
create_posts(posts, total: post_count, offset: offset) do |post|
next unless topic = topic_lookup_from_imported_post_id("thread-#{post["threadid"]}")
p = {
- id: post["postid"].to_i,
+ id: post["postid"],
user_id: user_id_from_imported_user_id(post["userid"]) || Discourse::SYSTEM_USER_ID,
topic_id: topic[:topic_id],
raw: preprocess_post_raw(post["raw"]),
- created_at: Time.at(post["dateline"].to_i),
+ created_at: parse_timestamp(post["dateline"]),
hidden: post["visible"].to_i == 0,
}
if parent = topic_lookup_from_imported_post_id(post["parentid"])
@@ -214,9 +283,32 @@ class ImportScripts::VBulletin < ImportScripts::Base
Topic.exec_sql(sql, @closed_topic_ids)
end
+ def post_process_posts
+ puts "", "Postprocessing posts..."
+
+ current = 0
+ max = Post.count
+
+ Post.find_each do |post|
+ begin
+ new_raw = postprocess_post_raw(post.raw)
+ if new_raw != post.raw
+ post.raw = new_raw
+ post.save
+ end
+ ensure
+ print_status(current += 1, max)
+ end
+ end
+ end
+
def preprocess_post_raw(raw)
return "" if raw.blank?
+ # decode HTML entities
+ raw = @htmlentities.decode(raw)
+
+ # fix whitespaces
raw = raw.gsub(/(\\r)?\\n/, "\n")
.gsub("\\t", "\t")
@@ -301,6 +393,77 @@ class ImportScripts::VBulletin < ImportScripts::Base
raw
end
+ def postprocess_post_raw(raw)
+ # [QUOTE=;]...[/QUOTE]
+ raw = raw.gsub(/\[quote=([^;]+);(\d+)\](.+?)\[\/quote\]/im) do
+ old_username, post_id, quote = $1, $2, $3
+
+ if @old_username_to_new_usernames.has_key?(old_username)
+ old_username = @old_username_to_new_usernames[old_username]
+ end
+
+ if topic_lookup = topic_lookup_from_imported_post_id(post_id)
+ post_number = topic_lookup[:post_number]
+ topic_id = topic_lookup[:topic_id]
+ "\n[quote=\"#{old_username},post:#{post_number},topic:#{topic_id}\"]\n#{quote}\n[/quote]\n"
+ else
+ "\n[quote=\"#{old_username}\"]\n#{quote}\n[/quote]\n"
+ end
+ end
+
+ # [THREAD][/THREAD]
+ # ==> http://my.discourse.org/t/slug/
+ raw = raw.gsub(/\[thread\](\d+)\[\/thread\]/i) do
+ thread_id = $1
+ if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}")
+ topic_lookup[:url]
+ else
+ $&
+ end
+ end
+
+ # [THREAD=]...[/THREAD]
+ # ==> [...](http://my.discourse.org/t/slug/)
+ raw = raw.gsub(/\[thread=(\d+)\](.+?)\[\/thread\]/i) do
+ thread_id, link = $1, $2
+ if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}")
+ url = topic_lookup[:url]
+ "[#{link}](#{url})"
+ else
+ $&
+ end
+ end
+
+ # [POST][/POST]
+ # ==> http://my.discourse.org/t/slug//
+ raw = raw.gsub(/\[post\](\d+)\[\/post\]/i) do
+ post_id = $1
+ if topic_lookup = topic_lookup_from_imported_post_id(post_id)
+ topic_lookup[:url]
+ else
+ $&
+ end
+ end
+
+ # [POST=]...[/POST]
+ # ==> [...](http://my.discourse.org/t///)
+ raw = raw.gsub(/\[post=(\d+)\](.+?)\[\/post\]/i) do
+ post_id, link = $1, $2
+ if topic_lookup = topic_lookup_from_imported_post_id(post_id)
+ url = topic_lookup[:url]
+ "[#{link}](#{url})"
+ else
+ $&
+ end
+ end
+
+ raw
+ end
+
+ def parse_timestamp(timestamp)
+ Time.zone.at(@tz.utc_to_local(timestamp))
+ end
+
def fake_email
SecureRandom.hex << "@domain.com"
end
From 23fe0cfb4ed48234176b86379e67f9538bf57425 Mon Sep 17 00:00:00 2001
From: Alex Williams
Date: Mon, 19 Jan 2015 10:52:02 -0500
Subject: [PATCH 057/230] Fix spelling in contact_email_missing message.
---
config/locales/server.en.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index eb81c14712..03ca237684 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -599,7 +599,7 @@ en:
image_magick_warning: 'The server is configured to create thumbnails of large images, but ImageMagick is not installed. Install ImageMagick using your favorite package manager or download the latest release.'
failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/discourse.conf file and ensure that the mail server settings are correct. See the failed jobs in Sidekiq.'
default_logo_warning: "Set the graphic logos for your site. Update logo_url, logo_small_url, and favicon_url in Site Settings."
- contact_email_missing: "Enter a site contact email address so you can be reached you urgent matters regarding your site. Update it in Site Settings."
+ contact_email_missing: "Enter a site contact email address so you can be reached for urgent matters regarding your site. Update it in Site Settings."
contact_email_invalid: "The site contact email is invalid. Update it in Site Settings."
title_nag: "Enter the name of your site. Update title in Site Settings."
site_description_missing: "Enter a one sentence description of your site that will appear in search results. Update site_description in Site Settings."
From 7412ff4da70e8d15d1cc8fc845389cdc72cf9d2b Mon Sep 17 00:00:00 2001
From: Neil Lalonde
Date: Mon, 19 Jan 2015 12:36:56 -0500
Subject: [PATCH 058/230] FIX: suspended users are logged out when they are
suspended. Show a reason for suspension when they try to log in.
---
.../discourse/controllers/login.js.es6 | 6 ++++++
app/controllers/admin/users_controller.rb | 1 +
lib/auth/result.rb | 20 +++++++++++++------
3 files changed, 21 insertions(+), 6 deletions(-)
diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6
index f405e6ee1b..8e6c722c6a 100644
--- a/app/assets/javascripts/discourse/controllers/login.js.es6
+++ b/app/assets/javascripts/discourse/controllers/login.js.es6
@@ -160,6 +160,12 @@ export default DiscourseController.extend(ModalFunctionality, {
this.set('authenticate', null);
return;
}
+ if (options.suspended) {
+ this.send('showLogin');
+ this.flash(options.suspended_message, 'error');
+ this.set('authenticate', null);
+ return;
+ }
// Reload the page if we're authenticated
if (options.authenticated) {
if (window.location.pathname === Discourse.getURL('/login')) {
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index e62999fe01..c0f2469524 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -53,6 +53,7 @@ class Admin::UsersController < Admin::AdminController
@user.suspended_at = DateTime.now
@user.save!
StaffActionLogger.new(current_user).log_user_suspend(@user, params[:reason])
+ MessageBus.publish "/logout", @user.id, user_ids: [@user.id]
render nothing: true
end
diff --git a/lib/auth/result.rb b/lib/auth/result.rb
index 3b0a36b5de..505541ea4c 100644
--- a/lib/auth/result.rb
+++ b/lib/auth/result.rb
@@ -19,12 +19,20 @@ class Auth::Result
if requires_invite
{ requires_invite: true }
elsif user
- {
- authenticated: !!authenticated,
- awaiting_activation: !!awaiting_activation,
- awaiting_approval: !!awaiting_approval,
- not_allowed_from_ip_address: !!not_allowed_from_ip_address
- }
+ if user.suspended?
+ {
+ suspended: true,
+ suspended_message: I18n.t( user.suspend_reason ? "login.suspended_with_reason" : "login.suspended",
+ {date: I18n.l(user.suspended_till, format: :date_only), reason: user.suspend_reason} )
+ }
+ else
+ {
+ authenticated: !!authenticated,
+ awaiting_activation: !!awaiting_activation,
+ awaiting_approval: !!awaiting_approval,
+ not_allowed_from_ip_address: !!not_allowed_from_ip_address
+ }
+ end
else
{
email: email,
From 5e751ce90a2803ca34a93bb5f1d1306e29eb4da0 Mon Sep 17 00:00:00 2001
From: Arpit Jalan
Date: Tue, 20 Jan 2015 00:20:01 +0530
Subject: [PATCH 059/230] FEATURE: :gift: rate limit invites for non-staff
users
---
app/models/invite.rb | 9 +++++++++
config/locales/client.en.yml | 2 +-
config/locales/server.en.yml | 1 +
config/site_settings.yml | 1 +
spec/models/invite_spec.rb | 2 ++
5 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/app/models/invite.rb b/app/models/invite.rb
index 28fb95526d..36194915ed 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -1,6 +1,11 @@
+require_dependency 'rate_limiter'
+
class Invite < ActiveRecord::Base
+ include RateLimiter::OnCreateRecord
include Trashable
+ rate_limit :limit_invites_per_day
+
belongs_to :user
belongs_to :topic
belongs_to :invited_by, class_name: 'User'
@@ -184,6 +189,10 @@ class Invite < ActiveRecord::Base
Jobs.enqueue(:invite_email, invite_id: self.id)
end
+ def limit_invites_per_day
+ RateLimiter.new(invited_by, "invites-per-day:#{Date.today}", SiteSetting.max_invites_per_day, 1.day.to_i)
+ end
+
def self.base_directory
File.join(Rails.root, "public", "uploads", "csv", RailsMultisite::ConnectionManagement.current_db)
end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index a9686319ee..95c269f0b5 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1018,7 +1018,7 @@ en:
email_placeholder: 'name@example.com'
success: "We mailed out an invitation to {{email}}. We'll notify you when the invitation is redeemed. Check the invitations tab on your user page to keep track of your invites."
- error: "Sorry, we couldn't invite that person. Perhaps they are already a user?"
+ error: "Sorry, we couldn't invite that person. Perhaps they are already a user? (Invites are rate limited)"
login_reply: 'Log In to Reply'
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index eb81c14712..9c6f113686 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -831,6 +831,7 @@ en:
max_edits_per_day: "Maximum number of edits per user per day."
max_topics_per_day: "Maximum number of topics a user can create per day."
max_private_messages_per_day: "Maximum number of private messages users can create per day."
+ max_invites_per_day: "Maximum number of invites a user can send per day."
suggested_topics: "Number of suggested topics shown at the bottom of a topic."
limit_suggested_to_category: "Only show topics from the current category in suggested topics."
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 2f22e397b2..c9b02b6058 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -611,6 +611,7 @@ rate_limits:
max_bookmarks_per_day: 20
max_flags_per_day: 20
max_edits_per_day: 30
+ max_invites_per_day: 10
max_topics_in_first_day: 5
max_replies_in_first_day: 10
diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb
index c64beed0fd..a7a1b02a2b 100644
--- a/spec/models/invite_spec.rb
+++ b/spec/models/invite_spec.rb
@@ -4,6 +4,8 @@ describe Invite do
it { is_expected.to validate_presence_of :invited_by_id }
+ it { is_expected.to rate_limit }
+
let(:iceking) { 'iceking@adventuretime.ooo' }
context 'user validators' do
From 4c0129ccddb9037467133d37e5a93520a5f86f77 Mon Sep 17 00:00:00 2001
From: Neil Lalonde
Date: Mon, 19 Jan 2015 15:30:16 -0500
Subject: [PATCH 060/230] PERF: slow user pages in admin. add an index for
trust level 3 calculations, and memoize query results
---
app/models/trust_level3_requirements.rb | 20 +++++++++----------
...92813_add_posts_index_including_deleted.rb | 5 +++++
2 files changed, 15 insertions(+), 10 deletions(-)
create mode 100644 db/migrate/20150119192813_add_posts_index_including_deleted.rb
diff --git a/app/models/trust_level3_requirements.rb b/app/models/trust_level3_requirements.rb
index a42775e123..919db83174 100644
--- a/app/models/trust_level3_requirements.rb
+++ b/app/models/trust_level3_requirements.rb
@@ -134,12 +134,12 @@ class TrustLevel3Requirements
end
def num_flagged_by_users
- PostAction.with_deleted
- .where(post_id: flagged_post_ids)
- .where.not(user_id: @user.id)
- .where.not(agreed_at: nil)
- .pluck(:user_id)
- .uniq.count
+ @_num_flagged_by_users ||= PostAction.with_deleted
+ .where(post_id: flagged_post_ids)
+ .where.not(user_id: @user.id)
+ .where.not(agreed_at: nil)
+ .pluck(:user_id)
+ .uniq.count
end
def max_flagged_by_users
@@ -212,9 +212,9 @@ class TrustLevel3Requirements
end
def flagged_post_ids
- @user.posts
- .with_deleted
- .where('created_at > ? AND (spam_count > 0 OR inappropriate_count > 0)', TIME_PERIOD.days.ago)
- .pluck(:id)
+ @_flagged_post_ids ||= @user.posts
+ .with_deleted
+ .where('created_at > ? AND (spam_count > 0 OR inappropriate_count > 0)', TIME_PERIOD.days.ago)
+ .pluck(:id)
end
end
diff --git a/db/migrate/20150119192813_add_posts_index_including_deleted.rb b/db/migrate/20150119192813_add_posts_index_including_deleted.rb
new file mode 100644
index 0000000000..20d6e588df
--- /dev/null
+++ b/db/migrate/20150119192813_add_posts_index_including_deleted.rb
@@ -0,0 +1,5 @@
+class AddPostsIndexIncludingDeleted < ActiveRecord::Migration
+ def change
+ add_index :posts, [:user_id, :created_at]
+ end
+end
From 350554e198084ee241143cc55891e80fd1c8c00a Mon Sep 17 00:00:00 2001
From: Robin Ward
Date: Tue, 20 Jan 2015 11:36:28 -0500
Subject: [PATCH 061/230] UX: Change category badge style to use stripes
---
.../discourse/components/category-drop.js.es6 | 23 ++--
.../components/category-group.js.es6 | 4 +-
.../controllers/edit-category.js.es6 | 3 +-
.../discourse/controllers/history.js.es6 | 13 ++-
.../discourse/helpers/category-link.js.es6 | 64 ++++++++++-
app/assets/javascripts/discourse/lib/html.js | 108 +-----------------
.../javascripts/discourse/models/category.js | 1 +
.../category-group-autocomplete.raw.hbs | 2 +-
.../templates/components/category-drop.hbs | 14 +--
.../templates/discovery/categories.hbs | 2 +-
.../discourse/templates/header.hbs | 4 +-
.../templates/list/category-column.raw.hbs | 2 +-
.../mobile/components/basic-topic-list.hbs | 2 +-
.../templates/mobile/discovery/categories.hbs | 2 +-
.../mobile/list/topic_list_item.raw.hbs | 2 +-
.../templates/modal/edit-category-general.hbs | 2 +-
.../discourse/templates/site-map.hbs | 2 +-
.../javascripts/discourse/templates/topic.hbs | 2 +-
.../discourse/views/category-chooser.js.es6 | 7 +-
.../javascripts/discourse/views/topic.js.es6 | 3 +-
.../stylesheets/common/base/_topic-list.scss | 23 ++--
.../common/components/badges.css.scss | 4 +-
app/assets/stylesheets/desktop/user.scss | 3 +
app/helpers/user_notifications_helper.rb | 11 +-
app/views/user_notifications/digest.html.erb | 2 +-
.../lib/category-badge-test.js.es6 | 41 +++++++
test/javascripts/lib/html-test.js.es6 | 40 -------
27 files changed, 175 insertions(+), 211 deletions(-)
create mode 100644 test/javascripts/lib/category-badge-test.js.es6
diff --git a/app/assets/javascripts/discourse/components/category-drop.js.es6 b/app/assets/javascripts/discourse/components/category-drop.js.es6
index b05b9dcd77..527a4fb479 100644
--- a/app/assets/javascripts/discourse/components/category-drop.js.es6
+++ b/app/assets/javascripts/discourse/components/category-drop.js.es6
@@ -1,11 +1,5 @@
-/**
- Renders a drop down for selecting a category
+var get = Ember.get;
- @class CategoryDropComponent
- @extends Ember.Component
- @namespace Discourse
- @module Discourse
-**/
export default Ember.Component.extend({
classNameBindings: ['category::no-category', 'categories:has-drop'],
tagName: 'li',
@@ -44,11 +38,20 @@ export default Ember.Component.extend({
badgeStyle: function() {
var category = this.get('category');
+
if (category) {
- return Discourse.HTML.categoryStyle(category);
- } else {
- return "background-color: #eee; color: #333";
+ var color = get(category, 'color'),
+ textColor = get(category, 'text_color');
+
+ if (color || textColor) {
+ var style = "";
+ if (color) { style += "background-color: #" + color + "; "; }
+ if (textColor) { style += "color: #" + textColor + "; "; }
+ return style;
+ }
}
+
+ return "background-color: #eee; color: #333";
}.property('category'),
clickEventName: function() {
diff --git a/app/assets/javascripts/discourse/components/category-group.js.es6 b/app/assets/javascripts/discourse/components/category-group.js.es6
index 32434e74a9..14caa32bfe 100644
--- a/app/assets/javascripts/discourse/components/category-group.js.es6
+++ b/app/assets/javascripts/discourse/components/category-group.js.es6
@@ -1,3 +1,5 @@
+import { categoryBadgeHTML } from 'discourse/helpers/category-link';
+
export default Ember.Component.extend({
_initializeAutocomplete: function(){
@@ -25,7 +27,7 @@ export default Ember.Component.extend({
},
template: template,
transformComplete: function(category) {
- return Discourse.HTML.categoryBadge(category, {allowUncategorized: true});
+ return categoryBadgeHTML(category, {allowUncategorized: true});
}
});
}.on('didInsertElement')
diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6
index 443d3bcb7c..5928e97ea0 100644
--- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6
+++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6
@@ -1,5 +1,6 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
+import { categoryBadgeHTML } from 'discourse/helpers/category-link';
// Modal for editing / creating a category
export default ObjectController.extend(ModalFunctionality, {
@@ -69,7 +70,7 @@ export default ObjectController.extend(ModalFunctionality, {
parent_category_id: parseInt(this.get('parent_category_id'),10),
read_restricted: this.get('model.read_restricted')
});
- return Discourse.HTML.categoryBadge(c, {showParent: true, link: false});
+ return categoryBadgeHTML(c, {link: false});
}.property('parent_category_id', 'categoryName', 'color', 'text_color'),
// background colors are available as a pipe-separated string
diff --git a/app/assets/javascripts/discourse/controllers/history.js.es6 b/app/assets/javascripts/discourse/controllers/history.js.es6
index 2a00dc18d5..55302edc9c 100644
--- a/app/assets/javascripts/discourse/controllers/history.js.es6
+++ b/app/assets/javascripts/discourse/controllers/history.js.es6
@@ -1,5 +1,6 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
+import { categoryBadgeHTML } from 'discourse/helpers/category-link';
// This controller handles displaying of history
export default ObjectController.extend(ModalFunctionality, {
@@ -22,14 +23,14 @@ export default ObjectController.extend(ModalFunctionality, {
hide: function(postId, postVersion) {
var self = this;
- Discourse.Post.hideRevision(postId, postVersion).then(function (result) {
+ Discourse.Post.hideRevision(postId, postVersion).then(function () {
self.refresh(postId, postVersion);
});
},
show: function(postId, postVersion) {
var self = this;
- Discourse.Post.showRevision(postId, postVersion).then(function (result) {
+ Discourse.Post.showRevision(postId, postVersion).then(function () {
self.refresh(postId, postVersion);
});
},
@@ -68,7 +69,7 @@ export default ObjectController.extend(ModalFunctionality, {
var changes = this.get("category_changes");
if (changes) {
var category = Discourse.Category.findById(changes["previous"]);
- return Discourse.HTML.categoryBadge(category, { allowUncategorized: true });
+ return categoryBadgeHTML(category, { allowUncategorized: true });
}
}.property("category_changes"),
@@ -76,12 +77,12 @@ export default ObjectController.extend(ModalFunctionality, {
var changes = this.get("category_changes");
if (changes) {
var category = Discourse.Category.findById(changes["current"]);
- return Discourse.HTML.categoryBadge(category, { allowUncategorized: true });
+ return categoryBadgeHTML(category, { allowUncategorized: true });
}
}.property("category_changes"),
wiki_diff: function() {
- var changes = this.get("wiki_changes")
+ var changes = this.get("wiki_changes");
if (changes) {
return changes["current"] ?
'' :
@@ -93,7 +94,7 @@ export default ObjectController.extend(ModalFunctionality, {
var moderator = Discourse.Site.currentProp('post_types.moderator_action');
var changes = this.get("post_type_changes");
if (changes) {
- return changes["current"] == moderator ?
+ return changes["current"] === moderator ?
'' :
'';
}
diff --git a/app/assets/javascripts/discourse/helpers/category-link.js.es6 b/app/assets/javascripts/discourse/helpers/category-link.js.es6
index 412a64df99..ea71350dfa 100644
--- a/app/assets/javascripts/discourse/helpers/category-link.js.es6
+++ b/app/assets/javascripts/discourse/helpers/category-link.js.es6
@@ -1,4 +1,63 @@
import registerUnbound from 'discourse/helpers/register-unbound';
+import { iconHTML } from 'discourse/helpers/fa-icon';
+
+var get = Em.get,
+ escapeExpression = Handlebars.Utils.escapeExpression;
+
+function categoryStripe(tagName, category, extraClasses, href) {
+ if (!category) { return ""; }
+
+ var color = Em.get(category, 'color'),
+ style = color ? "style='background-color: #" + color + ";'" : "";
+
+ return "<" + tagName + " class='badge-category-parent" + extraClasses + "' " + style + " href=\"" + href + "\">" + tagName + ">";
+}
+
+export function categoryBadgeHTML(category, opts) {
+ opts = opts || {};
+
+ if ((!category) ||
+ (!opts.allowUncategorized &&
+ Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id") &&
+ Discourse.SiteSettings.suppress_uncategorized_badge
+ )
+ ) return "";
+
+ var description = get(category, 'description_text'),
+ restricted = get(category, 'read_restricted'),
+ url = Discourse.getURL("/c/") + Discourse.Category.slugFor(category),
+ href = (opts.link === false ? '' : url),
+ tagName = (opts.link === false || opts.link === "false" ? 'span' : 'a'),
+ extraClasses = (opts.extraClasses ? (' ' + opts.extraClasses) : '');
+
+ var html = "";
+
+ var parentCat = Discourse.Category.findById(category.get('parent_category_id'));
+ if (opts.hideParent) { parentCat = null; }
+ html += categoryStripe(tagName, parentCat, extraClasses, href);
+
+ if (parentCat !== category) {
+ html += categoryStripe(tagName, category, extraClasses, href);
+ }
+
+ var classNames = "badge-category clear-badge" + extraClasses;
+ if (restricted) { classNames += " restricted"; }
+
+ html += "<" + tagName + ' href="' + href + '" ' +
+ 'data-drop-close="true" class="' + classNames + '"' +
+ (description ? 'title="' + escapeExpression(description) + '" ' : '') +
+ ">";
+
+ var name = escapeExpression(get(category, 'name'));
+ if (restricted) {
+ html += "
" + iconHTML('lock') + " " + name + "
";
+ } else {
+ html += name;
+ }
+ html += "" + tagName + ">";
+
+ return "" + html + "";
+}
export function categoryLinkHTML(category, options) {
var categoryOptions = {};
@@ -9,12 +68,11 @@ export function categoryLinkHTML(category, options) {
if (options) {
if (options.allowUncategorized) { categoryOptions.allowUncategorized = true; }
- if (options.showParent) { categoryOptions.showParent = true; }
- if (options.onlyStripe) { categoryOptions.onlyStripe = true; }
if (options.link !== undefined) { categoryOptions.link = options.link; }
if (options.extraClasses) { categoryOptions.extraClasses = options.extraClasses; }
+ if (options.hideParent) { categoryOptions.hideParent = true; }
}
- return new Handlebars.SafeString(Discourse.HTML.categoryBadge(category, categoryOptions));
+ return new Handlebars.SafeString(categoryBadgeHTML(category, categoryOptions));
}
registerUnbound('category-link', categoryLinkHTML);
diff --git a/app/assets/javascripts/discourse/lib/html.js b/app/assets/javascripts/discourse/lib/html.js
index 8eb7a3be96..ccb807d6a8 100644
--- a/app/assets/javascripts/discourse/lib/html.js
+++ b/app/assets/javascripts/discourse/lib/html.js
@@ -1,11 +1,3 @@
-/**
- Helpers to build HTML strings as well as custom fragments.
-
- @class HTML
- @namespace Discourse
- @module Discourse
-**/
-
var customizations = {};
Discourse.HTML = {
@@ -15,9 +7,6 @@ Discourse.HTML = {
using `setCustomHTML(key, html)`. This is used by a handlebars helper to find
the HTML content it wants. It will also check the `PreloadStore` for any server
side preloaded HTML.
-
- @method getCustomHTML
- @param {String} key to lookup
**/
getCustomHTML: function(key) {
var c = customizations[key];
@@ -31,104 +20,9 @@ Discourse.HTML = {
}
},
- /**
- Set a fragment of HTML by key. It can then be looked up with `getCustomHTML(key)`.
-
- @method setCustomHTML
- @param {String} key to store the html
- @param {String} html fragment to store
- **/
+ // Set a fragment of HTML by key. It can then be looked up with `getCustomHTML(key)`.
setCustomHTML: function(key, html) {
customizations[key] = html;
- },
-
- /**
- Returns the CSS styles for a category
-
- @method categoryStyle
- @param {Discourse.Category} category the category whose link we want
- **/
- categoryStyle: function(category) {
- var color = Em.get(category, 'color'),
- textColor = Em.get(category, 'text_color');
-
- if (!color && !textColor) { return; }
-
- // Add the custom style if we need to
- var style = "";
- if (color) { style += "background-color: #" + color + "; "; }
- if (textColor) { style += "color: #" + textColor + "; "; }
- return style;
- },
-
- /**
- Create a category badge
-
- @method categoryBadge
- @param {Discourse.Category} category the category whose link we want
- @param {Object} opts The options for the category link
- @param {Boolean} opts.allowUncategorized Whether we allow rendering of the uncategorized category (default false)
- @param {Boolean} opts.showParent Whether to visually show whether category is a sub-category (default false)
- @param {Boolean} opts.link Whether this category badge should link to the category (default true)
- @param {String} opts.extraClasses add this string to the class attribute of the badge
- @returns {String} the html category badge
- **/
- categoryBadge: function(category, opts) {
- opts = opts || {};
-
- if ((!category) ||
- (!opts.allowUncategorized &&
- Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id") &&
- Discourse.SiteSettings.suppress_uncategorized_badge
- )
- ) return "";
-
- var name = Em.get(category, 'name'),
- description = Em.get(category, 'description_text'),
- restricted = Em.get(category, 'read_restricted'),
- url = Discourse.getURL("/c/") + Discourse.Category.slugFor(category),
- elem = (opts.link === false ? 'span' : 'a'),
- extraClasses = (opts.extraClasses ? (' ' + opts.extraClasses) : ''),
- html = "<" + elem + " href=\"" + (opts.link === false ? '' : url) + "\" ",
- categoryStyle;
-
- // Parent stripe implies onlyStripe
- if (opts.onlyStripe) { opts.showParent = true; }
-
- html += "data-drop-close=\"true\" class=\"badge-category" + (restricted ? ' restricted' : '' ) +
- (opts.onlyStripe ? ' clear-badge' : '') +
- extraClasses + "\" ";
- name = Handlebars.Utils.escapeExpression(name);
-
- // Add description if we have it, without tags. Server has sanitized the description value.
- if (description) html += "title=\"" + Handlebars.Utils.escapeExpression(description) + "\" ";
-
- if (!opts.onlyStripe) {
- categoryStyle = Discourse.HTML.categoryStyle(category);
- if (categoryStyle) {
- html += "style=\"" + categoryStyle + "\" ";
- }
- }
-
- if (restricted) {
- html += ">