From 09ef5f613ef5fdf74554707de1fdccc935c6b0b9 Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Tue, 17 May 2016 01:12:09 -0300 Subject: [PATCH 01/71] FEATURE: add setting permanent_session_cookie to configure session stickiness Now admins can turn make the login cookie die after the browser is closed, so the user needs to log in everytime. --- config/locales/server.en.yml | 1 + config/site_settings.yml | 1 + lib/auth/default_current_user_provider.rb | 6 +++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 57a68ba5b2..78790781b0 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -890,6 +890,7 @@ en: post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site. WARNING: enabling this for a live site will revoke access for existing non-staff users!" pending_users_reminder_delay: "Notify moderators if new users have been waiting for approval for longer than this many hours. Set to -1 to disable notifications." + permanent_session_cookie: "Use a permanent cookie that persists after closing the browser. When disabling this, you may want to log out everyone programmatically." ga_tracking_code: "Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" ga_domain_name: "Google analytics (ga.js) domain name, eg: mysite.com; see http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" diff --git a/config/site_settings.yml b/config/site_settings.yml index 6a73beaa8e..c785c73dfd 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -289,6 +289,7 @@ login: pending_users_reminder_delay: min: -1 default: 8 + permanent_session_cookie: true users: min_username_length: diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index 0bc2bf34e7..5755f28549 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -66,7 +66,11 @@ class Auth::DefaultCurrentUserProvider user.auth_token = SecureRandom.hex(16) user.save! end - cookies.permanent[TOKEN_COOKIE] = { value: user.auth_token, httponly: true } + if SiteSetting.permanent_session_cookie + cookies.permanent[TOKEN_COOKIE] = { value: user.auth_token, httponly: true } + else + cookies[TOKEN_COOKIE] = { value: user.auth_token, httponly: true } + end make_developer_admin(user) enable_bootstrap_mode(user) @env[CURRENT_USER_KEY] = user From 09b92dd3456473ca8a09a685555f155b528e32bf Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 10 Jun 2016 11:00:15 +0800 Subject: [PATCH 02/71] Bump minimum Ruby version to 2.3. --- .travis.yml | 3 --- README.md | 2 +- app/models/admin_dashboard_data.rb | 6 +----- config/boot.rb | 6 +----- config/initializers/006-mini_profiler.rb | 7 +------ config/initializers/099-unicorn.rb | 5 ----- config/locales/server.en.yml | 1 - 7 files changed, 4 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index 43ef9e8166..eb934cd915 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,9 +24,6 @@ matrix: fast_finish: true rvm: - - 2.0.0 - - 2.1 - - 2.2 - 2.3.1 services: diff --git a/README.md b/README.md index f74dcb0be4..6cdf4c101b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Browse [lots more notable Discourse instances](http://www.discourse.org/faq/cust 2. If you're familiar with how Rails works and are comfortable setting up your own environment, use our [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md). -Before you get started, ensure you have the following minimum versions: [Ruby 2.0.0+](http://www.ruby-lang.org/en/downloads/), [PostgreSQL 9.3+](http://www.postgresql.org/download/), [Redis 2.6+](http://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first! +Before you get started, ensure you have the following minimum versions: [Ruby 2.3+](http://www.ruby-lang.org/en/downloads/), [PostgreSQL 9.3+](http://www.postgresql.org/download/), [Redis 2.6+](http://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first! ## Setting up Discourse diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 66a31f8875..a8103aa7de 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -90,7 +90,7 @@ class AdminDashboardData 'dashboard.poll_pop3_auth_error' ] - add_problem_check :rails_env_check, :ruby_version_check, :host_names_check, + add_problem_check :rails_env_check, :host_names_check, :ram_check, :google_oauth2_config_check, :facebook_config_check, :twitter_config_check, :github_config_check, :s3_config_check, :image_magick_check, @@ -249,10 +249,6 @@ class AdminDashboardData I18n.t('dashboard.notification_email_warning') if !SiteSetting.notification_email.present? || SiteSetting.notification_email == SiteSetting.defaults[:notification_email] end - def ruby_version_check - I18n.t('dashboard.ruby_version_warning') if RUBY_VERSION == '2.0.0' and RUBY_PATCHLEVEL < 247 - end - def subfolder_ends_in_slash_check I18n.t('dashboard.subfolder_ends_in_slash') if Discourse.base_uri =~ /\/$/ end diff --git a/config/boot.rb b/config/boot.rb index 517524d879..afa16ad5af 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,10 +1,6 @@ if ENV['DISCOURSE_DUMP_HEAP'] == "1" require 'objspace' - begin - ObjectSpace.trace_object_allocations_start - rescue NoMethodError - puts "Heap dumps not available for Ruby #{RUBY_VERSION} (> 2.1 required)" - end + ObjectSpace.trace_object_allocations_start end require 'rubygems' diff --git a/config/initializers/006-mini_profiler.rb b/config/initializers/006-mini_profiler.rb index 9ef0c9ec92..c8ab968581 100644 --- a/config/initializers/006-mini_profiler.rb +++ b/config/initializers/006-mini_profiler.rb @@ -2,12 +2,7 @@ if Rails.configuration.respond_to?(:load_mini_profiler) && Rails.configuration.load_mini_profiler require 'rack-mini-profiler' require 'flamegraph' - - begin - require 'memory_profiler' if RUBY_VERSION >= "2.1.0" - rescue => e - STDERR.put "#{e} failed to require mini profiler" - end + require 'memory_profiler' # initialization is skipped so trigger it Rack::MiniProfilerRails.initialize!(Rails.application) diff --git a/config/initializers/099-unicorn.rb b/config/initializers/099-unicorn.rb index 43e8cb75f7..e6152b69d7 100644 --- a/config/initializers/099-unicorn.rb +++ b/config/initializers/099-unicorn.rb @@ -4,9 +4,4 @@ if defined? Unicorn::HttpServer ObjectSpace.each_object(Unicorn::HttpServer) do |s| s.extend(Scheduler::Defer::Unicorn) end - - if ENV['UNICORN_ENABLE_OOBGC'] == '1' && RUBY_VERSION < "2.2.0" - require 'middleware/unicorn_oobgc' - Middleware::UnicornOobgc.init - end end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b85c82744b..456a7558e6 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -773,7 +773,6 @@ en: dashboard: rails_env_warning: "Your server is running in %{env} mode." - ruby_version_warning: "You are running a version of Ruby 2.0.0 that is known to have problems. Upgrade to patch level 247 or later." host_names_warning: "Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname." gc_warning: 'Your server is using default ruby garbage collection parameters, which will not give you the best performance. Read this topic on performance tuning: Tuning Ruby and Rails for Discourse.' sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by sidekiq. Please ensure at least one sidekiq process is running. Learn about Sidekiq here.' From ccb5eed717aed11e54ae3e0f38a41380147dc711 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Tue, 14 Jun 2016 14:17:39 +1000 Subject: [PATCH 03/71] exit if RETRY is false --- lib/tasks/qunit.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake index 07e9a48b6e..5aa1d79012 100644 --- a/lib/tasks/qunit.rake +++ b/lib/tasks/qunit.rake @@ -52,6 +52,7 @@ task "qunit:test" => :environment do begin sh(cmd) rescue + exit if ENV['RETRY'].present? && ENV['RETRY'] == 'false' sleep 2 tries += 1 retry unless tries == 10 From 9bf381b95c5f29fce9c7a40e7a3d55da2ba8d9df Mon Sep 17 00:00:00 2001 From: scossar Date: Mon, 20 Jun 2016 14:03:24 -0700 Subject: [PATCH 04/71] get urlWithCDN before appending protocol --- app/assets/javascripts/discourse/lib/utilities.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index 491e2b6a2b..95aa9b8b07 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -251,7 +251,8 @@ Discourse.Utilities = { uploadLocation: function(url) { if (Discourse.CDN) { - return Discourse.CDN.startsWith('//') ? "http:" + Discourse.getURLWithCDN(url) : Discourse.getURLWithCDN(url); + url = Discourse.getURLWithCDN(url); + return url.startsWith('//') ? 'http:' + url : url; } else if (Discourse.SiteSettings.enable_s3_uploads) { return 'https:' + url; } else { From 94a4af6af77b3d6366f4c7c685d9edd2b7e9be43 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 21 Jun 2016 12:59:33 -0400 Subject: [PATCH 05/71] FIX: If posts are deleted they should be updated in consistency jobs --- app/models/topic_featured_users.rb | 25 +++++++++++++----------- spec/models/topic_featured_users_spec.rb | 18 ++++++++++++----- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/models/topic_featured_users.rb b/app/models/topic_featured_users.rb index 9d6297a4cb..f28c555b4c 100644 --- a/app/models/topic_featured_users.rb +++ b/app/models/topic_featured_users.rb @@ -25,6 +25,7 @@ class TopicFeaturedUsers def self.ensure_consistency!(topic_id=nil) filter = "#{"AND t.id = #{topic_id.to_i}" if topic_id}" + filter2 = "#{"AND tt.id = #{topic_id.to_i}" if topic_id}" sql = < COALESCE(featured_user1,-99) OR - COALESCE(featured_user2_id,-99) <> COALESCE(featured_user2,-99) OR - COALESCE(featured_user3_id,-99) <> COALESCE(featured_user3,-99) OR - COALESCE(featured_user4_id,-99) <> COALESCE(featured_user4,-99) + COALESCE(tt.featured_user1_id,-99) <> COALESCE(x.featured_user1,-99) OR + COALESCE(tt.featured_user2_id,-99) <> COALESCE(x.featured_user2,-99) OR + COALESCE(tt.featured_user3_id,-99) <> COALESCE(x.featured_user3,-99) OR + COALESCE(tt.featured_user4_id,-99) <> COALESCE(x.featured_user4,-99) ) +#{filter2} SQL Topic.exec_sql(sql) diff --git a/spec/models/topic_featured_users_spec.rb b/spec/models/topic_featured_users_spec.rb index 71ff4a5423..09459115ea 100644 --- a/spec/models/topic_featured_users_spec.rb +++ b/spec/models/topic_featured_users_spec.rb @@ -1,12 +1,12 @@ require 'rails_helper' describe TopicFeaturedUsers do - it 'ensures consistenct' do + it 'ensures consistency' do t = Fabricate(:topic) Fabricate(:post, topic_id: t.id, user_id: t.user_id) p2 = Fabricate(:post, topic_id: t.id) - Fabricate(:post, topic_id: t.id, user_id: p2.user_id) + p3 = Fabricate(:post, topic_id: t.id, user_id: p2.user_id) p4 = Fabricate(:post, topic_id: t.id) p5 = Fabricate(:post, topic_id: t.id) @@ -14,11 +14,9 @@ describe TopicFeaturedUsers do featured_user2_id: 70, featured_user3_id: 12, featured_user4_id: 7, - last_post_user_id: p5.user_id - ) + last_post_user_id: p5.user_id) TopicFeaturedUsers.ensure_consistency! - t.reload expect(t.featured_user1_id).to eq(p2.user_id) @@ -26,6 +24,16 @@ describe TopicFeaturedUsers do expect(t.featured_user3_id).to eq(nil) expect(t.featured_user4_id).to eq(nil) + # after removing a post + p2.update_column(:deleted_at, Time.now) + p3.update_column(:hidden, true) + TopicFeaturedUsers.ensure_consistency! + t.reload + + expect(t.featured_user1_id).to eq(p4.user_id) + expect(t.featured_user2_id).to eq(nil) + expect(t.featured_user3_id).to eq(nil) + expect(t.featured_user4_id).to eq(nil) end end From 7337b2953fe2ed5af425c43f0d6c7f89f69acd56 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Tue, 21 Jun 2016 17:01:23 -0700 Subject: [PATCH 06/71] slightly less giant poll percentages --- plugins/poll/assets/stylesheets/common/poll.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/poll/assets/stylesheets/common/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss index 1d25e2d574..e0fdd22b9a 100644 --- a/plugins/poll/assets/stylesheets/common/poll.scss +++ b/plugins/poll/assets/stylesheets/common/poll.scss @@ -115,9 +115,10 @@ div.poll { } .percentage { - font-size: 25px; + font-size: 20px; float: right; color: $text-color; + margin-left: 5px; } .bar-back { From 3701a8ada2a96a7a147843d6d48bc672fd677bd3 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 22 Jun 2016 10:56:23 +1000 Subject: [PATCH 07/71] FIX: missing in action wrench on short topics --- .../javascripts/discourse/widgets/topic-timeline.js.es6 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 index 756d0b073d..06ab9b343f 100644 --- a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 @@ -209,14 +209,16 @@ export default createWidget('topic-timeline', { const stream = attrs.topic.get('postStream.stream'); const { currentUser } = this; - if (stream.length < 3) { return; } - let result = []; if (currentUser && currentUser.get('canManageTopic')) { result.push(h('div.timeline-controls', this.attach('topic-admin-menu-button', { topic }))); } + if (stream.length < 3) { + return result; + } + const bottomAge = relativeAge(new Date(topic.last_posted_at), { addAgo: true, defaultFormat: timelineDate }); result = result.concat([this.attach('link', { className: 'start-date', From 6e4ff45e4419096ebbe47101528acaee1e76098a Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 22 Jun 2016 17:28:46 +1000 Subject: [PATCH 08/71] FIX: deleting a topic result not updated on screen --- .../javascripts/discourse/controllers/topic.js.es6 | 1 - lib/post_destroyer.rb | 9 +++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 0062c13bda..76d64427e8 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -758,7 +758,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { }, deleteTopic() { - this.unsubscribe(); this.get('content').destroy(Discourse.User.current()); }, diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index b1538585af..c0ac588d94 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -109,8 +109,11 @@ class PostDestroyer # When a user 'deletes' their own post. We just change the text. def mark_for_deletion I18n.with_locale(SiteSetting.default_locale) do + + # don't call revise from within transaction, high risk of deadlock + @post.revise(@user, { raw: I18n.t('js.post.deleted_by_author', count: SiteSetting.delete_removed_posts_after) }, force_new_version: true) + Post.transaction do - @post.revise(@user, { raw: I18n.t('js.post.deleted_by_author', count: SiteSetting.delete_removed_posts_after) }, force_new_version: true) @post.update_column(:user_deleted, true) @post.update_flagged_posts_count @post.topic_links.each(&:destroy) @@ -122,9 +125,11 @@ class PostDestroyer Post.transaction do @post.update_column(:user_deleted, false) @post.skip_unique_check = true - @post.revise(@user, { raw: @post.revisions.last.modifications["raw"][0] }, force_new_version: true) @post.update_flagged_posts_count end + + # has internal transactions, if we nest then there are some very high risk deadlocks + @post.revise(@user, { raw: @post.revisions.last.modifications["raw"][0] }, force_new_version: true) end private From 2ecd0da59f52dd8828a7b7fcba8170e285a82ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 22 Jun 2016 15:50:49 +0200 Subject: [PATCH 09/71] REFACTOR: use same code path for handling emails via API and POP --- app/controllers/admin/email_controller.rb | 2 +- app/jobs/scheduled/poll_mailbox.rb | 96 +------------------- lib/email/processor.rb | 102 ++++++++++++++++++++++ 3 files changed, 105 insertions(+), 95 deletions(-) create mode 100644 lib/email/processor.rb diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index 55b1349659..832efd85e4 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -52,7 +52,7 @@ class Admin::EmailController < Admin::AdminController def handle_mail params.require(:email) - Email::Receiver.new(params[:email]).process! + Email::Processor.process!(params[:email]) render text: "email was processed" end diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb index 89de500cb8..837bef8505 100644 --- a/app/jobs/scheduled/poll_mailbox.rb +++ b/app/jobs/scheduled/poll_mailbox.rb @@ -1,5 +1,6 @@ require 'net/pop' require_dependency 'email/receiver' +require_dependency 'email/processor' require_dependency 'email/sender' require_dependency 'email/message_builder' @@ -21,88 +22,7 @@ module Jobs end def process_popmail(popmail) - begin - mail_string = popmail.pop - receiver = Email::Receiver.new(mail_string) - receiver.process! - rescue Email::Receiver::BouncedEmailError => e - log_email_process_failure(mail_string, e) - - set_incoming_email_rejection_message( - receiver.incoming_email, - I18n.t("emails.incoming.errors.bounced_email_error") - ) - rescue Email::Receiver::AutoGeneratedEmailReplyError => e - log_email_process_failure(mail_string, e) - - set_incoming_email_rejection_message( - receiver.incoming_email, - I18n.t("emails.incoming.errors.auto_generated_email_reply") - ) - rescue => e - rejection_message = handle_failure(mail_string, e) - if rejection_message.present? && receiver && (incoming_email = receiver.incoming_email) - set_incoming_email_rejection_message(incoming_email, rejection_message.body.to_s) - end - end - end - - def handle_failure(mail_string, e) - log_email_process_failure(mail_string, e) - - message_template = case e - when Email::Receiver::EmptyEmailError then :email_reject_empty - when Email::Receiver::NoBodyDetectedError then :email_reject_empty - when Email::Receiver::UserNotFoundError then :email_reject_user_not_found - when Email::Receiver::ScreenedEmailError then :email_reject_screened_email - when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated - when Email::Receiver::InactiveUserError then :email_reject_inactive_user - when Email::Receiver::BlockedUserError then :email_reject_blocked_user - when Email::Receiver::BadDestinationAddress then :email_reject_bad_destination_address - when Email::Receiver::StrangersNotAllowedError then :email_reject_strangers_not_allowed - when Email::Receiver::InsufficientTrustLevelError then :email_reject_insufficient_trust_level - when Email::Receiver::ReplyUserNotMatchingError then :email_reject_reply_user_not_matching - when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found - when Email::Receiver::TopicClosedError then :email_reject_topic_closed - when Email::Receiver::InvalidPost then :email_reject_invalid_post - when ActiveRecord::Rollback then :email_reject_invalid_post - when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action - when Discourse::InvalidAccess then :email_reject_invalid_access - when RateLimiter::LimitExceeded then :email_reject_rate_limit_specified - end - - template_args = {} - client_message = nil - - # there might be more information available in the exception - if message_template == :email_reject_invalid_post && e.message.size > 6 - message_template = :email_reject_invalid_post_specified - template_args[:post_error] = e.message - end - - if message_template == :email_reject_rate_limit_specified - template_args[:rate_limit_description] = e.description - end - - if message_template == :email_reject_auto_generated - template_args[:mark_as_reply_to_auto_generated] = true - end - - if message_template - # inform the user about the rejection - message = Mail::Message.new(mail_string) - template_args[:former_title] = message.subject - template_args[:destination] = message.to - template_args[:site_name] = SiteSetting.title - - client_message = RejectionMailer.send_rejection(message_template, message.from, template_args) - Email::Sender.new(client_message, message_template).send - else - mark_as_errored! - Discourse.handle_job_exception(e, error_context(@args, "Unrecognized error type when processing incoming email", mail: mail_string)) - end - - client_message + Email::Processor.process!(popmail.pop) end POLL_MAILBOX_TIMEOUT_ERROR_KEY = "poll_mailbox_timeout_error_key".freeze @@ -148,18 +68,6 @@ module Jobs $redis.zadd(POLL_MAILBOX_ERRORS_KEY, now, now.to_s) end - private - - def set_incoming_email_rejection_message(incoming_email, message) - incoming_email.update_attributes!(rejection_message: message) - end - - def log_email_process_failure(mail_string, exception) - if SiteSetting.log_mail_processing_failures - Rails.logger.warn("Email can not be processed: #{exception}\n\n#{mail_string}") - end - end - def add_admin_dashboard_problem_message(i18n_key) AdminDashboardData.add_problem_message( i18n_key, diff --git a/lib/email/processor.rb b/lib/email/processor.rb new file mode 100644 index 0000000000..85dace97f6 --- /dev/null +++ b/lib/email/processor.rb @@ -0,0 +1,102 @@ +module Email + + class Processor + + def initialize(mail) + @mail = mail + end + + def self.process!(mail) + Email::Processor.new(mail).process! + end + + def process! + begin + receiver = Email::Receiver.new(@mail) + receiver.process! + rescue Email::Receiver::BouncedEmailError => e + log_email_process_failure(@mail, e) + set_incoming_email_rejection_message(receiver.incoming_email, I18n.t("emails.incoming.errors.bounced_email_error")) + rescue Email::Receiver::AutoGeneratedEmailReplyError => e + log_email_process_failure(@mail, e) + set_incoming_email_rejection_message(receiver.incoming_email, I18n.t("emails.incoming.errors.auto_generated_email_reply")) + rescue => e + log_email_process_failure(@mail, e) + + rejection_message = handle_failure(@mail, e) + if rejection_message.present? && receiver && (incoming_email = receiver.incoming_email) + set_incoming_email_rejection_message(incoming_email, rejection_message.body.to_s) + end + end + end + + private + + def handle_failure(mail_string, e) + message_template = case e + when Email::Receiver::EmptyEmailError then :email_reject_empty + when Email::Receiver::NoBodyDetectedError then :email_reject_empty + when Email::Receiver::UserNotFoundError then :email_reject_user_not_found + when Email::Receiver::ScreenedEmailError then :email_reject_screened_email + when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated + when Email::Receiver::InactiveUserError then :email_reject_inactive_user + when Email::Receiver::BlockedUserError then :email_reject_blocked_user + when Email::Receiver::BadDestinationAddress then :email_reject_bad_destination_address + when Email::Receiver::StrangersNotAllowedError then :email_reject_strangers_not_allowed + when Email::Receiver::InsufficientTrustLevelError then :email_reject_insufficient_trust_level + when Email::Receiver::ReplyUserNotMatchingError then :email_reject_reply_user_not_matching + when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found + when Email::Receiver::TopicClosedError then :email_reject_topic_closed + when Email::Receiver::InvalidPost then :email_reject_invalid_post + when ActiveRecord::Rollback then :email_reject_invalid_post + when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action + when Discourse::InvalidAccess then :email_reject_invalid_access + when RateLimiter::LimitExceeded then :email_reject_rate_limit_specified + end + + template_args = {} + client_message = nil + + # there might be more information available in the exception + if message_template == :email_reject_invalid_post && e.message.size > 6 + message_template = :email_reject_invalid_post_specified + template_args[:post_error] = e.message + end + + if message_template == :email_reject_rate_limit_specified + template_args[:rate_limit_description] = e.description + end + + if message_template == :email_reject_auto_generated + template_args[:mark_as_reply_to_auto_generated] = true + end + + if message_template + # inform the user about the rejection + message = Mail::Message.new(mail_string) + template_args[:former_title] = message.subject + template_args[:destination] = message.to + template_args[:site_name] = SiteSetting.title + + client_message = RejectionMailer.send_rejection(message_template, message.from, template_args) + Email::Sender.new(client_message, message_template).send + else + Discourse.handle_job_exception(e, error_context(@args, "Unrecognized error type when processing incoming email", mail: mail_string)) + end + + client_message + end + + def set_incoming_email_rejection_message(incoming_email, message) + incoming_email.update_attributes!(rejection_message: message) + end + + def log_email_process_failure(mail_string, exception) + if SiteSetting.log_mail_processing_failures + Rails.logger.warn("Email can not be processed: #{exception}\n\n#{mail_string}") + end + end + + end + +end From fc9cfd698d775fe4cad1631b04b64c37079f5844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 22 Jun 2016 16:32:50 +0200 Subject: [PATCH 10/71] UX: admin flags needed some :heart: --- .../admin/templates/flags-list.hbs | 242 +++++++++--------- .../stylesheets/common/admin/admin_base.scss | 50 +--- 2 files changed, 136 insertions(+), 156 deletions(-) diff --git a/app/assets/javascripts/admin/templates/flags-list.hbs b/app/assets/javascripts/admin/templates/flags-list.hbs index d6211e700e..e315ce0b9e 100644 --- a/app/assets/javascripts/admin/templates/flags-list.hbs +++ b/app/assets/javascripts/admin/templates/flags-list.hbs @@ -1,150 +1,154 @@ {{#if model.length}} - {{#load-more tagName="table" className="admin-flags" selector="tbody tr" action="loadMore"}} - - - - - {{i18n 'admin.flags.flagged_by'}} - {{#if adminOldFlagsView}}{{i18n 'admin.flags.resolved_by'}}{{/if}} - - - - {{#each content as |flaggedPost|}} - + {{#load-more selector="tbody tr" action="loadMore"}} + + + + + + + + + + + {{#each content as |flaggedPost|}} + - + - + - - - - - - - {{#if flaggedPost.topicFlagged}} - - - - - {{/if}} - {{#each flaggedPost.conversations as |c|}} - - - + + + + {{#if flaggedPost.topicFlagged}} + + + + + {{/if}} + + {{#each flaggedPost.conversations as |c|}} + + + + + {{/each}} + + {{#unless adminOldFlagsView}} + + - + + + {{/unless}} + {{/each}} - - - - - {{/each}} - - + +
{{i18n 'admin.flags.flagged_by'}}{{#if adminOldFlagsView}}{{i18n 'admin.flags.resolved_by'}}{{/if}}
- {{#if flaggedPost.postAuthorFlagged}} - {{#if flaggedPost.user}} - {{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="small"}}{{/link-to}} - {{#if flaggedPost.wasEdited}}{{/if}} + + {{#if flaggedPost.postAuthorFlagged}} + {{#if flaggedPost.user}} + {{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="large"}}{{/link-to}} + {{#if flaggedPost.wasEdited}}{{/if}} + {{/if}} {{/if}} - {{/if}} - {{#if adminActiveFlagsView}} - {{#if flaggedPost.previous_flags_count}} - {{flaggedPost.previous_flags_count}} + {{#if adminActiveFlagsView}} + {{#if flaggedPost.previous_flags_count}} + {{flaggedPost.previous_flags_count}} + {{/if}} {{/if}} - {{/if}} - -

- {{#if flaggedPost.topic.isPrivateMessage}} - {{fa-icon "envelope"}} +

+

+ {{#if flaggedPost.topic.isPrivateMessage}} + {{fa-icon "envelope"}} + {{/if}} + {{topic-status topic=flaggedPost.topic}} + {{{unbound flaggedPost.topic.fancyTitle}}} +

+ {{#if flaggedPost.postAuthorFlagged}} +

{{{flaggedPost.excerpt}}}

{{/if}} - {{topic-status topic=flaggedPost.topic}} - {{{unbound flaggedPost.topic.fancyTitle}}} - - {{#if flaggedPost.postAuthorFlagged}} - {{{flaggedPost.excerpt}}} - {{/if}} -
- - - {{#each flaggedPost.flaggers as |flagger|}} - - - - - {{/each}} - -
- {{#link-to 'adminUser' flagger.user}} - {{avatar flagger.user imageSize="small"}} - {{/link-to}} - - {{#link-to 'adminUser' flagger.user}} - {{flagger.user.username}} - {{/link-to}} - {{format-age flagger.flaggedAt}} -
- {{flagger.flagType}} -
-
- {{#if adminOldFlagsView}} + {{#each flaggedPost.flaggers as |flagger|}} {{/each}}
- {{#link-to 'adminUser' flagger.disposedBy}} - {{avatar flagger.disposedBy imageSize="small"}} + {{#link-to 'adminUser' flagger.user}} + {{avatar flagger.user imageSize="medium"}} {{/link-to}} - {{format-age flagger.disposedAt}} - {{{flagger.dispositionIcon}}} - {{#if flagger.tookAction}} - - {{/if}} + {{#link-to 'adminUser' flagger.user}} + {{flagger.user.username}} + {{/link-to}} + {{format-age flagger.flaggedAt}} +
+ {{flagger.flagType}}
- {{/if}} -
-
- {{{i18n 'admin.flags.topic_flagged'}}} {{i18n 'admin.flags.visit_topic'}} -
-
- {{#if c.response}} -

- {{#link-to 'adminUser' c.response.user}}{{avatar c.response.user imageSize="small"}}{{/link-to}} {{{c.response.excerpt}}} -

- {{#if c.reply}} +
+ {{#if adminOldFlagsView}} + + + {{#each flaggedPost.flaggers as |flagger|}} + + + + + {{/each}} + +
+ {{#link-to 'adminUser' flagger.disposedBy}} + {{avatar flagger.disposedBy imageSize="medium"}} + {{/link-to}} + + {{format-age flagger.disposedAt}} + {{{flagger.dispositionIcon}}} + {{#if flagger.tookAction}} + + {{/if}} +
+ {{/if}} +
+
+ {{{i18n 'admin.flags.topic_flagged'}}} {{i18n 'admin.flags.visit_topic'}} +
+
+
+ {{#if c.response}}

- {{#link-to 'adminUser' c.reply.user}}{{avatar c.reply.user imageSize="small"}}{{/link-to}} {{{c.reply.excerpt}}} - {{#if c.hasMore}} - {{i18n 'admin.flags.more'}} - {{/if}} + {{#link-to 'adminUser' c.response.user}}{{avatar c.response.user imageSize="medium"}}{{/link-to}} {{{c.response.excerpt}}}

+ {{#if c.reply}} +

+ {{#link-to 'adminUser' c.reply.user}}{{avatar c.reply.user imageSize="medium"}}{{/link-to}} {{{c.reply.excerpt}}} + {{#if c.hasMore}} + {{i18n 'admin.flags.more'}} + {{/if}} +

+ {{/if}} + + + {{/if}} - - - +
+
+ {{#if adminActiveFlagsView}} + + {{#if flaggedPost.postHidden}} + + {{else}} + + {{/if}} + + {{/if}} - -
- {{#if adminActiveFlagsView}} - - {{#if flaggedPost.postHidden}} - - {{else}} - - {{/if}} - - - {{/if}} -
{{/load-more}} {{else}} diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 41b62309f0..a96ae3e084 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -818,24 +818,21 @@ section.details { } } - .admin-flags { - table-layout: fixed; + .hidden-post td.excerpt, + .hidden-post td.user { + opacity: 0.5; + } + + .deleted td.excerpt, + .deleted td.user { + background-color: scale-color($danger, $lightness: 70%); + } - .hidden-post td.excerpt, .hidden-post td.user { opacity: 0.5; } - .deleted td.excerpt, .deleted td.user { background-color: scale-color($danger, $lightness: 70%); } .message { background-color: dark-light-diff($highlight, $secondary, 50%, -70%); } .message:hover { background-color: dark-light-diff($highlight, $secondary, 60%, -60%); } - td { vertical-align: top; } - th { text-align: left; } - .user { - width: 25px; - padding: 8px 0 0 0; - text-align: center; - } + .excerpt { - max-width: 700px; - width: 75%; padding: 8px; word-wrap: break-word; .fa { display: inline-block; } @@ -846,34 +843,13 @@ section.details { margin-bottom: 5px; } } - .flaggers { - font-size: 11px; - padding: 8px 0 0 5px; - .avatar { - width: 25px; - } - table { - max-width: 145px; - } - tr { - height: 44px; - } - td { - vertical-align: middle; - padding: 3px; - line-height: 1.4; - } - } + + .flagged-posts { background: $danger; } + .action { button { margin: 4px; } text-align: right; - padding-bottom: 20px; } - td p { - font-size: 0.929em; - margin: 0 0 5px 0; - } - .flagged-posts { background: $danger; } } /* Dashboard */ From 3646d45110b9b5a3dc610a17d3a0093f090c103b Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 22 Jun 2016 23:34:39 +0800 Subject: [PATCH 11/71] FIX: Voters arrow shown on polls with invalid public config. --- .../discourse/templates/components/poll-results-number.hbs | 2 +- .../discourse/templates/components/poll-results-standard.hbs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-number.hbs b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-number.hbs index c00255a098..ace81e9979 100644 --- a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-number.hbs +++ b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-number.hbs @@ -2,6 +2,6 @@ {{{averageRating}}} -{{#if poll.public}} +{{#if isPublic}} {{poll-results-number-voters poll=poll}} {{/if}} diff --git a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs index 5f939cbc87..d518048087 100644 --- a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs +++ b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs @@ -10,7 +10,7 @@
- {{#if poll.public}} + {{#if isPublic}} {{poll-results-standard-voters option=option}} {{/if}} From d0a51df4d0a9fbb135ef22eb7c535c096fc2cf71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 22 Jun 2016 18:09:11 +0200 Subject: [PATCH 12/71] use standard rails logger --- lib/email/processor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/email/processor.rb b/lib/email/processor.rb index 85dace97f6..30dcef9275 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -81,7 +81,7 @@ module Email client_message = RejectionMailer.send_rejection(message_template, message.from, template_args) Email::Sender.new(client_message, message_template).send else - Discourse.handle_job_exception(e, error_context(@args, "Unrecognized error type when processing incoming email", mail: mail_string)) + Rails.logger.error("Unrecognized error type (#{e}) when processing incoming email\n\nMail:\n#{mail_string}") end client_message From 8c51d34100a98d2b1548965bc4a9c5432913a5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 22 Jun 2016 20:41:21 +0200 Subject: [PATCH 13/71] FIX: receiving a bounce from a deleted user --- lib/email/receiver.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 32c809d185..5c184cecf6 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -150,15 +150,17 @@ module Email bounce_key = verp[/\+verp-(\h{32})@/, 1] if bounce_key && (email_log = EmailLog.find_by(bounce_key: bounce_key)) email_log.update_columns(bounced: true) - - if @mail.error_status.present? - if @mail.error_status.start_with?("4.") - Email::Receiver.update_bounce_score(email_log.user.email, SOFT_BOUNCE_SCORE) - elsif @mail.error_status.start_with?("5.") - Email::Receiver.update_bounce_score(email_log.user.email, HARD_BOUNCE_SCORE) + email = email_log.user.try(:email) || @from_email + if email.present? + if @mail.error_status.present? + if @mail.error_status.start_with?("4.") + Email::Receiver.update_bounce_score(email, SOFT_BOUNCE_SCORE) + elsif @mail.error_status.start_with?("5.") + Email::Receiver.update_bounce_score(email, HARD_BOUNCE_SCORE) + end + elsif is_auto_generated? + Email::Receiver.update_bounce_score(email, HARD_BOUNCE_SCORE) end - elsif is_auto_generated? - Email::Receiver.update_bounce_score(email_log.user.email, HARD_BOUNCE_SCORE) end end end From 81a3559b29c94d650c456eff7689c4636872ca6e Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 22 Jun 2016 16:48:32 -0700 Subject: [PATCH 14/71] adjust full page search result blurb colors --- app/assets/stylesheets/common/base/search.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index 3eaee928a4..b86e68f2b0 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -48,13 +48,13 @@ line-height: 20px; word-wrap: break-word; max-width: 640px; - color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); .date { - color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); } .search-highlight { - color: dark-light-choose(scale-color($primary, $lightness: 25%), scale-color($secondary, $lightness: 75%)); + color: dark-light-choose(scale-color($primary, $lightness: 10%), scale-color($secondary, $lightness: 90%)); } } From ef285579d68d18baa1ff8372772031e3884445d0 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 22 Jun 2016 17:22:26 -0700 Subject: [PATCH 15/71] accidentally inverted dark/light on fps blurb --- app/assets/stylesheets/common/base/search.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index b86e68f2b0..12b37d9ffe 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -48,9 +48,9 @@ line-height: 20px; word-wrap: break-word; max-width: 640px; - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); .date { - color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); } .search-highlight { From 5bfc9cf69ede127460bc14d1c7f109841d4e8dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 23 Jun 2016 12:27:05 +0200 Subject: [PATCH 16/71] Allow API to create staged users --- app/controllers/users_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index af4708fb7e..32785d4800 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -675,8 +675,9 @@ class UsersController < ApplicationController end def user_params - params.permit(:name, :email, :password, :username, :active) - .merge(ip_address: request.remote_ip, registration_ip_address: request.remote_ip, + params.permit(:name, :email, :password, :username, :active, :staged) + .merge(ip_address: request.remote_ip, + registration_ip_address: request.remote_ip, locale: user_locale) end From d396c89a21a1242cf8377b9fa1a9e745e16f8b09 Mon Sep 17 00:00:00 2001 From: Erlend Sogge Heggen Date: Thu, 23 Jun 2016 14:42:40 +0200 Subject: [PATCH 17/71] Calibrating mention-bot (#4286) - No need for it on PRs by team members - Should not immediately jump into action New mention-bot functionality courtesy of @saiqulhaq! --- .mention-bot | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.mention-bot b/.mention-bot index 16c45ceb88..6508c7e867 100644 --- a/.mention-bot +++ b/.mention-bot @@ -1,5 +1,8 @@ { - "maxReviewers": 2, - "message": "Thanks @pullRequester for your pull request :+1:. By analyzing the blame information on this pull request, I identified @reviewers to be potential reviewers.", - "requiredOrgs": ["discourse"] + "maxReviewers": 2, + "message": "Thanks @pullRequester for your pull request :+1:. By analyzing the blame information on this pull request, I identified @reviewers to be potential reviewers.", + "requiredOrgs": ["discourse"], + "skipCollaboratorPR": true, + "delayed": true, + "delayedUntil": "6d" } From 789a6aeb218f87395b82eb680e455e3147bac3b4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 24 Jun 2016 11:20:35 +0800 Subject: [PATCH 18/71] FIX: Public poll not showing. --- plugins/poll/assets/javascripts/discourse/templates/poll.hbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/poll/assets/javascripts/discourse/templates/poll.hbs b/plugins/poll/assets/javascripts/discourse/templates/poll.hbs index af63c2a4b1..dc8c415913 100644 --- a/plugins/poll/assets/javascripts/discourse/templates/poll.hbs +++ b/plugins/poll/assets/javascripts/discourse/templates/poll.hbs @@ -2,9 +2,9 @@
{{#if showingResults}} {{#if isNumber}} - {{poll-results-number poll=poll}} + {{poll-results-number isPublic=isPublic poll=poll}} {{else}} - {{poll-results-standard poll=poll}} + {{poll-results-standard isPublic=isPublic poll=poll}} {{/if}} {{else}}
    From 3232ce8265a72a8cca6058fbcf1e670d41dd158f Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 24 Jun 2016 14:06:27 +0530 Subject: [PATCH 19/71] FIX: better error message when trying to approve post for closed/deleted topic --- app/controllers/queued_posts_controller.rb | 16 ++++++++++------ app/models/queued_post.rb | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/controllers/queued_posts_controller.rb b/app/controllers/queued_posts_controller.rb index d13de97081..6b225b5e65 100644 --- a/app/controllers/queued_posts_controller.rb +++ b/app/controllers/queued_posts_controller.rb @@ -25,13 +25,17 @@ class QueuedPostsController < ApplicationController end state = params[:queued_post][:state] - if state == 'approved' - qp.approve!(current_user) - elsif state == 'rejected' - qp.reject!(current_user) - if params[:queued_post][:delete_user] == 'true' && guardian.can_delete_user?(qp.user) - UserDestroyer.new(current_user).destroy(qp.user, user_deletion_opts) + begin + if state == 'approved' + qp.approve!(current_user) + elsif state == 'rejected' + qp.reject!(current_user) + if params[:queued_post][:delete_user] == 'true' && guardian.can_delete_user?(qp.user) + UserDestroyer.new(current_user).destroy(qp.user, user_deletion_opts) + end end + rescue StandardError => e + return render_json_error e.message end render_serialized(qp, QueuedPostSerializer, root: :queued_posts) diff --git a/app/models/queued_post.rb b/app/models/queued_post.rb index 9dd479a73c..351187a2ec 100644 --- a/app/models/queued_post.rb +++ b/app/models/queued_post.rb @@ -70,7 +70,7 @@ class QueuedPost < ActiveRecord::Base created_post = creator.create unless created_post && creator.errors.blank? - raise StandardError, "Failed to create post #{raw[0..100]} #{creator.errors.full_messages.inspect}" + raise StandardError.new(creator.errors.full_messages.join(" ")) end end From 3501c86cc8cb06786a75a5a1b5b14409c5b4a868 Mon Sep 17 00:00:00 2001 From: "Hanwen (Steinway) Wu" Date: Fri, 24 Jun 2016 13:59:01 -0400 Subject: [PATCH 20/71] Fix frozen email string problem in mbox.rb --- script/import_scripts/mbox.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/import_scripts/mbox.rb b/script/import_scripts/mbox.rb index 98be215ada..8d61451088 100755 --- a/script/import_scripts/mbox.rb +++ b/script/import_scripts/mbox.rb @@ -124,7 +124,7 @@ class ImportScripts::Mbox < ImportScripts::Base if mail.from.present? from_email = mail.from.dup if from_email.kind_of?(Array) - from_email = from_email.first + from_email = from_email.first.dup end from_email.gsub!(/ at /, '@') From ccf9b7067135f37bde23c80456d21b2d1924f858 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 24 Jun 2016 17:14:47 -0400 Subject: [PATCH 21/71] When restoring a backup, disable emails. This prevents accidental sending of emails after a restore before the admin has had a chance to review everything. --- app/controllers/admin/backups_controller.rb | 1 + spec/controllers/admin/backups_controller_spec.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/controllers/admin/backups_controller.rb b/app/controllers/admin/backups_controller.rb index 22e57fcc9a..3539e2fa47 100644 --- a/app/controllers/admin/backups_controller.rb +++ b/app/controllers/admin/backups_controller.rb @@ -77,6 +77,7 @@ class Admin::BackupsController < Admin::AdminController client_id: params.fetch(:client_id), publish_to_message_bus: true, } + SiteSetting.set_and_log(:disable_emails, true, current_user) BackupRestore.restore!(current_user.id, opts) rescue BackupRestore::OperationRunningError render json: failed_json.merge(message: I18n.t("backup.operation_already_running")) diff --git a/spec/controllers/admin/backups_controller_spec.rb b/spec/controllers/admin/backups_controller_spec.rb index 050201a6ab..82b9e69dd5 100644 --- a/spec/controllers/admin/backups_controller_spec.rb +++ b/spec/controllers/admin/backups_controller_spec.rb @@ -151,10 +151,12 @@ describe Admin::BackupsController do describe ".restore" do it "starts a restore" do + expect(SiteSetting.disable_emails).to eq(false) BackupRestore.expects(:restore!).with(@admin.id, filename: backup_filename, publish_to_message_bus: true, client_id: "foo") xhr :post, :restore, id: backup_filename, client_id: "foo" + expect(SiteSetting.disable_emails).to eq(true) expect(response).to be_success end From 2ecbd71bdc853a3b420f8e2bbc12fae8cdd692a3 Mon Sep 17 00:00:00 2001 From: Lincoln Lee Date: Sun, 26 Jun 2016 00:01:15 +0800 Subject: [PATCH 22/71] UX: fix group header font color Change according to group header background color --- app/assets/stylesheets/desktop/user.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 71b2484cce..1fa12e753b 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -251,7 +251,7 @@ .details { padding: 15px; margin: 0; - color: dark-light-choose($secondary, lighten($primary, 10%)); + color: dark-light-choose(lighten($primary, 10%), $secondary); } } From c3d4219d415144719b4115847f396c4f324560aa Mon Sep 17 00:00:00 2001 From: Sajjad Hashemian Date: Sat, 25 Jun 2016 22:29:01 +0430 Subject: [PATCH 23/71] Set trim_trailing_whitespace false for markdown --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 65d44abc70..527c2c27d0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,4 +11,4 @@ indent_style = space indent_size = 2 [*.md] -trim_trailing_whitespace = true +trim_trailing_whitespace = false From 589bae5c033f63944dc5304809f51aedb3ff40cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Sun, 26 Jun 2016 13:27:34 +0200 Subject: [PATCH 24/71] try to fix badly encoded emails --- lib/email/receiver.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 5c184cecf6..3f33e3a38c 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -32,7 +32,7 @@ module Email def initialize(mail_string) raise EmptyEmailError if mail_string.blank? @staged_users_created = 0 - @raw_email = mail_string + @raw_email = try_to_encode(mail_string, "UTF-8") || try_to_encode(mail_string, "ISO-8859-1") || mail_string @mail = Mail.new(@raw_email) @message_id = @mail.message_id.presence || Digest::MD5.hexdigest(mail_string) end @@ -236,14 +236,16 @@ module Email return nil if string.blank? - # 1) use the charset provided - if mail_part.charset.present? - fixed = try_to_encode(string, mail_part.charset) + # common encodings + encodings = ["UTF-8", "ISO-8859-1"] + encodings.unshift(mail_part.charset) if mail_part.charset.present? + + encodings.uniq.each do |encoding| + fixed = try_to_encode(string, encoding) return fixed if fixed.present? end - # 2) try most used encodings - try_to_encode(string, "UTF-8") || try_to_encode(string, "ISO-8859-1") + nil end def try_to_encode(string, encoding) From 74e93d2260df0a9828bdf84947dc0e9c05eeb25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Sun, 26 Jun 2016 14:20:22 +0200 Subject: [PATCH 25/71] FIX: Reply As New Topic from all 3 different places --- app/assets/javascripts/discourse/controllers/share.js.es6 | 3 ++- app/assets/javascripts/discourse/views/share.js.es6 | 2 +- app/assets/javascripts/discourse/widgets/post.js.es6 | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/share.js.es6 b/app/assets/javascripts/discourse/controllers/share.js.es6 index b494cb1d73..97f1fb4d26 100644 --- a/app/assets/javascripts/discourse/controllers/share.js.es6 +++ b/app/assets/javascripts/discourse/controllers/share.js.es6 @@ -33,7 +33,8 @@ export default Ember.Controller.extend({ replyAsNewTopic() { const topicController = this.get("controllers.topic"); const postStream = topicController.get("model.postStream"); - const post = postStream.findLoadedPost(this.get("postId")); + const postId = this.get("postId") || postStream.findPostIdForPostNumber(1); + const post = postStream.findLoadedPost(postId); topicController.send("replyAsNewTopic", post); this.send("close"); }, diff --git a/app/assets/javascripts/discourse/views/share.js.es6 b/app/assets/javascripts/discourse/views/share.js.es6 index e70eb17114..39d743cbbc 100644 --- a/app/assets/javascripts/discourse/views/share.js.es6 +++ b/app/assets/javascripts/discourse/views/share.js.es6 @@ -92,7 +92,7 @@ export default Ember.View.extend({ const $currentTarget = $(e.currentTarget), url = $currentTarget.data('share-url'), postNumber = $currentTarget.data('post-number'), - postId = $currentTarget.data('post-id'), + postId = $currentTarget.closest('article').data('post-id'), date = $currentTarget.children().data('time'); showPanel($currentTarget, url, postNumber, date, postId); return false; diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index d51a9ac77d..abee4ada81 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -162,7 +162,6 @@ createWidget('post-meta-data', { href: attrs.shareUrl, 'data-share-url': attrs.shareUrl, 'data-post-number': attrs.post_number, - 'data-post-id': attrs.id, } }, dateNode(createdAt)) )); From 800081f6067e996d3a757f1fb521168ea02250ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Sun, 26 Jun 2016 19:25:45 +0200 Subject: [PATCH 26/71] FIX: staged users weren't able to reply in restricted categories --- app/models/category.rb | 75 +++++++------------ lib/guardian.rb | 4 + lib/guardian/post_guardian.rb | 1 - spec/components/email/receiver_spec.rb | 14 +++- .../emails/staged_reply_restricted.eml | 9 +++ 5 files changed, 53 insertions(+), 50 deletions(-) create mode 100644 spec/fixtures/emails/staged_reply_restricted.eml diff --git a/app/models/category.rb b/app/models/category.rb index 92f0b75172..a120391f77 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -60,32 +60,21 @@ class Category < ActiveRecord::Base has_many :category_tag_groups, dependent: :destroy has_many :tag_groups, through: :category_tag_groups - scope :latest, ->{ order('topic_count desc') } + scope :latest, -> { order('topic_count DESC') } - scope :secured, ->(guardian = nil) { + scope :secured, -> (guardian = nil) { ids = guardian.secure_category_ids if guardian if ids.present? - where("NOT categories.read_restricted or categories.id in (:cats)", cats: ids).references(:categories) + where("NOT categories.read_restricted OR categories.id IN (:cats)", cats: ids).references(:categories) else where("NOT categories.read_restricted").references(:categories) end } - scope :topic_create_allowed, ->(guardian) { - if guardian.anonymous? - where("1=0") - else - scoped_to_permissions(guardian, [:full]) - end - } - - scope :post_create_allowed, ->(guardian) { - if guardian.anonymous? - where("1=0") - else - scoped_to_permissions(guardian, [:create_post, :full]) - end - } + TOPIC_CREATION_PERMISSIONS ||= [:full] + POST_CREATION_PERMISSIONS ||= [:create_post, :full] + scope :topic_create_allowed, -> (guardian) { scoped_to_permissions(guardian, TOPIC_CREATION_PERMISSIONS) } + scope :post_create_allowed, -> (guardian) { scoped_to_permissions(guardian, POST_CREATION_PERMISSIONS) } delegate :post_template, to: 'self.class' @@ -98,7 +87,7 @@ class Category < ActiveRecord::Base end def self.scoped_to_permissions(guardian, permission_types) - if guardian && guardian.is_admin? + if guardian.try(:is_admin?) all elsif !guardian || guardian.anonymous? if permission_types.include?(:readonly) @@ -107,28 +96,19 @@ class Category < ActiveRecord::Base where("1 = 0") end else - permission_types = permission_types.map{ |permission_type| - CategoryGroup.permission_types[permission_type] - } - where("categories.id in ( - SELECT cg.category_id FROM category_groups cg - WHERE permission_type in (:permissions) AND - ( - group_id IN ( - SELECT g.group_id FROM group_users g where g.user_id = :user_id - ) - ) - ) - OR - categories.id in ( - SELECT cg.category_id FROM category_groups cg - WHERE permission_type in (:permissions) AND group_id = :everyone - ) - OR - categories.id NOT in (SELECT cg.category_id FROM category_groups cg) - ", permissions: permission_types, - user_id: guardian.user.id, - everyone: Group[:everyone].id) + permissions = permission_types.map { |p| CategoryGroup.permission_types[p] } + where("(:staged AND LENGTH(COALESCE(email_in, '')) > 0 AND email_in_allow_strangers) + OR categories.id NOT IN (SELECT category_id FROM category_groups) + OR categories.id IN ( + SELECT category_id + FROM category_groups + WHERE permission_type IN (:permissions) + AND (group_id = :everyone OR group_id IN (SELECT group_id FROM group_users WHERE user_id = :user_id)) + )", + staged: guardian.is_staged?, + permissions: permissions, + user_id: guardian.user.id, + everyone: Group[:everyone].id) end end @@ -139,14 +119,13 @@ class Category < ActiveRecord::Base .group("topics.category_id") .visible.to_sql - Category.exec_sql < x.topic_count OR c.post_count <> x.post_count) - + SET topic_count = x.topic_count, + post_count = x.post_count + FROM (#{topics_with_post_count}) x + WHERE x.category_id = c.id + AND (c.topic_count <> x.topic_count OR c.post_count <> x.post_count) SQL # Yes, there are a lot of queries happening below. diff --git a/lib/guardian.rb b/lib/guardian.rb index 8f4593943c..28235ed802 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -78,6 +78,10 @@ class Guardian ) end + def is_staged? + @user.staged? + end + # Can the user see the object? def can_see?(obj) if obj diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 2c883d15a1..7ff13bf0e4 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -76,7 +76,6 @@ module PostGuardian # Creating Method def can_create_post?(parent) - (!SpamRule::AutoBlock.block?(@user) || (!!parent.try(:private_message?) && parent.allowed_users.include?(@user))) && ( !parent || !parent.category || diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 533eea27ec..bd60b249b3 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -107,8 +107,9 @@ describe Email::Receiver do context "reply" do let(:reply_key) { "4f97315cc828096c9cb34c6f1a0d6fe8" } + let(:category) { Fabricate(:category) } let(:user) { Fabricate(:user, email: "discourse@bar.com") } - let(:topic) { create_topic(user: user) } + let(:topic) { create_topic(category: category, user: user) } let(:post) { create_post(topic: topic, user: user) } let!(:email_log) { Fabricate(:email_log, reply_key: reply_key, user: user, topic: topic, post: post) } @@ -214,6 +215,17 @@ describe Email::Receiver do expect { process(:auto_generated_unblocked) }.to change { topic.posts.count } end + it "allows staged users to reply to a restricted category" do + user.update_columns(staged: true) + + category.email_in = "category@bar.com" + category.email_in_allow_strangers = true + category.set_permissions(Group[:trust_level_4] => :full) + category.save + + expect { process(:staged_reply_restricted) }.to change { topic.posts.count } + end + describe 'Unsubscribing via email' do let(:last_email) { ActionMailer::Base.deliveries.last } diff --git a/spec/fixtures/emails/staged_reply_restricted.eml b/spec/fixtures/emails/staged_reply_restricted.eml new file mode 100644 index 0000000000..6f753212db --- /dev/null +++ b/spec/fixtures/emails/staged_reply_restricted.eml @@ -0,0 +1,9 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jun 2016 00:12:43 +0100 +Message-ID: <54@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 + +This is a reply from a staged user in a topic in a restricted category. From 83309752ae0fc224fadc73cd410dc682e7e54b3c Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 27 Jun 2016 00:00:45 +0530 Subject: [PATCH 27/71] FEATURE: new site setting 'code formatting style' --- .../discourse/components/d-editor.js.es6 | 26 ++++++++++--------- config/locales/client.en.yml | 1 + config/locales/server.en.yml | 2 ++ config/site_settings.yml | 7 +++++ .../components/d-editor-test.js.es6 | 2 ++ 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 6ff564d54f..90ef37e0c7 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -63,18 +63,7 @@ class Toolbar { perform: e => e.applySurround('> ', '', 'code_text') }); - this.addButton({ - id: 'code', - group: 'insertions', - shortcut: 'Shift+C', - perform(e) { - if (e.selected.value.indexOf("\n") !== -1) { - e.applySurround(' ', '', 'code_text'); - } else { - e.applySurround('`', '`', 'code_text'); - } - }, - }); + this.addButton({id: 'code', group: 'insertions', shortcut: 'Shift+C', action: 'formatCode'}); this.addButton({ id: 'bullet', @@ -530,6 +519,19 @@ export default Ember.Component.extend({ this.set('insertLinkHidden', false); }, + formatCode() { + const sel = this._getSelected(); + if (sel.value.indexOf("\n") !== -1) { + return (this.siteSettings.code_formatting_style == "4-spaces-indent") ? + this._applySurround(sel, ' ', '', 'code_text') : + this._addText(sel, '```\n' + sel.value + '\n```'); + } else { + return (this.siteSettings.code_formatting_style == "4-spaces-indent") ? + this._applySurround(sel, '`', '`', 'code_text') : + this._applySurround(sel, '```\n', '\n```', 'paste_code_text'); + } + }, + insertLink() { const origLink = this.get('linkUrl'); const linkUrl = (origLink.indexOf('://') === -1) ? `http://${origLink}` : origLink; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3d142ef2c0..0f83190a42 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1055,6 +1055,7 @@ en: quote_text: "Blockquote" code_title: "Preformatted text" code_text: "indent preformatted text by 4 spaces" + paste_code_text: "type or paste code here" upload_title: "Upload" upload_description: "enter upload description here" olist_title: "Numbered List" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 45cd768c1d..bb07b0708b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1315,6 +1315,8 @@ en: auto_close_messages_post_count: "Maximum number of posts allowed in a message before it is automatically closed (0 to disable)" auto_close_topics_post_count: "Maximum number of posts allowed in a topic before it is automatically closed (0 to disable)" + code_formatting_style: "Code button in composer will default to this code formatting style" + default_email_digest_frequency: "How often users receive summary emails by default." default_include_tl0_in_digests: "Include posts from new users in summary emails by default. Users can change this in their preferences." default_email_private_messages: "Send an email when someone messages the user by default." diff --git a/config/site_settings.yml b/config/site_settings.yml index d3b255bcf3..bbc3bad8e1 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -511,6 +511,13 @@ posting: min: 0 auto_close_messages_post_count: 500 auto_close_topics_post_count: 10000 + code_formatting_style: + client: true + type: enum + default: '4-spaces-indent' + choices: + - 4-spaces-indent + - code-fences email: email_time_window_mins: diff --git a/test/javascripts/components/d-editor-test.js.es6 b/test/javascripts/components/d-editor-test.js.es6 index 23252b214d..16bbbd063d 100644 --- a/test/javascripts/components/d-editor-test.js.es6 +++ b/test/javascripts/components/d-editor-test.js.es6 @@ -257,6 +257,7 @@ testCase('link modal (link with description)', function(assert) { componentTest('advanced code', { template: '{{d-editor value=value}}', setup() { + this.siteSettings.code_formatting_style = '4-spaces-indent'; this.set('value', `function xyz(x, y, z) { if (y === z) { @@ -286,6 +287,7 @@ componentTest('advanced code', { componentTest('code button', { template: '{{d-editor value=value}}', setup() { + this.siteSettings.code_formatting_style = '4-spaces-indent'; this.set('value', "first line\n\nsecond line\n\nthird line"); }, From 1b80f1ea394b81be5409f59a9d541b496a7a52e3 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 27 Jun 2016 01:11:56 +0530 Subject: [PATCH 28/71] Fix the build :fired: --- app/assets/javascripts/discourse/components/d-editor.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 90ef37e0c7..0c0b9c4331 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -522,11 +522,11 @@ export default Ember.Component.extend({ formatCode() { const sel = this._getSelected(); if (sel.value.indexOf("\n") !== -1) { - return (this.siteSettings.code_formatting_style == "4-spaces-indent") ? + return (this.siteSettings.code_formatting_style === "4-spaces-indent") ? this._applySurround(sel, ' ', '', 'code_text') : this._addText(sel, '```\n' + sel.value + '\n```'); } else { - return (this.siteSettings.code_formatting_style == "4-spaces-indent") ? + return (this.siteSettings.code_formatting_style === "4-spaces-indent") ? this._applySurround(sel, '`', '`', 'code_text') : this._applySurround(sel, '```\n', '\n```', 'paste_code_text'); } From 63b8797667a7f68dd28cbff2dc2860e0d2a61266 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 21 Jun 2016 17:41:53 +0800 Subject: [PATCH 29/71] FIX: Incorrect model for embedded post widget. --- .../javascripts/discourse/lib/transform-post.js.es6 | 5 ++++- app/assets/javascripts/discourse/widgets/post.js.es6 | 9 +++++++-- .../javascripts/initializers/extend-for-poll.js.es6 | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index 315a05f290..7723897970 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -19,7 +19,7 @@ export function includeAttributes(...attributes) { export function transformBasicPost(post) { // Note: it can be dangerous to not use `get` in Ember code, but this is significantly // faster and has tests to confirm it works. We only call `get` when the property is a CP - return { + const postAtts = { id: post.id, hidden: post.hidden, deleted: post.get('deleted'), @@ -73,6 +73,9 @@ export function transformBasicPost(post) { replyCount: post.reply_count, }; + _additionalAttributes.forEach(a => postAtts[a] = post[a]); + + return postAtts; } diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index d51a9ac77d..01df08f0fa 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -268,7 +268,9 @@ createWidget('post-contents', { const repliesBelow = state.repliesBelow; if (repliesBelow.length) { - result.push(h('section.embedded-posts.bottom', repliesBelow.map(p => this.attach('embedded-post', p)))); + result.push(h('section.embedded-posts.bottom', repliesBelow.map(p => { + return this.attach('embedded-post', p, { model: this.store.createRecord('post', p) }); + }))); } return result; @@ -339,7 +341,10 @@ createWidget('post-article', { html(attrs, state) { const rows = [h('a.tabLoc', { attributes: { href: ''} })]; if (state.repliesAbove.length) { - const replies = state.repliesAbove.map(p => this.attach('embedded-post', p, { state: { above: true } })); + const replies = state.repliesAbove.map(p => { + return this.attach('embedded-post', p, { model: this.store.createRecord('post', p), state: { above: true } }); + }); + rows.push(h('div.row', h('section.embedded-posts.top.topic-body.offset2', replies))); } diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 index d099372092..d6a0bb4249 100644 --- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 @@ -108,6 +108,7 @@ function initializePolls(api) { _pollViews = postPollViews; } + api.includePostAttributes("polls", "polls_votes"); api.decorateCooked(createPollViews, { onlyStream: true }); api.cleanupStream(cleanUpPollViews); } From db1b0e223539c95baf8d7471b94b32248af41f32 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sun, 26 Jun 2016 23:34:35 -0700 Subject: [PATCH 30/71] minor copyedit --- 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 bb07b0708b..b7c74a51d6 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -622,7 +622,7 @@ en: unsubscribed: title: "Unsubscribed!" - description: "You have been unsubscribed. If you wish to change your email settings visit your user preferences." + description: "You have been unsubscribed. To change your email settings visit your user preferences." topic_description: "To re-subscribe to %{link}, use the notification control at the bottom or right of the topic." unsubscribe: From 3ad1423c5390595cdf80242815d6e43412a12ae7 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 27 Jun 2016 16:48:45 +1000 Subject: [PATCH 31/71] UX: autofocus the edit reason text field --- app/assets/javascripts/discourse/templates/composer.hbs | 2 +- app/assets/stylesheets/desktop/compose.scss | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 4ad7703159..290a0815bd 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -37,7 +37,7 @@ {{#if canEdit}} {{#if showEditReason}}
    - {{text-field value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}} + {{text-field autofocus="true" value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}}
    {{else}} {{i18n 'composer.show_edit_reason'}} diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 7137ef9db9..738fb583e8 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -284,9 +284,10 @@ margin-left: 10px; top: 18px; #edit-reason { - margin: 0; + margin: -7px 0 0; padding: 5px; float: left; + width: 300px; } } #reply-title { From 994063ac723ad024ab2322877a96308e411b7137 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 27 Jun 2016 15:06:13 +0800 Subject: [PATCH 32/71] UX: Disable toolbar by default on Android devices. --- app/assets/javascripts/discourse/controllers/composer.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 6438e6e879..4e2ddb29b6 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -70,7 +70,7 @@ export default Ember.Controller.extend({ // iPhone 6 is 375, anything narrower and toolbar should // be default disabled. // That said we should remember the state - this._toolbarEnabled = $(window).width() > 370; + this._toolbarEnabled = $(window).width() > 370 && !this.capabilities.isAndroid; } return this._toolbarEnabled || storedVal === "true"; }, From ea46e5dd57cd8bd0c09e1a302f5a5b0b590d1a90 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 27 Jun 2016 17:22:24 +1000 Subject: [PATCH 33/71] UX: add minimum height for zoomed composer on mobile --- app/assets/javascripts/discourse/lib/safari-hacks.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 index 888bf34fc6..1aa215f534 100644 --- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 +++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 @@ -73,7 +73,8 @@ function positioningWorkaround($fixedElement) { fixedElement.style.top = '0px'; - fixedElement.style.height = parseInt(window.innerHeight*0.6) + "px"; + const height = Math.max(parseInt(window.innerHeight*0.6), 350); + fixedElement.style.height = height + "px"; // I used to do this, but it seems like we don't need to with position // fixed From 5eda2f43c64734f7fc3db15828d585df28d9bf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 27 Jun 2016 14:36:57 +0200 Subject: [PATCH 34/71] small topic/category guardians refactor --- lib/guardian/category_guardian.rb | 6 +++--- lib/guardian/topic_guardian.rb | 28 ++++++---------------------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/lib/guardian/category_guardian.rb b/lib/guardian/category_guardian.rb index b711c12d03..4a419ce1e9 100644 --- a/lib/guardian/category_guardian.rb +++ b/lib/guardian/category_guardian.rb @@ -45,9 +45,9 @@ module CategoryGuardian end def can_see_category?(category) - is_admin? || - !category.read_restricted || - (@user.staged? && category.email_in.present? && category.email_in_allow_strangers) || + return true if is_admin? + return true if !category.read_restricted + return true if is_staged? && category.email_in.present? && category.email_in_allow_strangers secure_category_ids.include?(category.id) end diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb index 007b77db4c..fabf1c879e 100644 --- a/lib/guardian/topic_guardian.rb +++ b/lib/guardian/topic_guardian.rb @@ -68,43 +68,27 @@ module TopicGuardian end def can_reply_as_new_topic?(topic) - authenticated? && topic && not(topic.private_message?) && @user.has_trust_level?(TrustLevel[1]) + authenticated? && topic && !topic.private_message? && @user.has_trust_level?(TrustLevel[1]) end def can_see_deleted_topics? is_staff? end - def can_see_topic?(topic) + def can_see_topic?(topic, hide_deleted=true) return false unless topic - # Admins can see everything return true if is_admin? - # Deleted topics - return false if topic.deleted_at && !can_see_deleted_topics? + return false if hide_deleted && topic.deleted_at && !can_see_deleted_topics? if topic.private_message? - return authenticated? && - topic.all_allowed_users.where(id: @user.id).exists? + return authenticated? && topic.all_allowed_users.where(id: @user.id).exists? end - # not secure, or I can see it - !topic.read_restricted_category? || can_see_category?(topic.category) + can_see_category?(topic.category) end def can_see_topic_if_not_deleted?(topic) - return false unless topic - # Admins can see everything - return true if is_admin? - # Deleted topics - # return false if topic.deleted_at && !can_see_deleted_topics? - - if topic.private_message? - return authenticated? && - topic.all_allowed_users.where(id: @user.id).exists? - end - - # not secure, or I can see it - !topic.read_restricted_category? || can_see_category?(topic.category) + can_see_topic?(topic, false) end def filter_allowed_categories(records) From f3905fd99ab2d26a98a9554f61be25c2922383ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 27 Jun 2016 22:08:49 +0200 Subject: [PATCH 35/71] FIX: S3 CDN wasn't applied to lightboxed images --- lib/cooked_post_processor.rb | 10 +++++++--- lib/pretty_text.rb | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 9d9e1fac38..4e344c5f5c 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -321,20 +321,24 @@ class CookedPostProcessor if SiteSetting.login_required @doc.css("a.attachment[href]").each do |a| href = a["href"].to_s - a["href"] = UrlHelper.schemaless UrlHelper.absolute(href, nil) if UrlHelper.is_local(href) + a["href"] = UrlHelper.schemaless UrlHelper.absolute_without_cdn(href) if UrlHelper.is_local(href) end end + use_s3_cdn = SiteSetting.s3_cdn_url.present? && SiteSetting.enable_s3_uploads + %w{href data-download-href}.each do |selector| @doc.css("a[#{selector}]").each do |a| - href = a["#{selector}"].to_s - a["#{selector}"] = UrlHelper.schemaless UrlHelper.absolute(href) if UrlHelper.is_local(href) + href = a[selector].to_s + a[selector] = UrlHelper.schemaless UrlHelper.absolute(href) if UrlHelper.is_local(href) + a[selector] = a[selector].sub(Discourse.store.absolute_base_url, SiteSetting.s3_cdn_url) if use_s3_cdn end end @doc.css("img[src]").each do |img| src = img["src"].to_s img["src"] = UrlHelper.schemaless UrlHelper.absolute(src) if UrlHelper.is_local(src) + img["src"] = img["src"].sub(Discourse.store.absolute_base_url, SiteSetting.s3_cdn_url) if use_s3_cdn end end diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index bf6fab878a..7ac41d2442 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -296,7 +296,7 @@ JS add_rel_nofollow_to_user_content(doc) end - if SiteSetting.s3_cdn_url.present? && SiteSetting.enable_s3_uploads + if SiteSetting.enable_s3_uploads && SiteSetting.s3_cdn_url.present? add_s3_cdn(doc) end From 32b22996d03fc361014093bf30f2882486ad668b Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 27 Jun 2016 16:17:00 -0400 Subject: [PATCH 36/71] FEATURE: vanilla_mysql importer can import tags --- script/import_scripts/vanilla_mysql.rb | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/script/import_scripts/vanilla_mysql.rb b/script/import_scripts/vanilla_mysql.rb index 9bf33fd637..3001149b24 100644 --- a/script/import_scripts/vanilla_mysql.rb +++ b/script/import_scripts/vanilla_mysql.rb @@ -19,9 +19,22 @@ class ImportScripts::VanillaSQL < ImportScripts::Base password: "pa$$word", database: VANILLA_DB ) + + @import_tags = false + begin + r = @client.query("select count(*) count from #{TABLE_PREFIX}Tag where countdiscussions > 0") + @import_tags = true if r.first["count"].to_i > 0 + rescue => e + puts "Tags won't be imported. #{e.message}" + end end def execute + if @import_tags + SiteSetting.tagging_enabled = true + SiteSetting.max_tags_per_topic = 10 + end + import_users import_avatars import_categories @@ -182,6 +195,8 @@ class ImportScripts::VanillaSQL < ImportScripts::Base def import_topics puts "", "importing topics..." + tag_names_sql = "select t.name as tag_name from GDN_Tag t, GDN_TagDiscussion td where t.tagid = td.tagid and td.discussionid = {discussionid} and t.name != '';" + total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}Discussion;").first['count'] batches(BATCH_SIZE) do |offset| @@ -203,7 +218,13 @@ class ImportScripts::VanillaSQL < ImportScripts::Base title: discussion['Name'], category: category_id_from_imported_category_id(discussion['CategoryID']), raw: clean_up(discussion['Body']), - created_at: Time.zone.at(discussion['DateInserted']) + created_at: Time.zone.at(discussion['DateInserted']), + post_create_action: proc do |post| + if @import_tags + tag_names = @client.query(tag_names_sql.gsub('{discussionid}', discussion['DiscussionID'].to_s)).map {|row| row['tag_name']} + DiscourseTagging.tag_topic_by_names(post.topic, staff_guardian, tag_names) + end + end } end end @@ -327,6 +348,10 @@ class ImportScripts::VanillaSQL < ImportScripts::Base raw end + def staff_guardian + @_staff_guardian ||= Guardian.new(Discourse.system_user) + end + def mysql_query(sql) @client.query(sql) # @client.query(sql, cache_rows: false) #segfault: cache_rows: false causes segmentation fault From 376881845cc8845f3ecb1f82464783f8bb09a6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 27 Jun 2016 22:26:05 +0200 Subject: [PATCH 37/71] always strip s/mime signatures in incoming emails --- lib/email/receiver.rb | 3 +++ lib/validators/upload_validator.rb | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 3f33e3a38c..604d0220fe 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -429,6 +429,9 @@ module Email def create_post_with_attachments(options={}) # deal with attachments @mail.attachments.each do |attachment| + # always strip S/MIME signatures + next if attachment.content_type == "application/pkcs7-mime".freeze + tmp = Tempfile.new("discourse-email-attachment") begin # read attachment diff --git a/lib/validators/upload_validator.rb b/lib/validators/upload_validator.rb index b200fc16c3..23cfe2f811 100644 --- a/lib/validators/upload_validator.rb +++ b/lib/validators/upload_validator.rb @@ -5,7 +5,11 @@ module Validators; end class Validators::UploadValidator < ActiveModel::Validator def validate(upload) - return true if upload.is_attachment_for_group_message && SiteSetting.allow_all_attachments_for_group_messages + # allow all attachments except S/MIME signatures + # cf. https://meta.discourse.org/t/strip-s-mime-signatures/46371 + if upload.is_attachment_for_group_message && SiteSetting.allow_all_attachments_for_group_messages + return upload.original_filename != "smime.p7s".freeze + end extension = File.extname(upload.original_filename)[1..-1] || "" From d18e9a52931387ce0cf7b7ab3f3d1a8975414020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 27 Jun 2016 22:48:27 +0200 Subject: [PATCH 38/71] validator was on the wrong site setting --- config/site_settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index bbc3bad8e1..a76a0070f4 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -541,9 +541,9 @@ email: validator: "ReplyByEmailEnabledValidator" reply_by_email_address: default: '' - validator: "AlternativeReplyByEmailAddressesValidator" alternative_reply_by_email_addresses: default: '' + validator: "AlternativeReplyByEmailAddressesValidator" manual_polling_enabled: default: false pop3_polling_enabled: From 156953bc555909ae665087dfbe8d24b6f1e6fe22 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 28 Jun 2016 08:33:11 +0800 Subject: [PATCH 39/71] UX: Better alignment on group page. --- app/assets/stylesheets/desktop/user.scss | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 1fa12e753b..12d53ebc22 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -122,16 +122,15 @@ // name hacky so lastpass does not freak out // -search- means it is bypassed #add-user-to-group { - button, .ac-wrap { - float: left; - } - button { - margin-top: 3px; - margin-left: 10px; - } - #user-search-selector { - width: 400px; - } + button, .ac-wrap { + float: left; + } + button { + margin-top: 5px; + } + #user-search-selector { + width: 400px; + } } table.group-members { @@ -249,7 +248,7 @@ &.group { .details { - padding: 15px; + padding: 15px 0px; margin: 0; color: dark-light-choose(lighten($primary, 10%), $secondary); } From fc81209564f525b69cce8aafbcf87bd5f90f5993 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 28 Jun 2016 08:37:36 +0800 Subject: [PATCH 40/71] UX: Missing loading wheel on user notifications page. --- .../discourse/controllers/user-notifications.js.es6 | 7 +++++-- .../javascripts/discourse/templates/user/notifications.hbs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 index b61c7d45f6..8e860c2de5 100644 --- a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 @@ -1,9 +1,12 @@ +import { observes } from 'ember-addons/ember-computed-decorators'; + export default Ember.ArrayController.extend({ needs: ['application'], - _showFooter: function() { + @observes('model.canLoadMore') + _showFooter() { this.set("controllers.application.showFooter", !this.get("model.canLoadMore")); - }.observes("model.canLoadMore"), + }, currentPath: Em.computed.alias('controllers.application.currentPath'), diff --git a/app/assets/javascripts/discourse/templates/user/notifications.hbs b/app/assets/javascripts/discourse/templates/user/notifications.hbs index bfdba81bee..7a9f5420bf 100644 --- a/app/assets/javascripts/discourse/templates/user/notifications.hbs +++ b/app/assets/javascripts/discourse/templates/user/notifications.hbs @@ -1,4 +1,3 @@ -
    {{#mobile-nav class='notifications-nav' desktopClass='notification-list action-list nav-stacked' currentPath=currentPath}} {{#if model}} @@ -25,5 +24,6 @@
    {{#load-more class="notification-history user-stream" selector=".user-stream .notification" action="loadMore"}} {{outlet}} + {{conditional-loading-spinner condition=model.loadingMore}} {{/load-more}}
    From 9ed79d8ecd6cd2ee12f37dbefe25b651facc1509 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 28 Jun 2016 09:29:42 +0800 Subject: [PATCH 41/71] Add Bullet gem to detect N+1 queries. --- Gemfile | 1 + Gemfile.lock | 5 +++++ config/environments/development.rb | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/Gemfile b/Gemfile index 8717d335be..dbea62d2a9 100644 --- a/Gemfile +++ b/Gemfile @@ -144,6 +144,7 @@ group :test, :development do gem 'spork-rails' gem 'pry-nav' gem 'byebug', require: ENV['RM_INFO'].nil? + gem 'bullet' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index ae4a85a5b8..bf82d95b14 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -62,6 +62,9 @@ GEM binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) + bullet (5.0.0) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.9.0) byebug (8.2.1) certified (1.0.0) coderay (1.1.0) @@ -395,6 +398,7 @@ GEM unicorn (5.1.0) kgio (~> 2.6) raindrops (~> 0.7) + uniform_notifier (1.9.0) PLATFORMS ruby @@ -407,6 +411,7 @@ DEPENDENCIES barber better_errors binding_of_caller + bullet byebug certified discourse-qunit-rails diff --git a/config/environments/development.rb b/config/environments/development.rb index 7d656b6323..da633983b8 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -52,4 +52,10 @@ Discourse::Application.configure do config.developer_emails = emails.split(",").map(&:downcase).map(&:strip) end + config.after_initialize do + Bullet.enable = true + Bullet.alert = true + Bullet.console = true + Bullet.rails_logger = true + end end From 3e07658fb295c6d95b42a0d767901dcd9d678e73 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 28 Jun 2016 11:53:45 +0800 Subject: [PATCH 42/71] Don't alert. --- config/environments/development.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index da633983b8..2d7f03c48a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -54,7 +54,6 @@ Discourse::Application.configure do config.after_initialize do Bullet.enable = true - Bullet.alert = true Bullet.console = true Bullet.rails_logger = true end From 1411eedad3d1715d7a58a08ab77630f73844a1c7 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 28 Jun 2016 18:34:20 +1000 Subject: [PATCH 43/71] FEATURE: offer to unwatch categories when unwatching category --- .../discourse/controllers/preferences.js.es6 | 29 ++++++++++++++++++- .../javascripts/discourse/models/user.js.es6 | 21 +++++++++++++- app/controllers/users_controller.rb | 4 +++ app/models/topic_user.rb | 26 +++++++++++++++++ config/locales/client.en.yml | 7 +++++ spec/models/topic_user_spec.rb | 28 ++++++++++++++++++ 6 files changed, 113 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index c9dd265c4c..d37c428d69 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -2,6 +2,7 @@ import { setting } from 'discourse/lib/computed'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import computed from "ember-addons/ember-computed-decorators"; +import { categoryBadgeHTML } from "discourse/helpers/category-link"; export default Ember.Controller.extend(CanCheckEmails, { @@ -134,6 +135,25 @@ export default Ember.Controller.extend(CanCheckEmails, { this.set('saved', false); const model = this.get('model'); + + + // watched status changes warn user + const changedWatch = model.changedCategoryNotifications("watched"); + + if (changedWatch.remove.length > 0 && !this.get("warnedRemoveWatch")) { + var categories = Discourse.Category.findByIds(changedWatch.remove).map((cat) => { + return categoryBadgeHTML(cat); + }).join(" "); + bootbox.confirm(I18n.t('user.warn_unwatch.message', {categories: categories}), + I18n.t('user.warn_unwatch.no_value', {count: changedWatch.remove.length}), I18n.t('user.warn_unwatch.yes_value'), + (yes)=>{ + this.set('unwatchCategoryTopics', yes ? changedWatch.remove : false); + this.send('save'); + }); + this.set("warnedRemoveWatch", true); + return; + } + const userFields = this.get('userFields'); // Update the user fields @@ -148,12 +168,19 @@ export default Ember.Controller.extend(CanCheckEmails, { // Cook the bio for preview model.set('name', this.get('newNameInput')); - return model.save().then(() => { + var options = {}; + if (this.get('warnedRemoveWatch') && this.get('unwatchCategoryTopics')) { + options["unwatchCategoryTopics"] = this.get("unwatchCategoryTopics"); + } + + return model.save(options).then(() => { if (Discourse.User.currentProp('id') === model.get('id')) { Discourse.User.currentProp('name', model.get('name')); } model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw')))); this.set('saved', true); + this.set("unwatchTopics", false); + this.set('warnedRemoveWatch', false); }).catch(popupAjaxError); }, diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 666fc5614b..6fabd1b865 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -141,7 +141,7 @@ const User = RestModel.extend({ return Discourse.User.create(this.getProperties(Object.keys(this))); }, - save() { + save(options) { const data = this.getProperties( 'bio_raw', 'website', @@ -177,8 +177,12 @@ const User = RestModel.extend({ data[s] = this.get(`user_option.${s}`); }); + var updatedState = {}; + ['muted','watched','tracked'].forEach(s => { let cats = this.get(s + 'Categories').map(c => c.get('id')); + updatedState[s + '_category_ids'] = cats; + // HACK: denote lack of categories if (cats.length === 0) { cats = [-1]; } data[s + '_category_ids'] = cats; @@ -188,6 +192,10 @@ const User = RestModel.extend({ data['edit_history_public'] = this.get('user_option.edit_history_public'); } + if (options && options.unwatchCategoryTopics) { + data.unwatch_category_topics = options.unwatchCategoryTopics; + } + // TODO: We can remove this when migrated fully to rest model. this.set('isSaving', true); return Discourse.ajax(`/users/${this.get('username_lower')}`, { @@ -197,6 +205,7 @@ const User = RestModel.extend({ this.set('bio_excerpt', result.user.bio_excerpt); const userProps = Em.getProperties(this.get('user_option'),'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); Discourse.User.current().setProperties(userProps); + this.setProperties(updatedState); }).finally(() => { this.set('isSaving', false); }); @@ -352,6 +361,16 @@ const User = RestModel.extend({ this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids)); }, + changedCategoryNotifications: function(type) { + const ids = this.get(type + "Categories").map(c => c.id); + const oldIds = this.get(type + "_category_ids"); + + return { + add: _.difference(ids, oldIds), + remove: _.difference(oldIds, ids), + } + }, + @computed("can_delete_account", "reply_count", "topic_count") canDeleteAccount(canDeleteAccount, replyCount, topicCount) { return !Discourse.SiteSettings.enable_sso && canDeleteAccount && ((replyCount || 0) + (topicCount || 0)) <= 1; diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 32785d4800..8ebc13380c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -89,6 +89,10 @@ class UsersController < ApplicationController user = fetch_user_from_params guardian.ensure_can_edit!(user) + if params[:unwatch_category_topics] + TopicUser.unwatch_categories!(user, params[:unwatch_category_topics]) + end + if params[:user_fields].present? params[:custom_fields] = {} unless params[:custom_fields].present? diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 78480319c0..3079385ee9 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -59,6 +59,32 @@ class TopicUser < ActiveRecord::Base topic_user.save end + def unwatch_categories!(user, category_ids) + + track_threshold = user.user_option.auto_track_topics_after_msecs + + sql = < :track_threshold AND :track_threshold >= 0 THEN :tracking + ELSE :regular + end + FROM topics t + WHERE t.id = tu.topic_id AND tu.notification_level <> :muted AND category_id IN (:category_ids) AND tu.user_id = :user_id +SQL + + exec_sql(sql, + watching: notification_levels[:watching], + tracking: notification_levels[:tracking], + regular: notification_levels[:regular], + muted: notification_levels[:muted], + category_ids: category_ids, + user_id: user.id, + track_threshold: track_threshold + ) + end + # Find the information specific to a user in a forum topic def lookup_for(user, topics) # If the user isn't logged in, there's no last read posts diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0f83190a42..bee0eafe91 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -574,6 +574,13 @@ en: failed_to_move: "Failed to move selected messages (perhaps your network is down)" select_all: "Select All" + warn_unwatch: + message: "Also stop watching previously watched topics in {{categories}}?" + yes_value: "Yes, unwatch topics" + no_value: + one: "No, only unwatch category" + other: "No, only unwatch categories" + change_password: success: "(email sent)" in_progress: "(sending email)" diff --git a/spec/models/topic_user_spec.rb b/spec/models/topic_user_spec.rb index 5b609c0c8d..3259316c22 100644 --- a/spec/models/topic_user_spec.rb +++ b/spec/models/topic_user_spec.rb @@ -2,6 +2,34 @@ require 'rails_helper' describe TopicUser do + describe "#unwatch_categories!" do + it "correctly unwatches categories" do + + op_topic = Fabricate(:topic) + another_topic = Fabricate(:topic) + tracked_topic = Fabricate(:topic) + + user = op_topic.user + watching = TopicUser.notification_levels[:watching] + regular = TopicUser.notification_levels[:regular] + tracking = TopicUser.notification_levels[:tracking] + + TopicUser.change(user.id, op_topic, notification_level: watching) + TopicUser.change(user.id, another_topic, notification_level: watching) + TopicUser.change(user.id, tracked_topic, notification_level: watching, total_msecs_viewed: SiteSetting.default_other_auto_track_topics_after_msecs + 1) + + TopicUser.unwatch_categories!(user, [Fabricate(:category).id, Fabricate(:category).id]) + expect(TopicUser.get(another_topic, user).notification_level).to eq(watching) + + TopicUser.unwatch_categories!(user, [op_topic.category_id]) + + expect(TopicUser.get(op_topic, user).notification_level).to eq(watching) + expect(TopicUser.get(another_topic, user).notification_level).to eq(regular) + expect(TopicUser.get(tracked_topic, user).notification_level).to eq(tracking) + end + + end + describe '#notification_levels' do context "verify enum sequence" do before do From 214f5bff5cd6ee8260bf294e244fff899049beb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 28 Jun 2016 16:42:05 +0200 Subject: [PATCH 44/71] don't send more than 1 reply per day to auto-generated emails --- config/locales/server.en.yml | 1 - lib/email/message_builder.rb | 7 ---- lib/email/processor.rb | 33 +++++++++++------- lib/email/receiver.rb | 34 +++++-------------- spec/components/email/message_builder_spec.rb | 7 ---- spec/components/email/receiver_spec.rb | 4 --- spec/fixtures/emails/bounced_email_2.eml | 29 ---------------- spec/jobs/poll_mailbox_spec.rb | 9 ----- 8 files changed, 30 insertions(+), 94 deletions(-) delete mode 100644 spec/fixtures/emails/bounced_email_2.eml diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b7c74a51d6..c7ab81feb0 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -73,7 +73,6 @@ en: topic_not_found_error: "Happens when a reply came in but the related topic has been deleted." topic_closed_error: "Happens when a reply came in but the related topic has been closed." bounced_email_error: "Email is a bounced email report." - auto_generated_email_reply: "Email contains a reply to an auto generated email." screened_email_error: "Happens when the sender's email address was already screened." errors: &errors diff --git a/lib/email/message_builder.rb b/lib/email/message_builder.rb index f099b11f1e..79fe8a46b4 100644 --- a/lib/email/message_builder.rb +++ b/lib/email/message_builder.rb @@ -17,9 +17,6 @@ module Email class MessageBuilder attr_reader :template_args - REPLY_TO_AUTO_GENERATED_HEADER_KEY = "X-Discourse-Reply-to-Auto-Generated".freeze - REPLY_TO_AUTO_GENERATED_HEADER_VALUE = "marked".freeze - def initialize(to, opts=nil) @to = to @opts = opts || {} @@ -138,10 +135,6 @@ module Email result['List-Unsubscribe'] = "<#{template_args[:user_preferences_url]}>" end - if @opts[:mark_as_reply_to_auto_generated] - result[REPLY_TO_AUTO_GENERATED_HEADER_KEY] = REPLY_TO_AUTO_GENERATED_HEADER_VALUE - end - result['X-Discourse-Post-Id'] = @opts[:post_id].to_s if @opts[:post_id] result['X-Discourse-Topic-Id'] = @opts[:topic_id].to_s if @opts[:topic_id] diff --git a/lib/email/processor.rb b/lib/email/processor.rb index 30dcef9275..4287eab81d 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -15,16 +15,14 @@ module Email receiver = Email::Receiver.new(@mail) receiver.process! rescue Email::Receiver::BouncedEmailError => e + # never reply to bounced emails log_email_process_failure(@mail, e) set_incoming_email_rejection_message(receiver.incoming_email, I18n.t("emails.incoming.errors.bounced_email_error")) - rescue Email::Receiver::AutoGeneratedEmailReplyError => e - log_email_process_failure(@mail, e) - set_incoming_email_rejection_message(receiver.incoming_email, I18n.t("emails.incoming.errors.auto_generated_email_reply")) rescue => e log_email_process_failure(@mail, e) - - rejection_message = handle_failure(@mail, e) - if rejection_message.present? && receiver && (incoming_email = receiver.incoming_email) + incoming_email = receiver.try(:incoming_email) + rejection_message = handle_failure(@mail, e, incoming_email) + if rejection_message.present? set_incoming_email_rejection_message(incoming_email, rejection_message.body.to_s) end end @@ -32,7 +30,7 @@ module Email private - def handle_failure(mail_string, e) + def handle_failure(mail_string, e, incoming_email) message_template = case e when Email::Receiver::EmptyEmailError then :email_reject_empty when Email::Receiver::NoBodyDetectedError then :email_reject_empty @@ -67,10 +65,6 @@ module Email template_args[:rate_limit_description] = e.description end - if message_template == :email_reject_auto_generated - template_args[:mark_as_reply_to_auto_generated] = true - end - if message_template # inform the user about the rejection message = Mail::Message.new(mail_string) @@ -79,7 +73,11 @@ module Email template_args[:site_name] = SiteSetting.title client_message = RejectionMailer.send_rejection(message_template, message.from, template_args) - Email::Sender.new(client_message, message_template).send + + # don't send more than 1 reply per day to auto-generated emails + if !incoming_email.try(:is_auto_generated) || can_reply_to_auto_generated?(message.from) + Email::Sender.new(client_message, message_template).send + end else Rails.logger.error("Unrecognized error type (#{e}) when processing incoming email\n\nMail:\n#{mail_string}") end @@ -87,6 +85,17 @@ module Email client_message end + def can_reply_to_auto_generated?(email) + key = "auto_generated_reply:#{email}:#{Date.today}" + + if $redis.setnx(key, "1") + $redis.expire(key, 25.hours) + true + else + false + end + end + def set_incoming_email_rejection_message(incoming_email, message) incoming_email.update_attributes!(rejection_message: message) end diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 604d0220fe..583c762cd0 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -13,7 +13,6 @@ module Email class ScreenedEmailError < ProcessingError; end class UserNotFoundError < ProcessingError; end class AutoGeneratedEmailError < ProcessingError; end - class AutoGeneratedEmailReplyError < ProcessingError; end class BouncedEmailError < ProcessingError; end class NoBodyDetectedError < ProcessingError; end class InactiveUserError < ProcessingError; end @@ -82,8 +81,7 @@ module Email if is_auto_generated? @incoming_email.update_columns(is_auto_generated: true) - raise AutoGeneratedEmailReplyError if check_reply_to_auto_generated_header - raise AutoGeneratedEmailError if SiteSetting.block_auto_generated_emails? + raise AutoGeneratedEmailError if SiteSetting.block_auto_generated_emails? end if action = subscription_action_for(body, subject) @@ -151,20 +149,20 @@ module Email if bounce_key && (email_log = EmailLog.find_by(bounce_key: bounce_key)) email_log.update_columns(bounced: true) email = email_log.user.try(:email) || @from_email - if email.present? - if @mail.error_status.present? - if @mail.error_status.start_with?("4.") - Email::Receiver.update_bounce_score(email, SOFT_BOUNCE_SCORE) - elsif @mail.error_status.start_with?("5.") - Email::Receiver.update_bounce_score(email, HARD_BOUNCE_SCORE) - end - elsif is_auto_generated? + if email.present? && @mail.error_status.present? + if @mail.error_status.start_with?("4.") + Email::Receiver.update_bounce_score(email, SOFT_BOUNCE_SCORE) + else @mail.error_status.start_with?("5.") Email::Receiver.update_bounce_score(email, HARD_BOUNCE_SCORE) end end end end + if is_auto_generated? + Email::Receiver.update_bounce_score(@from_email, SOFT_BOUNCE_SCORE) + end + true end @@ -534,20 +532,6 @@ module Email !topic.topic_allowed_groups.where("group_id IN (SELECT group_id FROM group_users WHERE user_id = ?)", user.id).exists? end - private - - def check_reply_to_auto_generated_header - headers = Mail::Header.new(@mail.body.to_s.gsub("\r\n\r\n", "\r\n")).to_a - - index = headers.find_index do |f| - f.name == Email::MessageBuilder::REPLY_TO_AUTO_GENERATED_HEADER_KEY - end - - if index - headers[index].value == Email::MessageBuilder::REPLY_TO_AUTO_GENERATED_HEADER_VALUE - end - end - end end diff --git a/spec/components/email/message_builder_spec.rb b/spec/components/email/message_builder_spec.rb index e63e704e20..e5baacde56 100644 --- a/spec/components/email/message_builder_spec.rb +++ b/spec/components/email/message_builder_spec.rb @@ -142,7 +142,6 @@ describe Email::MessageBuilder do body: 'hello world', topic_id: 1234, post_id: 4567, - mark_as_reply_to_auto_generated: true ) end @@ -154,12 +153,6 @@ describe Email::MessageBuilder do expect(message_with_header_args.header_args['X-Discourse-Topic-Id']).to eq('1234') end - it "marks the email as replying to an auto generated email" do - expect(message_with_header_args.header_args[ - Email::MessageBuilder::REPLY_TO_AUTO_GENERATED_HEADER_KEY - ]).to eq(Email::MessageBuilder::REPLY_TO_AUTO_GENERATED_HEADER_VALUE) - end - end context "unsubscribe link" do diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index bd60b249b3..573719437f 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -61,10 +61,6 @@ describe Email::Receiver do expect(IncomingEmail.last.is_bounce).to eq(true) end - it "raises an AutoGeneratedEmailReplyError when email contains a marked reply" do - expect { process(:bounced_email_2) }.to raise_error(Email::Receiver::AutoGeneratedEmailReplyError) - end - context "bounces to VERP" do let(:bounce_key) { "14b08c855160d67f2e0c2f8ef36e251e" } diff --git a/spec/fixtures/emails/bounced_email_2.eml b/spec/fixtures/emails/bounced_email_2.eml deleted file mode 100644 index d17ba08ffd..0000000000 --- a/spec/fixtures/emails/bounced_email_2.eml +++ /dev/null @@ -1,29 +0,0 @@ -Delivered-To: someguy@discourse.org -Date: Thu, 7 Apr 2016 19:04:30 +0900 (JST) -From: MAILER-DAEMON@b-s-c.co.jp (Mail Delivery System) -Subject: Undelivered Mail Returned to Sender -To: someguy@discourse.org -MIME-Version: 1.0 -Content-Type: multipart/report; report-type=delivery-status; - boundary="18F5D18A0075.1460023470/some@daemon.com" - -This is a MIME-encapsulated message. - ---18F5D18A0075.1460023470/some@daemon.com -Content-Description: Notification -Content-Type: text/plain; charset=us-ascii - -Your email bounced - ---18F5D18A0075.1460023470/some@daemon.com -Content-Description: Undelivered Message -Content-Type: message/rfc822 - -Return-Path: -Date: Thu, 07 Apr 2016 03:04:28 -0700 (PDT) -From: someguy@discourse.org -X-Discourse-Reply-to-Auto-Generated: marked - -This is the body - ---18F5D18A0075.1460023470/some@daemon.com-- diff --git a/spec/jobs/poll_mailbox_spec.rb b/spec/jobs/poll_mailbox_spec.rb index 10566133ea..b6856bcdb3 100644 --- a/spec/jobs/poll_mailbox_spec.rb +++ b/spec/jobs/poll_mailbox_spec.rb @@ -90,15 +90,6 @@ describe Jobs::PollMailbox do ) end - it "does not reply to an email containing a reply to an auto generated email" do - expect { process_popmail(:bounced_email_2) }.to_not change { ActionMailer::Base.deliveries.count } - - incoming_email = IncomingEmail.last - - expect(incoming_email.rejection_message).to eq( - I18n.t("emails.incoming.errors.auto_generated_email_reply") - ) - end end end From f406b9a7985fa90307dcee8d3fd738f6368e6e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 28 Jun 2016 16:49:47 +0200 Subject: [PATCH 45/71] fix lint --- app/assets/javascripts/discourse/models/user.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 6fabd1b865..7b5c9e4790 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -368,7 +368,7 @@ const User = RestModel.extend({ return { add: _.difference(ids, oldIds), remove: _.difference(oldIds, ids), - } + }; }, @computed("can_delete_account", "reply_count", "topic_count") From 76766a25bfa56f07b4c4885f91e6f120fab494c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 28 Jun 2016 17:22:34 +0200 Subject: [PATCH 46/71] FIX: wrong translation key --- app/jobs/regular/notify_mailing_list_subscribers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index bbae4bb8b3..804dcd19d2 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -44,7 +44,7 @@ module Jobs user_id: user.id, post_id: post.id, skipped: true, - skipped_reason: "[MailingList] #{I18n.t('email_log.exceeded_limit')}" + skipped_reason: "[MailingList] #{I18n.t('email_log.exceeded_emails_limit')}" ) else message = UserNotifications.mailing_list_notify(user, post) From 61ce5c210c9a5811c74c7856de51c93409610b8d Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 28 Jun 2016 15:52:38 -0400 Subject: [PATCH 47/71] FIX: S3Cdn link clicks weren't working --- app/models/topic_link_click.rb | 23 ++++++++++++++++++----- spec/models/topic_link_click_spec.rb | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app/models/topic_link_click.rb b/app/models/topic_link_click.rb index 907db52b11..1b31196d95 100644 --- a/app/models/topic_link_click.rb +++ b/app/models/topic_link_click.rb @@ -30,11 +30,24 @@ class TopicLinkClick < ActiveRecord::Base urls << url.sub(/\?.*$/, '') if url.include?('?') # add a cdn link - if uri && Discourse.asset_host.present? - cdn_uri = URI.parse(Discourse.asset_host) rescue nil - if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path) - is_cdn_link = true - urls << uri.path[(cdn_uri.path.length)..-1] + if uri + if Discourse.asset_host.present? + cdn_uri = URI.parse(Discourse.asset_host) rescue nil + if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path) + is_cdn_link = true + urls << uri.path[(cdn_uri.path.length)..-1] + end + end + + if SiteSetting.s3_cdn_url.present? + cdn_uri = URI.parse(SiteSetting.s3_cdn_url) rescue nil + if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path) + is_cdn_link = true + + path = uri.path[(cdn_uri.path.length)..-1] + urls << path + urls << "#{Discourse.store.absolute_base_url}#{path}" + end end end diff --git a/spec/models/topic_link_click_spec.rb b/spec/models/topic_link_click_spec.rb index 5dd27a3fe8..0709a0a0b3 100644 --- a/spec/models/topic_link_click_spec.rb +++ b/spec/models/topic_link_click_spec.rb @@ -144,6 +144,24 @@ describe TopicLinkClick do end + context "s3 cdns" do + + it "works with s3 urls" do + SiteSetting.s3_cdn_url = "https://discourse-s3-cdn.global.ssl.fastly.net" + + post = Fabricate(:post, topic: @topic, raw: "[test](//test.localhost/uploads/default/my-test-link)") + TopicLink.extract_from(post) + + url = TopicLinkClick.create_from( + url: "https://discourse-s3-cdn.global.ssl.fastly.net/my-test-link", + topic_id: @topic.id, + ip: '127.0.0.3') + + expect(url).to be_present + end + + end + end context 'with a HTTPS version of the same URL' do From 8e5a22ba5d56d27350dd9fcbbbab11973623b6c9 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 28 Jun 2016 16:35:19 -0400 Subject: [PATCH 48/71] Support for mapping multiple mbox imports into categories --- script/import_scripts/mbox.rb | 56 ++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/script/import_scripts/mbox.rb b/script/import_scripts/mbox.rb index 98be215ada..6c1565ecf9 100755 --- a/script/import_scripts/mbox.rb +++ b/script/import_scripts/mbox.rb @@ -5,13 +5,19 @@ class ImportScripts::Mbox < ImportScripts::Base # CHANGE THESE BEFORE RUNNING THE IMPORTER BATCH_SIZE = 1000 - CATEGORY_ID = 6 MBOX_DIR = File.expand_path("~/import/site") # Remove to not split individual files SPLIT_AT = /^From (.*) at/ + # Will create a category if it doesn't exist + CATEGORY_MAPPINGS = { + "default" => "uncategorized", + # ex: "jobs-folder" => "jobs" + } + def execute + import_categories create_email_indices create_user_indices massage_indices @@ -20,6 +26,14 @@ class ImportScripts::Mbox < ImportScripts::Base import_replies end + def import_categories + mappings = CATEGORY_MAPPINGS.values - ['uncategorized'] + + create_categories(mappings) do |c| + {id: c, name: c} + end + end + def open_db SQLite3::Database.new("#{MBOX_DIR}/index.db") end @@ -43,6 +57,12 @@ class ImportScripts::Mbox < ImportScripts::Base def all_messages files = Dir["#{MBOX_DIR}/messages/*"] + CATEGORY_MAPPINGS.keys.each do |k| + files << Dir["#{MBOX_DIR}/#{k}/*"] + end + + files.flatten! + files.each_with_index do |f, idx| if SPLIT_AT.present? msg = "" @@ -52,7 +72,7 @@ class ImportScripts::Mbox < ImportScripts::Base if line =~ SPLIT_AT if !msg.empty? mail = Mail.read_from_string(msg) - yield mail + yield mail, f print_status(idx, files.size) msg = "" end @@ -62,14 +82,14 @@ class ImportScripts::Mbox < ImportScripts::Base if !msg.empty? mail = Mail.read_from_string(msg) - yield mail + yield mail, f print_status(idx, files.size) msg = "" end else raw = File.read(f) mail = Mail.read_from_string(raw) - yield mail + yield mail, f print_status(idx, files.size) end @@ -155,7 +175,8 @@ class ImportScripts::Mbox < ImportScripts::Base title VARCHAR(255) NOT NULL, reply_to VARCHAR(955) NULL, email_date DATETIME NOT NULL, - message TEXT NOT NULL + message TEXT NOT NULL, + category VARCHAR(255) NOT NULL ); SQL @@ -164,7 +185,12 @@ class ImportScripts::Mbox < ImportScripts::Base puts "", "creating indices" - all_messages do |mail| + all_messages do |mail, filename| + + directory = filename.sub("#{MBOX_DIR}/", '').split("/")[0] + + category = CATEGORY_MAPPINGS[directory] || CATEGORY_MAPPINGS['default'] || 'uncategorized' + msg_id = mail['Message-ID'].to_s # Many ways to get a name @@ -174,9 +200,16 @@ class ImportScripts::Mbox < ImportScripts::Base reply_to = mail['In-Reply-To'].to_s email_date = mail['date'].to_s - db.execute "INSERT OR IGNORE INTO emails (msg_id, from_email, from_name, title, reply_to, email_date, message) - VALUES (?, ?, ?, ?, ?, ?, ?)", - [msg_id, from_email, from_name, title, reply_to, email_date, mail.to_s] + db.execute "INSERT OR IGNORE INTO emails (msg_id, + from_email, + from_name, + title, + reply_to, + email_date, + message, + category) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + [msg_id, from_email, from_name, title, reply_to, email_date, mail.to_s, category] end ensure db.close @@ -273,7 +306,8 @@ class ImportScripts::Mbox < ImportScripts::Base from_name, title, email_date, - message + message, + category FROM emails WHERE reply_to IS NULL") @@ -320,7 +354,7 @@ class ImportScripts::Mbox < ImportScripts::Base title: clean_title(title), user_id: user_id_from_imported_user_id(from_email) || Discourse::SYSTEM_USER_ID, created_at: mail.date, - category: CATEGORY_ID, + category: t[6], raw: clean_raw(raw), cook_method: Post.cook_methods[:email] } end From 8fbcda5bf1f200dc4da8e29d84fd77d2c0360bdf Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Tue, 28 Jun 2016 14:52:17 -0700 Subject: [PATCH 49/71] minor copyedit --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index bee0eafe91..23b0c7e4d0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -509,7 +509,7 @@ en: enable: "Enable Notifications" currently_disabled: "" each_browser_note: "Note: You have to change this setting on every browser you use." - dismiss_notifications: "Mark all as Read" + dismiss_notifications: "Dismiss All" dismiss_notifications_tooltip: "Mark all unread notifications as read" disable_jump_reply: "Don't jump to my post after I reply" dynamic_favicon: "Show new / updated topic count on browser icon" From b4cb2e367ca54fb17f6cff3911b68469feb564e5 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 29 Jun 2016 10:43:40 +1000 Subject: [PATCH 50/71] FIX: require full name at signup when display is suppressed and required --- .../javascripts/discourse/controllers/create-account.js.es6 | 4 ++++ .../javascripts/discourse/templates/modal/create-account.hbs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index 9ff65be628..45e7d7bc95 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -67,6 +67,10 @@ export default Ember.Controller.extend(ModalFunctionality, { usernameRequired: Ember.computed.not('authOptions.omit_username'), + fullnameRequired: function() { + return this.get('siteSettings.full_name_required') || this.get('siteSettings.enable_names'); + }.property(), + passwordRequired: function() { return Ember.isEmpty(this.get('authOptions.auth_provider')); }.property('authOptions.auth_provider'), diff --git a/app/assets/javascripts/discourse/templates/modal/create-account.hbs b/app/assets/javascripts/discourse/templates/modal/create-account.hbs index 5e5d9b497b..ca5485a271 100644 --- a/app/assets/javascripts/discourse/templates/modal/create-account.hbs +++ b/app/assets/javascripts/discourse/templates/modal/create-account.hbs @@ -35,7 +35,7 @@ {{/if}} - {{#if siteSettings.enable_names}} + {{#if fullnameRequired}} From e2214149359fc6ccfe6b88ebd3b8c55029bd725b Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 28 Jun 2016 10:01:00 +0800 Subject: [PATCH 51/71] PERF: Remove N+1 queries on user messages page. --- app/controllers/application_controller.rb | 6 ++++-- app/controllers/list_controller.rb | 2 +- app/serializers/topic_list_item_serializer.rb | 1 + app/serializers/topic_list_serializer.rb | 1 + lib/topic_query.rb | 4 ++-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5ac4811442..4f60aef2df 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -293,13 +293,15 @@ class ApplicationController < ActionController::Base Middleware::AnonymousCache.anon_cache(request.env, time_length) end - def fetch_user_from_params(opts=nil) + def fetch_user_from_params(opts=nil, eager_load = []) opts ||= {} user = if params[:username] username_lower = params[:username].downcase.chomp('.json') find_opts = { username_lower: username_lower } find_opts[:active] = true unless opts[:include_inactive] || current_user.try(:staff?) - User.find_by(find_opts) + result = User + (result = result.includes(*eager_load)) if !eager_load.empty? + result.find_by(find_opts) elsif params[:external_id] external_id = params[:external_id].chomp('.json') SingleSignOnRecord.find_by(external_id: external_id).try(:user) diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index ef36592ef5..b3bc5d1931 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -109,7 +109,7 @@ class ListController < ApplicationController [:topics_by, :private_messages, :private_messages_sent, :private_messages_unread, :private_messages_archive, :private_messages_group, :private_messages_group_archive].each do |action| define_method("#{action}") do list_opts = build_topic_list_options - target_user = fetch_user_from_params(include_inactive: current_user.try(:staff?)) + target_user = fetch_user_from_params({ include_inactive: current_user.try(:staff?) }, [:user_stat, :user_option]) guardian.ensure_can_see_private_messages!(target_user.id) unless action == :topics_by list = generate_list_for(action.to_s, target_user, list_opts) url_prefix = "topics" unless action == :topics_by diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb index d3763cef16..a04c1eeae3 100644 --- a/app/serializers/topic_list_item_serializer.rb +++ b/app/serializers/topic_list_item_serializer.rb @@ -67,6 +67,7 @@ class TopicListItemSerializer < ListableTopicSerializer def include_tags? SiteSetting.tagging_enabled end + def tags object.tags.map(&:name) end diff --git a/app/serializers/topic_list_serializer.rb b/app/serializers/topic_list_serializer.rb index c0321c7058..5d4364d411 100644 --- a/app/serializers/topic_list_serializer.rb +++ b/app/serializers/topic_list_serializer.rb @@ -26,6 +26,7 @@ class TopicListSerializer < ApplicationSerializer def include_tags? SiteSetting.tagging_enabled && SiteSetting.show_filter_by_tag end + def tags Tag.top_tags end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index d2add0c9bb..d611e54439 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -355,10 +355,10 @@ class TopicQuery options = @options options.reverse_merge!(per_page: per_page_setting) - result = Topic + result = Topic.includes(:tags) if type == :group - result = result.includes(:allowed_groups) + result = result.includes(:allowed_users) result = result.where("topics.id IN (SELECT topic_id FROM topic_allowed_groups WHERE group_id IN ( SELECT group_id FROM group_users WHERE user_id = #{user.id.to_i}) AND From 4a143d584b07956904964f031d916370389e7fc5 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 29 Jun 2016 09:36:32 +0800 Subject: [PATCH 52/71] Don't log bullet alerts in the console. --- config/environments/development.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 2d7f03c48a..992a231e8e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -54,7 +54,6 @@ Discourse::Application.configure do config.after_initialize do Bullet.enable = true - Bullet.console = true Bullet.rails_logger = true end end From ef93e75f8076a3a986aaf682ec80e808edd259e4 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 29 Jun 2016 12:00:07 +1000 Subject: [PATCH 53/71] correct #4293 no need to muck with site settings, messes up repeat runs --- spec/components/post_destroyer_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index aacad3ca96..bbd3cfc34c 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -195,10 +195,7 @@ describe PostDestroyer do describe 'basic destroying' do it "as the creator of the post, doesn't delete the post" do - SiteSetting.stubs(:unique_posts_mins).returns(5) - SiteSetting.stubs(:delete_removed_posts_after).returns(24) - - post2 = create_post # Create it here instead of with "let" so unique_posts_mins can do its thing + post2 = create_post @orig = post2.cooked PostDestroyer.new(post2.user, post2).destroy @@ -216,6 +213,7 @@ describe PostDestroyer do expect(post2.version).to eq(3) expect(post2.user_deleted).to eq(false) expect(post2.cooked).to eq(@orig) + end context "as a moderator" do From 41842460b483d80741af873c3841579a2a13852e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 28 Jun 2016 16:14:06 +0800 Subject: [PATCH 54/71] UX: Collapse users when composing a private message. --- .../components/composer-user-selector.js.es6 | 74 +++++++++++++++++++ .../discourse/components/user-selector.js.es6 | 15 ++-- .../discourse/lib/autocomplete.js.es6 | 7 +- .../components/composer-user-selector.hbs | 17 +++++ .../discourse/templates/composer.hbs | 8 +- .../discourse/views/composer.js.es6 | 7 ++ .../stylesheets/common/base/compose.scss | 11 +++ .../common/base/composer-user-selector.scss | 10 +++ 8 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/composer-user-selector.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs create mode 100644 app/assets/stylesheets/common/base/composer-user-selector.scss diff --git a/app/assets/javascripts/discourse/components/composer-user-selector.js.es6 b/app/assets/javascripts/discourse/components/composer-user-selector.js.es6 new file mode 100644 index 0000000000..d977d6e309 --- /dev/null +++ b/app/assets/javascripts/discourse/components/composer-user-selector.js.es6 @@ -0,0 +1,74 @@ +import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + showSelector: true, + shouldHide: false, + defaultUsernameCount: 0, + + @observes('usernames') + _checkWidth() { + let width = 0; + const $acWrap = this.$().find('.ac-wrap'); + const limit = $acWrap.width(); + this.set('defaultUsernameCount', 0); + + $acWrap.find('.item').toArray().forEach(item => { + width += $(item).outerWidth(true); + const result = (width < limit); + + if (result) this.incrementProperty('defaultUsernameCount'); + return result; + }); + + if (width >= limit) { + this.set('shouldHide', true); + } else { + this.set('shouldHide', false); + }; + }, + + @observes('shouldHide') + _setFocus() { + const selector = '#reply-control #reply-title, #reply-control .d-editor-input'; + + if (this.get('shouldHide')) { + $(selector).on('focus.composer-user-selector', () => { + this.set('showSelector', false); + this.appEvents.trigger("composer:resize"); + }); + } else { + $(selector).off('focus.composer-user-selector'); + } + }, + + @computed('usernames') + splitUsernames(usernames) { + return usernames.split(','); + }, + + @computed('splitUsernames', 'defaultUsernameCount') + limitedUsernames(splitUsernames, count) { + return splitUsernames.slice(0, count).join(", "); + }, + + @computed('splitUsernames', 'defaultUsernameCount') + hiddenUsersCount(splitUsernames, count) { + return `${splitUsernames.length - count} ${I18n.t('more')}`; + }, + + actions: { + toggleSelector() { + this.set("showSelector", true); + + Ember.run.schedule('afterRender', () => { + this.$().find('input').focus(); + }); + }, + + triggerResize() { + this.appEvents.trigger("composer:resize"); + const $this = this.$().find('.ac-wrap'); + if ($this.height() >= 150) $this.scrollTop($this.height()); + }, + } +}); diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index ab695e8b2b..862f0fae49 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -1,9 +1,11 @@ +import { observes } from 'ember-addons/ember-computed-decorators'; import TextField from 'discourse/components/text-field'; import userSearch from 'discourse/lib/user-search'; export default TextField.extend({ - _initializeAutocomplete: function() { + didInsertElement() { + this._super(); var self = this, selected = [], groups = [], @@ -63,6 +65,7 @@ export default TextField.extend({ self.set('hasGroups', hasGroups); selected = items; + if (self.get('onChangeCallback')) self.sendAction('onChangeCallback'); }, reverseTransform: function(i) { @@ -70,19 +73,21 @@ export default TextField.extend({ } }); - }.on('didInsertElement'), + }, - _removeAutocomplete: function() { + willDestroyElement() { + this._super(); this.$().autocomplete('destroy'); - }.on('willDestroyElement'), + }, // THIS IS A HUGE HACK TO SUPPORT CLEARING THE INPUT + @observes('usernames') _clearInput: function() { if (arguments.length > 1) { if (Em.isEmpty(this.get("usernames"))) { this.$().parent().find("a").click(); } } - }.observes("usernames") + } }); diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index a142947da6..a2edb6b886 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -101,13 +101,16 @@ export default function(options) { transformed = _.isArray(transformedItem) ? transformedItem : [transformedItem || item]; var divs = transformed.map(function(itm) { - var d = $("
    " + itm + "
    "); - var prev = me.parent().find('.item:last'); + let d = $(`
    ${itm}
    `); + const $parent = me.parent(); + const prev = $parent.find('.item:last'); + if (prev.length === 0) { me.parent().prepend(d); } else { prev.after(d); } + inputSelectedItems.push(itm); return d[0]; }); diff --git a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs new file mode 100644 index 0000000000..71b5e8176d --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs @@ -0,0 +1,17 @@ +{{#if showSelector}} + {{user-selector topicId=topicId + excludeCurrentUser='true' + onChangeCallback='triggerResize' + id="private-message-users" + includeMentionableGroups='true' + class="span8" + placeholderKey="composer.users_placeholder" + tabindex="1" + usernames=usernames + hasGroups=hasGroups}} +{{else}} +
    + {{limitedUsernames}} + {{hiddenUsersCount}} +
    +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 290a0815bd..4471943e16 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -48,13 +48,7 @@ {{#if model.canEditTitle}}
    {{#if model.creatingPrivateMessage}} - {{user-selector topicId=topicModel.id - excludeCurrentUser="true" - id="private-message-users" - includeMentionableGroups="true" - class="span8" - placeholderKey="composer.users_placeholder" - tabindex="1" + {{composer-user-selector topicId=topicModel.id usernames=model.targetUsernames hasGroups=model.hasTargetGroups }} diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 5d6924718b..0542b6d8da 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -103,6 +103,13 @@ const ComposerView = Ember.View.extend({ triggerOpen(); }); positioningWorkaround(this.$()); + + this.appEvents.on('composer:resize', this, this.resize); + }, + + willDestroyElement() { + this._super(); + this.appEvents.off('composer:resize', this, this.resize); }, click() { diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 4bb02e82dd..dfadbd6fb8 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -97,6 +97,8 @@ div.ac-wrap div.item a.remove, .remove-link { } div.ac-wrap { + overflow: auto; + max-height: 150px; background-color: $secondary; border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); padding: 5px 4px 1px 4px; @@ -110,6 +112,15 @@ div.ac-wrap { line-height: 20px; } } + + .ac-collapsed-button { + float: left; + border-radius: 20px; + position: relative; + top: -2px; + margin-right: 10px; + } + input[type="text"] { float: left; margin-bottom: 4px; diff --git a/app/assets/stylesheets/common/base/composer-user-selector.scss b/app/assets/stylesheets/common/base/composer-user-selector.scss new file mode 100644 index 0000000000..f3ea48c8ab --- /dev/null +++ b/app/assets/stylesheets/common/base/composer-user-selector.scss @@ -0,0 +1,10 @@ +div.ac-wrap.composer-user-selector-limited { + width: 400px; + + .btn-small { + border-radius: 10px; + position: relative; + top: -2px; + float: none; + } +} From 20359788dcfe720ac5cacb5c06f6b3b10b35b7d5 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 27 Jun 2016 17:26:43 +0800 Subject: [PATCH 55/71] Rename `SiteSetting#use_https` to `force_https`. --- app/controllers/user_avatars_controller.rb | 2 +- app/models/about.rb | 2 +- app/models/site_setting.rb | 4 +-- config/initializers/050-force_https.rb | 2 +- config/locales/server.de.yml | 2 +- config/locales/server.en.yml | 2 +- config/locales/server.es.yml | 2 +- config/locales/server.pt.yml | 2 +- config/site_settings.yml | 2 +- ..._use_https_name_change_in_site_settings.rb | 5 ++++ lib/discourse.rb | 2 +- lib/onebox/engine/discourse_local_onebox.rb | 2 +- lib/site_setting_extension.rb | 28 +++++++++++++++++++ spec/components/discourse_spec.rb | 4 +-- spec/components/email/styles_spec.rb | 4 +-- spec/models/site_setting_spec.rb | 28 +++++++++++++++++-- 16 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 db/migrate/20160627104436_use_https_name_change_in_site_settings.rb diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index a346910543..2cc10b43c5 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -120,7 +120,7 @@ class UserAvatarsController < ApplicationController def proxy_avatar(url) if url[0..1] == "//" - url = (SiteSetting.use_https ? "https:" : "http:") + url + url = (SiteSetting.force_https ? "https:" : "http:") + url end sha = Digest::SHA1.hexdigest(url) diff --git a/app/models/about.rb b/app/models/about.rb index 1c96d032e2..dd87169ece 100644 --- a/app/models/about.rb +++ b/app/models/about.rb @@ -18,7 +18,7 @@ class About end def https - SiteSetting.use_https + SiteSetting.force_https end def title diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 266a482f89..9b694bca95 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -23,6 +23,7 @@ class SiteSetting < ActiveRecord::Base end load_settings(File.join(Rails.root, 'config', 'site_settings.yml')) + setup_deprecated_methods unless Rails.env.test? && ENV['LOAD_PLUGINS'] != "1" Dir[File.join(Rails.root, "plugins", "*", "config", "settings.yml")].each do |file| @@ -85,7 +86,7 @@ class SiteSetting < ActiveRecord::Base end def self.scheme - use_https? ? "https" : "http" + force_https? ? "https" : "http" end def self.default_categories_selected @@ -108,7 +109,6 @@ class SiteSetting < ActiveRecord::Base def self.email_polling_enabled? SiteSetting.manual_polling_enabled? || SiteSetting.pop3_polling_enabled? end - end # == Schema Information diff --git a/config/initializers/050-force_https.rb b/config/initializers/050-force_https.rb index 0e91da8b24..7ac71e1135 100644 --- a/config/initializers/050-force_https.rb +++ b/config/initializers/050-force_https.rb @@ -6,7 +6,7 @@ class Discourse::ForceHttpsMiddleware end def call(env) - env['rack.url_scheme'] = 'https' if SiteSetting.use_https + env['rack.url_scheme'] = 'https' if SiteSetting.force_https @app.call(env) end diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index d0a1797e54..ac54be06dd 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -765,7 +765,7 @@ de: notification_email: "Die E-Mail-Adresse die als \"From:\" Absender aller wichtiger System-E-Mails benutzt wird. Die benutzte Domain sollte über korrekte SPF, DKIM und PTR Einträge verfügen, damit E-Mails sicher zugestellt werden können." email_custom_headers: "Eine durch senkrechte Striche getrennte Liste von eigenen E-Mail Headerzeilen" email_subject: "Format der Betreffzeile in Standard-E-Mails. Siehe https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" - use_https: "Erzwinge HTTPS für deine Seite. ACHTUNG: aktiviere dies nicht, bevor HTTPS nicht vollständig eingerichtet ist und auf jeden Fall überall funktioniert! Hast du alle CDN-Netzwerke, alle Logins über Soziale Netzwerke, alle externe Logos / Abhängigkeiten geprüft, um sicherzustellen, dass sie auch alle HTTPS-kompatibel sind?" + force_https: "Erzwinge HTTPS für deine Seite. ACHTUNG: aktiviere dies nicht, bevor HTTPS nicht vollständig eingerichtet ist und auf jeden Fall überall funktioniert! Hast du alle CDN-Netzwerke, alle Logins über Soziale Netzwerke, alle externe Logos / Abhängigkeiten geprüft, um sicherzustellen, dass sie auch alle HTTPS-kompatibel sind?" summary_score_threshold: "Mindestpunktzahl, die ein Beitrag benötigt, um in der \"Thema zusammenfassen\"-Ansicht zu erscheinen." summary_posts_required: "Mindestanzahl an Beiträgen in einem Thema, bevor die \"Thema zusammenfassen\"-Funktion aktiviert wird." summary_likes_required: "Mindestanzahl an \"Gefällt mir\" Wertungen in einem Thema, bevor die \"Thema zusammenfassen\" Funktion aktiviert wird." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index c7ab81feb0..3f994a19d7 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -865,7 +865,7 @@ en: notification_email: "The from: email address used when sending all essential system emails. The domain specified here must have SPF, DKIM and reverse PTR records set correctly for email to arrive." email_custom_headers: "A pipe-delimited list of custom email headers" email_subject: "Customizable subject format for standard emails. See https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" - use_https: "Force your site to use HTTPS only. WARNING: do NOT enable this until you verify HTTPS is fully set up and working absolutely everywhere! Did you check your CDN, all social logins, and any external logos / dependencies to make sure they are all HTTPS compatible, too?" + force_https: "Force your site to use HTTPS only. WARNING: do NOT enable this until you verify HTTPS is fully set up and working absolutely everywhere! Did you check your CDN, all social logins, and any external logos / dependencies to make sure they are all HTTPS compatible, too?" summary_score_threshold: "The minimum score required for a post to be included in 'Summarize This Topic'" summary_posts_required: "Minimum posts in a topic before 'Summarize This Topic' is enabled" summary_likes_required: "Minimum likes in a topic before 'Summarize This Topic' is enabled" diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 7bd60d17b7..7dfa8445aa 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -776,7 +776,7 @@ es: notification_email: "La dirección de correo electrónico \"remitente\", utilizada al enviar todos los emails esenciales de sistema. El dominio especificado debe tener correctamente configurados los registros SPF, DKIM y PTR inversos para que los emails se reciban correctamente." email_custom_headers: "Lista de emails separados por una barra" email_subject: "Formato de asunto personalizable para emails estándar. Mira https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" - use_https: "Forzar al sitio a utilizar sólo HTTPS. AVISO: ¡NO actives esta opción a menos que verifiques completamente la configuración y funcione correctamente en todas partes! ¿Has verificado también que el CDN, los inicios de sesión social y cualquier logo externo / dependencia son compatibles con HTTPS?" + force_https: "Forzar al sitio a utilizar sólo HTTPS. AVISO: ¡NO actives esta opción a menos que verifiques completamente la configuración y funcione correctamente en todas partes! ¿Has verificado también que el CDN, los inicios de sesión social y cualquier logo externo / dependencia son compatibles con HTTPS?" summary_score_threshold: "La puntuación mínima requerida para que un post sea incluido en el 'Resumen de este tema\"" summary_posts_required: "El mínimo número de posts en un tema para habilitar el 'Resumen de este tema'" summary_likes_required: "Mínimo de \"me gusta\" en un tema para habilitar 'Resumen de este tema'" diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index 6d57c9ca12..c754e9d7b2 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -786,7 +786,7 @@ pt: notification_email: "Para: endereço de email usado ao enviar emails essenciais do sistema. O domínio especificado aqui deverá ter SPF, DKIM e registos PTR inversos configurados corretamente para a chegada do email." email_custom_headers: "A lista delimitada por barras verticais de cabeçalhos de e-mail personalizados" email_subject: "Formato de assunto personalizável para emails padrão. Veja https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" - use_https: "Forçar o site a usar apenas HTTPS. ALERTA: NÃO active esta opção enquanto não verificar que o HTTPS está completamente configurado e funcional. Verificou a sua CDN, todos os logins por rede social, e quaisquer logos ou outras dependências externas para garantir que também são compatíveis com HTTPS?" + force_https: "Forçar o site a usar apenas HTTPS. ALERTA: NÃO active esta opção enquanto não verificar que o HTTPS está completamente configurado e funcional. Verificou a sua CDN, todos os logins por rede social, e quaisquer logos ou outras dependências externas para garantir que também são compatíveis com HTTPS?" summary_score_threshold: "Pontuação mínima necessária para que uma mensagem seja incluída em 'Resumir Este Tópico'" summary_posts_required: "Número mínimo de mensagens num tópico antes que 'Resumir Este Tópico' seja ativo." summary_likes_required: "Número mínimo de gostos num tópico antes que 'Resumir Este Tópico' seja ativo." diff --git a/config/site_settings.yml b/config/site_settings.yml index a76a0070f4..f1fe8472ef 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -760,7 +760,7 @@ trust: client: true security: - use_https: false + force_https: false enable_escaped_fragments: true allow_index_in_robots_txt: true enable_noscript_support: true diff --git a/db/migrate/20160627104436_use_https_name_change_in_site_settings.rb b/db/migrate/20160627104436_use_https_name_change_in_site_settings.rb new file mode 100644 index 0000000000..b093e7345f --- /dev/null +++ b/db/migrate/20160627104436_use_https_name_change_in_site_settings.rb @@ -0,0 +1,5 @@ +class UseHttpsNameChangeInSiteSettings < ActiveRecord::Migration + def up + execute "UPDATE site_settings SET name = 'force_https' WHERE name = 'use_https'" + end +end diff --git a/lib/discourse.rb b/lib/discourse.rb index 762c1c0eaa..5e9524fae2 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -198,7 +198,7 @@ module Discourse default_port = 80 protocol = "http" - if SiteSetting.use_https? + if SiteSetting.force_https? protocol = "https" default_port = 443 end diff --git a/lib/onebox/engine/discourse_local_onebox.rb b/lib/onebox/engine/discourse_local_onebox.rb index f0ea5a32de..3e022e0526 100644 --- a/lib/onebox/engine/discourse_local_onebox.rb +++ b/lib/onebox/engine/discourse_local_onebox.rb @@ -45,7 +45,7 @@ module Onebox case route[:controller] when 'uploads' - url.gsub!("http:", "https:") if SiteSetting.use_https + url.gsub!("http:", "https:") if SiteSetting.force_https if File.extname(uri.path) =~ /^.(mov|mp4|webm|ogv)$/ return "" elsif File.extname(uri.path) =~ /^.(mp3|ogg|wav)$/ diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 4395a8c907..35ba859329 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -453,6 +453,28 @@ module SiteSettingExtension @validator_mapping[type_name] end + DEPRECATED_SETTINGS = [ + ['use_https', 'force_https', '1.7'] + ] + + def setup_deprecated_methods + DEPRECATED_SETTINGS.each do |old_setting, new_setting, version| + define_singleton_method old_setting do + logger.warn("`SiteSetting##{name}` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting##{new_setting}` instead") + self.public_send new_setting + end + + define_singleton_method "#{old_setting}?" do + logger.warn("`SiteSetting##{name}?` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting##{new_setting}?` instead") + self.public_send "#{new_setting}?" + end + + define_singleton_method "#{old_setting}=" do |val| + logger.warn("`SiteSetting##{name}=` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting##{new_setting}=` instead") + self.public_send "#{new_setting}=", val + end + end + end def setup_methods(name) clean_name = name.to_s.sub("?", "").to_sym @@ -488,4 +510,10 @@ module SiteSettingExtension url end + private + + def logger + Rails.logger + end + end diff --git a/spec/components/discourse_spec.rb b/spec/components/discourse_spec.rb index b8b108519e..f864ac46a4 100644 --- a/spec/components/discourse_spec.rb +++ b/spec/components/discourse_spec.rb @@ -18,7 +18,7 @@ describe Discourse do context 'base_url' do context 'when https is off' do before do - SiteSetting.expects(:use_https?).returns(false) + SiteSetting.expects(:force_https?).returns(false) end it 'has a non https base url' do @@ -28,7 +28,7 @@ describe Discourse do context 'when https is on' do before do - SiteSetting.expects(:use_https?).returns(true) + SiteSetting.expects(:force_https?).returns(true) end it 'has a non-ssl base url' do diff --git a/spec/components/email/styles_spec.rb b/spec/components/email/styles_spec.rb index 1f8bb953bf..d9824f4cf7 100644 --- a/spec/components/email/styles_spec.rb +++ b/spec/components/email/styles_spec.rb @@ -105,7 +105,7 @@ describe Email::Styles do context "without https" do before do - SiteSetting.stubs(:use_https).returns(false) + SiteSetting.stubs(:force_https).returns(false) end it "rewrites the href to have http" do @@ -126,7 +126,7 @@ describe Email::Styles do context "with https" do before do - SiteSetting.stubs(:use_https).returns(true) + SiteSetting.stubs(:force_https).returns(true) end it "rewrites the forum URL to have https" do diff --git a/spec/models/site_setting_spec.rb b/spec/models/site_setting_spec.rb index b4ac6a8ff2..c2f358c2c9 100644 --- a/spec/models/site_setting_spec.rb +++ b/spec/models/site_setting_spec.rb @@ -70,17 +70,41 @@ describe SiteSetting do end describe "scheme" do + before do + SiteSetting.force_https = true + end + it "returns http when ssl is disabled" do - SiteSetting.use_https = false + SiteSetting.force_https = false expect(SiteSetting.scheme).to eq("http") end it "returns https when using ssl" do - SiteSetting.expects(:use_https).returns(true) expect(SiteSetting.scheme).to eq("https") end end + context 'deprecated site settings' do + before do + SiteSetting.force_https = true + end + + after do + SiteSetting.force_https = false + end + + describe '#use_https' do + it 'should act as a proxy to the new methods' do + expect(SiteSetting.use_https).to eq(true) + expect(SiteSetting.use_https?).to eq(true) + + SiteSetting.use_https = false + + expect(SiteSetting.force_https).to eq(false) + expect(SiteSetting.force_https?).to eq(false) + end + end + end end From 918b015bdb617925ef01c14bb65b1ef905823590 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 29 Jun 2016 15:23:29 +0800 Subject: [PATCH 56/71] Move comment to the right place. --- config/site_settings.yml | 22 +++++++++++++--------- lib/site_setting_extension.rb | 3 --- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index 6e82e7d14a..01336b31c7 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1,14 +1,18 @@ # Available options: # -# default - The default value of the setting. -# client - Set to true if the javascript should have access to this setting's value. -# refresh - Set to true if clients should refresh when the setting is changed. -# min - For a string setting, the minimum length. For an integer setting, the minimum value. -# max - For a string setting, the maximum length. For an integer setting, the maximum value. -# regex - A regex that the value must match. -# validator - The name of the class that will be use to validate the value of the setting. -# enum - The setting has a fixed set of allowed values, and only one can be chosen. -# Set to the class name that defines the set. +# default - The default value of the setting. +# client - Set to true if the javascript should have access to this setting's value. +# refresh - Set to true if clients should refresh when the setting is changed. +# min - For a string setting, the minimum length. For an integer setting, the minimum value. +# max - For a string setting, the maximum length. For an integer setting, the maximum value. +# regex - A regex that the value must match. +# validator - The name of the class that will be use to validate the value of the setting. +# enum - The setting has a fixed set of allowed values, and only one can be chosen. +# Set to the class name that defines the set. +# shadowed_by_global - "Shadow" a site setting with a GlobalSetting. If the GlobalSetting +# exists it will be used instead of the setting and the setting will be hidden. +# Useful for things like API keys on multisite. +# # type: email - Must be a valid email address. # type: username - Must match the username of an existing user. # type: list - A list of values, chosen from a set of valid values defined in the choices option. diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 35ba859329..e6c501d9c7 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -117,9 +117,6 @@ module SiteSettingExtension hidden_settings << name end - # You can "shadow" a site setting with a GlobalSetting. If the GlobalSetting - # exists it will be used instead of the setting and the setting will be hidden. - # Useful for things like API keys on multisite. if opts[:shadowed_by_global] && GlobalSetting.respond_to?(name) val = GlobalSetting.send(name) unless val.nil? || (val == ''.freeze) From 136b1b504d0dbcbf1d6908e17a0f45f78783158e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 29 Jun 2016 15:23:50 +0800 Subject: [PATCH 57/71] Allow `force_https` to be shadowed by a global setting. --- config/site_settings.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index 01336b31c7..150b60fb13 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -765,7 +765,9 @@ trust: client: true security: - force_https: false + force_https: + default: false + shadowed_by_global: true enable_escaped_fragments: true allow_index_in_robots_txt: true enable_noscript_support: true From e4074f75b17a98f533b410e9c10fcc69d03fe29d Mon Sep 17 00:00:00 2001 From: Mark Wingerd Date: Wed, 29 Jun 2016 07:41:54 -0700 Subject: [PATCH 58/71] Stop URLs from being censored (#4288) URLs that contained a censored word were being altered by censored-words.js and ulimately this broke the links. As an example www.expertsexchange.com would get censored when it would link to a legitimate website. This URL blocking functionality should be handled through other settings. --- app/assets/javascripts/discourse/lib/censored-words.js | 4 ++-- test/javascripts/lib/markdown-test.js.es6 | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/censored-words.js b/app/assets/javascripts/discourse/lib/censored-words.js index 246bc2a0b0..482b193483 100644 --- a/app/assets/javascripts/discourse/lib/censored-words.js +++ b/app/assets/javascripts/discourse/lib/censored-words.js @@ -7,14 +7,14 @@ Discourse.CensoredWords = { if (!censorRegexp) { var split = censored.split("|"); if (split && split.length) { - censorRegexp = new RegExp("\\b(?:" + split.map(function (t) { return "(" + t.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ")"; }).join("|") + ")\\b", "ig"); + censorRegexp = new RegExp("(\\b(?:" + split.map(function (t) { return "(" + t.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ")"; }).join("|") + ")\\b)(?![^\\(]*\\))", "ig"); } } if (censorRegexp) { var m = censorRegexp.exec(text); while (m && m[0]) { var replacement = new Array(m[0].length+1).join('■'); - text = text.replace(new RegExp("\\b" + m[0] + "\\b", "ig"), replacement); + text = text.replace(new RegExp("(\\b" + m[0] + "\\b)(?![^\\(]*\\))", "ig"), replacement); m = censorRegexp.exec(text); } diff --git a/test/javascripts/lib/markdown-test.js.es6 b/test/javascripts/lib/markdown-test.js.es6 index 6e8f6e86a0..0fbae94311 100644 --- a/test/javascripts/lib/markdown-test.js.es6 +++ b/test/javascripts/lib/markdown-test.js.es6 @@ -569,6 +569,9 @@ test("censoring", function() { cooked("you are a whizzer! I love cheesewhiz. Whiz.", "

    you are a ■■■■■■■! I love cheesewhiz. ■■■■.

    ", "it censors words even if previous partial matches exist."); + cooked("The link still works. [whiz](http://www.whiz.com)", + "

    The link still works. ■■■■

    ", + "it won't break links by censoring them."); }); test("code blocks/spans hoisting", function() { From deda9a69088e7597b1a8455b1624eb82a72d4878 Mon Sep 17 00:00:00 2001 From: acshi Date: Wed, 29 Jun 2016 10:59:48 -0400 Subject: [PATCH 59/71] Prevent creation of empty entry in _connectorCache for raw templates. (#4296) --- app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 index b15685f0e6..cd7363d9fb 100644 --- a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 +++ b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 @@ -86,13 +86,12 @@ function buildConnectorCache() { }); findOutlets(Ember.TEMPLATES, function(outletName, resource, uniqueName) { - _connectorCache[outletName] = _connectorCache[outletName] || []; - const mixin = {templateName: resource.replace('javascripts/', '')}; let viewClass = uniqueViews[uniqueName]; if (viewClass) { // We are going to add it back with the proper template + _connectorCache[outletName] = _connectorCache[outletName] || []; _connectorCache[outletName].removeObject(viewClass); } else { if (!/\.raw$/.test(uniqueName)) { @@ -101,6 +100,7 @@ function buildConnectorCache() { } if (viewClass) { + _connectorCache[outletName] = _connectorCache[outletName] || []; _connectorCache[outletName].pushObject(viewClass.extend(mixin)); } else { // we have a raw template From ad16329b5cefae7da2967bfad21f55aa236457db Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 29 Jun 2016 21:31:50 +0530 Subject: [PATCH 60/71] Update onebox gem --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index bf82d95b14..aad2028d0a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -218,7 +218,7 @@ GEM omniauth-twitter (1.2.1) json (~> 1.3) omniauth-oauth (~> 1.1) - onebox (1.5.41) + onebox (1.5.42) htmlentities (~> 4.3.4) moneta (~> 0.8) multi_json (~> 1.11) From 0eaf76fc8855aa3e3778174f963fbba4561cc84d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 29 Jun 2016 14:24:13 -0400 Subject: [PATCH 61/71] FIX: add missing outlet on topic list page when filtered by tag --- app/assets/javascripts/discourse/templates/tags/show.hbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/discourse/templates/tags/show.hbs b/app/assets/javascripts/discourse/templates/tags/show.hbs index dcecef2c9c..db3ecce1e4 100644 --- a/app/assets/javascripts/discourse/templates/tags/show.hbs +++ b/app/assets/javascripts/discourse/templates/tags/show.hbs @@ -36,6 +36,8 @@
+{{plugin-outlet "discovery-list-container-top"}} +