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