forked from pingcap/discourse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathuser_name_suggester.rb
150 lines (119 loc) · 4.29 KB
/
user_name_suggester.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# frozen_string_literal: true
module UserNameSuggester
GENERIC_NAMES = ['i', 'me', 'info', 'support', 'admin', 'webmaster', 'hello', 'mail', 'office', 'contact', 'team']
def self.suggest(name_or_email, allowed_username = nil)
return unless name_or_email.present?
name = parse_name_from_email(name_or_email)
find_available_username_based_on(name, allowed_username)
end
def self.parse_name_from_email(name_or_email)
return name_or_email if name_or_email !~ User::EMAIL
# When '[email protected]' take 'walter'
name = Regexp.last_match[1]
# When '[email protected]' take 'eviltrout'
name = Regexp.last_match[2] if GENERIC_NAMES.include?(name)
name
end
def self.find_available_username_based_on(name, allowed_username = nil)
name = fix_username(name)
offset = nil
i = 1
attempt = name
normalized_attempt = User.normalize_username(attempt)
original_allowed_username = allowed_username
allowed_username = User.normalize_username(allowed_username) if allowed_username
until (
normalized_attempt == allowed_username ||
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
SQL
if count > 0
params = {
count: count + 10,
name: normalized,
allowed_normalized: allowed_username || ''
}
# increasing the search space a bit to allow for some extra noise
gs = (1..count).map do |c|
"SELECT #{c} AS n"
end.join(" UNION ")
available = DB.query_single(<<~SQL, params).first
SELECT n FROM (#{gs}) AS numbers
LEFT JOIN users ON (
username_lower = concat(:name, cast(n AS char))
) AND (
username_lower <> :allowed_normalized
)
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}"
normalized_attempt = User.normalize_username(attempt)
i += 1
end
until normalized_attempt == allowed_username || User.username_available?(attempt) || i > 200
attempt = SecureRandom.hex[1..SiteSetting.max_username_length]
normalized_attempt = User.normalize_username(attempt)
i += 1
end
if allowed_username == normalized_attempt
original_allowed_username
else
attempt
end
end
def self.fix_username(name)
rightsize_username(sanitize_username(name))
end
def self.sanitize_username(name)
name = name.to_s.dup
if SiteSetting.unicode_usernames
name.unicode_normalize!
else
name = ActiveSupport::Inflector.transliterate(name)
end
name.gsub!(UsernameValidator.invalid_char_pattern, '_')
name.chars.map! { |c| UsernameValidator.whitelisted_char?(c) ? c : '_' } if UsernameValidator.char_whitelist_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.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