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. -Atom   +Atom   Soylent 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 = <
-
This is a test topic +
This is a test topic slight_smile

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