diff --git a/app/logical/user_name_validator.rb b/app/logical/user_name_validator.rb index 8d44cb064..99ecce9dc 100644 --- a/app/logical/user_name_validator.rb +++ b/app/logical/user_name_validator.rb @@ -7,14 +7,33 @@ # # @see https://guides.rubyonrails.org/active_record_validations.html#custom-validators class UserNameValidator < ActiveModel::EachValidator - def validate_each(rec, attr, value) - name = value + ALLOWED_PUNCTUATION = "_.-" # All other punctuation characters are forbidden - rec.errors.add(attr, "already exists") if User.find_by_name(name).present? - rec.errors.add(attr, "must be more than 1 character long") if name.length <= 1 - rec.errors.add(attr, "must be less than 25 characters long") if name.length >= 25 - rec.errors.add(attr, "cannot have whitespace or colons") if name =~ /[[:space:]]|:/ - rec.errors.add(attr, "cannot begin or end with an underscore") if name =~ /\A_|_\z/ - rec.errors.add(attr, "is not allowed") if name =~ Regexp.union(Danbooru.config.user_name_blacklist) + def validate_each(rec, attr, name) + forbidden_characters = name.delete(ALLOWED_PUNCTUATION).chars.grep(/[[:punct:]]/).uniq + + if rec.new_record? && User.find_by_name(name).present? + rec.errors.add(attr, "already exists") + elsif name.length <= 1 + rec.errors.add(attr, "must be more than 1 character long") + elsif name.length >= 25 + rec.errors.add(attr, "must be less than 25 characters long") + elsif name =~ /[[:space:]]/ + rec.errors.add(attr, "can't contain whitespace") + elsif name =~ /\A[[:punct:]]/ + rec.errors.add(attr, "can't start with '#{name.first}'") + elsif name =~ /[[:punct:]]\z/ + rec.errors.add(attr, "can't end with '#{name.last}'") + elsif name =~ /__/ + rec.errors.add(attr, "can't contain multiple underscores in a row") + elsif forbidden_characters.present? + rec.errors.add(attr, "can't contain #{forbidden_characters.map { |c| "'#{c}'" }.to_sentence}") + elsif name !~ /\A([a-zA-Z0-9]|\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}|[#{ALLOWED_PUNCTUATION}])+\z/ + rec.errors.add(attr, "must contain only basic letters or numbers") + elsif name =~ /\Auser_\d+\z/i + rec.errors.add(attr, "can't be the same as a deleted user") + elsif name =~ Regexp.union(Danbooru.config.user_name_blacklist) + rec.errors.add(attr, "is not allowed") + end end end diff --git a/app/models/user.rb b/app/models/user.rb index 7a55daa9d..323fe94d2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class User < ApplicationRecord + extend Memoist + class PrivilegeError < StandardError; end module Levels @@ -204,6 +206,18 @@ class User < ApplicationRecord errors.add(:base, "Can't enable privacy mode without a Gold account") end end + + def name_errors + User.validators_on(:name).each do |validator| + validator.validate_each(self, :name, name) + end + + errors + end + + def name_invalid? + name_errors.present? + end end concerning :AuthenticationMethods do @@ -695,4 +709,6 @@ class User < ApplicationRecord def self.available_includes [:inviter] end + + memoize :name_errors end diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb index fe035f1fb..5c45e848c 100644 --- a/app/views/layouts/default.html.erb +++ b/app/views/layouts/default.html.erb @@ -104,6 +104,13 @@ <%= render "users/dmail_notice" %> <% end %> + <% if !CurrentUser.user.is_anonymous? && CurrentUser.user.name_invalid? %> +
You can request a name change once per week. Your previous names will still - be visible on your profile to other Danbooru members, but they won't be visible - to search engines.
+ <% if CurrentUser.user.name_invalid? %> +Your current username is invalid. You must change your username to continue + using <%= Danbooru.config.canonical_app_name %>.
+ +
+ Current name: <%= CurrentUser.user.name %>.
+ Error: <%= CurrentUser.user.name_errors.full_messages.join(". ").html_safe %>.
+