diff --git a/app/controllers/admin/settings/others_controller.rb b/app/controllers/admin/settings/others_controller.rb new file mode 100644 index 000000000..113d0c84f --- /dev/null +++ b/app/controllers/admin/settings/others_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::OthersController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_others_path + end +end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 526aa0f97..7071394b7 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -14,7 +14,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity private def create_status - return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity? + return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity? || reject_pattern? with_redis_lock("create:#{object_uri}") do return if delete_arrived_first?(object_uri) || poll_vote? @@ -410,6 +410,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity Tombstone.exists?(uri: object_uri) end + def reject_pattern? + Setting.reject_pattern.present? && @object['content']&.match?(Setting.reject_pattern) + end + def forward_for_reply return unless @status.distributable? && @json['signature'].present? && reply_to_local? diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 515909d5c..b58e77bb3 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -39,6 +39,7 @@ class Form::AdminSettings authorized_fetch app_icon favicon + reject_pattern ).freeze INTEGER_KEYS = %i( @@ -85,6 +86,7 @@ class Form::AdminSettings validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) } + validates :reject_pattern, regexp_syntax: true, if: -> { defined?(@reject_pattern) } validates :status_page_url, url: true, allow_blank: true validate :validate_site_uploads diff --git a/app/validators/regexp_syntax_validator.rb b/app/validators/regexp_syntax_validator.rb new file mode 100644 index 000000000..dd8dc80c9 --- /dev/null +++ b/app/validators/regexp_syntax_validator.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class RegexpSyntaxValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if value.blank? + + Regexp.compile(value) + rescue RegexpError => e + record.errors.add(attribute, I18n.t('applications.invalid_regexp', message: e.message)) + end +end diff --git a/app/views/admin/settings/others/show.html.haml b/app/views/admin/settings/others/show.html.haml new file mode 100644 index 000000000..06dcf2bf8 --- /dev/null +++ b/app/views/admin/settings/others/show.html.haml @@ -0,0 +1,19 @@ +- content_for :page_title do + = t('admin.settings.others.title') + +- content_for :heading do + %h2= t('admin.settings.title') + = render partial: 'admin/settings/shared/links' + += simple_form_for @admin_settings, url: admin_settings_others_path, html: { method: :patch } do |f| + = render 'shared/error_messages', object: @admin_settings + + %p.lead= t('admin.settings.others.preamble') + + %h4= t('admin.settings.others.activitypub') + + .fields-group + = f.input :reject_pattern, wrapper: :with_block_label, as: :text, label: t('admin.settings.reject_pattern.title'), hint: t('admin.settings.reject_pattern.desc_html'), input_html: { rows: 8 } + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/settings/shared/_links.html.haml b/app/views/admin/settings/shared/_links.html.haml index c03e5cfd9..57d0589a6 100644 --- a/app/views/admin/settings/shared/_links.html.haml +++ b/app/views/admin/settings/shared/_links.html.haml @@ -7,3 +7,4 @@ primary.item :discovery, safe_join([material_symbol('search'), t('admin.settings.discovery.title')]), admin_settings_discovery_path primary.item :content_retention, safe_join([material_symbol('history'), t('admin.settings.content_retention.title')]), admin_settings_content_retention_path primary.item :appearance, safe_join([material_symbol('computer'), t('admin.settings.appearance.title')]), admin_settings_appearance_path + primary.item :others, safe_join([material_symbol('manufacturing'), t('admin.settings.others.title')]), admin_settings_others_path diff --git a/config/locales/en.yml b/config/locales/en.yml index 6d0c6d3e0..6e1ec2ff7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -781,6 +781,10 @@ en: all: To everyone disabled: To no one users: To logged-in local users + others: + activitypub: ActivityPub + preamble: Other settings, including customizing behavior + title: Other settings registrations: moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone! preamble: Control who can create an account on your server. @@ -791,6 +795,9 @@ en: none: Nobody can sign up open: Anyone can sign up warning_hint: We recommend using “Approval required for sign up” unless you are confident your moderation team can handle spam and malicious registrations in a timely fashion. + reject_pattern: + desc_html: Set a regular expression pattern to inspect Create Activity content, and refuse Activity if you match + title: Reject Pattern security: authorized_fetch: Require authentication from federated servers authorized_fetch_hint: Requiring authentication from federated servers enables stricter enforcement of both user-level and server-level blocks. However, this comes at the cost of a performance penalty, reduces the reach of your replies, and may introduce compatibility issues with some federated services. In addition, this will not prevent dedicated actors from fetching your public posts and accounts. @@ -1079,6 +1086,7 @@ en: applications: created: Application successfully created destroyed: Application successfully deleted + invalid_regexp: 'The provided Regexp is invalid: %{message}' logout: Logout regenerate_token: Regenerate access token token_regenerated: Access token successfully regenerated diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 3dba6fa5b..90a2ee1ba 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -50,6 +50,7 @@ namespace :admin do resource :about, only: [:show, :update], controller: 'about' resource :appearance, only: [:show, :update], controller: 'appearance' resource :discovery, only: [:show, :update], controller: 'discovery' + resource :others, only: [:show, :update], controller: 'others' end resources :site_uploads, only: [:destroy] diff --git a/config/settings.yml b/config/settings.yml index 297bf0281..599ba46a6 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -38,6 +38,7 @@ defaults: &defaults require_invite_text: false backups_retention_period: 7 captcha_enabled: false + reject_pattern: '' development: <<: *defaults