{{model.description}}
+ {{plugin-outlet name="about-after-description" + connectorTagName='section' + tagName='' + args=(hash model=model)}} + {{#if model.admins}}Your post was flagged by the community. Please see your messages.
' user_must_edit: "This post was flagged by the community and is temporarily hidden.
" + ignored: + hidden_content: 'Hidden content
' + archetypes: regular: title: "Regular Topic" @@ -1896,6 +1901,8 @@ en: max_allowed_message_recipients: "Maximum recipients allowed in a message." watched_words_regular_expressions: "Watched words are regular expressions." + returning_users_days: "How many days should pass before a user is considered to be returning." + 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_personal_messages: "Send an email when someone messages the user by default." diff --git a/config/site_settings.yml b/config/site_settings.yml index 2acedd3ec46..8bd22447f95 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -807,6 +807,8 @@ posting: default: false client: true shadowed_by_global: true + returning_users_days: + default: 60 email: email_time_window_mins: @@ -1251,7 +1253,7 @@ security: list_type: compact blacklisted_crawler_user_agents: type: list - default: "mauibot|semrushbot|ahrefsbot" + default: "mauibot|semrushbot|ahrefsbot|blexbot" list_type: compact slow_down_crawler_user_agents: type: list diff --git a/db/migrate/20190306154335_migrate_google_user_info.rb b/db/migrate/20190306154335_migrate_google_user_info.rb new file mode 100644 index 00000000000..a3a9c0e1c37 --- /dev/null +++ b/db/migrate/20190306154335_migrate_google_user_info.rb @@ -0,0 +1,27 @@ +class MigrateGoogleUserInfo < ActiveRecord::Migration[5.2] + def up + execute <<~SQL + INSERT INTO user_associated_accounts ( + provider_name, + provider_uid, + user_id, + info, + last_used, + created_at, + updated_at + ) SELECT + 'google_oauth2', + google_user_id, + user_id, + json_build_object('email', email, 'first_name', first_name, 'last_name', last_name, 'name', name), + updated_at, + created_at, + updated_at + FROM google_user_infos + SQL + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index 297fa50976b..118d0812af5 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -1,5 +1,4 @@ -class Auth::GoogleOAuth2Authenticator < Auth::Authenticator - +class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator def name "google_oauth2" end @@ -8,77 +7,10 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator SiteSetting.enable_google_oauth2_logins end - def description_for_user(user) - info = GoogleUserInfo.find_by(user_id: user.id) - info&.email || info&.name || "" - end - - def can_revoke? - true - end - - def revoke(user, skip_remote: false) - info = GoogleUserInfo.find_by(user_id: user.id) - raise Discourse::NotFound if info.nil? - - # We get a temporary token from google upon login but do not need it, and do not store it. - # Therefore we do not have any way to revoke the token automatically on google's end - - info.destroy! - true - end - - def can_connect_existing_user? - true - end - - def after_authenticate(auth_hash, existing_account: nil) - session_info = parse_hash(auth_hash) - google_hash = session_info[:google] - - result = ::Auth::Result.new - result.email = session_info[:email] - result.email_valid = session_info[:email_valid] - result.name = session_info[:name] - - result.extra_data = google_hash - - user_info = ::GoogleUserInfo.find_by(google_user_id: google_hash[:google_user_id]) - - if existing_account && (user_info.nil? || existing_account.id != user_info.user_id) - user_info.destroy! if user_info - result.user = existing_account - user_info = GoogleUserInfo.create!({ user_id: result.user.id }.merge(google_hash)) - else - result.user = user_info&.user - end - - if !result.user && !result.email.blank? && result.email_valid - result.user = User.find_by_email(result.email) - if result.user - # we've matched an existing user to this login attempt... - if result.user.google_user_info && result.user.google_user_info.google_user_id != google_hash[:google_user_id] - # but the user has changed the google account used to log in... - if result.user.google_user_info.email != google_hash[:email] - # the user changed their email, go ahead and scrub the old record - result.user.google_user_info.destroy! - else - # same email address but different account? likely a takeover scenario - result.failed = true - result.failed_reason = I18n.t('errors.conflicting_google_user_id') - return result - end - end - ::GoogleUserInfo.create({ user_id: result.user.id }.merge(google_hash)) - end - end - - result - end - - def after_create_account(user, auth) - data = auth[:extra_data] - GoogleUserInfo.create({ user_id: user.id }.merge(data)) + def primary_email_verified?(auth_token) + # note, emails that come back from google via omniauth are always valid + # this protects against future regressions + auth_token[:extra][:raw_info][:email_verified] end def register_middleware(omniauth) @@ -95,37 +27,8 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present? strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ") end - }, - skip_jwt: true + } } - # jwt encoding is causing auth to fail in quite a few conditions - # skipping omniauth.provider :google_oauth2, options end - - protected - - def parse_hash(hash) - extra = hash[:extra][:raw_info] - - h = {} - - h[:email] = hash[:info][:email] - h[:name] = hash[:info][:name] - h[:email_valid] = extra[:email_verified] - - h[:google] = { - google_user_id: hash[:uid] || extra[:sub], - email: extra[:email], - first_name: extra[:given_name], - last_name: extra[:family_name], - gender: extra[:gender], - name: extra[:name], - link: extra[:hd], - profile_link: extra[:profile], - picture: extra[:picture] - } - - h - end end diff --git a/lib/auth/managed_authenticator.rb b/lib/auth/managed_authenticator.rb index a56989c587d..1d21453a371 100644 --- a/lib/auth/managed_authenticator.rb +++ b/lib/auth/managed_authenticator.rb @@ -10,6 +10,12 @@ class Auth::ManagedAuthenticator < Auth::Authenticator true end + def primary_email_verified?(auth_token) + # Omniauth providers should only provide verified emails in the :info hash. + # This method allows additional checks to be added + true + end + def can_revoke? true end @@ -35,7 +41,11 @@ class Auth::ManagedAuthenticator < Auth::Authenticator end # Matching an account by email - if match_by_email && association.user.nil? && (user = User.find_by_email(auth_token.dig(:info, :email))) + if primary_email_verified?(auth_token) && + match_by_email && + association.user.nil? && + (user = User.find_by_email(auth_token.dig(:info, :email))) + UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user association.user = user end @@ -60,7 +70,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result.email = info[:email] result.name = "#{info[:first_name]} #{info[:last_name]}" result.username = info[:nickname] - result.email_valid = true if result.email + result.email_valid = primary_email_verified?(auth_token) if result.email result.extra_data = { provider: auth_token[:provider], uid: auth_token[:uid] diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index f382df5ea36..57870cfab95 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -29,6 +29,7 @@ module BackupRestore @client_id = opts[:client_id] @filename = opts[:filename] @publish_to_message_bus = opts[:publish_to_message_bus] || false + @disable_emails = opts.fetch(:disable_emails, true) ensure_restore_is_enabled ensure_no_operation_is_running @@ -402,9 +403,11 @@ module BackupRestore log "Reloading site settings..." SiteSetting.refresh! - log "Disabling outgoing emails for non-staff users..." - user = User.find_by_email(@user_info[:email]) || Discourse.system_user - SiteSetting.set_and_log(:disable_emails, 'non-staff', user) + if @disable_emails + log "Disabling outgoing emails for non-staff users..." + user = User.find_by_email(@user_info[:email]) || Discourse.system_user + SiteSetting.set_and_log(:disable_emails, 'non-staff', user) + end end def clear_emoji_cache diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 736626194d7..c7f706d05c2 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -899,6 +899,22 @@ module Email create_post_with_attachments(options) end + def notification_level_for(body) + # since we are stripping save all this work on long replies + return nil if body.length > 40 + + body = body.strip.downcase + case body + when "mute" + NotificationLevels.topic_levels[:muted] + when "track" + NotificationLevels.topic_levels[:tracking] + when "watch" + NotificationLevels.topic_levels[:watching] + else nil + end + end + def create_reply(options = {}) raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed? raise BouncedEmailError if options[:bounce] && options[:topic].archetype != Archetype.private_message @@ -908,6 +924,8 @@ module Email if post_action_type = post_action_for(options[:raw]) create_post_action(options[:user], options[:post], post_action_type) + elsif notification_level = notification_level_for(options[:raw]) + TopicUser.change(options[:user].id, options[:post].topic_id, notification_level: notification_level) else raise TopicClosedError if options[:topic].closed? options[:topic_id] = options[:topic].id diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 21fba9804ef..377d11e5508 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -86,7 +86,7 @@ module JsLocaleHelper end def self.load_translations_merged(*locales) - locales = locales.compact + locales = locales.uniq.compact @loaded_merges ||= {} @loaded_merges[locales.join('-')] ||= begin all_translations = {} diff --git a/lib/post_creator.rb b/lib/post_creator.rb index d8bee4047ca..2dd11e649db 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -165,6 +165,7 @@ class PostCreator transaction do build_post_stats create_topic + create_post_notice save_post extract_links track_topic @@ -247,7 +248,11 @@ class PostCreator post.word_count = post.raw.scan(/[[:word:]]+/).size whisper = post.post_type == Post.types[:whisper] - post.post_number ||= Topic.next_post_number(post.topic_id, post.reply_to_post_number.present?, whisper) + increase_posts_count = !post.topic&.private_message? || post.post_type != Post.types[:small_action] + post.post_number ||= Topic.next_post_number(post.topic_id, + reply: post.reply_to_post_number.present?, + whisper: whisper, + post: increase_posts_count) cooking_options = post.cooking_options || {} cooking_options[:topic_id] = post.topic_id @@ -508,6 +513,21 @@ class PostCreator @user.update_attributes(last_posted_at: @post.created_at) end + def create_post_notice + last_post_time = Post.where(user_id: @user.id) + .order(created_at: :desc) + .limit(1) + .pluck(:created_at) + .first + + if !last_post_time + @post.custom_fields["post_notice_type"] = "first" + elsif SiteSetting.returning_users_days > 0 && last_post_time < SiteSetting.returning_users_days.days.ago + @post.custom_fields["post_notice_type"] = "returning" + @post.custom_fields["post_notice_time"] = last_post_time.iso8601 + end + end + def publish return if @opts[:import_mode] || @post.post_number == 1 @post.publish_change_to_clients! :created diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index cf36b72d6f9..16e91f4c586 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -61,19 +61,11 @@ class PostDestroyer mark_for_deletion(delete_removed_posts_after) end DiscourseEvent.trigger(:post_destroyed, @post, @opts, @user) - WebHook.enqueue_hooks(:post, :post_destroyed, - id: @post.id, - category_id: @post&.topic&.category_id, - payload: payload - ) if WebHook.active_web_hooks(:post).exists? + WebHook.enqueue_post_hooks(:post_destroyed, @post, payload) if @post.is_first_post? && @post.topic DiscourseEvent.trigger(:topic_destroyed, @post.topic, @user) - WebHook.enqueue_hooks(:topic, :topic_destroyed, - id: topic.id, - category_id: topic&.category_id, - payload: topic_payload - ) if WebHook.active_web_hooks(:topic).exists? + WebHook.enqueue_topic_hooks(:topic_destroyed, @post.topic, topic_payload) end end @@ -147,7 +139,7 @@ class PostDestroyer update_user_counts TopicUser.update_post_action_cache(post_id: @post.id) DB.after_commit do - if @opts[:defer_flags].to_s == "true" + if @opts[:defer_flags] defer_flags else agree_with_flags diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index e3a89fe4611..d4d8ce56f87 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -230,10 +230,11 @@ module PrettyText return title unless SiteSetting.enable_emoji? set = SiteSetting.emoji_set.inspect + custom = Emoji.custom.map { |e| [e.name, e.url] }.to_h.to_json protect do v8.eval(<<~JS) __paths = #{paths_json}; - __performEmojiUnescape(#{title.inspect}, { getURL: __getURL, emojiSet: #{set} }); + __performEmojiUnescape(#{title.inspect}, { getURL: __getURL, emojiSet: #{set}, customEmoji: #{custom} }); JS end end diff --git a/lib/search.rb b/lib/search.rb index 56ebc31a431..3ffbda872fb 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -687,7 +687,7 @@ class Search def groups_search groups = Group .visible_groups(@guardian.user, "name ASC", include_everyone: false) - .where("groups.name ILIKE ?", "%#{@term}%") + .where("name ILIKE :term OR full_name ILIKE :term", term: "%#{@term}%") groups.each { |group| @results.add(group) } end diff --git a/lib/stylesheet/importer.rb b/lib/stylesheet/importer.rb index 49ff26d8e70..19d9676120f 100644 --- a/lib/stylesheet/importer.rb +++ b/lib/stylesheet/importer.rb @@ -144,9 +144,8 @@ COMMENT end def to_scss_variable(name, value) - escaped = value.to_s.gsub('"', "\\22") - escaped.gsub!("\n", "\\A") - "$#{name}: unquote(\"#{escaped}\");\n" + escaped = SassC::Script::Value::String.quote(value, sass: true) + "$#{name}: unquote(#{escaped});\n" end def imports(asset, parent_path) diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb index df3259fdbd1..f6946b2e325 100644 --- a/lib/svg_sprite/svg_sprite.rb +++ b/lib/svg_sprite/svg_sprite.rb @@ -118,6 +118,7 @@ module SvgSprite "globe", "globe-americas", "hand-point-right", + "hands-helping", "heading", "heart", "home", @@ -224,10 +225,10 @@ module SvgSprite icons = all_icons(theme_ids) doc = File.open("#{Rails.root}/vendor/assets/svg-icons/fontawesome/solid.svg") { |f| Nokogiri::XML(f) } - fa_license = doc.at('//comment()').text svg_subset = """