Add validations to admin settings (#10348)
* Add validations to admin settings - Validate correct HTML markup - Validate presence of contact username & e-mail - Validate that all usernames are valid - Validate that enums have expected values * Fix code style issue * Fix tests
This commit is contained in:
		
							parent
							
								
									55a9658ad8
								
							
						
					
					
						commit
						555c4e11ba
					
				| 
						 | 
					@ -2,84 +2,29 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module Admin
 | 
					module Admin
 | 
				
			||||||
  class SettingsController < BaseController
 | 
					  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
 | 
					 | 
				
			||||||
      theme
 | 
					 | 
				
			||||||
      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
 | 
					 | 
				
			||||||
    ).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
 | 
					 | 
				
			||||||
    ).freeze
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    UPLOAD_SETTINGS = %w(
 | 
					 | 
				
			||||||
      thumbnail
 | 
					 | 
				
			||||||
      hero
 | 
					 | 
				
			||||||
      mascot
 | 
					 | 
				
			||||||
    ).freeze
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def edit
 | 
					    def edit
 | 
				
			||||||
      authorize :settings, :show?
 | 
					      authorize :settings, :show?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @admin_settings = Form::AdminSettings.new
 | 
					      @admin_settings = Form::AdminSettings.new
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update
 | 
					    def update
 | 
				
			||||||
      authorize :settings, :update?
 | 
					      authorize :settings, :update?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      settings_params.each do |key, value|
 | 
					      @admin_settings = Form::AdminSettings.new(settings_params)
 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if @admin_settings.save
 | 
				
			||||||
        flash[:notice] = I18n.t('generic.changes_saved_msg')
 | 
					        flash[:notice] = I18n.t('generic.changes_saved_msg')
 | 
				
			||||||
        redirect_to edit_admin_settings_path
 | 
					        redirect_to edit_admin_settings_path
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        render :edit
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def settings_params
 | 
					    def settings_params
 | 
				
			||||||
      params.require(:form_admin_settings).permit(ADMIN_SETTINGS)
 | 
					      params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def value_for_update(key, value)
 | 
					 | 
				
			||||||
      if BOOLEAN_SETTINGS.include?(key)
 | 
					 | 
				
			||||||
        value == '1'
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        value
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,49 +3,90 @@
 | 
				
			||||||
class Form::AdminSettings
 | 
					class Form::AdminSettings
 | 
				
			||||||
  include ActiveModel::Model
 | 
					  include ActiveModel::Model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  delegate(
 | 
					  KEYS = %i(
 | 
				
			||||||
    :site_contact_username,
 | 
					    site_contact_username
 | 
				
			||||||
    :site_contact_username=,
 | 
					    site_contact_email
 | 
				
			||||||
    :site_contact_email,
 | 
					    site_title
 | 
				
			||||||
    :site_contact_email=,
 | 
					    site_short_description
 | 
				
			||||||
    :site_title,
 | 
					    site_description
 | 
				
			||||||
    :site_title=,
 | 
					    site_extended_description
 | 
				
			||||||
    :site_short_description,
 | 
					    site_terms
 | 
				
			||||||
    :site_short_description=,
 | 
					    registrations_mode
 | 
				
			||||||
    :site_description,
 | 
					    closed_registrations_message
 | 
				
			||||||
    :site_description=,
 | 
					    open_deletion
 | 
				
			||||||
    :site_extended_description,
 | 
					    timeline_preview
 | 
				
			||||||
    :site_extended_description=,
 | 
					    show_staff_badge
 | 
				
			||||||
    :site_terms,
 | 
					    bootstrap_timeline_accounts
 | 
				
			||||||
    :site_terms=,
 | 
					    theme
 | 
				
			||||||
    :registrations_mode,
 | 
					    min_invite_role
 | 
				
			||||||
    :registrations_mode=,
 | 
					    activity_api_enabled
 | 
				
			||||||
    :closed_registrations_message,
 | 
					    peers_api_enabled
 | 
				
			||||||
    :closed_registrations_message=,
 | 
					    show_known_fediverse_at_about_page
 | 
				
			||||||
    :open_deletion,
 | 
					    preview_sensitive_media
 | 
				
			||||||
    :open_deletion=,
 | 
					    custom_css
 | 
				
			||||||
    :timeline_preview,
 | 
					    profile_directory
 | 
				
			||||||
    :timeline_preview=,
 | 
					  ).freeze
 | 
				
			||||||
    :show_staff_badge,
 | 
					
 | 
				
			||||||
    :show_staff_badge=,
 | 
					  BOOLEAN_KEYS = %i(
 | 
				
			||||||
    :bootstrap_timeline_accounts,
 | 
					    open_deletion
 | 
				
			||||||
    :bootstrap_timeline_accounts=,
 | 
					    timeline_preview
 | 
				
			||||||
    :theme,
 | 
					    show_staff_badge
 | 
				
			||||||
    :theme=,
 | 
					    activity_api_enabled
 | 
				
			||||||
    :min_invite_role,
 | 
					    peers_api_enabled
 | 
				
			||||||
    :min_invite_role=,
 | 
					    show_known_fediverse_at_about_page
 | 
				
			||||||
    :activity_api_enabled,
 | 
					    preview_sensitive_media
 | 
				
			||||||
    :activity_api_enabled=,
 | 
					    profile_directory
 | 
				
			||||||
    :peers_api_enabled,
 | 
					  ).freeze
 | 
				
			||||||
    :peers_api_enabled=,
 | 
					
 | 
				
			||||||
    :show_known_fediverse_at_about_page,
 | 
					  UPLOAD_KEYS = %i(
 | 
				
			||||||
    :show_known_fediverse_at_about_page=,
 | 
					    thumbnail
 | 
				
			||||||
    :preview_sensitive_media,
 | 
					    hero
 | 
				
			||||||
    :preview_sensitive_media=,
 | 
					    mascot
 | 
				
			||||||
    :custom_css,
 | 
					  ).freeze
 | 
				
			||||||
    :custom_css=,
 | 
					
 | 
				
			||||||
    :profile_directory,
 | 
					  attr_accessor(*KEYS)
 | 
				
			||||||
    :profile_directory=,
 | 
					
 | 
				
			||||||
    to: Setting
 | 
					  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|
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def initialize_attributes
 | 
				
			||||||
 | 
					    KEYS.each do |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
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@
 | 
				
			||||||
  = t('admin.settings.title')
 | 
					  = t('admin.settings.title')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch } do |f|
 | 
					= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch } do |f|
 | 
				
			||||||
 | 
					  = render 'shared/error_messages', object: @admin_settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .fields-group
 | 
					  .fields-group
 | 
				
			||||||
    = f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title')
 | 
					    = f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -586,6 +586,9 @@ en:
 | 
				
			||||||
      content: We're sorry, but something went wrong on our end.
 | 
					      content: We're sorry, but something went wrong on our end.
 | 
				
			||||||
      title: This page is not correct
 | 
					      title: This page is not correct
 | 
				
			||||||
    noscript_html: To use the Mastodon web application, please enable JavaScript. Alternatively, try one of the <a href="%{apps_path}">native apps</a> for Mastodon for your platform.
 | 
					    noscript_html: To use the Mastodon web application, please enable JavaScript. Alternatively, try one of the <a href="%{apps_path}">native apps</a> 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:
 | 
					  exports:
 | 
				
			||||||
    archive_takeout:
 | 
					    archive_takeout:
 | 
				
			||||||
      date: Date
 | 
					      date: Date
 | 
				
			||||||
| 
						 | 
					@ -633,6 +636,8 @@ en:
 | 
				
			||||||
    validation_errors:
 | 
					    validation_errors:
 | 
				
			||||||
      one: Something isn't quite right yet! Please review the error below
 | 
					      one: Something isn't quite right yet! Please review the error below
 | 
				
			||||||
      other: Something isn't quite right yet! Please review %{count} errors below
 | 
					      other: Something isn't quite right yet! Please review %{count} errors below
 | 
				
			||||||
 | 
					  html_validator:
 | 
				
			||||||
 | 
					    invalid_markup: contains invalid HTML markup
 | 
				
			||||||
  identity_proofs:
 | 
					  identity_proofs:
 | 
				
			||||||
    active: Active
 | 
					    active: Active
 | 
				
			||||||
    authorize: Yes, authorize
 | 
					    authorize: Yes, authorize
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,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|
 | 
					    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 :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 :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 :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? }
 | 
					      admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,10 @@ RSpec.describe Admin::SettingsController, type: :controller do
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    describe 'PUT #update' do
 | 
					    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
 | 
					      describe 'for a record that doesnt exist' do
 | 
				
			||||||
        around do |example|
 | 
					        around do |example|
 | 
				
			||||||
          before = Setting.site_extended_description
 | 
					          before = Setting.site_extended_description
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue