about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/concerns/account_interactions.rb10
-rw-r--r--app/models/custom_filter.rb28
-rw-r--r--app/models/custom_filter_status.rb37
-rw-r--r--app/models/email_domain_block.rb64
-rw-r--r--app/models/form/status_filter_batch_action.rb34
-rw-r--r--app/models/ip_block.rb1
-rw-r--r--app/models/user.rb2
7 files changed, 144 insertions, 32 deletions
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index a7401362f..9b358d338 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -249,15 +249,7 @@ module AccountInteractions
 
   def status_matches_filters(status)
     active_filters = CustomFilter.cached_filters_for(id)
-
-    filter_matches = active_filters.filter_map do |filter, rules|
-      next if rules[:keywords].blank?
-
-      match = rules[:keywords].match(status.proper.searchable_text)
-      FilterResultPresenter.new(filter: filter, keyword_matches: [match.to_s]) unless match.nil?
-    end
-
-    filter_matches
+    CustomFilter.apply_cached_filters(active_filters, status)
   end
 
   def followers_for_local_distribution
diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb
index 985eab125..da2a91493 100644
--- a/app/models/custom_filter.rb
+++ b/app/models/custom_filter.rb
@@ -34,6 +34,7 @@ class CustomFilter < ApplicationRecord
 
   belongs_to :account
   has_many :keywords, class_name: 'CustomFilterKeyword', foreign_key: :custom_filter_id, inverse_of: :custom_filter, dependent: :destroy
+  has_many :statuses, class_name: 'CustomFilterStatus', foreign_key: :custom_filter_id, inverse_of: :custom_filter, dependent: :destroy
   accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
 
   validates :title, :context, presence: true
@@ -62,8 +63,10 @@ class CustomFilter < ApplicationRecord
 
   def self.cached_filters_for(account_id)
     active_filters = Rails.cache.fetch("filters:v3:#{account_id}") do
+      filters_hash = {}
+
       scope = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()'))
-      scope.to_a.group_by(&:custom_filter).map do |filter, keywords|
+      scope.to_a.group_by(&:custom_filter).each do |filter, keywords|
         keywords.map! do |keyword|
           if keyword.whole_word
             sb = /\A[[:word:]]/.match?(keyword.keyword) ? '\b' : ''
@@ -74,13 +77,34 @@ class CustomFilter < ApplicationRecord
             /#{Regexp.escape(keyword.keyword)}/i
           end
         end
-        [filter, { keywords: Regexp.union(keywords) }]
+
+        filters_hash[filter.id] = { keywords: Regexp.union(keywords), filter: filter }
+      end.to_h
+
+      scope = CustomFilterStatus.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()'))
+      scope.to_a.group_by(&:custom_filter).each do |filter, statuses|
+        filters_hash[filter.id] ||= { filter: filter }
+        filters_hash[filter.id].merge!(status_ids: statuses.map(&:status_id))
       end
+
+      filters_hash.values.map { |cache| [cache.delete(:filter), cache] }
     end.to_a
 
     active_filters.select { |custom_filter, _| !custom_filter.expired? }
   end
 
+  def self.apply_cached_filters(cached_filters, status)
+    cached_filters.filter_map do |filter, rules|
+      match = rules[:keywords].match(status.proper.searchable_text) if rules[:keywords].present?
+      keyword_matches = [match.to_s] unless match.nil?
+
+      status_matches = [status.id, status.reblog_of_id].compact & rules[:status_ids] if rules[:status_ids].present?
+
+      next if keyword_matches.blank? && status_matches.blank?
+      FilterResultPresenter.new(filter: filter, keyword_matches: keyword_matches, status_matches: status_matches)
+    end
+  end
+
   def prepare_cache_invalidation!
     @should_invalidate_cache = true
   end
diff --git a/app/models/custom_filter_status.rb b/app/models/custom_filter_status.rb
new file mode 100644
index 000000000..b6bea1394
--- /dev/null
+++ b/app/models/custom_filter_status.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: custom_filter_statuses
+#
+#  id               :bigint(8)        not null, primary key
+#  custom_filter_id :bigint(8)        not null
+#  status_id        :bigint(8)        default(""), not null
+#  created_at       :datetime         not null
+#  updated_at       :datetime         not null
+#
+
+class CustomFilterStatus < ApplicationRecord
+  belongs_to :custom_filter
+  belongs_to :status
+
+  validates :status, uniqueness: { scope: :custom_filter }
+  validate :validate_status_access
+
+  before_save :prepare_cache_invalidation!
+  before_destroy :prepare_cache_invalidation!
+  after_commit :invalidate_cache!
+
+  private
+
+  def validate_status_access
+    errors.add(:status_id, :invalid) unless StatusPolicy.new(custom_filter.account, status).show?
+  end
+
+  def prepare_cache_invalidation!
+    custom_filter.prepare_cache_invalidation!
+  end
+
+  def invalidate_cache!
+    custom_filter.invalidate_cache!
+  end
+end
diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb
index 0e1e663c1..f9d74332b 100644
--- a/app/models/email_domain_block.rb
+++ b/app/models/email_domain_block.rb
@@ -30,32 +30,56 @@ class EmailDomainBlock < ApplicationRecord
     @history ||= Trends::History.new('email_domain_blocks', id)
   end
 
-  def self.block?(domain_or_domains, attempt_ip: nil)
-    domains = Array(domain_or_domains).map do |str|
-      domain = begin
-        if str.include?('@')
-          str.split('@', 2).last
-        else
-          str
-        end
-      end
+  class Matcher
+    def initialize(domain_or_domains, attempt_ip: nil)
+      @uris       = extract_uris(domain_or_domains)
+      @attempt_ip = attempt_ip
+    end
 
-      TagManager.instance.normalize_domain(domain) if domain.present?
-    rescue Addressable::URI::InvalidURIError
-      nil
+    def match?
+      blocking? || invalid_uri?
     end
 
-    # If some of the inputs passed in are invalid, we definitely want to
-    # block the attempt, but we also want to register hits against any
-    # other valid matches
+    private
 
-    blocked = domains.any?(&:nil?)
+    def invalid_uri?
+      @uris.any?(&:nil?)
+    end
 
-    where(domain: domains).find_each do |block|
-      blocked = true
-      block.history.add(attempt_ip) if attempt_ip.present?
+    def blocking?
+      blocks = EmailDomainBlock.where(domain: domains_with_variants).order(Arel.sql('char_length(domain) desc'))
+      blocks.each { |block| block.history.add(@attempt_ip) } if @attempt_ip.present?
+      blocks.any?
     end
 
-    blocked
+    def domains_with_variants
+      @uris.flat_map do |uri|
+        next if uri.nil?
+
+        segments = uri.normalized_host.split('.')
+
+        segments.map.with_index { |_, i| segments[i..-1].join('.') }
+      end
+    end
+
+    def extract_uris(domain_or_domains)
+      Array(domain_or_domains).map do |str|
+        domain = begin
+          if str.include?('@')
+            str.split('@', 2).last
+          else
+            str
+          end
+        end
+
+        Addressable::URI.new.tap { |u| u.host = domain.strip } if domain.present?
+      rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
+        nil
+      end
+    end
+  end
+
+  def self.block?(domain_or_domains, attempt_ip: nil)
+    Matcher.new(domain_or_domains, attempt_ip: attempt_ip).match?
   end
 end
diff --git a/app/models/form/status_filter_batch_action.rb b/app/models/form/status_filter_batch_action.rb
new file mode 100644
index 000000000..d87bd5cc4
--- /dev/null
+++ b/app/models/form/status_filter_batch_action.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class Form::StatusFilterBatchAction
+  include ActiveModel::Model
+  include AccountableConcern
+  include Authorization
+
+  attr_accessor :current_account, :type,
+                :status_filter_ids, :filter_id
+
+  def save!
+    process_action!
+  end
+
+  private
+
+  def status_filters
+    filter = current_account.custom_filters.find(filter_id)
+    filter.statuses.where(id: status_filter_ids)
+  end
+
+  def process_action!
+    return if status_filter_ids.empty?
+
+    case type
+    when 'remove'
+      handle_remove!
+    end
+  end
+
+  def handle_remove!
+    status_filters.destroy_all
+  end
+end
diff --git a/app/models/ip_block.rb b/app/models/ip_block.rb
index aedd3ca0d..e1ab59806 100644
--- a/app/models/ip_block.rb
+++ b/app/models/ip_block.rb
@@ -19,6 +19,7 @@ class IpBlock < ApplicationRecord
 
   enum severity: {
     sign_up_requires_approval: 5000,
+    sign_up_block: 5500,
     no_access: 9999,
   }
 
diff --git a/app/models/user.rb b/app/models/user.rb
index ffad4ae5a..46f66526e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -94,7 +94,7 @@ class User < ApplicationRecord
   validates :invite_request, presence: true, on: :create, if: :invite_text_required?
 
   validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
-  validates_with BlacklistedEmailValidator, if: -> { !confirmed? }
+  validates_with BlacklistedEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
   validates_with EmailMxValidator, if: :validate_email_dns?
   validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create