mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 09:13:45 +08:00
19d95c64af
This PR doesn't change any behavior, but just removes code that wasn't in use. This is a pretty dangerous place to change, since it gets called during user's registration. At the same time the refactoring is very straightforward, it's clear that this code wasn't doing any work (it still needs to be double-checked during review though). Also, the test coverage of UserNameSuggester is good.
149 lines
4.2 KiB
Ruby
149 lines
4.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module UserNameSuggester
|
|
GENERIC_NAMES = ['i', 'me', 'info', 'support', 'admin', 'webmaster', 'hello', 'mail', 'office', 'contact', 'team']
|
|
LAST_RESORT_USERNAME = "user"
|
|
|
|
def self.suggest(name_or_email)
|
|
name = parse_name_from_email(name_or_email)
|
|
find_available_username_based_on(name)
|
|
end
|
|
|
|
def self.parse_name_from_email(name_or_email)
|
|
return name_or_email if name_or_email.to_s !~ User::EMAIL
|
|
|
|
# When 'walter@white.com' take 'walter'
|
|
name = Regexp.last_match[1]
|
|
|
|
# When 'me@eviltrout.com' take 'eviltrout'
|
|
name = Regexp.last_match[2] if GENERIC_NAMES.include?(name)
|
|
name
|
|
end
|
|
|
|
def self.find_available_username_based_on(name)
|
|
name = fix_username(name)
|
|
offset = nil
|
|
i = 1
|
|
|
|
attempt = name
|
|
until User.username_available?(attempt) || i > 100
|
|
|
|
if offset.nil?
|
|
normalized = User.normalize_username(name)
|
|
similar = "#{normalized}(0|1|2|3|4|5|6|7|8|9)+"
|
|
|
|
count = DB.query_single(<<~SQL, like: "#{normalized}%", similar: similar).first
|
|
SELECT count(*) FROM users
|
|
WHERE username_lower LIKE :like AND
|
|
username_lower SIMILAR TO :similar
|
|
SQL
|
|
|
|
if count > 0
|
|
|
|
params = {
|
|
count: count + 10,
|
|
name: normalized
|
|
}
|
|
|
|
# increasing the search space a bit to allow for some extra noise
|
|
available = DB.query_single(<<~SQL, params).first
|
|
WITH numbers AS (SELECT generate_series(1, :count) AS n)
|
|
|
|
SELECT n FROM numbers
|
|
LEFT JOIN users ON (username_lower = :name || n::varchar)
|
|
WHERE users.id IS NULL
|
|
ORDER by n ASC
|
|
LIMIT 1
|
|
SQL
|
|
|
|
# we start at 1
|
|
offset = available.to_i - 1
|
|
offset = 0 if offset < 0
|
|
else
|
|
offset = 0
|
|
end
|
|
end
|
|
|
|
suffix = (i + offset).to_s
|
|
|
|
max_length = User.username_length.end - suffix.length
|
|
attempt = "#{truncate(name, max_length)}#{suffix}"
|
|
i += 1
|
|
end
|
|
|
|
until User.username_available?(attempt) || i > 200
|
|
attempt = SecureRandom.hex[1..SiteSetting.max_username_length]
|
|
i += 1
|
|
end
|
|
|
|
attempt
|
|
end
|
|
|
|
def self.fix_username(name)
|
|
fixed_username = sanitize_username(name)
|
|
if fixed_username.empty?
|
|
fixed_username << sanitize_username(I18n.t('fallback_username'))
|
|
fixed_username << LAST_RESORT_USERNAME if fixed_username.empty?
|
|
end
|
|
|
|
rightsize_username(fixed_username)
|
|
end
|
|
|
|
def self.sanitize_username(name)
|
|
name = name.to_s.dup
|
|
|
|
if SiteSetting.unicode_usernames
|
|
name.unicode_normalize!
|
|
|
|
# TODO: Jan 2022, review if still needed
|
|
# see: https://meta.discourse.org/t/unicode-username-with-as-the-final-char-leads-to-an-error-loading-profile-page/173182
|
|
if name.include?('Σ')
|
|
ctx = MiniRacer::Context.new
|
|
name = ctx.eval("#{name.to_s.to_json}.toLowerCase()")
|
|
ctx.dispose
|
|
end
|
|
else
|
|
name = ActiveSupport::Inflector.transliterate(name)
|
|
end
|
|
|
|
name.gsub!(UsernameValidator.invalid_char_pattern, '_')
|
|
name = apply_allowlist(name) if UsernameValidator.char_allowlist_exists?
|
|
name.gsub!(UsernameValidator::INVALID_LEADING_CHAR_PATTERN, '')
|
|
name.gsub!(UsernameValidator::CONFUSING_EXTENSIONS, "_")
|
|
name.gsub!(UsernameValidator::INVALID_TRAILING_CHAR_PATTERN, '')
|
|
name.gsub!(UsernameValidator::REPEATED_SPECIAL_CHAR_PATTERN, '_')
|
|
name
|
|
end
|
|
|
|
def self.apply_allowlist(name)
|
|
name.grapheme_clusters
|
|
.map { |c| UsernameValidator.allowed_char?(c) ? c : '_' }
|
|
.join
|
|
end
|
|
|
|
def self.rightsize_username(name)
|
|
name = truncate(name, User.username_length.end)
|
|
name.gsub!(UsernameValidator::INVALID_TRAILING_CHAR_PATTERN, '')
|
|
|
|
missing_char_count = User.username_length.begin - name.grapheme_clusters.size
|
|
name << '1' * missing_char_count if missing_char_count > 0
|
|
name
|
|
end
|
|
|
|
def self.truncate(name, max_grapheme_clusters)
|
|
clusters = name.grapheme_clusters
|
|
|
|
if clusters.size > max_grapheme_clusters
|
|
clusters = clusters[0..max_grapheme_clusters - 1]
|
|
name = clusters.join
|
|
end
|
|
|
|
while name.length > UsernameValidator::MAX_CHARS
|
|
clusters.pop
|
|
name = clusters.join
|
|
end
|
|
|
|
name
|
|
end
|
|
end
|