Change domain blocks to automatically support subdomains (#11138)

* Change domain blocks to automatically support subdomains

If a more authoritative domain is blocked (example.com), then the
same block will be applied to a subdomain (foo.example.com)

* Match subdomains of existing accounts when blocking/unblocking domains

* Improve code style
This commit is contained in:
Eugen Rochko 2019-06-22 00:13:10 +02:00 committed by GitHub
parent 49ebda4d49
commit 707ddf7808
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 89 additions and 25 deletions

View file

@ -13,7 +13,7 @@ module Admin
authorize :domain_block, :create? authorize :domain_block, :create?
@domain_block = DomainBlock.new(resource_params) @domain_block = DomainBlock.new(resource_params)
existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save @domain_block.save

View file

@ -18,7 +18,7 @@ module Admin
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
@domain_block = DomainBlock.find_by(domain: params[:id]) @domain_block = DomainBlock.rule_for(params[:id])
end end
private private

View file

@ -39,6 +39,6 @@ class MediaProxyController < ApplicationController
end end
def reject_media? def reject_media?
DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media? DomainBlock.reject_media?(@media_attachment.account.domain)
end end
end end

View file

@ -380,7 +380,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def skip_download? def skip_download?
return @skip_download if defined?(@skip_download) return @skip_download if defined?(@skip_download)
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? @skip_download ||= DomainBlock.reject_media?(@account.domain)
end end
def reply_to_local? def reply_to_local?

View file

@ -23,7 +23,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
private private
def skip_reports? def skip_reports?
DomainBlock.find_by(domain: @account.domain)&.reject_reports? DomainBlock.reject_reports?(@account.domain)
end end
def object_uris def object_uris

View file

@ -148,7 +148,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
end end
def save_media def save_media
do_not_download = DomainBlock.find_by(domain: @account.domain)&.reject_media? do_not_download = DomainBlock.reject_media?(@account.domain)
media_attachments = [] media_attachments = []
@xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link| @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
@ -176,7 +176,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
end end
def save_emojis(parent) def save_emojis(parent)
do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? do_not_download = DomainBlock.reject_media?(parent.account.domain)
return if do_not_download return if do_not_download

View file

@ -98,6 +98,7 @@ class Account < ApplicationRecord
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) } scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
scope :popular, -> { order('account_stats.followers_count desc') } scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
delegate :email, delegate :email,
:unconfirmed_email, :unconfirmed_email,

View file

@ -39,6 +39,7 @@ class CustomEmoji < ApplicationRecord
scope :local, -> { where(domain: nil) } scope :local, -> { where(domain: nil) }
scope :remote, -> { where.not(domain: nil) } scope :remote, -> { where.not(domain: nil) }
scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) } scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
remotable_attachment :image, LIMIT remotable_attachment :image, LIMIT

View file

@ -24,14 +24,41 @@ class DomainBlock < ApplicationRecord
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
def self.blocked?(domain) class << self
where(domain: domain, severity: :suspend).exists? def suspend?(domain)
!!rule_for(domain)&.suspend?
end
def silence?(domain)
!!rule_for(domain)&.silence?
end
def reject_media?(domain)
!!rule_for(domain)&.reject_media?
end
def reject_reports?(domain)
!!rule_for(domain)&.reject_reports?
end
alias blocked? suspend?
def rule_for(domain)
return if domain.blank?
uri = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') }
segments = uri.normalized_host.split('.')
variants = segments.map.with_index { |_, i| segments[i..-1].join('.') }
where(domain: variants[0..-2]).order(Arel.sql('char_length(domain) desc')).first
end
end end
def stricter_than?(other_block) def stricter_than?(other_block)
return true if suspend? return true if suspend?
return false if other_block.suspend? && (silence? || noop?) return false if other_block.suspend? && (silence? || noop?)
return false if other_block.silence? && noop? return false if other_block.silence? && noop?
(reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports) (reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports)
end end

View file

@ -8,7 +8,7 @@ class Instance
def initialize(resource) def initialize(resource)
@domain = resource.domain @domain = resource.domain
@accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count @accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count
@domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.find_by(domain: domain) @domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain)
end end
def cached_sample_accounts def cached_sample_accounts

View file

@ -205,7 +205,7 @@ class ActivityPub::ProcessAccountService < BaseService
def domain_block def domain_block
return @domain_block if defined?(@domain_block) return @domain_block if defined?(@domain_block)
@domain_block = DomainBlock.find_by(domain: @domain) @domain_block = DomainBlock.rule_for(@domain)
end end
def key_changed? def key_changed?

View file

@ -76,7 +76,7 @@ class BlockDomainService < BaseService
end end
def blocked_domain_accounts def blocked_domain_accounts
Account.where(domain: blocked_domain) Account.by_domain_and_subdomains(blocked_domain)
end end
def media_from_blocked_domain def media_from_blocked_domain
@ -84,6 +84,6 @@ class BlockDomainService < BaseService
end end
def emojis_from_blocked_domains def emojis_from_blocked_domains
CustomEmoji.where(domain: blocked_domain) CustomEmoji.by_domain_and_subdomains(blocked_domain)
end end
end end

View file

@ -146,7 +146,7 @@ class ResolveAccountService < BaseService
def domain_block def domain_block
return @domain_block if defined?(@domain_block) return @domain_block if defined?(@domain_block)
@domain_block = DomainBlock.find_by(domain: @domain) @domain_block = DomainBlock.rule_for(@domain)
end end
def atom_url def atom_url

View file

@ -14,7 +14,8 @@ class UnblockDomainService < BaseService
end end
def blocked_accounts def blocked_accounts
scope = Account.where(domain: domain_block.domain) scope = Account.by_domain_and_subdomains(domain_block.domain)
if domain_block.silence? if domain_block.silence?
scope.where(silenced_at: @domain_block.created_at) scope.where(silenced_at: @domain_block.created_at)
else else

View file

@ -26,7 +26,7 @@ class UpdateRemoteProfileService < BaseService
account.note = remote_profile.note || '' account.note = remote_profile.note || ''
account.locked = remote_profile.locked? account.locked = remote_profile.locked?
if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media? if !account.suspended? && !DomainBlock.reject_media?(account.domain)
if remote_profile.avatar.present? if remote_profile.avatar.present?
account.avatar_remote_url = remote_profile.avatar account.avatar_remote_url = remote_profile.avatar
else else
@ -46,7 +46,7 @@ class UpdateRemoteProfileService < BaseService
end end
def save_emojis def save_emojis
do_not_download = DomainBlock.find_by(domain: account.domain)&.reject_media? do_not_download = DomainBlock.reject_media?(account.domain)
return if do_not_download return if do_not_download

View file

@ -687,6 +687,23 @@ RSpec.describe Account, type: :model do
end end
end end
describe 'by_domain_and_subdomains' do
it 'returns exact domain matches' do
account = Fabricate(:account, domain: 'example.com')
expect(Account.by_domain_and_subdomains('example.com')).to eq [account]
end
it 'returns subdomains' do
account = Fabricate(:account, domain: 'foo.example.com')
expect(Account.by_domain_and_subdomains('example.com')).to eq [account]
end
it 'does not return partially matching domains' do
account = Fabricate(:account, domain: 'grexample.com')
expect(Account.by_domain_and_subdomains('example.com')).to_not eq [account]
end
end
describe 'expiring' do describe 'expiring' do
it 'returns remote accounts with followers whose subscription expiration date is past or not given' do it 'returns remote accounts with followers whose subscription expiration date is past or not given' do
local = Fabricate(:account, domain: nil) local = Fabricate(:account, domain: nil)

View file

@ -21,23 +21,40 @@ RSpec.describe DomainBlock, type: :model do
end end
end end
describe 'blocked?' do describe '.blocked?' do
it 'returns true if the domain is suspended' do it 'returns true if the domain is suspended' do
Fabricate(:domain_block, domain: 'domain', severity: :suspend) Fabricate(:domain_block, domain: 'example.com', severity: :suspend)
expect(DomainBlock.blocked?('domain')).to eq true expect(DomainBlock.blocked?('example.com')).to eq true
end end
it 'returns false even if the domain is silenced' do it 'returns false even if the domain is silenced' do
Fabricate(:domain_block, domain: 'domain', severity: :silence) Fabricate(:domain_block, domain: 'example.com', severity: :silence)
expect(DomainBlock.blocked?('domain')).to eq false expect(DomainBlock.blocked?('example.com')).to eq false
end end
it 'returns false if the domain is not suspended nor silenced' do it 'returns false if the domain is not suspended nor silenced' do
expect(DomainBlock.blocked?('domain')).to eq false expect(DomainBlock.blocked?('example.com')).to eq false
end end
end end
describe 'stricter_than?' do describe '.rule_for' do
it 'returns rule matching a blocked domain' do
block = Fabricate(:domain_block, domain: 'example.com')
expect(DomainBlock.rule_for('example.com')).to eq block
end
it 'returns a rule matching a subdomain of a blocked domain' do
block = Fabricate(:domain_block, domain: 'example.com')
expect(DomainBlock.rule_for('sub.example.com')).to eq block
end
it 'returns a rule matching a blocked subdomain' do
block = Fabricate(:domain_block, domain: 'sub.example.com')
expect(DomainBlock.rule_for('sub.example.com')).to eq block
end
end
describe '#stricter_than?' do
it 'returns true if the new block has suspend severity while the old has lower severity' do it 'returns true if the new block has suspend severity while the old has lower severity' do
suspend = DomainBlock.new(domain: 'domain', severity: :suspend) suspend = DomainBlock.new(domain: 'domain', severity: :suspend)
silence = DomainBlock.new(domain: 'domain', severity: :silence) silence = DomainBlock.new(domain: 'domain', severity: :silence)