diff --git a/liberapay/models/community.py b/liberapay/models/community.py index e7b8cea56b..aa4e30b121 100644 --- a/liberapay/models/community.py +++ b/liberapay/models/community.py @@ -5,6 +5,7 @@ from psycopg2 import IntegrityError from liberapay.exceptions import CommunityAlreadyExists, InvalidCommunityName +from liberapay.utils.unconfusable import unconfusable_string name_maxlength = 40 @@ -38,8 +39,18 @@ def create(cls, name, creator_id, lang='mul'): name = unicodedata.normalize('NFKC', name) if name_re.match(name) is None: raise InvalidCommunityName(name) + try: with cls.db.get_cursor() as cursor: + unconfusable_name = unconfusable_string(name) + all_names = cursor.all(""" + SELECT name + FROM communities + """) + for existing_name in all_names: + if unconfusable_name == unconfusable_string(existing_name): + raise CommunityAlreadyExists + p_id = cursor.one(""" INSERT INTO participants (kind, status, join_time) diff --git a/liberapay/utils/unconfusable.py b/liberapay/utils/unconfusable.py new file mode 100644 index 0000000000..98495186ae --- /dev/null +++ b/liberapay/utils/unconfusable.py @@ -0,0 +1,13 @@ +from confusable_homoglyphs import confusables + +# Convert an Unicode string to its equivalent replacing all confusable homoglyphs +# to its common/latin equivalent +def unconfusable_string(s): + unconfusable_string = '' + for c in s: + confusable = confusables.is_confusable(c, preferred_aliases=['COMMON', 'LATIN']) + if confusable: + # if the character is confusable we replace it with the first prefered alias + c = confusable[0]['homoglyphs'][0]['c'] + unconfusable_string += c + return unconfusable_string diff --git a/requirements_base.txt b/requirements_base.txt index b4e1847e87..4175dbeebf 100644 --- a/requirements_base.txt +++ b/requirements_base.txt @@ -243,3 +243,7 @@ cryptography==2.4.2 \ asn1crypto==0.24.0 \ --hash=sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87 \ --hash=sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49 + +confusable_homoglyphs==3.2.0 \ + --hash=sha256:3b4a0d9fa510669498820c91a0bfc0c327568cecec90648cf3819d4a6fc6a751 \ + --hash=sha256:e3ce611028d882b74a5faa69e3cbb5bd4dcd9f69936da6e73d33eda42c917944 diff --git a/tests/py/test_communities.py b/tests/py/test_communities.py index 22e197b809..ab3ce5d98c 100644 --- a/tests/py/test_communities.py +++ b/tests/py/test_communities.py @@ -1,6 +1,6 @@ import json -from liberapay.exceptions import AuthRequired +from liberapay.exceptions import AuthRequired, CommunityAlreadyExists from liberapay.models.community import Community from liberapay.testing import Harness @@ -101,6 +101,22 @@ def test_join_and_leave(self): communities = self.bob.get_communities() assert len(communities) == 0 + def test_create_community_already_taken(self): + with self.assertRaises(CommunityAlreadyExists): + Community.create('test', self.alice.id) + + def test_create_community_already_taken_is_case_insensitive(self): + with self.assertRaises(CommunityAlreadyExists): + Community.create('TeSt', self.alice.id) + + def test_create_community_already_taken_with_confusable_homoglyphs(self): + latin_string = 'AlaskaJazz' + mixed_string = 'ΑlaskaJazz' + + Community.create(latin_string, self.bob.id) + with self.assertRaises(CommunityAlreadyExists): + Community.create(mixed_string, self.alice.id) + class TestCommunityEdit(Harness): diff --git a/tests/py/test_utils.py b/tests/py/test_utils.py index cc12787f85..6a5cab5e15 100644 --- a/tests/py/test_utils.py +++ b/tests/py/test_utils.py @@ -7,7 +7,7 @@ from liberapay import utils from liberapay.i18n.currencies import Money, MoneyBasket from liberapay.testing import Harness -from liberapay.utils import markdown, b64encode_s, b64decode_s, cbor +from liberapay.utils import markdown, b64encode_s, b64decode_s, cbor, unconfusable from liberapay.wireup import CSP @@ -244,3 +244,14 @@ def test_csp_handles_valueless_directives_correctly(self): csp2 = CSP(csp) assert csp == csp2 assert csp2.directives[b'upgrade-insecure-requests'] == b'' + + # Unconfusable + # ============ + + def test_unconfusable_string(self): + self.assertEqual('user2', unconfusable.unconfusable_string('user2')) + self.assertEqual('alice', unconfusable.unconfusable_string('alice')) + latin_string = 'AlaskaJazz' + mixed_string = 'ΑlaskaJazz' + self.assertNotEqual(latin_string, mixed_string) + self.assertEqual(latin_string, unconfusable.unconfusable_string(mixed_string))