diff --git a/Gemfile b/Gemfile
index 088e25eea2..1a2c2f78f4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.2'
gem 'dotenv-rails', '~> 2.7'
-gem 'aws-sdk-s3', '~> 1.33', require: false
+gem 'aws-sdk-s3', '~> 1.34', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 572a454ea4..fc9e9a6dca 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -76,17 +76,17 @@ GEM
av (0.9.0)
cocaine (~> 0.5.3)
aws-eventstream (1.0.2)
- aws-partitions (1.144.0)
- aws-sdk-core (3.48.0)
+ aws-partitions (1.145.0)
+ aws-sdk-core (3.48.2)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.15.0)
- aws-sdk-core (~> 3, >= 3.48.0)
+ aws-sdk-kms (1.16.0)
+ aws-sdk-core (~> 3, >= 3.48.2)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.33.0)
- aws-sdk-core (~> 3, >= 3.48.0)
+ aws-sdk-s3 (1.34.0)
+ aws-sdk-core (~> 3, >= 3.48.2)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sigv4 (1.1.0)
@@ -336,7 +336,7 @@ GEM
mimemagic (~> 0.3.2)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
- memory_profiler (0.9.12)
+ memory_profiler (0.9.13)
method_source (0.9.2)
microformats (4.1.0)
json (~> 2.1)
@@ -660,7 +660,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.6)
addressable (~> 2.6)
annotate (~> 2.7)
- aws-sdk-s3 (~> 1.33)
+ aws-sdk-s3 (~> 1.34)
better_errors (~> 2.5)
binding_of_caller (~> 0.7)
bootsnap (~> 1.4)
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index a64e98868d..dc1c79b7fd 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -2,94 +2,29 @@
module Admin
class SettingsController < BaseController
- ADMIN_SETTINGS = %w(
- site_contact_username
- site_contact_email
- site_title
- site_short_description
- site_description
- site_extended_description
- site_terms
- registrations_mode
- closed_registrations_message
- open_deletion
- timeline_preview
- show_staff_badge
- bootstrap_timeline_accounts
- flavour
- skin
- flavour_and_skin
- thumbnail
- hero
- mascot
- min_invite_role
- activity_api_enabled
- peers_api_enabled
- show_known_fediverse_at_about_page
- preview_sensitive_media
- custom_css
- profile_directory
- hide_followers_count
- ).freeze
-
- BOOLEAN_SETTINGS = %w(
- open_deletion
- timeline_preview
- show_staff_badge
- activity_api_enabled
- peers_api_enabled
- show_known_fediverse_at_about_page
- preview_sensitive_media
- profile_directory
- hide_followers_count
- ).freeze
-
- UPLOAD_SETTINGS = %w(
- thumbnail
- hero
- mascot
- ).freeze
-
def edit
authorize :settings, :show?
+
@admin_settings = Form::AdminSettings.new
end
def update
authorize :settings, :update?
- settings = settings_params
- flavours_and_skin = settings.delete('flavour_and_skin')
- if flavours_and_skin
- settings['flavour'], settings['skin'] = flavours_and_skin.split('/', 2)
- end
+ @admin_settings = Form::AdminSettings.new(settings_params)
- settings.each do |key, value|
- if UPLOAD_SETTINGS.include?(key)
- upload = SiteUpload.where(var: key).first_or_initialize(var: key)
- upload.update(file: value)
- else
- setting = Setting.where(var: key).first_or_initialize(var: key)
- setting.update(value: value_for_update(key, value))
- end
+ if @admin_settings.save
+ flash[:notice] = I18n.t('generic.changes_saved_msg')
+ redirect_to edit_admin_settings_path
+ else
+ render :edit
end
-
- flash[:notice] = I18n.t('generic.changes_saved_msg')
- redirect_to edit_admin_settings_path
end
private
def settings_params
- params.require(:form_admin_settings).permit(ADMIN_SETTINGS)
- end
-
- def value_for_update(key, value)
- if BOOLEAN_SETTINGS.include?(key)
- value == '1'
- else
- value
- end
+ params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
end
end
end
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 9ef45e425a..3ea1047865 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -475,6 +475,42 @@ code {
}
}
}
+
+ &__overlay-area {
+ position: relative;
+
+ &__overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba($ui-base-color, 0.65);
+ backdrop-filter: blur(2px);
+ border-radius: 4px;
+
+ &__content {
+ text-align: center;
+
+ &.rich-formatting {
+ &,
+ p {
+ color: $primary-text-color;
+ }
+ }
+ }
+ }
+ }
+}
+
+.block-icon {
+ display: block;
+ margin: 0 auto;
+ margin-bottom: 10px;
+ font-size: 24px;
}
.flash-message {
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index 929c65793e..0fcbd06054 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -3,57 +3,108 @@
class Form::AdminSettings
include ActiveModel::Model
- delegate(
- :site_contact_username,
- :site_contact_username=,
- :site_contact_email,
- :site_contact_email=,
- :site_title,
- :site_title=,
- :site_short_description,
- :site_short_description=,
- :site_description,
- :site_description=,
- :site_extended_description,
- :site_extended_description=,
- :site_terms,
- :site_terms=,
- :registrations_mode,
- :registrations_mode=,
- :closed_registrations_message,
- :closed_registrations_message=,
- :open_deletion,
- :open_deletion=,
- :timeline_preview,
- :timeline_preview=,
- :show_staff_badge,
- :show_staff_badge=,
- :bootstrap_timeline_accounts,
- :bootstrap_timeline_accounts=,
- :hide_followers_count,
- :hide_followers_count=,
- :flavour,
- :flavour=,
- :skin,
- :skin=,
- :min_invite_role,
- :min_invite_role=,
- :activity_api_enabled,
- :activity_api_enabled=,
- :peers_api_enabled,
- :peers_api_enabled=,
- :show_known_fediverse_at_about_page,
- :show_known_fediverse_at_about_page=,
- :preview_sensitive_media,
- :preview_sensitive_media=,
- :custom_css,
- :custom_css=,
- :profile_directory,
- :profile_directory=,
- to: Setting
- )
+ KEYS = %i(
+ site_contact_username
+ site_contact_email
+ site_title
+ site_short_description
+ site_description
+ site_extended_description
+ site_terms
+ registrations_mode
+ closed_registrations_message
+ open_deletion
+ timeline_preview
+ show_staff_badge
+ bootstrap_timeline_accounts
+ flavour
+ skin
+ min_invite_role
+ activity_api_enabled
+ peers_api_enabled
+ show_known_fediverse_at_about_page
+ preview_sensitive_media
+ custom_css
+ profile_directory
+ hide_followers_count
+ flavour_and_skin
+ ).freeze
+
+ BOOLEAN_KEYS = %i(
+ open_deletion
+ timeline_preview
+ show_staff_badge
+ activity_api_enabled
+ peers_api_enabled
+ show_known_fediverse_at_about_page
+ preview_sensitive_media
+ profile_directory
+ hide_followers_count
+ ).freeze
+
+ UPLOAD_KEYS = %i(
+ thumbnail
+ hero
+ mascot
+ ).freeze
+
+ PSEUDO_KEYS = %i(
+ flavour_and_skin
+ ).freeze
+
+ attr_accessor(*KEYS)
+
+ validates :site_short_description, :site_description, :site_extended_description, :site_terms, :closed_registrations_message, html: true
+ validates :registrations_mode, inclusion: { in: %w(open approved none) }
+ validates :min_invite_role, inclusion: { in: %w(disabled user moderator admin) }
+ validates :site_contact_email, :site_contact_username, presence: true
+ validates :site_contact_username, existing_username: true
+ validates :bootstrap_timeline_accounts, existing_username: { multiple: true }
+
+ def initialize(_attributes = {})
+ super
+ initialize_attributes
+ end
+
+ def save
+ return false unless valid?
+
+ KEYS.each do |key|
+ next if PSEUDO_KEYS.include?(key)
+ value = instance_variable_get("@#{key}")
+
+ if UPLOAD_KEYS.include?(key)
+ upload = SiteUpload.where(var: key).first_or_initialize(var: key)
+ upload.update(file: value)
+ else
+ setting = Setting.where(var: key).first_or_initialize(var: key)
+ setting.update(value: typecast_value(key, value))
+ end
+ end
+ end
def flavour_and_skin
"#{Setting.flavour}/#{Setting.skin}"
end
+
+ def flavour_and_skin=(value)
+ @flavour, @skin = value.split('/', 2)
+ end
+
+ private
+
+ def initialize_attributes
+ KEYS.each do |key|
+ next if PSEUDO_KEYS.include?(key)
+ instance_variable_set("@#{key}", Setting.public_send(key)) if instance_variable_get("@#{key}").nil?
+ end
+ end
+
+ def typecast_value(key, value)
+ if BOOLEAN_KEYS.include?(key)
+ value == '1'
+ else
+ value
+ end
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 47657a6702..66c1543ffb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -124,7 +124,8 @@ class User < ApplicationRecord
end
def confirm
- new_user = !confirmed?
+ new_user = !confirmed?
+ self.approved = true if open_registrations?
super
@@ -136,7 +137,8 @@ class User < ApplicationRecord
end
def confirm!
- new_user = !confirmed?
+ new_user = !confirmed?
+ self.approved = true if open_registrations?
skip_confirmation!
save!
@@ -264,7 +266,11 @@ class User < ApplicationRecord
private
def set_approved
- self.approved = Setting.registrations_mode == 'open' || invited?
+ self.approved = open_registrations? || invited?
+ end
+
+ def open_registrations?
+ Setting.registrations_mode == 'open'
end
def sanitize_languages
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
index 94a2c1692b..d234516e0c 100644
--- a/app/presenters/instance_presenter.rb
+++ b/app/presenters/instance_presenter.rb
@@ -8,6 +8,7 @@ class InstancePresenter
:site_description,
:site_extended_description,
:site_terms,
+ :closed_registrations_message,
to: Setting
)
diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb
index 0cace6c005..81af9ef3a4 100644
--- a/app/services/vote_service.rb
+++ b/app/services/vote_service.rb
@@ -11,14 +11,14 @@ class VoteService < BaseService
@choices = choices
@votes = []
- return if @poll.expired?
-
ApplicationRecord.transaction do
@choices.each do |choice|
@votes << @poll.votes.create!(account: @account, choice: choice)
end
end
+ ActivityTracker.increment('activity:interactions')
+
if @poll.account.local?
distribute_poll!
else
diff --git a/app/validators/existing_username_validator.rb b/app/validators/existing_username_validator.rb
new file mode 100644
index 0000000000..4388a0c983
--- /dev/null
+++ b/app/validators/existing_username_validator.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class ExistingUsernameValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if value.blank?
+
+ if options[:multiple]
+ missing_usernames = value.split(',').map { |username| username unless Account.find_local(username) }.compact
+ record.errors.add(attribute, I18n.t('existing_username_validator.not_found_multiple', usernames: missing_usernames.join(', '))) if missing_usernames.any?
+ else
+ record.errors.add(attribute, I18n.t('existing_username_validator.not_found')) unless Account.find_local(value)
+ end
+ end
+
+ private
+
+ def valid_html?(str)
+ Nokogiri::HTML.fragment(str).to_s == str
+ end
+end
diff --git a/app/validators/html_validator.rb b/app/validators/html_validator.rb
new file mode 100644
index 0000000000..882c35d413
--- /dev/null
+++ b/app/validators/html_validator.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class HtmlValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if value.blank?
+ record.errors.add(attribute, I18n.t('html_validator.invalid_markup')) unless valid_html?(value)
+ end
+
+ private
+
+ def valid_html?(str)
+ Nokogiri::HTML.fragment(str).to_s == str
+ end
+end
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index 9cb4eb2bc3..4823ff375d 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -1,16 +1,23 @@
= simple_form_for(new_user, url: user_registration_path) do |f|
- %p.lead= t('about.federation_hint_html', instance: content_tag(:strong, site_hostname))
+ .simple_form__overlay-area
+ %p.lead= t('about.federation_hint_html', instance: content_tag(:strong, site_hostname))
- .fields-group
- = f.simple_fields_for :account do |account_fields|
- = account_fields.input :username, wrapper: :with_label, autofocus: true, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "@#{site_hostname}", hint: false, disabled: closed_registrations?
+ .fields-group
+ = f.simple_fields_for :account do |account_fields|
+ = account_fields.input :username, wrapper: :with_label, autofocus: true, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "@#{site_hostname}", hint: false, disabled: closed_registrations?
- = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
- = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
- = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
+ = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
+ = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
+ = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
- .fields-group
- = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), disabled: closed_registrations?
+ .fields-group
+ = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), disabled: closed_registrations?
- .actions
- = f.button :button, sign_up_message, type: :submit, class: 'button button-primary', disabled: closed_registrations?
+ .actions
+ = f.button :button, sign_up_message, type: :submit, class: 'button button-primary', disabled: closed_registrations?
+
+ - if closed_registrations? && @instance_presenter.closed_registrations_message.present?
+ .simple_form__overlay-area__overlay
+ .simple_form__overlay-area__overlay__content.rich-formatting
+ .block-icon= fa_icon 'warning'
+ = @instance_presenter.closed_registrations_message.html_safe
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 9995e0b2ad..475fb3a2f4 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -2,6 +2,7 @@
= t('admin.settings.title')
= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch } do |f|
+ = render 'shared/error_messages', object: @admin_settings
.fields-group
= f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title')
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 7e1d92884f..f88706434a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -431,13 +431,13 @@ en:
desc_html: Show a staff badge on a user page
title: Show staff badge
site_description:
- desc_html: Introductory paragraph on the frontpage. Describe what makes this Mastodon server special and anything else important. You can use HTML tags, in particular <a>
and <em>
.
+ desc_html: Introductory paragraph on the API. Describe what makes this Mastodon server special and anything else important. You can use HTML tags, in particular <a>
and <em>
.
title: Server description
site_description_extended:
desc_html: A good place for your code of conduct, rules, guidelines and other things that set your server apart. You can use HTML tags
title: Custom extended information
site_short_description:
- desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph. If empty, defaults to server description.
+ desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph.
title: Short server description
site_terms:
desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags
@@ -589,6 +589,9 @@ en:
content: We're sorry, but something went wrong on our end.
title: This page is not correct
noscript_html: To use the Mastodon web application, please enable JavaScript. Alternatively, try one of the native apps for Mastodon for your platform.
+ existing_username_validator:
+ not_found: could not find a local user with that username
+ not_found_multiple: could not find %{usernames}
exports:
archive_takeout:
date: Date
@@ -637,6 +640,8 @@ en:
validation_errors:
one: Something isn't quite right yet! Please review the error below
other: Something isn't quite right yet! Please review %{count} errors below
+ html_validator:
+ invalid_markup: contains invalid HTML markup
identity_proofs:
active: Active
authorize: Yes, authorize
diff --git a/config/navigation.rb b/config/navigation.rb
index 86c2572d79..34b5661889 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -14,7 +14,7 @@ SimpleNavigation::Configuration.run do |navigation|
settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
- settings.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*}
+ settings.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*}, if: proc { current_account.identity_proofs.exists? }
end
primary.item :flavours, safe_join([fa_icon('paint-brush fw'), t('settings.flavours')]), settings_flavours_url do |flavours|
@@ -43,7 +43,7 @@ SimpleNavigation::Configuration.run do |navigation|
primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_dashboard_url, if: proc { current_user.staff? } do |admin|
admin.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_url
- admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }
+ admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings}
admin.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis}
admin.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/relays}
admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb
index 34f6bbdae0..6cf0ee20a6 100644
--- a/spec/controllers/admin/settings_controller_spec.rb
+++ b/spec/controllers/admin/settings_controller_spec.rb
@@ -19,6 +19,10 @@ RSpec.describe Admin::SettingsController, type: :controller do
end
describe 'PUT #update' do
+ before do
+ allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true)
+ end
+
describe 'for a record that doesnt exist' do
around do |example|
before = Setting.site_extended_description