diff --git a/README.md b/README.md
index 66451788f9b..359c8189f14 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
-
+
Browse [lots more notable Discourse instances](http://www.discourse.org/faq/customers/).
diff --git a/app/assets/javascripts/discourse/components/csv-uploader.js.es6 b/app/assets/javascripts/discourse/components/csv-uploader.js.es6
index d89aaae30b7..70637588687 100644
--- a/app/assets/javascripts/discourse/components/csv-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/csv-uploader.js.es6
@@ -6,6 +6,10 @@ export default Em.Component.extend(UploadMixin, {
tagName: "span",
uploadUrl: "/invites/upload_csv",
+ validateUploadedFilesOptions() {
+ return { csvOnly: true };
+ },
+
@computed("uploading")
uploadButtonText(uploading) {
return uploading ? I18n.t("uploading") : I18n.t("user.invited.bulk_invite.text");
diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6
index 154f92553b5..5eeb3a358c4 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js.es6
+++ b/app/assets/javascripts/discourse/lib/utilities.js.es6
@@ -192,6 +192,11 @@ export function validateUploadedFile(file, opts) {
bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedImagesExtensions() }));
return false;
}
+ } else if (opts["csvOnly"]) {
+ if (!(/\.csv$/i).test(name)) {
+ bootbox.alert(I18n.t('user.invited.bulk_invite.error'));
+ return false;
+ }
} else {
if (!authorizesAllExtensions() && !isAuthorizedFile(name)) {
bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedExtensions() }));
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6
index 4172a2a46f2..96b798a3269 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6
@@ -1,9 +1,18 @@
import { register } from 'pretty-text/engines/discourse-markdown/bbcode';
+import { registerOption } from 'pretty-text/pretty-text';
+import { performEmojiUnescape } from 'pretty-text/emoji';
+
+registerOption((siteSettings, opts) => {
+ opts.enableEmoji = siteSettings.enable_emoji;
+ opts.emojiSet = siteSettings.emoji_set;
+});
+
export function setup(helper) {
register(helper, 'quote', {noWrap: true, singlePara: true}, (contents, bbParams, options) => {
const params = {'class': 'quote'};
let username = null;
+ const opts = helper.getOptions();
if (bbParams) {
const paramsSplit = bbParams.split(/\,\s*/);
@@ -52,7 +61,16 @@ export function setup(helper) {
if (postNumber > 0) { href += "/" + postNumber; }
// get rid of username said stuff
header.pop();
- header.push(['a', {'href': href}, topicInfo.title]);
+
+ let title = topicInfo.title;
+
+ if (opts.enableEmoji) {
+ title = performEmojiUnescape(topicInfo.title, {
+ getURL: opts.getURL, emojiSet: opts.emojiSet
+ });
+ }
+
+ header.push(['a', {'href': href}, title]);
}
}
diff --git a/app/assets/stylesheets/common/foundation/base.scss b/app/assets/stylesheets/common/foundation/base.scss
index b4267e703fe..2a3c5588d02 100644
--- a/app/assets/stylesheets/common/foundation/base.scss
+++ b/app/assets/stylesheets/common/foundation/base.scss
@@ -85,6 +85,7 @@ fieldset {
pre code {
overflow: auto;
+ tab-size: 4;
}
// TODO figure out a clean place to put stuff like this
diff --git a/app/controllers/admin/backups_controller.rb b/app/controllers/admin/backups_controller.rb
index 3c7855668c9..2c71943b488 100644
--- a/app/controllers/admin/backups_controller.rb
+++ b/app/controllers/admin/backups_controller.rb
@@ -95,7 +95,14 @@ class Admin::BackupsController < Admin::AdminController
def readonly
enable = params.fetch(:enable).to_s == "true"
- enable ? Discourse.enable_readonly_mode(user_enabled: true) : Discourse.disable_readonly_mode(user_enabled: true)
+ readonly_mode_key = Discourse::USER_READONLY_MODE_KEY
+
+ if enable
+ Discourse.enable_readonly_mode(readonly_mode_key)
+ else
+ Discourse.disable_readonly_mode(readonly_mode_key)
+ end
+
render nothing: true
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 2ee6db17710..2359bc31e2a 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -46,6 +46,7 @@ class Admin::UsersController < Admin::AdminController
def delete_all_posts
@user = User.find_by(id: params[:user_id])
@user.delete_all_posts!(guardian)
+ # staff action logs will have an entry for each post
render nothing: true
end
@@ -182,6 +183,8 @@ class Admin::UsersController < Admin::AdminController
@user.trust_level_locked = new_lock == "true"
@user.save
+ StaffActionLogger.new(current_user).log_lock_trust_level(@user)
+
unless @user.trust_level_locked
p = Promotion.new(@user)
2.times{ p.review }
@@ -210,12 +213,14 @@ class Admin::UsersController < Admin::AdminController
def activate
guardian.ensure_can_activate!(@user)
@user.activate
+ StaffActionLogger.new(current_user).log_user_activate(@user, I18n.t('user.activated_by_staff'))
render json: success_json
end
def deactivate
guardian.ensure_can_deactivate!(@user)
@user.deactivate
+ StaffActionLogger.new(current_user).log_user_deactivate(@user, I18n.t('user.deactivated_by_staff'))
refresh_browser @user
render nothing: true
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index ae62b9b8732..5b614bc3dc6 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -156,9 +156,9 @@ class InvitesController < ApplicationController
Scheduler::Defer.later("Upload CSV") do
begin
- data = if extension == ".csv"
+ data = if extension.downcase == ".csv"
path = Invite.create_csv(file, name)
- Jobs.enqueue(:bulk_invite, filename: "#{name}.csv", current_user_id: current_user.id)
+ Jobs.enqueue(:bulk_invite, filename: "#{name}#{extension}", current_user_id: current_user.id)
{url: path}
else
failed_json.merge(errors: [I18n.t("bulk_invite.file_should_be_csv")])
diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb
index 9d2332e1268..9a8fc915ce3 100644
--- a/app/controllers/metadata_controller.rb
+++ b/app/controllers/metadata_controller.rb
@@ -17,7 +17,7 @@ class MetadataController < ApplicationController
name: SiteSetting.title,
short_name: SiteSetting.title,
display: 'standalone',
- orientation: 'portrait',
+ orientation: 'any',
start_url: "#{Discourse.base_uri}/",
background_color: "##{ColorScheme.hex_for_name('secondary')}",
theme_color: "##{ColorScheme.hex_for_name('header_background')}",
diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb
index e393eee85af..aeef876c491 100644
--- a/app/mailers/user_notifications.rb
+++ b/app/mailers/user_notifications.rb
@@ -3,6 +3,7 @@ require_dependency 'email/message_builder'
require_dependency 'age_words'
class UserNotifications < ActionMailer::Base
+ include UserNotificationsHelper
helper :application
default charset: 'UTF-8'
@@ -106,20 +107,27 @@ class UserNotifications < ActionMailer::Base
end
@popular_topics = topics_for_digest[0,SiteSetting.digest_topics]
- @other_new_for_you = topics_for_digest.size > SiteSetting.digest_topics ? topics_for_digest[SiteSetting.digest_topics..-1] : []
-
- @popular_posts = if SiteSetting.digest_posts > 0
- Post.order("posts.score DESC")
- .for_mailing_list(user, min_date)
- .where('posts.post_type = ?', Post.types[:regular])
- .where('posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false')
- .where("posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 5.0)
- .limit(SiteSetting.digest_posts)
- else
- []
- end
if @popular_topics.present?
+ @other_new_for_you = topics_for_digest.size > SiteSetting.digest_topics ? topics_for_digest[SiteSetting.digest_topics..-1] : []
+
+ @popular_posts = if SiteSetting.digest_posts > 0
+ Post.order("posts.score DESC")
+ .for_mailing_list(user, min_date)
+ .where('posts.post_type = ?', Post.types[:regular])
+ .where('posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false')
+ .where("posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 5.0)
+ .limit(SiteSetting.digest_posts)
+ else
+ []
+ end
+
+ @excerpts = {}
+
+ @popular_topics.map do |t|
+ @excerpts[t.first_post.id] = email_excerpt(t.first_post.cooked) if t.first_post.present?
+ end
+
# Try to find 3 interesting stats for the top of the digest
new_topics_count = Topic.new_since_last_seen(user, min_date).count
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 6851b210dce..13e7916f1b1 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -1,3 +1,5 @@
+require_dependency 'slug'
+
class Badge < ActiveRecord::Base
# NOTE: These badge ids are not in order! They are grouped logically.
# When picking an id, *search* for it.
@@ -119,6 +121,10 @@ class Badge < ActiveRecord::Base
}
end
+ def awarded_for_trust_level?
+ id <= 4
+ end
+
def reset_grant_count!
self.grant_count = UserBadge.where(badge_id: id).count
save!
@@ -208,6 +214,7 @@ SQL
def i18n_name
self.name.downcase.tr(' ', '_')
end
+
end
# == Schema Information
diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb
index dd1c04820ef..a7600d48206 100644
--- a/app/models/global_setting.rb
+++ b/app/models/global_setting.rb
@@ -145,14 +145,14 @@ class GlobalSetting
attr_accessor :provider
end
-
- if Rails.env == "test"
- @provider = BlankProvider.new
- else
- @provider =
- FileProvider.from(File.expand_path('../../../config/discourse.conf', __FILE__)) ||
- EnvProvider.new
+ def self.configure!
+ if Rails.env == "test"
+ @provider = BlankProvider.new
+ else
+ @provider =
+ FileProvider.from(File.expand_path('../../../config/discourse.conf', __FILE__)) ||
+ EnvProvider.new
+ end
end
- load_defaults
end
diff --git a/app/models/user_history.rb b/app/models/user_history.rb
index 635014b1d08..e7db96a5f64 100644
--- a/app/models/user_history.rb
+++ b/app/models/user_history.rb
@@ -55,7 +55,10 @@ class UserHistory < ActiveRecord::Base
rate_limited_like: 37, # not used anymore
revoke_email: 38,
deactivate_user: 39,
- wizard_step: 40
+ wizard_step: 40,
+ lock_trust_level: 41,
+ unlock_trust_level: 42,
+ activate_user: 43
)
end
@@ -91,7 +94,10 @@ class UserHistory < ActiveRecord::Base
:revoke_moderation,
:backup_operation,
:revoke_email,
- :deactivate_user]
+ :deactivate_user,
+ :lock_trust_level,
+ :unlock_trust_level,
+ :activate_user]
end
def self.staff_action_ids
diff --git a/app/services/badge_granter.rb b/app/services/badge_granter.rb
index ece099e4f1f..426e24ee8e0 100644
--- a/app/services/badge_granter.rb
+++ b/app/services/badge_granter.rb
@@ -274,7 +274,7 @@ class BadgeGranter
/*where*/
RETURNING id, user_id, granted_at
)
- select w.*, username, locale FROM w
+ select w.*, username, locale, (u.admin OR u.moderator) AS staff FROM w
JOIN users u on u.id = w.user_id
"
@@ -315,6 +315,8 @@ class BadgeGranter
# Make this variable in this scope
notification = nil
+ next if (row.staff && badge.awarded_for_trust_level?)
+
I18n.with_locale(notification_locale) do
notification = Notification.create!(
user_id: row.user_id,
diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb
index 57c213b4e2f..9efe294e989 100644
--- a/app/services/staff_action_logger.rb
+++ b/app/services/staff_action_logger.rb
@@ -14,7 +14,6 @@ class StaffActionLogger
raise Discourse::InvalidParameters.new(:deleted_user) unless deleted_user && deleted_user.is_a?(User)
UserHistory.create( params(opts).merge({
action: UserHistory.actions[:delete_user],
- email: deleted_user.email,
ip_address: deleted_user.ip_address.to_s,
details: [:id, :username, :name, :created_at, :trust_level, :last_seen_at, :last_emailed_at].map { |x| "#{x}: #{deleted_user.send(x)}" }.join("\n")
}))
@@ -96,6 +95,14 @@ class StaffActionLogger
}))
end
+ def log_lock_trust_level(user, opts={})
+ raise Discourse::InvalidParameters.new(:user) unless user && user.is_a?(User)
+ UserHistory.create!( params(opts).merge({
+ action: UserHistory.actions[user.trust_level_locked ? :lock_trust_level : :unlock_trust_level],
+ target_user_id: user.id
+ }))
+ end
+
def log_site_setting_change(setting_name, previous_value, new_value, opts={})
raise Discourse::InvalidParameters.new(:setting_name) unless setting_name.present? && SiteSetting.respond_to?(setting_name)
UserHistory.create( params(opts).merge({
@@ -353,6 +360,15 @@ class StaffActionLogger
}))
end
+ def log_user_activate(user, reason, opts={})
+ raise Discourse::InvalidParameters.new(:user) unless user
+ UserHistory.create(params(opts).merge({
+ action: UserHistory.actions[:activate_user],
+ target_user_id: user.id,
+ details: reason
+ }))
+ end
+
def log_wizard_step(step, opts={})
raise Discourse::InvalidParameters.new(:step) unless step
UserHistory.create(params(opts).merge({
diff --git a/app/services/user_blocker.rb b/app/services/user_blocker.rb
index 5ee5bc01892..cda62083c95 100644
--- a/app/services/user_blocker.rb
+++ b/app/services/user_blocker.rb
@@ -17,8 +17,11 @@ class UserBlocker
unless @user.blocked?
@user.blocked = true
if @user.save
- SystemMessage.create(@user, @opts[:message] || :blocked_by_staff)
- StaffActionLogger.new(@by_user).log_block_user(@user) if @by_user
+ message_type = @opts[:message] || :blocked_by_staff
+ post = SystemMessage.create(@user, message_type)
+ if post && @by_user
+ StaffActionLogger.new(@by_user).log_block_user(@user, {context: "#{message_type}: '#{post.topic&.title rescue ''}'"})
+ end
end
else
false
diff --git a/app/views/user_notifications/digest.html.erb b/app/views/user_notifications/digest.html.erb
index c9261000b0c..66e5930732b 100644
--- a/app/views/user_notifications/digest.html.erb
+++ b/app/views/user_notifications/digest.html.erb
@@ -149,7 +149,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<%= t.user.username -%>
<% end %>
- <%- if show_image_with_url(t.image_url) && t.featured_link.nil? -%>
+ <%- if show_image_with_url(t.image_url) && t.featured_link.nil? && !(@excerpts[t.first_post&.id]||"").include?(t.image_url) -%>
|
@@ -163,7 +163,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
- <%= email_excerpt(t.first_post.cooked) %>
+ <%= @excerpts[t.first_post.id] %>
|
diff --git a/config/application.rb b/config/application.rb
index c098564340b..49b35de4743 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -6,8 +6,15 @@ require_relative '../lib/discourse_event'
require_relative '../lib/discourse_plugin'
require_relative '../lib/discourse_plugin_registry'
+require_relative '../lib/plugin_gem'
+
# Global config
require_relative '../app/models/global_setting'
+GlobalSetting.configure!
+unless Rails.env.test? && ENV['LOAD_PLUGINS'] != "1"
+ require_relative '../lib/custom_setting_providers'
+end
+GlobalSetting.load_defaults
require 'pry-rails' if Rails.env.development?
@@ -15,8 +22,10 @@ if defined?(Bundler)
Bundler.require(*Rails.groups(assets: %w(development test profile)))
end
+
module Discourse
class Application < Rails::Application
+
def config.database_configuration
if Rails.env.production?
GlobalSetting.database_config
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 4f78ac68218..bbe0aab2c83 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -833,6 +833,7 @@ en:
none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by uploading a CSV file."
text: "Bulk Invite from File"
success: "File uploaded successfully, you will be notified via message when the process is complete."
+ error: "Sorry, file should be of csv format."
password:
title: "Password"
@@ -2409,8 +2410,8 @@ en:
backups: "backups"
traffic_short: "Traffic"
traffic: "Application web requests"
- page_views: "API Requests"
- page_views_short: "API Requests"
+ page_views: "Pageviews"
+ page_views_short: "Pageviews"
show_traffic_report: "Show Detailed Traffic Report"
reports:
@@ -2914,6 +2915,10 @@ en:
deleted_tag: "deleted tag"
renamed_tag: "renamed tag"
revoke_email: "revoke email"
+ lock_trust_level: "lock trust level"
+ unlock_trust_level: "unlock trust level"
+ activate_user: "activate user"
+ deactivate_user: "deactivate user"
screened_emails:
title: "Screened Emails"
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index d4bb1d73f01..808beed3794 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -53,6 +53,7 @@ en:
purge_reason: "Automatically deleted as abandoned, deactivated account"
disable_remote_images_download_reason: "Remote images download was disabled because there wasn't enough disk space available."
anonymous: "Anonymous"
+ remove_posts_deleted_by_author: "Deleted by author"
emails:
incoming:
@@ -1615,6 +1616,8 @@ en:
user:
no_accounts_associated: "No accounts associated"
deactivated: "Was deactivated due to too many bounced emails to '%{email}'."
+ deactivated_by_staff: "Deactivated by staff"
+ activated_by_staff: "Activated by staff"
username:
short: "must be at least %{min} characters"
long: "must be no more than %{max} characters"
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 0a430713bb3..4344fab89e5 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -701,7 +701,7 @@ files:
default: 3072
authorized_extensions:
client: true
- default: 'jpg|jpeg|png|gif|csv'
+ default: 'jpg|jpeg|png|gif'
refresh: true
type: list
crawl_images:
diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb
index 2f2e75030cd..84500b50f77 100644
--- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb
+++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb
@@ -52,7 +52,7 @@ class PostgreSQLFallbackHandler
logger.warn "#{log_prefix}: Master server is active. Reconnecting..."
self.master_up(key)
- Discourse.disable_readonly_mode
+ Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY)
end
rescue => e
logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'"
@@ -103,7 +103,7 @@ module ActiveRecord
}))
verify_replica(connection)
- Discourse.enable_readonly_mode
+ Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY)
else
begin
connection = postgresql_connection(config)
diff --git a/lib/custom_setting_providers.rb b/lib/custom_setting_providers.rb
new file mode 100644
index 00000000000..370097d4247
--- /dev/null
+++ b/lib/custom_setting_providers.rb
@@ -0,0 +1,7 @@
+# Support for plugins to register custom setting providers. They can do this
+# by having a file, `register_provider.rb` in their root that will be run
+# at this point.
+
+Dir.glob(File.join(File.dirname(__FILE__), '../plugins', '*', "register_provider.rb")) do |p|
+ require p
+end
diff --git a/lib/discourse.rb b/lib/discourse.rb
index 9f27827f4c5..9c960ae3de9 100644
--- a/lib/discourse.rb
+++ b/lib/discourse.rb
@@ -113,24 +113,6 @@ module Discourse
end
end
- def self.last_read_only
- @last_read_only ||= {}
- end
-
- def self.recently_readonly?
- read_only = last_read_only[$redis.namespace]
- return false unless read_only
- read_only > 15.seconds.ago
- end
-
- def self.received_readonly!
- last_read_only[$redis.namespace] = Time.zone.now
- end
-
- def self.clear_readonly!
- last_read_only[$redis.namespace] = nil
- end
-
def self.disabled_plugin_names
plugins.select { |p| !p.enabled? }.map(&:name)
end
@@ -210,43 +192,66 @@ module Discourse
base_url_no_prefix + base_uri
end
- READONLY_MODE_KEY_TTL ||= 60
- READONLY_MODE_KEY ||= 'readonly_mode'.freeze
+ READONLY_MODE_KEY_TTL ||= 60
+ READONLY_MODE_KEY ||= 'readonly_mode'.freeze
+ PG_READONLY_MODE_KEY ||= 'readonly_mode:postgres'.freeze
USER_READONLY_MODE_KEY ||= 'readonly_mode:user'.freeze
- def self.enable_readonly_mode(user_enabled: false)
- if user_enabled
- $redis.set(USER_READONLY_MODE_KEY, 1)
+ READONLY_KEYS ||= [
+ READONLY_MODE_KEY,
+ PG_READONLY_MODE_KEY,
+ USER_READONLY_MODE_KEY
+ ]
+
+ def self.enable_readonly_mode(key = READONLY_MODE_KEY)
+ if key == USER_READONLY_MODE_KEY
+ $redis.set(key, 1)
else
- $redis.setex(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL, 1)
- keep_readonly_mode
+ $redis.setex(key, READONLY_MODE_KEY_TTL, 1)
+ keep_readonly_mode(key)
end
MessageBus.publish(readonly_channel, true)
true
end
- def self.keep_readonly_mode
+ def self.keep_readonly_mode(key)
# extend the expiry by 1 minute every 30 seconds
unless Rails.env.test?
Thread.new do
while readonly_mode?
- $redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL)
+ $redis.expire(key, READONLY_MODE_KEY_TTL)
sleep 30.seconds
end
end
end
end
- def self.disable_readonly_mode(user_enabled: false)
- key = user_enabled ? USER_READONLY_MODE_KEY : READONLY_MODE_KEY
+ def self.disable_readonly_mode(key = READONLY_MODE_KEY)
$redis.del(key)
MessageBus.publish(readonly_channel, false)
true
end
def self.readonly_mode?
- recently_readonly? || !!$redis.get(READONLY_MODE_KEY) || !!$redis.get(USER_READONLY_MODE_KEY)
+ recently_readonly? || READONLY_KEYS.any? { |key| !!$redis.get(key) }
+ end
+
+ def self.last_read_only
+ @last_read_only ||= {}
+ end
+
+ def self.recently_readonly?
+ return false unless read_only = last_read_only[$redis.namespace]
+ read_only > 15.seconds.ago
+ end
+
+ def self.received_readonly!
+ last_read_only[$redis.namespace] = Time.zone.now
+ end
+
+ def self.clear_readonly!
+ last_read_only[$redis.namespace] = nil
end
def self.request_refresh!
diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb
index 561757a5612..3acdafe96fd 100644
--- a/lib/email/receiver.rb
+++ b/lib/email/receiver.rb
@@ -156,7 +156,7 @@ module Email
elsif bounce_score >= SiteSetting.bounce_score_threshold
# NOTE: we check bounce_score before sending emails, nothing to do
# here other than log it happened.
- reason = I18n.t("user.email.revoked", email: user.email, date: user.user_stat.reset_bounce_score_after)
+ reason = I18n.t("user.email.revoked", date: user.user_stat.reset_bounce_score_after)
StaffActionLogger.new(Discourse.system_user).log_revoke_email(user, reason)
end
end
@@ -239,6 +239,8 @@ module Email
end
def parse_from_field(mail)
+ return unless mail[:from]
+
if mail[:from].errors.blank?
mail[:from].address_list.addresses.each do |address_field|
address_field.decoded
diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb
index a41ab8af8c0..393e4eb0867 100644
--- a/lib/plugin/instance.rb
+++ b/lib/plugin/instance.rb
@@ -363,27 +363,7 @@ JS
#
# This is a very rough initial implementation
def gem(name, version, opts = {})
- gems_path = File.dirname(path) + "/gems/#{RUBY_VERSION}"
- spec_path = gems_path + "/specifications"
- spec_file = spec_path + "/#{name}-#{version}.gemspec"
- unless File.exists? spec_file
- command = "gem install #{name} -v #{version} -i #{gems_path} --no-document --ignore-dependencies"
- if opts[:source]
- command << " --source #{opts[:source]}"
- end
- puts command
- puts `#{command}`
- end
- if File.exists? spec_file
- spec = Gem::Specification.load spec_file
- spec.activate
- unless opts[:require] == false
- require opts[:require_name] ? opts[:require_name] : name
- end
- else
- puts "You are specifying the gem #{name} in #{path}, however it does not exist!"
- exit(-1)
- end
+ PluginGem.load(path, name, version, opts)
end
def enabled_site_setting(setting=nil)
diff --git a/lib/plugin_gem.rb b/lib/plugin_gem.rb
new file mode 100644
index 00000000000..48df5e57c8b
--- /dev/null
+++ b/lib/plugin_gem.rb
@@ -0,0 +1,27 @@
+module PluginGem
+ def self.load(path, name, version, opts=nil)
+ opts ||= {}
+
+ gems_path = File.dirname(path) + "/gems/#{RUBY_VERSION}"
+ spec_path = gems_path + "/specifications"
+ spec_file = spec_path + "/#{name}-#{version}.gemspec"
+ unless File.exists? spec_file
+ command = "gem install #{name} -v #{version} -i #{gems_path} --no-document --ignore-dependencies"
+ if opts[:source]
+ command << " --source #{opts[:source]}"
+ end
+ puts command
+ puts `#{command}`
+ end
+ if File.exists? spec_file
+ spec = Gem::Specification.load spec_file
+ spec.activate
+ unless opts[:require] == false
+ require opts[:require_name] ? opts[:require_name] : name
+ end
+ else
+ puts "You are specifying the gem #{name} in #{path}, however it does not exist!"
+ exit(-1)
+ end
+ end
+end
diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb
index ec4047bce18..49fd91b887d 100644
--- a/lib/post_destroyer.rb
+++ b/lib/post_destroyer.rb
@@ -29,7 +29,7 @@ class PostDestroyer
pa.post_action_type_id IN (?)
)", PostActionType.notify_flag_type_ids)
.each do |post|
- PostDestroyer.new(Discourse.system_user, post).destroy
+ PostDestroyer.new(Discourse.system_user, post, {context: I18n.t('remove_posts_deleted_by_author')}).destroy
end
end
diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb
index fa61e9efc2f..1c0c2f0e1a1 100644
--- a/lib/pretty_text/helpers.rb
+++ b/lib/pretty_text/helpers.rb
@@ -50,7 +50,7 @@ module PrettyText
topic = Topic.find_by(id: topic_id)
if topic && Guardian.new.can_see?(topic)
{
- title: topic.title,
+ title: Rack::Utils.escape_html(topic.title),
href: topic.url
}
end
diff --git a/script/import_scripts/bbpress.rb b/script/import_scripts/bbpress.rb
index 26184192c5a..738535fa1a7 100644
--- a/script/import_scripts/bbpress.rb
+++ b/script/import_scripts/bbpress.rb
@@ -16,6 +16,8 @@ class ImportScripts::Bbpress < ImportScripts::Base
BATCH_SIZE ||= 1000
BB_PRESS_PW ||= ENV['BBPRESS_PW'] || ""
BB_PRESS_USER ||= ENV['BBPRESS_USER'] || "root"
+ BB_PRESS_PREFIX ||= ENV['BBPRESS_PREFIX'] || "wp_"
+
def initialize
super
@@ -37,12 +39,12 @@ class ImportScripts::Bbpress < ImportScripts::Base
puts "", "importing users..."
last_user_id = -1
- total_users = bbpress_query("SELECT COUNT(*) count FROM wp_users WHERE user_email LIKE '%@%'").first["count"]
+ total_users = bbpress_query("SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}users WHERE user_email LIKE '%@%'").first["count"]
batches(BATCH_SIZE) do |offset|
users = bbpress_query(<<-SQL
SELECT id, user_nicename, display_name, user_email, user_registered, user_url
- FROM wp_users
+ FROM #{BB_PRESS_PREFIX}users
WHERE user_email LIKE '%@%'
AND id > #{last_user_id}
ORDER BY id
@@ -62,7 +64,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
users_description = {}
bbpress_query(<<-SQL
SELECT user_id, meta_value description
- FROM wp_usermeta
+ FROM #{BB_PRESS_PREFIX}usermeta
WHERE user_id IN (#{user_ids_sql})
AND meta_key = 'description'
SQL
@@ -71,7 +73,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
users_last_activity = {}
bbpress_query(<<-SQL
SELECT user_id, meta_value last_activity
- FROM wp_usermeta
+ FROM #{BB_PRESS_PREFIX}usermeta
WHERE user_id IN (#{user_ids_sql})
AND meta_key = 'last_activity'
SQL
@@ -97,7 +99,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
categories = bbpress_query(<<-SQL
SELECT id, post_name, post_parent
- FROM wp_posts
+ FROM #{BB_PRESS_PREFIX}posts
WHERE post_type = 'forum'
AND LENGTH(COALESCE(post_name, '')) > 0
ORDER BY post_parent, id
@@ -119,7 +121,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
last_post_id = -1
total_posts = bbpress_query(<<-SQL
SELECT COUNT(*) count
- FROM wp_posts
+ FROM #{BB_PRESS_PREFIX}posts
WHERE post_status <> 'spam'
AND post_type IN ('topic', 'reply')
SQL
@@ -134,7 +136,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
post_title,
post_type,
post_parent
- FROM wp_posts
+ FROM #{BB_PRESS_PREFIX}posts
WHERE post_status <> 'spam'
AND post_type IN ('topic', 'reply')
AND id > #{last_post_id}
@@ -155,7 +157,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
posts_likes = {}
bbpress_query(<<-SQL
SELECT post_id, meta_value likes
- FROM wp_postmeta
+ FROM #{BB_PRESS_PREFIX}postmeta
WHERE post_id IN (#{post_ids_sql})
AND meta_key = 'Likes'
SQL
diff --git a/script/import_scripts/drupal-6.rb b/script/import_scripts/drupal-6.rb
new file mode 100644
index 00000000000..1df513df409
--- /dev/null
+++ b/script/import_scripts/drupal-6.rb
@@ -0,0 +1,210 @@
+require "mysql2"
+require File.expand_path(File.dirname(__FILE__) + "/base.rb")
+
+class ImportScripts::Drupal < ImportScripts::Base
+
+ DRUPAL_DB = ENV['DRUPAL_DB'] || "newsite3"
+ VID = ENV['DRUPAL_VID'] || 1
+
+ def initialize
+ super
+
+ @client = Mysql2::Client.new(
+ host: "localhost",
+ username: "root",
+ #password: "password",
+ database: DRUPAL_DB
+ )
+ end
+
+ def categories_query
+ @client.query("SELECT tid, name, description FROM term_data WHERE vid = #{VID}")
+ end
+
+ def execute
+ create_users(@client.query("SELECT uid id, name, mail email, created FROM users;")) do |row|
+ {id: row['id'], username: row['name'], email: row['email'], created_at: Time.zone.at(row['created'])}
+ end
+
+ # You'll need to edit the following query for your Drupal install:
+ #
+ # * Drupal allows duplicate category names, so you may need to exclude some categories or rename them here.
+ # * Table name may be term_data.
+ # * May need to select a vid other than 1.
+ create_categories(categories_query) do |c|
+ {id: c['tid'], name: c['name'], description: c['description']}
+ end
+
+ # "Nodes" in Drupal are divided into types. Here we import two types,
+ # and will later import all the comments/replies for each node.
+ # You will need to figure out what the type names are on your install and edit the queries to match.
+ if ENV['DRUPAL_IMPORT_BLOG']
+ create_blog_topics
+ end
+
+ create_forum_topics
+
+ create_replies
+
+ begin
+ create_admin(email: 'neil.lalonde@discourse.org', username: UserNameSuggester.suggest('neil'))
+ rescue => e
+ puts '', "Failed to create admin user"
+ puts e.message
+ end
+ end
+
+ def create_blog_topics
+ puts '', "creating blog topics"
+
+ create_category({
+ name: 'Blog',
+ user_id: -1,
+ description: "Articles from the blog"
+ }, nil) unless Category.find_by_name('Blog')
+
+ results = @client.query("
+ SELECT n.nid nid,
+ n.title title,
+ n.uid uid,
+ n.created created,
+ n.sticky sticky,
+ nr.body body
+ FROM node n
+ LEFT JOIN node_revisions nr ON nr.vid=n.vid
+ WHERE n.type = 'blog'
+ AND n.status = 1
+ ", cache_rows: false)
+
+ create_posts(results) do |row|
+ {
+ id: "nid:#{row['nid']}",
+ user_id: user_id_from_imported_user_id(row['uid']) || -1,
+ category: 'Blog',
+ raw: row['body'],
+ created_at: Time.zone.at(row['created']),
+ pinned_at: row['sticky'].to_i == 1 ? Time.zone.at(row['created']) : nil,
+ title: row['title'].try(:strip),
+ custom_fields: {import_id: "nid:#{row['nid']}"}
+ }
+ end
+ end
+
+ def create_forum_topics
+ puts '', "creating forum topics"
+
+ total_count = @client.query("
+ SELECT COUNT(*) count
+ FROM node n
+ LEFT JOIN forum f ON f.vid=n.vid
+ WHERE n.type = 'forum'
+ AND n.status = 1
+ ").first['count']
+
+ batch_size = 1000
+
+ batches(batch_size) do |offset|
+ results = @client.query("
+ SELECT n.nid nid,
+ n.title title,
+ f.tid tid,
+ n.uid uid,
+ n.created created,
+ n.sticky sticky,
+ nr.body body
+ FROM node n
+ LEFT JOIN forum f ON f.vid=n.vid
+ LEFT JOIN node_revisions nr ON nr.vid=n.vid
+ WHERE node.type = 'forum'
+ AND node.status = 1
+ LIMIT #{batch_size}
+ OFFSET #{offset};
+ ", cache_rows: false)
+
+ break if results.size < 1
+
+ next if all_records_exist? :posts, results.map {|p| "nid:#{p['nid']}"}
+
+ create_posts(results, total: total_count, offset: offset) do |row|
+ {
+ id: "nid:#{row['nid']}",
+ user_id: user_id_from_imported_user_id(row['uid']) || -1,
+ category: category_id_from_imported_category_id(row['tid']),
+ raw: row['body'],
+ created_at: Time.zone.at(row['created']),
+ pinned_at: row['sticky'].to_i == 1 ? Time.zone.at(row['created']) : nil,
+ title: row['title'].try(:strip)
+ }
+ end
+ end
+ end
+
+ def create_replies
+ puts '', "creating replies in topics"
+
+ if ENV['DRUPAL_IMPORT_BLOG']
+ node_types = "('forum','blog')"
+ else
+ node_types = "('forum')"
+ end
+
+ total_count = @client.query("
+ SELECT COUNT(*) count
+ FROM comments c
+ LEFT JOIN node n ON n.nid=c.nid
+ WHERE node.type IN #{node_types}
+ AND node.status = 1
+ AND comments.status=0;
+ ").first['count']
+
+ batch_size = 1000
+
+ batches(batch_size) do |offset|
+ results = @client.query("
+ SELECT c.cid,
+ c.pid,
+ c.nid,
+ c.uid,
+ c.timestamp,
+ c.comment body
+ FROM comments c
+ LEFT JOIN node n ON n.nid=c.nid
+ WHERE n.type IN #{node_types}
+ AND n.status = 1
+ AND c.status=0
+ LIMIT #{batch_size}
+ OFFSET #{offset};
+ ", cache_rows: false)
+
+ break if results.size < 1
+
+ next if all_records_exist? :posts, results.map {|p| "cid:#{p['cid']}"}
+
+ create_posts(results, total: total_count, offset: offset) do |row|
+ topic_mapping = topic_lookup_from_imported_post_id("nid:#{row['nid']}")
+ if topic_mapping && topic_id = topic_mapping[:topic_id]
+ h = {
+ id: "cid:#{row['cid']}",
+ topic_id: topic_id,
+ user_id: user_id_from_imported_user_id(row['uid']) || -1,
+ raw: row['body'],
+ created_at: Time.zone.at(row['timestamp']),
+ }
+ if row['pid']
+ parent = topic_lookup_from_imported_post_id("cid:#{row['pid']}")
+ h[:reply_to_post_number] = parent[:post_number] if parent and parent[:post_number] > 1
+ end
+ h
+ else
+ puts "No topic found for comment #{row['cid']}"
+ nil
+ end
+ end
+ end
+ end
+
+end
+
+if __FILE__==$0
+ ImportScripts::Drupal.new.perform
+end
diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb
index a9e61dbea15..74f48940754 100644
--- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb
+++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb
@@ -42,8 +42,13 @@ describe ActiveRecord::ConnectionHandling do
end
after do
- with_multisite_db(multisite_db) { Discourse.disable_readonly_mode }
- Discourse.disable_readonly_mode
+ pg_readonly_mode_key = Discourse::PG_READONLY_MODE_KEY
+
+ with_multisite_db(multisite_db) do
+ Discourse.disable_readonly_mode(pg_readonly_mode_key)
+ end
+
+ Discourse.disable_readonly_mode(pg_readonly_mode_key)
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
end
diff --git a/spec/components/discourse_spec.rb b/spec/components/discourse_spec.rb
index 5d441eacc33..b1b21cfd5c9 100644
--- a/spec/components/discourse_spec.rb
+++ b/spec/components/discourse_spec.rb
@@ -118,7 +118,7 @@ describe Discourse do
context 'user enabled readonly mode' do
it "adds a key in redis and publish a message through the message bus" do
expect($redis.get(user_readonly_mode_key)).to eq(nil)
- message = MessageBus.track_publish { Discourse.enable_readonly_mode(user_enabled: true) }.first
+ message = MessageBus.track_publish { Discourse.enable_readonly_mode(user_readonly_mode_key) }.first
assert_readonly_mode(message, user_readonly_mode_key)
end
end
@@ -160,10 +160,10 @@ describe Discourse do
end
it "returns true when user enabled readonly mode key is present in redis" do
- Discourse.enable_readonly_mode(user_enabled: true)
+ Discourse.enable_readonly_mode(user_readonly_mode_key)
expect(Discourse.readonly_mode?).to eq(true)
- Discourse.disable_readonly_mode(user_enabled: true)
+ Discourse.disable_readonly_mode(user_readonly_mode_key)
expect(Discourse.readonly_mode?).to eq(false)
end
end
diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb
index 0a7b0a1bed0..47c7c4a1dac 100644
--- a/spec/components/email/receiver_spec.rb
+++ b/spec/components/email/receiver_spec.rb
@@ -383,37 +383,46 @@ describe Email::Receiver do
expect(Post.last.raw).to match(/discourse\.rb/)
end
- it "handles forwarded emails" do
- SiteSetting.enable_forwarded_emails = true
- expect { process(:forwarded_email_1) }.to change(Topic, :count)
+ context "with forwarded emails enabled" do
+ before { SiteSetting.enable_forwarded_emails = true }
- forwarded_post, last_post = *Post.last(2)
+ it "handles forwarded emails" do
+ expect { process(:forwarded_email_1) }.to change(Topic, :count)
- expect(forwarded_post.user.email).to eq("some@one.com")
- expect(last_post.user.email).to eq("ba@bar.com")
+ forwarded_post, last_post = *Post.last(2)
- expect(forwarded_post.raw).to match(/XoXo/)
- expect(last_post.raw).to match(/can you have a look at this email below/)
+ expect(forwarded_post.user.email).to eq("some@one.com")
+ expect(last_post.user.email).to eq("ba@bar.com")
- expect(last_post.post_type).to eq(Post.types[:regular])
- end
+ expect(forwarded_post.raw).to match(/XoXo/)
+ expect(last_post.raw).to match(/can you have a look at this email below/)
- it "handles weirdly forwarded emails" do
- group.add(Fabricate(:user, email: "ba@bar.com"))
- group.save
+ expect(last_post.post_type).to eq(Post.types[:regular])
+ end
- SiteSetting.enable_forwarded_emails = true
- expect { process(:forwarded_email_2) }.to change(Topic, :count)
+ it "handles weirdly forwarded emails" do
+ group.add(Fabricate(:user, email: "ba@bar.com"))
+ group.save
- forwarded_post, last_post = *Post.last(2)
+ SiteSetting.enable_forwarded_emails = true
+ expect { process(:forwarded_email_2) }.to change(Topic, :count)
- expect(forwarded_post.user.email).to eq("some@one.com")
- expect(last_post.user.email).to eq("ba@bar.com")
+ forwarded_post, last_post = *Post.last(2)
- expect(forwarded_post.raw).to match(/XoXo/)
- expect(last_post.raw).to match(/can you have a look at this email below/)
+ expect(forwarded_post.user.email).to eq("some@one.com")
+ expect(last_post.user.email).to eq("ba@bar.com")
+
+ expect(forwarded_post.raw).to match(/XoXo/)
+ expect(last_post.raw).to match(/can you have a look at this email below/)
+
+ expect(last_post.post_type).to eq(Post.types[:whisper])
+ end
+
+ # Who thought this was a good idea?!
+ it "doesn't blow up with localized email headers" do
+ expect { process(:forwarded_email_3) }.to change(Topic, :count)
+ end
- expect(last_post.post_type).to eq(Post.types[:whisper])
end
end
diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb
index 315b0cac408..b63d77584b4 100644
--- a/spec/components/pretty_text_spec.rb
+++ b/spec/components/pretty_text_spec.rb
@@ -10,10 +10,10 @@ describe PrettyText do
describe "off topic quoting" do
it "can correctly populate topic title" do
- topic = Fabricate(:topic, title: "this is a test topic")
+ topic = Fabricate(:topic, title: "this is a test topic :slight_smile:")
expected = <
ddd
HTML
diff --git a/spec/fixtures/emails/forwarded_email_3.eml b/spec/fixtures/emails/forwarded_email_3.eml
new file mode 100644
index 00000000000..1f37822b0a7
--- /dev/null
+++ b/spec/fixtures/emails/forwarded_email_3.eml
@@ -0,0 +1,18 @@
+Message-ID: <60@foo.bar.mail>
+From: Ba Bar
+To: Team
+Date: Mon, 9 Dec 2016 13:37:42 +0100
+Subject: Fwd: Ça Discourse ?
+
+@team, can you have a look at this email below?
+
+Objet: Ça Discourse ?
+Date: 2017-01-04 11:27
+De: Un Français
+À: ba@bar.com
+
+Bonjour,
+
+Ça Discourse bien aujourd'hui ?
+
+Bises
diff --git a/spec/services/staff_action_logger_spec.rb b/spec/services/staff_action_logger_spec.rb
index 676b899dce9..773f990d104 100644
--- a/spec/services/staff_action_logger_spec.rb
+++ b/spec/services/staff_action_logger_spec.rb
@@ -369,4 +369,42 @@ describe StaffActionLogger do
expect(user_history.action).to eq(UserHistory.actions[:create_category])
end
end
+
+ describe 'log_lock_trust_level' do
+ let(:user) { Fabricate(:user) }
+
+ it "raises an error when argument is missing" do
+ expect { logger.log_lock_trust_level(nil) }.to raise_error(Discourse::InvalidParameters)
+ end
+
+ it "creates a new UserHistory record" do
+ user.trust_level_locked = true
+ expect { logger.log_lock_trust_level(user) }.to change { UserHistory.count }.by(1)
+ user_history = UserHistory.last
+ expect(user_history.action).to eq(UserHistory.actions[:lock_trust_level])
+
+ user.trust_level_locked = false
+ expect { logger.log_lock_trust_level(user) }.to change { UserHistory.count }.by(1)
+ user_history = UserHistory.last
+ expect(user_history.action).to eq(UserHistory.actions[:unlock_trust_level])
+ end
+ end
+
+ describe 'log_user_activate' do
+ let(:user) { Fabricate(:user) }
+
+ it "raises an error when argument is missing" do
+ expect { logger.log_user_activate(nil, nil) }.to raise_error(Discourse::InvalidParameters)
+ end
+
+ it "creates a new UserHistory record" do
+ reason = "Staff activated from admin"
+ expect {
+ logger.log_user_activate(user, reason)
+ }.to change { UserHistory.count }.by(1)
+ user_history = UserHistory.last
+ expect(user_history.action).to eq(UserHistory.actions[:activate_user])
+ expect(user_history.details).to eq(reason)
+ end
+ end
end
diff --git a/spec/services/user_blocker_spec.rb b/spec/services/user_blocker_spec.rb
index b75b5766a79..6761c44b3d6 100644
--- a/spec/services/user_blocker_spec.rb
+++ b/spec/services/user_blocker_spec.rb
@@ -58,6 +58,14 @@ describe UserBlocker do
SystemMessage.expects(:create).never
expect(block_user).to eq(false)
end
+
+ it "logs it with context" do
+ SystemMessage.stubs(:create).returns(Fabricate.build(:post))
+ expect {
+ UserBlocker.block(user, Fabricate(:admin))
+ }.to change { UserHistory.count }.by(1)
+ expect(UserHistory.last.context).to be_present
+ end
end
describe 'unblock' do
@@ -81,6 +89,12 @@ describe UserBlocker do
SystemMessage.expects(:create).never
unblock_user
end
+
+ it "logs it" do
+ expect {
+ unblock_user
+ }.to change { UserHistory.count }.by(1)
+ end
end
describe 'hide_posts' do