forked from discourse/discourse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathuser_name_suggester.rb
163 lines (131 loc) · 4.78 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
151
152
153
154
155
156
157
158
159
160
161
162
163
# frozen_string_literal: true
module UserNameSuggester
GENERIC_NAMES = %w[i me info support admin webmaster hello mail office contact team]
LAST_RESORT_USERNAME = "user"
def self.suggest(*input, current_username: nil)
name =
input.find do |item|
parsed_name = parse_name_from_email(item)
break parsed_name if sanitize_username(parsed_name).present?
end
name = fix_username(name)
find_available_username_based_on(name, current_username)
end
def self.parse_name_from_email(name_or_email)
return name_or_email if name_or_email.to_s !~ 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, current_username = nil)
offset = nil
i = 1
attempt = name
normalized_attempt = User.normalize_username(attempt)
original_allowed_username = current_username
current_username = User.normalize_username(current_username) if current_username
until (normalized_attempt == current_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 AND
username_lower SIMILAR TO :similar
SQL
if count > 0
params = {
count: count + 10,
name: normalized,
allowed_normalized: current_username || "",
}
# 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
) 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 == current_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 current_username == normalized_attempt
original_allowed_username
else
attempt
end
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!
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