FEATURE: Add support for Unicode usernames and group names
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
parent
d07605d885
commit
a7bc1ecbae
|
@ -44,7 +44,7 @@ function addHashtag(buffer, matches, state) {
|
|||
export function setup(helper) {
|
||||
helper.registerPlugin(md => {
|
||||
const rule = {
|
||||
matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w-:]{1,101})/,
|
||||
matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101})/,
|
||||
onMatch: addHashtag
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -15,9 +15,14 @@ export class TextPostProcessRuler {
|
|||
|
||||
this.matcherIndex = [];
|
||||
|
||||
let rules = this.rules.map(
|
||||
r => "(" + r.rule.matcher.toString().slice(1, -1) + ")"
|
||||
);
|
||||
const rules = [];
|
||||
const flags = new Set("g");
|
||||
|
||||
this.rules.forEach(r => {
|
||||
const matcher = r.rule.matcher;
|
||||
rules.push(`(${matcher.source})`);
|
||||
matcher.flags.split("").forEach(f => flags.add(f));
|
||||
});
|
||||
|
||||
let i;
|
||||
let regexString = "";
|
||||
|
@ -41,7 +46,7 @@ export class TextPostProcessRuler {
|
|||
last = "x".match(regex).length - 1;
|
||||
}
|
||||
|
||||
this.matcher = new RegExp(rules.join("|"), "g");
|
||||
this.matcher = new RegExp(rules.join("|"), [...flags].join(""));
|
||||
return this.matcher;
|
||||
}
|
||||
|
||||
|
|
|
@ -313,7 +313,7 @@ class UsersController < ApplicationController
|
|||
params.require(:username) if !params[:email].present?
|
||||
return render(json: success_json)
|
||||
end
|
||||
username = params[:username]
|
||||
username = params[:username]&.unicode_normalize
|
||||
|
||||
target_user = user_from_params_or_current_user
|
||||
|
||||
|
|
|
@ -5,16 +5,27 @@ module Jobs
|
|||
|
||||
def execute(args)
|
||||
@user_id = args[:user_id]
|
||||
@old_username = args[:old_username]
|
||||
@new_username = args[:new_username]
|
||||
@old_username = args[:old_username].unicode_normalize
|
||||
@new_username = args[:new_username].unicode_normalize
|
||||
@avatar_img = PrettyText.avatar_img(args[:avatar_template], "tiny")
|
||||
|
||||
@raw_mention_regex = /(?:(?<![\w`_])|(?<=_))@#{@old_username}(?:(?![\w\-\.])|(?=[\-\.](?:\s|$)))/i
|
||||
@raw_mention_regex = /
|
||||
(?:
|
||||
(?<![\p{Alnum}\p{M}`]) # make sure there is no preceding letter, number or backtick
|
||||
)
|
||||
@#{@old_username}
|
||||
(?:
|
||||
(?![\p{Alnum}\p{M}_\-.`]) # make sure there is no trailing letter, number, underscore, dash, dot or backtick
|
||||
| # or
|
||||
(?=[-_.](?:\s|$)) # there is an underscore, dash or dot followed by a whitespace or end of line
|
||||
)
|
||||
/ix
|
||||
|
||||
@raw_quote_regex = /(\[quote\s*=\s*["'']?)#{@old_username}(\,?[^\]]*\])/i
|
||||
|
||||
cooked_username = PrettyText::Helpers.format_username(@old_username)
|
||||
@cooked_mention_username_regex = /^@#{cooked_username}$/i
|
||||
@cooked_mention_user_path_regex = /^\/u(?:sers)?\/#{cooked_username}$/i
|
||||
@cooked_mention_user_path_regex = /^\/u(?:sers)?\/#{CGI.escape(cooked_username)}$/i
|
||||
@cooked_quote_username_regex = /(?<=\s)#{cooked_username}(?=:)/i
|
||||
|
||||
update_posts
|
||||
|
|
|
@ -281,7 +281,7 @@ class Group < ActiveRecord::Base
|
|||
end
|
||||
|
||||
# don't allow shoddy localization to break this
|
||||
localized_name = I18n.t("groups.default_names.#{name}", locale: SiteSetting.default_locale).downcase
|
||||
localized_name = User.normalize_username(I18n.t("groups.default_names.#{name}", locale: SiteSetting.default_locale))
|
||||
validator = UsernameValidator.new(localized_name)
|
||||
|
||||
if validator.valid_format? && !User.username_exists?(localized_name)
|
||||
|
@ -621,14 +621,14 @@ class Group < ActiveRecord::Base
|
|||
# avoid strip! here, it works now
|
||||
# but may not continue to work long term, especially
|
||||
# once we start returning frozen strings
|
||||
if self.name != (stripped = self.name.strip)
|
||||
if self.name != (stripped = self.name.unicode_normalize.strip)
|
||||
self.name = stripped
|
||||
end
|
||||
|
||||
UsernameValidator.perform_validation(self, 'name') || begin
|
||||
name_lower = self.name.downcase
|
||||
normalized_name = User.normalize_username(self.name)
|
||||
|
||||
if self.will_save_change_to_name? && self.name_was&.downcase != name_lower && User.username_exists?(name_lower)
|
||||
if self.will_save_change_to_name? && User.normalize_username(self.name_was) != normalized_name && User.username_exists?(self.name)
|
||||
errors.add(:name, I18n.t("activerecord.errors.messages.taken"))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -68,7 +68,7 @@ class PostAnalyzer
|
|||
raw_mentions = cooked_stripped.css('.mention, .mention-group').map do |e|
|
||||
if name = e.inner_text
|
||||
name = name[1..-1]
|
||||
name.downcase! if name
|
||||
name = User.normalize_username(name)
|
||||
name
|
||||
end
|
||||
end
|
||||
|
|
|
@ -121,6 +121,11 @@ class SiteSetting < ActiveRecord::Base
|
|||
@attachment_filename_blacklist_regex ||= Regexp.union(SiteSetting.attachment_filename_blacklist.split("|"))
|
||||
end
|
||||
|
||||
def self.unicode_username_character_whitelist_regex
|
||||
@unicode_username_whitelist_regex = SiteSetting.unicode_username_character_whitelist.present? \
|
||||
? Regexp.new(SiteSetting.unicode_username_character_whitelist) : nil
|
||||
end
|
||||
|
||||
# helpers for getting s3 settings that fallback to global
|
||||
class Upload
|
||||
def self.s3_cdn_url
|
||||
|
|
|
@ -117,7 +117,7 @@ class User < ActiveRecord::Base
|
|||
after_create :ensure_in_trust_level_group
|
||||
after_create :set_default_categories_preferences
|
||||
|
||||
before_save :update_username_lower
|
||||
before_save :update_usernames
|
||||
before_save :ensure_password_is_hashed
|
||||
before_save :match_title_to_primary_group_changes
|
||||
before_save :check_if_title_is_badged_granted
|
||||
|
@ -224,8 +224,12 @@ class User < ActiveRecord::Base
|
|||
SiteSetting.min_username_length.to_i..SiteSetting.max_username_length.to_i
|
||||
end
|
||||
|
||||
def self.normalize_username(username)
|
||||
username.unicode_normalize.downcase if username.present?
|
||||
end
|
||||
|
||||
def self.username_available?(username, email = nil, allow_reserved_username: false)
|
||||
lower = username.downcase
|
||||
lower = normalize_username(username)
|
||||
return false if !allow_reserved_username && reserved_username?(lower)
|
||||
return true if !username_exists?(lower)
|
||||
|
||||
|
@ -234,10 +238,10 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.reserved_username?(username)
|
||||
lower = username.downcase
|
||||
username = normalize_username(username)
|
||||
|
||||
SiteSetting.reserved_usernames.split("|").any? do |reserved|
|
||||
!!lower.match("^#{Regexp.escape(reserved).gsub('\*', '.*')}$")
|
||||
SiteSetting.reserved_usernames.unicode_normalize.split("|").any? do |reserved|
|
||||
username.match?(/^#{Regexp.escape(reserved).gsub('\*', '.*')}$/)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -362,7 +366,7 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.find_by_username(username)
|
||||
find_by(username_lower: username.downcase)
|
||||
find_by(username_lower: normalize_username(username))
|
||||
end
|
||||
|
||||
def group_granted_trust_level
|
||||
|
@ -1293,30 +1297,31 @@ class User < ActiveRecord::Base
|
|||
self.trust_level ||= SiteSetting.default_trust_level
|
||||
end
|
||||
|
||||
def update_username_lower
|
||||
def update_usernames
|
||||
self.username.unicode_normalize!
|
||||
self.username_lower = username.downcase
|
||||
end
|
||||
|
||||
USERNAME_EXISTS_SQL = <<~SQL
|
||||
(SELECT users.id AS id, true as is_user FROM users
|
||||
WHERE users.username_lower = :username)
|
||||
(SELECT users.id AS id, true as is_user FROM users
|
||||
WHERE users.username_lower = :username)
|
||||
|
||||
UNION ALL
|
||||
UNION ALL
|
||||
|
||||
(SELECT groups.id, false as is_user FROM groups
|
||||
WHERE lower(groups.name) = :username)
|
||||
(SELECT groups.id, false as is_user FROM groups
|
||||
WHERE lower(groups.name) = :username)
|
||||
SQL
|
||||
|
||||
def self.username_exists?(username_lower)
|
||||
DB.exec(User::USERNAME_EXISTS_SQL, username: username_lower) > 0
|
||||
def self.username_exists?(username)
|
||||
username = normalize_username(username)
|
||||
DB.exec(User::USERNAME_EXISTS_SQL, username: username) > 0
|
||||
end
|
||||
|
||||
def username_validator
|
||||
username_format_validator || begin
|
||||
lower = username.downcase
|
||||
|
||||
existing = DB.query(
|
||||
USERNAME_EXISTS_SQL, username: lower
|
||||
USERNAME_EXISTS_SQL,
|
||||
username: self.class.normalize_username(username)
|
||||
)
|
||||
|
||||
user_id = existing.select { |u| u.is_user }.first&.id
|
||||
|
|
|
@ -16,9 +16,10 @@ class UsernameValidator
|
|||
end
|
||||
|
||||
def initialize(username)
|
||||
@username = username
|
||||
@username = username&.unicode_normalize
|
||||
@errors = []
|
||||
end
|
||||
|
||||
attr_accessor :errors
|
||||
attr_reader :username
|
||||
|
||||
|
@ -27,10 +28,11 @@ class UsernameValidator
|
|||
end
|
||||
|
||||
def valid_format?
|
||||
username_exist?
|
||||
username_present?
|
||||
username_length_min?
|
||||
username_length_max?
|
||||
username_char_valid?
|
||||
username_char_whitelisted?
|
||||
username_first_char_valid?
|
||||
username_last_char_valid?
|
||||
username_no_double_special?
|
||||
|
@ -39,62 +41,103 @@ class UsernameValidator
|
|||
end
|
||||
|
||||
CONFUSING_EXTENSIONS ||= /\.(js|json|css|htm|html|xml|jpg|jpeg|png|gif|bmp|ico|tif|tiff|woff)$/i
|
||||
MAX_CHARS ||= 60
|
||||
|
||||
ASCII_INVALID_CHAR_PATTERN ||= /[^\w.-]/
|
||||
UNICODE_INVALID_CHAR_PATTERN ||= /[^\p{Alnum}\p{M}._-]/
|
||||
INVALID_LEADING_CHAR_PATTERN ||= /^[^\p{Alnum}\p{M}_]+/
|
||||
INVALID_TRAILING_CHAR_PATTERN ||= /[^\p{Alnum}\p{M}]+$/
|
||||
REPEATED_SPECIAL_CHAR_PATTERN ||= /[-_.]{2,}/
|
||||
|
||||
private
|
||||
|
||||
def username_exist?
|
||||
def username_present?
|
||||
return unless errors.empty?
|
||||
unless username
|
||||
|
||||
if username.blank?
|
||||
self.errors << I18n.t(:'user.username.blank')
|
||||
end
|
||||
end
|
||||
|
||||
def username_length_min?
|
||||
return unless errors.empty?
|
||||
if username.length < User.username_length.begin
|
||||
|
||||
if username_grapheme_clusters.size < User.username_length.begin
|
||||
self.errors << I18n.t(:'user.username.short', min: User.username_length.begin)
|
||||
end
|
||||
end
|
||||
|
||||
def username_length_max?
|
||||
return unless errors.empty?
|
||||
if username.length > User.username_length.end
|
||||
|
||||
if username_grapheme_clusters.size > User.username_length.end
|
||||
self.errors << I18n.t(:'user.username.long', max: User.username_length.end)
|
||||
elsif username.length > MAX_CHARS
|
||||
self.errors << I18n.t(:'user.username.too_long')
|
||||
end
|
||||
end
|
||||
|
||||
def username_char_valid?
|
||||
return unless errors.empty?
|
||||
if username =~ /[^\w.-]/
|
||||
|
||||
if self.class.invalid_char_pattern.match?(username)
|
||||
self.errors << I18n.t(:'user.username.characters')
|
||||
end
|
||||
end
|
||||
|
||||
def username_char_whitelisted?
|
||||
return unless errors.empty? && self.class.char_whitelist_exists?
|
||||
|
||||
if username.chars.any? { |c| !self.class.whitelisted_char?(c) }
|
||||
self.errors << I18n.t(:'user.username.characters')
|
||||
end
|
||||
end
|
||||
|
||||
def username_first_char_valid?
|
||||
return unless errors.empty?
|
||||
if username[0] =~ /\W/
|
||||
|
||||
if INVALID_LEADING_CHAR_PATTERN.match?(username_grapheme_clusters.first)
|
||||
self.errors << I18n.t(:'user.username.must_begin_with_alphanumeric_or_underscore')
|
||||
end
|
||||
end
|
||||
|
||||
def username_last_char_valid?
|
||||
return unless errors.empty?
|
||||
if username[-1] =~ /[^A-Za-z0-9]/
|
||||
|
||||
if INVALID_TRAILING_CHAR_PATTERN.match?(username_grapheme_clusters.last)
|
||||
self.errors << I18n.t(:'user.username.must_end_with_alphanumeric')
|
||||
end
|
||||
end
|
||||
|
||||
def username_no_double_special?
|
||||
return unless errors.empty?
|
||||
if username =~ /[-_.]{2,}/
|
||||
|
||||
if REPEATED_SPECIAL_CHAR_PATTERN.match?(username)
|
||||
self.errors << I18n.t(:'user.username.must_not_contain_two_special_chars_in_seq')
|
||||
end
|
||||
end
|
||||
|
||||
def username_does_not_end_with_confusing_suffix?
|
||||
return unless errors.empty?
|
||||
if username =~ CONFUSING_EXTENSIONS
|
||||
|
||||
if CONFUSING_EXTENSIONS.match?(username)
|
||||
self.errors << I18n.t(:'user.username.must_not_end_with_confusing_suffix')
|
||||
end
|
||||
end
|
||||
|
||||
def username_grapheme_clusters
|
||||
@username_grapheme_clusters ||= username.grapheme_clusters
|
||||
end
|
||||
|
||||
def self.invalid_char_pattern
|
||||
SiteSetting.unicode_usernames ? UNICODE_INVALID_CHAR_PATTERN : ASCII_INVALID_CHAR_PATTERN
|
||||
end
|
||||
|
||||
def self.char_whitelist_exists?
|
||||
SiteSetting.unicode_usernames && SiteSetting.unicode_username_character_whitelist_regex.present?
|
||||
end
|
||||
|
||||
def self.whitelisted_char?(c)
|
||||
c.match?(/[\w.-]/) || c.match?(SiteSetting.unicode_username_character_whitelist_regex)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -912,9 +912,9 @@ en:
|
|||
submit: "Save preferences"
|
||||
digest_frequency:
|
||||
title: "You are receiving summary emails %{frequency}"
|
||||
select_title: 'Set summary emails frequency to:'
|
||||
|
||||
never: 'never'
|
||||
select_title: "Set summary emails frequency to:"
|
||||
|
||||
never: "never"
|
||||
every_30_minutes: "every 30 minutes"
|
||||
every_hour: "hourly"
|
||||
daily: "daily"
|
||||
|
@ -922,7 +922,6 @@ en:
|
|||
every_month: "every month"
|
||||
every_six_months: "every six months"
|
||||
|
||||
|
||||
user_api_key:
|
||||
title: "Authorize application access"
|
||||
authorize: "Authorize"
|
||||
|
@ -1474,6 +1473,8 @@ en:
|
|||
|
||||
min_username_length: "Minimum username length in characters. WARNING: if any existing users or groups have names shorter than this, your site will break!"
|
||||
max_username_length: "Maximum username length in characters. WARNING: if any existing users or groups have names longer than this, your site will break!"
|
||||
unicode_usernames: "Allow usernames and group names to contain Unicode letters and numbers."
|
||||
unicode_username_character_whitelist: "Regular expression to allow only some Unicode characters within usernames. ASCII letters and numbers will always be allowed and don't need to be included in the whitelist."
|
||||
|
||||
reserved_usernames: "Usernames for which signup is not allowed. Wildcard symbol * can be used to match any character zero or more times."
|
||||
|
||||
|
@ -2066,6 +2067,10 @@ en:
|
|||
low_weight_invalid: "You cannot set the weight to be greater or equal to 1 or smaller than 'category_search_priority_very_low_weight'."
|
||||
high_weight_invalid: "You cannot set the weight to be greater or equal to 1 or greater than 'category_search_priority_very_high_weight'."
|
||||
very_high_weight_invalid: "You cannot set the weight to be smaller than 'category_search_priority_high_weight'."
|
||||
unicode_username_whitelist:
|
||||
regex_invalid: "The regular expression is invalid: %{error}"
|
||||
leading_trailing_slash: "The regular expression must not start and end with a slash."
|
||||
unicode_usernames_avatars: "The internal system avatars do not support Unicode usernames."
|
||||
|
||||
placeholder:
|
||||
sso_provider_secrets:
|
||||
|
@ -2220,6 +2225,7 @@ en:
|
|||
username:
|
||||
short: "must be at least %{min} characters"
|
||||
long: "must be no more than %{max} characters"
|
||||
too_long: "is too long"
|
||||
characters: "must only include numbers, letters, dashes, and underscores"
|
||||
unique: "must be unique"
|
||||
blank: "must be present"
|
||||
|
@ -4272,8 +4278,7 @@ en:
|
|||
|
||||
privacy:
|
||||
title: "Access"
|
||||
description:
|
||||
"<p>Is your community open to everyone, or is it restricted by membership, invitation, or approval? If you prefer, you can set things up privately, then switch over to public later.</p>"
|
||||
description: "<p>Is your community open to everyone, or is it restricted by membership, invitation, or approval? If you prefer, you can set things up privately, then switch over to public later.</p>"
|
||||
|
||||
fields:
|
||||
privacy:
|
||||
|
|
|
@ -6,7 +6,7 @@ require_dependency "homepage_constraint"
|
|||
require_dependency "permalink_constraint"
|
||||
|
||||
# The following constants have been replaced with `RouteFormat` and are deprecated.
|
||||
USERNAME_ROUTE_FORMAT = /[\w.\-]+?/ unless defined? USERNAME_ROUTE_FORMAT
|
||||
USERNAME_ROUTE_FORMAT = /[%\w.\-]+?/ unless defined? USERNAME_ROUTE_FORMAT
|
||||
BACKUP_ROUTE_FORMAT = /.+\.(sql\.gz|tar\.gz|tgz)/i unless defined? BACKUP_ROUTE_FORMAT
|
||||
|
||||
Discourse::Application.routes.draw do
|
||||
|
|
|
@ -450,6 +450,15 @@ users:
|
|||
min: 8
|
||||
max: 60
|
||||
validator: "MaxUsernameLengthValidator"
|
||||
unicode_usernames:
|
||||
default: false
|
||||
client: true
|
||||
validator: "UnicodeUsernameValidator"
|
||||
unicode_username_character_whitelist:
|
||||
validator: "UnicodeUsernameWhitelistValidator"
|
||||
default: ""
|
||||
locale_default:
|
||||
de: "[äöüßÄÖÜẞ]"
|
||||
reserved_usernames:
|
||||
type: list
|
||||
list_type: compact
|
||||
|
@ -1107,8 +1116,9 @@ files:
|
|||
default: true
|
||||
client: true
|
||||
shadowed_by_global: true
|
||||
validator: "ExternalSystemAvatarsValidator"
|
||||
external_system_avatars_url:
|
||||
default: "/letter_avatar_proxy/v3/letter/{first_letter}/{color}/{size}.png"
|
||||
default: "/letter_avatar_proxy/v4/letter/{first_letter}/{color}/{size}.png"
|
||||
client: true
|
||||
regex: '^((https?:)?\/)?\/.+[^\/]'
|
||||
shadowed_by_global: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module RouteFormat
|
||||
|
||||
def self.username
|
||||
/[\w.\-]+?/
|
||||
/[%\w.\-]+?/
|
||||
end
|
||||
|
||||
def self.backup
|
||||
|
|
|
@ -1,33 +1,35 @@
|
|||
module UserNameSuggester
|
||||
GENERIC_NAMES = ['i', 'me', 'info', 'support', 'admin', 'webmaster', 'hello', 'mail', 'office', 'contact', 'team']
|
||||
|
||||
def self.suggest(name, allow_username = nil)
|
||||
return unless name.present?
|
||||
name = parse_name_from_email(name)
|
||||
find_available_username_based_on(name, allow_username)
|
||||
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)
|
||||
if name =~ 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)
|
||||
end
|
||||
def self.parse_name_from_email(name_or_email)
|
||||
return name_or_email if name_or_email !~ 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, allow_username = nil)
|
||||
def self.find_available_username_based_on(name, allowed_username = nil)
|
||||
name = fix_username(name)
|
||||
i = 1
|
||||
attempt = name
|
||||
until attempt == allow_username || User.username_available?(attempt) || i > 100
|
||||
until attempt == allowed_username || User.username_available?(attempt) || i > 100
|
||||
suffix = i.to_s
|
||||
max_length = User.username_length.end - suffix.length - 1
|
||||
attempt = "#{name[0..max_length]}#{suffix}"
|
||||
max_length = User.username_length.end - suffix.length
|
||||
attempt = "#{truncate(name, max_length)}#{suffix}"
|
||||
i += 1
|
||||
end
|
||||
until attempt == allow_username || User.username_available?(attempt) || i > 200
|
||||
until attempt == allowed_username || User.username_available?(attempt) || i > 200
|
||||
attempt = SecureRandom.hex[1..SiteSetting.max_username_length]
|
||||
i += 1
|
||||
end
|
||||
|
@ -39,28 +41,45 @@ module UserNameSuggester
|
|||
end
|
||||
|
||||
def self.sanitize_username(name)
|
||||
name = ActiveSupport::Inflector.transliterate(name.to_s)
|
||||
# 1. replace characters that aren't allowed with '_'
|
||||
name.gsub!(UsernameValidator::CONFUSING_EXTENSIONS, "_")
|
||||
name.gsub!(/[^\w.-]/, "_")
|
||||
# 2. removes unallowed leading characters
|
||||
name.gsub!(/^\W+/, "")
|
||||
# 3. removes unallowed trailing characters
|
||||
name = remove_unallowed_trailing_characters(name)
|
||||
# 4. unify special characters
|
||||
name.gsub!(/[-_.]{2,}/, "_")
|
||||
name
|
||||
end
|
||||
name = name.to_s
|
||||
|
||||
def self.remove_unallowed_trailing_characters(name)
|
||||
name.gsub!(/[^A-Za-z0-9]+$/, "")
|
||||
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 = name[0, User.username_length.end]
|
||||
name = remove_unallowed_trailing_characters(name)
|
||||
name.ljust(User.username_length.begin, '1')
|
||||
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
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
class ExternalSystemAvatarsValidator
|
||||
def initialize(opts = {})
|
||||
@opts = opts
|
||||
end
|
||||
|
||||
def valid_value?(value)
|
||||
@valid = value == "t" || !SiteSetting.unicode_usernames
|
||||
end
|
||||
|
||||
def error_message
|
||||
I18n.t("site_settings.errors.unicode_usernames_avatars") if !@valid
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
class UnicodeUsernameValidator
|
||||
def initialize(opts = {})
|
||||
@opts = opts
|
||||
end
|
||||
|
||||
def valid_value?(value)
|
||||
@valid = SiteSetting.external_system_avatars_enabled || value == "f"
|
||||
end
|
||||
|
||||
def error_message
|
||||
I18n.t("site_settings.errors.unicode_usernames_avatars") if !@valid
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
class UnicodeUsernameWhitelistValidator
|
||||
def initialize(opts = {})
|
||||
@opts = opts
|
||||
end
|
||||
|
||||
def valid_value?(value)
|
||||
@error_message = nil
|
||||
return true if value.blank?
|
||||
|
||||
if value.match?(/^\/.*\/[imxo]*$/)
|
||||
@error_message = I18n.t("site_settings.errors.unicode_username_whitelist.leading_trailing_slash")
|
||||
else
|
||||
begin
|
||||
Regexp.new(value)
|
||||
rescue RegexpError => e
|
||||
@error_message = I18n.t("site_settings.errors.unicode_username_whitelist.regex_invalid", error: e.message)
|
||||
end
|
||||
end
|
||||
|
||||
@error_message.blank?
|
||||
end
|
||||
|
||||
def error_message
|
||||
@error_message
|
||||
end
|
||||
end
|
|
@ -336,6 +336,21 @@ describe PrettyText do
|
|||
expect(PrettyText.cook(". http://test/@sam")).not_to include('mention')
|
||||
end
|
||||
|
||||
context "with Unicode usernames disabled" do
|
||||
before { SiteSetting.unicode_usernames = false }
|
||||
|
||||
it 'does not detect mention' do
|
||||
expect(PrettyText.cook("Hello @狮子")).to_not include("mention")
|
||||
end
|
||||
end
|
||||
|
||||
context "with Unicode usernames enabled" do
|
||||
before { SiteSetting.unicode_usernames = true }
|
||||
|
||||
it 'does detect mention' do
|
||||
expect(PrettyText.cook("Hello @狮子")).to match_html '<p>Hello <span class="mention">@狮子</span></p>'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "code fences" do
|
||||
|
|
|
@ -2,16 +2,10 @@ require 'rails_helper'
|
|||
require 'user_name_suggester'
|
||||
|
||||
describe UserNameSuggester do
|
||||
|
||||
describe 'name heuristics' do
|
||||
it 'is able to guess a decent username from an email' do
|
||||
expect(UserNameSuggester.suggest('bob@bob.com')).to eq('bob')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.suggest' do
|
||||
before do
|
||||
User.stubs(:username_length).returns(3..15)
|
||||
SiteSetting.min_username_length = 3
|
||||
SiteSetting.max_username_length = 15
|
||||
end
|
||||
|
||||
it "doesn't raise an error on nil username" do
|
||||
|
@ -26,10 +20,6 @@ describe UserNameSuggester do
|
|||
expect(UserNameSuggester.suggest("Darth%^Vader")).to eq('Darth_Vader')
|
||||
end
|
||||
|
||||
it "transliterates some characters" do
|
||||
expect(UserNameSuggester.suggest("Jørn")).to eq('Jorn')
|
||||
end
|
||||
|
||||
it 'adds 1 to an existing username' do
|
||||
user = Fabricate(:user)
|
||||
expect(UserNameSuggester.suggest(user.username)).to eq("#{user.username}1")
|
||||
|
@ -39,6 +29,10 @@ describe UserNameSuggester do
|
|||
expect(UserNameSuggester.suggest('a')).to eq('a11')
|
||||
end
|
||||
|
||||
it 'is able to guess a decent username from an email' do
|
||||
expect(UserNameSuggester.suggest('bob@example.com')).to eq('bob')
|
||||
end
|
||||
|
||||
it "has a special case for me and i emails" do
|
||||
expect(UserNameSuggester.suggest('me@eviltrout.com')).to eq('eviltrout')
|
||||
expect(UserNameSuggester.suggest('i@eviltrout.com')).to eq('eviltrout')
|
||||
|
@ -106,6 +100,57 @@ describe UserNameSuggester do
|
|||
User.stubs(:username_length).returns(8..8)
|
||||
expect(UserNameSuggester.suggest('uuuuuuu_u')).to eq('uuuuuuu1')
|
||||
end
|
||||
end
|
||||
|
||||
context "with Unicode usernames disabled" do
|
||||
before { SiteSetting.unicode_usernames = false }
|
||||
|
||||
it "transliterates some characters" do
|
||||
expect(UserNameSuggester.suggest('Jørn')).to eq('Jorn')
|
||||
end
|
||||
|
||||
it "replaces Unicode characters" do
|
||||
expect(UserNameSuggester.suggest('طائر')).to eq('111')
|
||||
expect(UserNameSuggester.suggest('πουλί')).to eq('111')
|
||||
end
|
||||
end
|
||||
|
||||
context "with Unicode usernames enabled" do
|
||||
before { SiteSetting.unicode_usernames = true }
|
||||
|
||||
it "does not transliterate" do
|
||||
expect(UserNameSuggester.suggest("Jørn")).to eq('Jørn')
|
||||
end
|
||||
|
||||
it "does not replace Unicode characters" do
|
||||
expect(UserNameSuggester.suggest('طائر')).to eq('طائر')
|
||||
expect(UserNameSuggester.suggest('πουλί')).to eq('πουλί')
|
||||
end
|
||||
|
||||
it "shortens usernames by counting grapheme clusters" do
|
||||
SiteSetting.max_username_length = 10
|
||||
expect(UserNameSuggester.suggest('बहुत-लंबा-उपयोगकर्ता-नाम')).to eq('बहुत-लंबा-उपयो')
|
||||
end
|
||||
|
||||
it "adds numbers if it's too short" do
|
||||
expect(UserNameSuggester.suggest('鳥')).to eq('鳥11')
|
||||
|
||||
# grapheme cluster consists of 3 code points
|
||||
expect(UserNameSuggester.suggest('য়া')).to eq('য়া11')
|
||||
end
|
||||
|
||||
it "normalizes usernames" do
|
||||
actual = 'Löwe' # NFD, "Lo\u0308we"
|
||||
expected = 'Löwe' # NFC, "L\u00F6we"
|
||||
|
||||
expect(UserNameSuggester.suggest(actual)).to eq(expected)
|
||||
end
|
||||
|
||||
it "does not suggest a username longer than max column size" do
|
||||
SiteSetting.max_username_length = 40
|
||||
|
||||
expect(UserNameSuggester.suggest('য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া'))
|
||||
.to eq('য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া-য়া')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ExternalSystemAvatarsValidator do
|
||||
subject { described_class.new }
|
||||
|
||||
it "disallows disabling external system avatars when Unicode usernames are enabled" do
|
||||
SiteSetting.unicode_usernames = true
|
||||
|
||||
expect(subject.valid_value?("f")).to eq(false)
|
||||
expect(subject.error_message).to eq(I18n.t("site_settings.errors.unicode_usernames_avatars"))
|
||||
|
||||
expect(subject.valid_value?("t")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
end
|
||||
|
||||
it "allows disabling external system avatars when Unicode usernames are disabled" do
|
||||
SiteSetting.unicode_usernames = false
|
||||
|
||||
expect(subject.valid_value?("t")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
|
||||
expect(subject.valid_value?("f")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe UnicodeUsernameValidator do
|
||||
subject { described_class.new }
|
||||
|
||||
it "disallows Unicode usernames when external system avatars are disabled" do
|
||||
SiteSetting.external_system_avatars_enabled = false
|
||||
|
||||
expect(subject.valid_value?("t")).to eq(false)
|
||||
expect(subject.error_message).to eq(I18n.t("site_settings.errors.unicode_usernames_avatars"))
|
||||
|
||||
expect(subject.valid_value?("f")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
end
|
||||
|
||||
it "allows Unicode usernames when external system avatars are enabled" do
|
||||
SiteSetting.external_system_avatars_enabled = true
|
||||
|
||||
expect(subject.valid_value?("t")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
|
||||
expect(subject.valid_value?("f")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
end
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe UnicodeUsernameWhitelistValidator do
|
||||
subject { described_class.new }
|
||||
|
||||
it "allows an empty whitelist" do
|
||||
expect(subject.valid_value?("")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
end
|
||||
|
||||
it "disallows leading and trailing slashes" do
|
||||
expected_error = I18n.t("site_settings.errors.unicode_username_whitelist.leading_trailing_slash")
|
||||
|
||||
expect(subject.valid_value?("/foo/")).to eq(false)
|
||||
expect(subject.error_message).to eq(expected_error)
|
||||
|
||||
expect(subject.valid_value?("foo/")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
|
||||
expect(subject.valid_value?("/foo")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
|
||||
expect(subject.valid_value?("f/o/o")).to eq(true)
|
||||
expect(subject.error_message).to be_blank
|
||||
|
||||
expect(subject.valid_value?("/foo/i")).to eq(false)
|
||||
expect(subject.error_message).to eq(expected_error)
|
||||
end
|
||||
|
||||
it "detects invalid regular expressions" do
|
||||
expected_error = I18n.t("site_settings.errors.unicode_username_whitelist.regex_invalid", error: "")
|
||||
|
||||
expect(subject.valid_value?("\\p{Foo}")).to eq(false)
|
||||
expect(subject.error_message).to start_with(expected_error)
|
||||
end
|
||||
end
|
|
@ -844,4 +844,13 @@ describe Group do
|
|||
group = Group.find(group.id)
|
||||
expect(group.flair_url).to eq("fab fa-bandcamp")
|
||||
end
|
||||
|
||||
context "Unicode usernames and group names" do
|
||||
before { SiteSetting.unicode_usernames = true }
|
||||
|
||||
it "should normalize the name" do
|
||||
group = Fabricate(:group, name: "Bücherwurm") # NFD
|
||||
expect(group.name).to eq("Bücherwurm") # NFC
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -477,13 +477,13 @@ describe User do
|
|||
|
||||
describe 'username format' do
|
||||
def assert_bad(username)
|
||||
user = Fabricate.build(:user)
|
||||
user = Fabricate(:user)
|
||||
user.username = username
|
||||
expect(user.valid?).to eq(false)
|
||||
end
|
||||
|
||||
def assert_good(username)
|
||||
user = Fabricate.build(:user)
|
||||
user = Fabricate(:user)
|
||||
user.username = username
|
||||
expect(user.valid?).to eq(true)
|
||||
end
|
||||
|
@ -494,39 +494,78 @@ describe User do
|
|||
assert_good("abcde")
|
||||
end
|
||||
|
||||
%w{ first.last
|
||||
first first-last
|
||||
_name first_last
|
||||
context 'when Unicode usernames are disabled' do
|
||||
before { SiteSetting.unicode_usernames = false }
|
||||
|
||||
%w{
|
||||
first.last
|
||||
first
|
||||
first-last
|
||||
_name
|
||||
first_last
|
||||
mc.hammer_nose
|
||||
UPPERCASE
|
||||
sgif
|
||||
}.each do |username|
|
||||
it "allows #{username}" do
|
||||
assert_good(username)
|
||||
}.each do |username|
|
||||
it "allows #{username}" do
|
||||
assert_good(username)
|
||||
end
|
||||
end
|
||||
|
||||
%w{
|
||||
traildot.
|
||||
has\ space
|
||||
double__underscore
|
||||
with%symbol
|
||||
Exclamation!
|
||||
@twitter
|
||||
my@email.com
|
||||
.tester
|
||||
sa$sy
|
||||
sam.json
|
||||
sam.xml
|
||||
sam.html
|
||||
sam.htm
|
||||
sam.js
|
||||
sam.woff
|
||||
sam.Png
|
||||
sam.gif
|
||||
}.each do |username|
|
||||
it "disallows #{username}" do
|
||||
assert_bad(username)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
%w{
|
||||
traildot.
|
||||
has\ space
|
||||
double__underscore
|
||||
with%symbol
|
||||
Exclamation!
|
||||
@twitter
|
||||
my@email.com
|
||||
.tester
|
||||
sa$sy
|
||||
sam.json
|
||||
sam.xml
|
||||
sam.html
|
||||
sam.htm
|
||||
sam.js
|
||||
sam.woff
|
||||
sam.Png
|
||||
sam.gif
|
||||
}.each do |username|
|
||||
it "disallows #{username}" do
|
||||
assert_bad(username)
|
||||
context 'when Unicode usernames are enabled' do
|
||||
before { SiteSetting.unicode_usernames = true }
|
||||
|
||||
%w{
|
||||
Джофрэй
|
||||
Джо.фрэй
|
||||
Джофр-эй
|
||||
Д.жофрэй
|
||||
乔夫雷
|
||||
乔夫_雷
|
||||
_乔夫雷
|
||||
}.each do |username|
|
||||
it "allows #{username}" do
|
||||
assert_good(username)
|
||||
end
|
||||
end
|
||||
|
||||
%w{
|
||||
.Джофрэй
|
||||
Джофрэй.
|
||||
Джо\ фрэй
|
||||
Джоф__рэй
|
||||
乔夫雷.js
|
||||
乔夫雷.
|
||||
乔夫%雷
|
||||
}.each do |username|
|
||||
it "disallows #{username}" do
|
||||
assert_bad(username)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -540,12 +579,12 @@ describe User do
|
|||
|
||||
it "should not allow saving if username is reused" do
|
||||
@codinghorror.username = @user.username
|
||||
expect(@codinghorror.save).to eq(false)
|
||||
expect(@codinghorror.save).to eq(false)
|
||||
end
|
||||
|
||||
it "should not allow saving if username is reused in different casing" do
|
||||
@codinghorror.username = @user.username.upcase
|
||||
expect(@codinghorror.save).to eq(false)
|
||||
expect(@codinghorror.save).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -585,6 +624,21 @@ describe User do
|
|||
Fabricate(:group, name: 'foo')
|
||||
expect(User.username_available?('Foo')).to eq(false)
|
||||
end
|
||||
|
||||
context "with Unicode usernames enabled" do
|
||||
before { SiteSetting.unicode_usernames = true }
|
||||
|
||||
it 'returns false when the username is taken, but the Unicode normalization form is different' do
|
||||
Fabricate(:user, username: "L\u00F6we") # NFC
|
||||
requested_username = "Lo\u0308we" # NFD
|
||||
expect(User.username_available?(requested_username)).to eq(false)
|
||||
end
|
||||
|
||||
it 'returns false when the username is taken and the case differs' do
|
||||
Fabricate(:user, username: 'LÖWE')
|
||||
expect(User.username_available?('löwe')).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.reserved_username?' do
|
||||
|
@ -597,7 +651,7 @@ describe User do
|
|||
end
|
||||
|
||||
it 'should not allow usernames matched against an expession' do
|
||||
SiteSetting.reserved_usernames = 'test)|*admin*|foo*|*bar|abc.def'
|
||||
SiteSetting.reserved_usernames = "test)|*admin*|foo*|*bar|abc.def|löwe|ka\u0308fer"
|
||||
|
||||
expect(User.reserved_username?('test')).to eq(false)
|
||||
expect(User.reserved_username?('abc9def')).to eq(false)
|
||||
|
@ -610,6 +664,11 @@ describe User do
|
|||
expect(User.reserved_username?('bar.foo')).to eq(false)
|
||||
expect(User.reserved_username?('foo.bar')).to eq(true)
|
||||
expect(User.reserved_username?('baz.bar')).to eq(true)
|
||||
|
||||
expect(User.reserved_username?('LÖwe')).to eq(true)
|
||||
expect(User.reserved_username?("Lo\u0308we")).to eq(true) # NFD
|
||||
expect(User.reserved_username?('löwe')).to eq(true) # NFC
|
||||
expect(User.reserved_username?('käfer')).to eq(true) # NFC
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -989,6 +1048,14 @@ describe User do
|
|||
expect(found_user).to eq bob
|
||||
end
|
||||
|
||||
it 'finds users with Unicode username' do
|
||||
SiteSetting.unicode_usernames = true
|
||||
user = Fabricate(:user, username: 'löwe')
|
||||
|
||||
expect(User.find_by_username('LÖWE')).to eq(user) # NFC
|
||||
expect(User.find_by_username("LO\u0308WE")).to eq(user) # NFD
|
||||
expect(User.find_by_username("lo\u0308we")).to eq(user) # NFD
|
||||
end
|
||||
end
|
||||
|
||||
describe "#new_user_posting_on_first_day?" do
|
||||
|
@ -1099,9 +1166,9 @@ describe User do
|
|||
before do
|
||||
Jobs.run_immediately!
|
||||
PostCreator.new(Fabricate(:user),
|
||||
raw: 'whatever this is a raw post',
|
||||
topic_id: topic.id,
|
||||
reply_to_post_number: post.post_number).create
|
||||
raw: 'whatever this is a raw post',
|
||||
topic_id: topic.id,
|
||||
reply_to_post_number: post.post_number).create
|
||||
end
|
||||
|
||||
it "resets the `posted_too_much` threshold" do
|
||||
|
@ -1170,7 +1237,7 @@ describe User do
|
|||
expect(user.small_avatar_url).to eq("//test.localhost/letter_avatar/sam/45/#{LetterAvatar.version}.png")
|
||||
|
||||
SiteSetting.external_system_avatars_enabled = true
|
||||
expect(user.small_avatar_url).to eq("//test.localhost/letter_avatar_proxy/v3/letter/s/5f9b8f/45.png")
|
||||
expect(user.small_avatar_url).to eq("//test.localhost/letter_avatar_proxy/v4/letter/s/5f9b8f/45.png")
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1193,7 +1260,7 @@ describe User do
|
|||
describe "update_posts_read!" do
|
||||
context "with a UserVisit record" do
|
||||
let!(:user) { Fabricate(:user) }
|
||||
let!(:now) { Time.zone.now }
|
||||
let!(:now) { Time.zone.now }
|
||||
before { user.update_last_seen!(now) }
|
||||
|
||||
it "with existing UserVisit record, increments the posts_read value" do
|
||||
|
@ -1301,17 +1368,17 @@ describe User do
|
|||
|
||||
before do
|
||||
PostCreator.new(Discourse.system_user,
|
||||
title: "Welcome to our Discourse",
|
||||
raw: "This is a welcome message",
|
||||
archetype: Archetype.private_message,
|
||||
target_usernames: [unactivated_old_with_system_pm.username],
|
||||
title: "Welcome to our Discourse",
|
||||
raw: "This is a welcome message",
|
||||
archetype: Archetype.private_message,
|
||||
target_usernames: [unactivated_old_with_system_pm.username],
|
||||
).create
|
||||
|
||||
PostCreator.new(user,
|
||||
title: "Welcome to our Discourse",
|
||||
raw: "This is a welcome message",
|
||||
archetype: Archetype.private_message,
|
||||
target_usernames: [unactivated_old_with_human_pm.username],
|
||||
title: "Welcome to our Discourse",
|
||||
raw: "This is a welcome message",
|
||||
archetype: Archetype.private_message,
|
||||
target_usernames: [unactivated_old_with_human_pm.username],
|
||||
).create
|
||||
end
|
||||
|
||||
|
@ -1357,10 +1424,10 @@ describe User do
|
|||
|
||||
let!(:group) {
|
||||
Fabricate(:group,
|
||||
automatic_membership_email_domains: "bar.com|wat.com",
|
||||
grant_trust_level: 1,
|
||||
title: "bars and wats",
|
||||
primary_group: true
|
||||
automatic_membership_email_domains: "bar.com|wat.com",
|
||||
grant_trust_level: 1,
|
||||
title: "bars and wats",
|
||||
primary_group: true
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1394,10 +1461,10 @@ describe User do
|
|||
|
||||
it "get attributes from the group" do
|
||||
user = Fabricate.build(:user,
|
||||
active: true,
|
||||
trust_level: 0,
|
||||
email: "foo@bar.com",
|
||||
password: "strongpassword4Uguys"
|
||||
active: true,
|
||||
trust_level: 0,
|
||||
email: "foo@bar.com",
|
||||
password: "strongpassword4Uguys"
|
||||
)
|
||||
|
||||
user.password_required!
|
||||
|
@ -1659,8 +1726,8 @@ describe User do
|
|||
end.first
|
||||
|
||||
expect(message.data[:recent]).to eq([
|
||||
[notification2.id, true], [notification.id, false]
|
||||
])
|
||||
[notification2.id, true], [notification.id, false]
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -2015,4 +2082,26 @@ describe User do
|
|||
expect(Discourse.system_user).not_to be_human
|
||||
end
|
||||
end
|
||||
|
||||
context "Unicode username" do
|
||||
before { SiteSetting.unicode_usernames = true }
|
||||
|
||||
let(:user) { Fabricate(:user, username: "Lo\u0308we") } # NFD
|
||||
|
||||
it "normalizes usernames" do
|
||||
expect(user.username).to eq("L\u00F6we") # NFC
|
||||
expect(user.username_lower).to eq("l\u00F6we") # NFC
|
||||
end
|
||||
|
||||
describe ".username_exists?" do
|
||||
it "normalizes username before executing query" do
|
||||
expect(User.username_exists?(user.username)).to eq(true)
|
||||
expect(User.username_exists?("Lo\u0308we")).to eq(true) # NFD
|
||||
expect(User.username_exists?("L\u00F6we")).to eq(true) # NFC
|
||||
expect(User.username_exists?("LO\u0308WE")).to eq(true) # NFD
|
||||
expect(User.username_exists?("l\u00D6wE")).to eq(true) # NFC
|
||||
expect(User.username_exists?("foo")).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,17 +1,205 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe UsernameValidator do
|
||||
context "#valid_format?" do
|
||||
it 'returns true when username is both valid and available' do
|
||||
expect(UsernameValidator.new('Available').valid_format?).to eq true
|
||||
def expect_valid(*usernames)
|
||||
usernames.each do |username|
|
||||
validator = UsernameValidator.new(username)
|
||||
|
||||
aggregate_failures do
|
||||
expect(validator.valid_format?).to eq(true), "expected '#{username}' to be valid"
|
||||
expect(validator.errors).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expect_invalid(*usernames, error_message:)
|
||||
usernames.each do |username|
|
||||
validator = UsernameValidator.new(username)
|
||||
|
||||
aggregate_failures do
|
||||
expect(validator.valid_format?).to eq(false), "expected '#{username}' to be invalid"
|
||||
expect(validator.errors).to include(error_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'ASCII username' do
|
||||
it 'is invalid when the username is blank' do
|
||||
expect_invalid('', error_message: I18n.t(:'user.username.blank'))
|
||||
end
|
||||
|
||||
it 'returns true when the username is valid but not available' do
|
||||
expect(UsernameValidator.new(Fabricate(:user).username).valid_format?).to eq true
|
||||
it 'is invalid when the username is too short' do
|
||||
SiteSetting.min_username_length = 4
|
||||
|
||||
expect_invalid('a', 'ab', 'abc',
|
||||
error_message: I18n.t(:'user.username.short', min: 4))
|
||||
end
|
||||
|
||||
it 'returns false when the username is not valid' do
|
||||
expect(UsernameValidator.new('not valid.name').valid_format?).to eq false
|
||||
it 'is valid when the username has the minimum lenght' do
|
||||
SiteSetting.min_username_length = 4
|
||||
|
||||
expect_valid('abcd')
|
||||
end
|
||||
|
||||
it 'is invalid when the username is too long' do
|
||||
SiteSetting.max_username_length = 8
|
||||
|
||||
expect_invalid('abcdefghi',
|
||||
error_message: I18n.t(:'user.username.long', max: 8))
|
||||
end
|
||||
|
||||
it 'is valid when the username has the maximum lenght' do
|
||||
SiteSetting.max_username_length = 8
|
||||
|
||||
expect_valid('abcdefgh')
|
||||
end
|
||||
|
||||
it 'is valid when the username contains alphanumeric characters, dots, underscores and dashes' do
|
||||
expect_valid('ab-cd.123_ABC-xYz')
|
||||
end
|
||||
|
||||
it 'is invalid when the username contains non-alphanumeric characters other than dots, underscores and dashes' do
|
||||
expect_invalid('abc|', 'a#bc', 'abc xyz',
|
||||
error_message: I18n.t(:'user.username.characters'))
|
||||
end
|
||||
|
||||
it 'is valid when the username starts with a alphanumeric character or underscore' do
|
||||
expect_valid('abcd', '1abc', '_abc')
|
||||
end
|
||||
|
||||
it 'is invalid when the username starts with a dot or dash' do
|
||||
expect_invalid('.abc', '-abc',
|
||||
error_message: I18n.t(:'user.username.must_begin_with_alphanumeric_or_underscore'))
|
||||
end
|
||||
|
||||
it 'is valid when the username ends with a alphanumeric character' do
|
||||
expect_valid('abcd', 'abc9')
|
||||
end
|
||||
|
||||
it 'is invalid when the username ends with an underscore, a dot or dash' do
|
||||
expect_invalid('abc_', 'abc.', 'abc-',
|
||||
error_message: I18n.t(:'user.username.must_end_with_alphanumeric'))
|
||||
end
|
||||
|
||||
it 'is invalid when the username contains consecutive underscores, dots or dashes' do
|
||||
expect_invalid('a__bc', 'a..bc', 'a--bc',
|
||||
error_message: I18n.t(:'user.username.must_not_contain_two_special_chars_in_seq'))
|
||||
end
|
||||
|
||||
it 'is invalid when the username ends with certain file extensions' do
|
||||
expect_invalid('abc.json', 'abc.png',
|
||||
error_message: I18n.t(:'user.username.must_not_end_with_confusing_suffix'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Unicode usernames are disabled' do
|
||||
before { SiteSetting.unicode_usernames = false }
|
||||
|
||||
include_examples 'ASCII username'
|
||||
|
||||
it 'is invalid when the username contains non-ASCII characters except dots, underscores and dashes' do
|
||||
expect_invalid('abcö', 'abc象',
|
||||
error_message: I18n.t(:'user.username.characters'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Unicode usernames are enabled' do
|
||||
before { SiteSetting.unicode_usernames = true }
|
||||
|
||||
context "ASCII usernames" do
|
||||
include_examples 'ASCII username'
|
||||
end
|
||||
|
||||
context "Unicode usernames" do
|
||||
before { SiteSetting.min_username_length = 1 }
|
||||
|
||||
it 'is invalid when the username is too short' do
|
||||
SiteSetting.min_username_length = 3
|
||||
|
||||
expect_invalid('鳥', 'পাখি',
|
||||
error_message: I18n.t(:'user.username.short', min: 3))
|
||||
end
|
||||
|
||||
it 'is valid when the username has the minimum lenght' do
|
||||
SiteSetting.min_username_length = 2
|
||||
|
||||
expect_valid('পাখি', 'طائر')
|
||||
end
|
||||
|
||||
it 'is invalid when the username is too long' do
|
||||
SiteSetting.max_username_length = 8
|
||||
|
||||
expect_invalid('חוטב_עצים', 'Holzfäller',
|
||||
error_message: I18n.t(:'user.username.long', max: 8))
|
||||
end
|
||||
|
||||
it 'is valid when the username has the maximum lenght' do
|
||||
SiteSetting.max_username_length = 9
|
||||
|
||||
expect_valid('Дровосек', 'چوب-لباسی', 'தமிழ்-தமிழ்')
|
||||
end
|
||||
|
||||
it 'is invalid when the username has too many Unicode codepoints' do
|
||||
SiteSetting.max_username_length = 30
|
||||
|
||||
expect_invalid('য়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়ায়া',
|
||||
error_message: I18n.t(:'user.username.too_long'))
|
||||
end
|
||||
|
||||
it 'is valid when the username contains Unicode letters' do
|
||||
expect_valid('鳥', 'طائر', 'թռչուն', 'πουλί', 'পাখি', 'madár', '새',
|
||||
'پرنده', 'птица', 'fågel', 'นก', 'پرندے', 'ציפור')
|
||||
end
|
||||
|
||||
it 'is valid when the username contains numbers from the Nd or Nl Unicode category' do
|
||||
expect_valid('arabic٠١٢٣٤٥٦٧٨٩', 'bengali০১২৩৪৫৬৭৮৯', 'romanⅥ', 'hangzhou〺')
|
||||
end
|
||||
|
||||
it 'is invalid when the username contains numbers from the No Unicode category' do
|
||||
expect_invalid('circled㊸', 'fraction¾',
|
||||
error_message: I18n.t(:'user.username.characters'))
|
||||
end
|
||||
|
||||
it 'is invalid when the username contains symbols or emojis' do
|
||||
SiteSetting.min_username_length = 1
|
||||
|
||||
expect_invalid('©', '⇨', '“', '±', '‿', '😃', '🚗',
|
||||
error_message: I18n.t(:'user.username.characters'))
|
||||
end
|
||||
|
||||
it 'is invalid when the username contains zero width join characters' do
|
||||
expect_invalid('ണ്', 'র্যাম',
|
||||
error_message: I18n.t(:'user.username.characters'))
|
||||
end
|
||||
|
||||
it 'is valid when the username ends with a Unicode Mark' do
|
||||
expect_valid('தமிழ்')
|
||||
end
|
||||
|
||||
it 'allows all Unicode letters when the whitelist is empty' do
|
||||
expect_valid('鳥')
|
||||
end
|
||||
|
||||
context "with Unicode whitelist" do
|
||||
before { SiteSetting.unicode_username_character_whitelist = "[äöüÄÖÜß]" }
|
||||
|
||||
it 'is invalid when username contains non-whitelisted letters' do
|
||||
expect_invalid('鳥', 'francès', error_message: I18n.t(:'user.username.characters'))
|
||||
end
|
||||
|
||||
it 'is valid when username contains only whitelisted letters' do
|
||||
expect_valid('Löwe', 'Ötzi')
|
||||
end
|
||||
|
||||
it 'is valid when username contains only ASCII letters and numbers regardless of whitelist' do
|
||||
expect_valid('a-z_A-Z.0-9')
|
||||
end
|
||||
|
||||
it 'is valid after resetting the site setting' do
|
||||
SiteSetting.unicode_username_character_whitelist = ""
|
||||
expect_valid('鳥')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -97,7 +97,7 @@ describe UsernameChanger do
|
|||
|
||||
block.call(post) if block
|
||||
|
||||
UsernameChanger.change(user, 'bar')
|
||||
UsernameChanger.change(user, args[:target_username] || 'bar')
|
||||
post.reload
|
||||
end
|
||||
|
||||
|
@ -130,13 +130,13 @@ describe UsernameChanger do
|
|||
|
||||
expect(post.raw).to eq(".@bar -@bar %@bar _@bar ,@bar ;@bar @@bar")
|
||||
expect(post.cooked).to match_html(<<~HTML)
|
||||
<p>.<a class="mention" href="/u/bar">@bar</a>
|
||||
-<a class="mention" href="/u/bar">@bar</a>
|
||||
%<a class="mention" href="/u/bar">@bar</a>
|
||||
_<a class="mention" href="/u/bar">@bar</a>
|
||||
,<a class="mention" href="/u/bar">@bar</a>
|
||||
;<a class="mention" href="/u/bar">@bar</a>
|
||||
@<a class="mention" href="/u/bar">@bar</a></p>
|
||||
<p>.<a class="mention" href="/u/bar">@bar</a>
|
||||
-<a class="mention" href="/u/bar">@bar</a>
|
||||
%<a class="mention" href="/u/bar">@bar</a>
|
||||
_<a class="mention" href="/u/bar">@bar</a>
|
||||
,<a class="mention" href="/u/bar">@bar</a>
|
||||
;<a class="mention" href="/u/bar">@bar</a>
|
||||
@<a class="mention" href="/u/bar">@bar</a></p>
|
||||
HTML
|
||||
end
|
||||
|
||||
|
@ -147,38 +147,55 @@ describe UsernameChanger do
|
|||
expect(post.cooked).to eq(%Q(<p>“<a class="mention" href="/u/bar">@bar</a>” ‘<a class="mention" href="/u/bar">@bar</a>’</p>))
|
||||
end
|
||||
|
||||
it 'replaces mentions when there are trailing symbols' do
|
||||
post = create_post_and_change_username(raw: "@foo. @foo, @foo: @foo; @foo-")
|
||||
it 'replaces Markdown formatted mentions' do
|
||||
post = create_post_and_change_username(raw: "**@foo** *@foo* _@foo_ ~~@foo~~")
|
||||
|
||||
expect(post.raw).to eq("@bar. @bar, @bar: @bar; @bar-")
|
||||
expect(post.raw).to eq("**@bar** *@bar* _@bar_ ~~@bar~~")
|
||||
expect(post.cooked).to match_html(<<~HTML)
|
||||
<p><a class="mention" href="/u/bar">@bar</a>.
|
||||
<a class="mention" href="/u/bar">@bar</a>,
|
||||
<a class="mention" href="/u/bar">@bar</a>:
|
||||
<a class="mention" href="/u/bar">@bar</a>;
|
||||
<a class="mention" href="/u/bar">@bar</a>-</p>
|
||||
<p><strong><a class="mention" href="/u/bar">@bar</a></strong>
|
||||
<em><a class="mention" href="/u/bar">@bar</a></em>
|
||||
<em><a class="mention" href="/u/bar">@bar</a></em>
|
||||
<s><a class="mention" href="/u/bar">@bar</a></s></p>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'does not replace mention when followed by an underscore' do
|
||||
post = create_post_and_change_username(raw: "@foo_")
|
||||
it 'replaces mentions when there are trailing symbols' do
|
||||
post = create_post_and_change_username(raw: "@foo. @foo, @foo: @foo; @foo_ @foo-")
|
||||
|
||||
expect(post.raw).to eq("@foo_")
|
||||
expect(post.cooked).to eq(%Q(<p><span class="mention">@foo_</span></p>))
|
||||
expect(post.raw).to eq("@bar. @bar, @bar: @bar; @bar_ @bar-")
|
||||
expect(post.cooked).to match_html(<<~HTML)
|
||||
<p><a class="mention" href="/u/bar">@bar</a>.
|
||||
<a class="mention" href="/u/bar">@bar</a>,
|
||||
<a class="mention" href="/u/bar">@bar</a>:
|
||||
<a class="mention" href="/u/bar">@bar</a>;
|
||||
<a class="mention" href="/u/bar">@bar</a>_
|
||||
<a class="mention" href="/u/bar">@bar</a>-</p>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'does not replace mention in cooked when mention contains a trailing underscore' do
|
||||
# Older versions of Discourse detected a trailing underscore as part of a username.
|
||||
# That doesn't happen anymore, so we need to do create the `cooked` for this test manually.
|
||||
post = create_post_and_change_username(raw: "@foobar @foo") do |p|
|
||||
p.update_columns(raw: p.raw.gsub("@foobar", "@foo_"), cooked: p.cooked.gsub("@foobar", "@foo_"))
|
||||
end
|
||||
|
||||
expect(post.raw).to eq("@bar_ @bar")
|
||||
expect(post.cooked).to eq(%Q(<p><span class="mention">@foo_</span> <a class="mention" href="/u/bar">@bar</a></p>))
|
||||
end
|
||||
|
||||
it 'does not replace mentions when there are leading alphanumeric chars' do
|
||||
post = create_post_and_change_username(raw: "a@foo 2@foo")
|
||||
post = create_post_and_change_username(raw: "@foo a@foo 2@foo")
|
||||
|
||||
expect(post.raw).to eq("a@foo 2@foo")
|
||||
expect(post.cooked).to eq(%Q(<p>a@foo 2@foo</p>))
|
||||
expect(post.raw).to eq("@bar a@foo 2@foo")
|
||||
expect(post.cooked).to eq(%Q(<p><a class="mention" href="/u/bar">@bar</a> a@foo 2@foo</p>))
|
||||
end
|
||||
|
||||
it 'does not replace username within email address' do
|
||||
post = create_post_and_change_username(raw: "mail@foo.com")
|
||||
post = create_post_and_change_username(raw: "@foo mail@foo.com")
|
||||
|
||||
expect(post.raw).to eq("mail@foo.com")
|
||||
expect(post.cooked).to eq(%Q(<p><a href="mailto:mail@foo.com">mail@foo.com</a></p>))
|
||||
expect(post.raw).to eq("@bar mail@foo.com")
|
||||
expect(post.cooked).to eq(%Q(<p><a class="mention" href="/u/bar">@bar</a> <a href="mailto:mail@foo.com">mail@foo.com</a></p>))
|
||||
end
|
||||
|
||||
it 'does not replace username in a mention of a similar username' do
|
||||
|
@ -191,11 +208,11 @@ describe UsernameChanger do
|
|||
|
||||
expect(post.raw).to eq("@bar @foobar @foo-bar @foo_bar @foo1")
|
||||
expect(post.cooked).to match_html(<<~HTML)
|
||||
<p><a class="mention" href="/u/bar">@bar</a>
|
||||
<a class="mention" href="/u/foobar">@foobar</a>
|
||||
<a class="mention" href="/u/foo-bar">@foo-bar</a>
|
||||
<a class="mention" href="/u/foo_bar">@foo_bar</a>
|
||||
<a class="mention" href="/u/foo1">@foo1</a></p>
|
||||
<p><a class="mention" href="/u/bar">@bar</a>
|
||||
<a class="mention" href="/u/foobar">@foobar</a>
|
||||
<a class="mention" href="/u/foo-bar">@foo-bar</a>
|
||||
<a class="mention" href="/u/foo_bar">@foo_bar</a>
|
||||
<a class="mention" href="/u/foo1">@foo1</a></p>
|
||||
HTML
|
||||
end
|
||||
|
||||
|
@ -253,6 +270,44 @@ describe UsernameChanger do
|
|||
expect(post.raw).to eq('<a class="mention">@bar</a> and <a class="mention">@someuser</a>')
|
||||
expect(post.cooked).to match_html('<p><a class="mention">@bar</a> and <a class="mention">@someuser</a></p>')
|
||||
end
|
||||
|
||||
context "Unicode usernames" do
|
||||
before { SiteSetting.unicode_usernames = true }
|
||||
let(:user) { Fabricate(:user, username: 'թռչուն') }
|
||||
|
||||
it 'it correctly updates mentions' do
|
||||
post = create_post_and_change_username(raw: "Hello @թռչուն", target_username: 'птица')
|
||||
|
||||
expect(post.raw).to eq("Hello @птица")
|
||||
expect(post.cooked).to eq(%Q(<p>Hello <a class="mention" href="/u/%D0%BF%D1%82%D0%B8%D1%86%D0%B0">@птица</a></p>))
|
||||
end
|
||||
|
||||
it 'does not replace mentions when there are leading alphanumeric chars' do
|
||||
post = create_post_and_change_username(raw: "Hello @թռչուն 鳥@թռչուն 2@թռչուն ٩@թռչուն", target_username: 'птица')
|
||||
|
||||
expect(post.raw).to eq("Hello @птица 鳥@թռչուն 2@թռչուն ٩@թռչուն")
|
||||
expect(post.cooked).to eq(%Q(<p>Hello <a class="mention" href="/u/%D0%BF%D1%82%D0%B8%D1%86%D0%B0">@птица</a> 鳥@թռչուն 2@թռչուն ٩@թռչուն</p>))
|
||||
end
|
||||
|
||||
it 'does not replace username in a mention of a similar username' do
|
||||
Fabricate(:user, username: 'թռչուն鳥')
|
||||
Fabricate(:user, username: 'թռչուն-鳥')
|
||||
Fabricate(:user, username: 'թռչուն_鳥')
|
||||
Fabricate(:user, username: 'թռչուն٩')
|
||||
|
||||
post = create_post_and_change_username(raw: "@թռչուն @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩", target_username: 'птица')
|
||||
|
||||
expect(post.raw).to eq("@птица @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩")
|
||||
expect(post.cooked).to match_html(<<~HTML)
|
||||
<p><a class="mention" href="/u/%D0%BF%D1%82%D0%B8%D1%86%D0%B0">@птица</a>
|
||||
<a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6%E9%B3%A5">@թռչուն鳥</a>
|
||||
<a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6-%E9%B3%A5">@թռչուն-鳥</a>
|
||||
<a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6_%E9%B3%A5">@թռչուն_鳥</a>
|
||||
<a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6%D9%A9">@թռչուն٩</a></p>
|
||||
HTML
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context 'quotes' do
|
||||
|
|
|
@ -94,7 +94,8 @@ Discourse.SiteSettingsOriginal = {
|
|||
emoji_set: "emoji_one",
|
||||
desktop_category_page_style: "categories_and_latest_topics",
|
||||
enable_mentions: true,
|
||||
enable_personal_messages: true
|
||||
enable_personal_messages: true,
|
||||
unicode_usernames: false
|
||||
};
|
||||
Discourse.SiteSettings = jQuery.extend(
|
||||
true,
|
||||
|
|
|
@ -555,6 +555,48 @@ QUnit.test("Mentions", assert => {
|
|||
'<p><small>a <span class="mention">@sam</span> c</small></p>',
|
||||
"it allows mentions within HTML tags"
|
||||
);
|
||||
|
||||
assert.cooked(
|
||||
"@_sam @1sam @ab-cd.123_ABC-xYz @sam1",
|
||||
'<p><span class="mention">@_sam</span> <span class="mention">@1sam</span> <span class="mention">@ab-cd.123_ABC-xYz</span> <span class="mention">@sam1</span></p>',
|
||||
"it detects mentions of valid usernames"
|
||||
);
|
||||
|
||||
assert.cooked(
|
||||
"@.sam @-sam @sam. @sam_ @sam-",
|
||||
'<p>@.sam @-sam <span class="mention">@sam</span>. <span class="mention">@sam</span>_ <span class="mention">@sam</span>-</p>',
|
||||
"it does not detect mentions of invalid usernames"
|
||||
);
|
||||
|
||||
assert.cookedOptions(
|
||||
"Hello @狮子",
|
||||
{ siteSettings: { unicode_usernames: false } },
|
||||
"<p>Hello @狮子</p>",
|
||||
"it does not detect mentions of Unicode usernames"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Mentions - Unicode usernames enabled", assert => {
|
||||
assert.cookedOptions(
|
||||
"Hello @狮子",
|
||||
{ siteSettings: { unicode_usernames: true } },
|
||||
'<p>Hello <span class="mention">@狮子</span></p>',
|
||||
"it detects mentions of Unicode usernames"
|
||||
);
|
||||
|
||||
assert.cookedOptions(
|
||||
"@狮子 @_狮子 @1狮子 @狮-ø.١٢٣_Ö-ழ் @狮子1",
|
||||
{ siteSettings: { unicode_usernames: true } },
|
||||
'<p><span class="mention">@狮子</span> <span class="mention">@_狮子</span> <span class="mention">@1狮子</span> <span class="mention">@狮-ø.١٢٣_Ö-ழ்</span> <span class="mention">@狮子1</span></p>',
|
||||
"it detects mentions of valid Unicode usernames"
|
||||
);
|
||||
|
||||
assert.cookedOptions(
|
||||
"@.狮子 @-狮子 @狮子. @狮子_ @狮子-",
|
||||
{ siteSettings: { unicode_usernames: true } },
|
||||
'<p>@.狮子 @-狮子 <span class="mention">@狮子</span>. <span class="mention">@狮子</span>_ <span class="mention">@狮子</span>-</p>',
|
||||
"it does not detect mentions of invalid Unicode usernames"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Mentions - disabled", assert => {
|
||||
|
|
Loading…
Reference in New Issue