Skip to content

Commit 8445fc7

Browse files
committed
apply new logic, add check for notable domains, add tests
1 parent baa5c5f commit 8445fc7

7 files changed

Lines changed: 172 additions & 22 deletions

File tree

osf/external/spam/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
DOMAIN_REGEX = re.compile(r'\W*(?P<protocol>\w+://)?(?P<www>www\.)?(?P<domain>([\w-]+\.)+[a-zA-Z]+)(?P<path>[/\-\.\w]*)?\W*')
1919
REDIRECT_CODES = {301, 302, 303, 307, 308}
20-
20+
NOTABLE_POST_NOMINALS = ['m.sc.', 'msc.', 'phd.', 'ph.d.', 'msc.pt', 'pt.', 'prof.', 'dr.', 'md.', 'jd.', 'esq.']
2121

2222
@celery_app.task()
2323
def reclassify_domain_references(notable_domain_id, current_note, previous_note):

osf/models/user.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from .institution import Institution
5555
from .institution_affiliation import InstitutionAffiliation
5656
from .mixins import AddonModelMixin, ShareIndexMixin
57-
from .spam import SpamMixin
57+
from .spam import SpamMixin, SpamStatus
5858
from .session import UserSessionMap
5959
from .tag import Tag
6060
from .validators import validate_email, validate_social, validate_history_item, has_domain_in_user_fields_for_names
@@ -1056,8 +1056,10 @@ def save(self, *args, **kwargs):
10561056

10571057
was_creating = self._state.adding
10581058
has_domain = False
1059+
should_mark_ham = False
10591060
if not self.is_spammy:
1060-
has_domain = has_domain_in_user_fields_for_names(self)
1061+
has_domain, status = has_domain_in_user_fields_for_names(self)
1062+
should_mark_ham = True if status == SpamStatus.HAM else False
10611063

10621064
if has_domain and was_creating:
10631065
raise ValidationError('Invalid personal information.')
@@ -1068,11 +1070,14 @@ def save(self, *args, **kwargs):
10681070
dirty_fields = self.get_dirty_fields(check_relationship=True)
10691071
ret = super().save(*args, **kwargs) # must save BEFORE spam check, as user needs guid.
10701072

1071-
if has_domain and self.is_hammy:
1072-
self.flag_spam()
1073+
if has_domain:
1074+
if self.is_hammy:
1075+
self.flag_spam()
1076+
if not self.is_hammy:
1077+
self.confirm_spam()
10731078

1074-
if has_domain and not was_creating and not self.is_hammy:
1075-
self.confirm_spam()
1079+
if was_creating and should_mark_ham:
1080+
self.confirm_ham()
10761081

10771082
if set(self.SPAM_USER_PROFILE_FIELDS.keys()).intersection(dirty_fields):
10781083
request = get_current_request()

osf/models/validators.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import re
2-
from urllib.parse import urlsplit
32
import waffle
4-
3+
from django.db.models import Q
54
from jsonschema import ValidationError as JsonSchemaValidationError, SchemaError, Draft7Validator, validate, validators
65
from django.conf import settings
76
from django.core.validators import URLValidator, validate_email as django_validate_email
@@ -12,9 +11,10 @@
1211
from osf.utils.registrations import FILE_VIEW_URL_REGEX
1312
from osf.utils.sanitize import strip_html
1413
from osf.exceptions import ValidationError, ValidationValueError, reraise_django_validation_errors, BlockedEmailError
15-
from osf.external.spam.tasks import DOMAIN_REGEX
14+
from osf.external.spam.tasks import DOMAIN_REGEX, NOTABLE_POST_NOMINALS
1615

1716
from website.language import SWITCH_VALIDATOR_ERROR
17+
from urlextract import URLExtract
1818

1919

2020
def validate_history_item(items):
@@ -115,18 +115,32 @@ def validate_email(value):
115115

116116

117117
def has_domain_in_user_fields_for_names(user):
118+
from osf.models import NotableDomain
119+
from .spam import SpamStatus
120+
notable_domain_list = set(
121+
NotableDomain.objects.filter(
122+
Q(note=NotableDomain.Note.ASSUME_HAM_UNTIL_REPORTED) |
123+
Q(note=NotableDomain.Note.IGNORED)
124+
)
125+
.values_list('domain', flat=True)
126+
.distinct()
127+
)
118128
name_content = ' '.join(getattr(user, field) or '' for field in user.DOMAIN_VALIDATION_FIELDS).strip()
119129
for match in DOMAIN_REGEX.finditer(name_content):
120130
candidate = match.group(0).strip()
131+
if candidate in notable_domain_list:
132+
return False, SpamStatus.HAM
121133
if looks_like_url(candidate):
122-
return True
123-
return False
134+
return True, SpamStatus.SPAM
135+
return False, SpamStatus.UNKNOWN
124136

125137
def looks_like_url(candidate):
126-
if candidate.startswith('www.'):
127-
candidate = f'https://{candidate}'
128-
parsed = urlsplit(candidate)
129-
return bool(parsed.scheme and parsed.netloc)
138+
candidate = candidate.strip()
139+
words = re.findall(r'[A-Za-z.]+', candidate.lower())
140+
if any(word in NOTABLE_POST_NOMINALS for word in words):
141+
return False
142+
extractor = URLExtract()
143+
return bool(extractor.find_urls(candidate))
130144

131145
def validate_subject_hierarchy_length(parent):
132146
from osf.models import Subject

osf_tests/test_user.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,6 +2086,57 @@ def test_validate_domains_field(self):
20862086
setattr(self.user, field, 'not a url')
20872087
self.user.save()
20882088

2089+
def test_validate_domain_fields_notable_domain_in_field(self):
2090+
NotableDomain.objects.get_or_create(domain='google.com', note=NotableDomain.Note.IGNORED)
2091+
user = OSFUser.create_unregistered(
2092+
fullname='google.com',
2093+
)
2094+
user.save()
2095+
assert user.is_spammy is False
2096+
assert user.is_hammy is True
2097+
2098+
def test_validate_domain_fields_notable_domain_in_field_for_existing_ham(self):
2099+
NotableDomain.objects.get_or_create(domain='google.com', note=NotableDomain.Note.IGNORED)
2100+
self.user.fullname = 'google.com'
2101+
self.user.confirm_ham(save=True)
2102+
self.user.save()
2103+
assert self.user.is_spammy is False
2104+
assert self.user.is_hammy is True
2105+
2106+
self.user.unspam(save=True)
2107+
self.user.fullname = 'not a url'
2108+
self.user.save()
2109+
2110+
def test_validate_domain_in_field_for_existing_not_ham(self):
2111+
self.user.fullname = 'google.com'
2112+
self.user.save()
2113+
assert self.user.is_spammy is True
2114+
assert self.user.is_hammy is False
2115+
2116+
self.user.unspam(save=True)
2117+
self.user.fullname = 'not a url'
2118+
self.user.save()
2119+
2120+
def test_validate_domain_fields_notable_domain_in_field_for_existing_not_ham(self):
2121+
NotableDomain.objects.get_or_create(domain='google.com', note=NotableDomain.Note.IGNORED)
2122+
self.user.fullname = 'google.com'
2123+
self.user.save()
2124+
assert self.user.is_spammy is False
2125+
assert self.user.is_hammy is False
2126+
2127+
self.user.unspam(save=True)
2128+
self.user.fullname = 'not a url'
2129+
self.user.save()
2130+
2131+
def test_validate_domain_fields_for_real(self):
2132+
self.user.fullname = 'Sandhya N.Sathesh'
2133+
self.user.save()
2134+
assert self.user.is_spammy is False
2135+
assert self.user.is_hammy is False
2136+
2137+
self.user.unspam(save=True)
2138+
self.user.fullname = 'not a url'
2139+
self.user.save()
20892140

20902141
class TestUserGdprDelete:
20912142

osf_tests/test_validators.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from osf.exceptions import ValidationValueError
33

44
from types import SimpleNamespace
5-
from osf.models import validators, OSFUser
5+
from osf.models import validators, OSFUser, NotableDomain, SpamStatus
66
from osf.models.validators import has_domain_in_user_fields_for_names
77
from osf_tests.factories import SubjectFactory
88

@@ -160,7 +160,9 @@ def test_validate_expand_subject_hierarchy():
160160
)
161161
def test_has_domain_in_user_fields(user_data):
162162
user = mock_user(user_data)
163-
assert has_domain_in_user_fields_for_names(user) is False
163+
result, message = has_domain_in_user_fields_for_names(user)
164+
assert result is False
165+
assert message == SpamStatus.UNKNOWN
164166

165167

166168
@pytest.mark.parametrize(
@@ -186,8 +188,55 @@ def test_has_domain_in_user_fields(user_data):
186188
'middle_names': 'www.google.com',
187189
'suffix': 'M.Sc.',
188190
},
191+
{
192+
'fullname': 'Judith Hateren',
193+
'given_name': 'Judith',
194+
'family_name': 'Hateren',
195+
'middle_names': 'google.com',
196+
'suffix': 'M.Sc.',
197+
},
189198
]
190199
)
191200
def test_has_domain_in_user_fields_fail(user_data):
192201
user = mock_user(user_data)
193-
assert has_domain_in_user_fields_for_names(user) is True
202+
result, message = has_domain_in_user_fields_for_names(user)
203+
assert result is True
204+
assert message == SpamStatus.SPAM
205+
206+
@pytest.mark.parametrize(
207+
'user_data',
208+
[
209+
{
210+
'fullname': 'Judith Sarah',
211+
'given_name': 'Judith',
212+
'family_name': 'Preuss',
213+
'middle_names': 'osf.io',
214+
'suffix' : 'M.Sc.',
215+
},
216+
]
217+
)
218+
def test_has_notable_domain_in_user_fields(user_data):
219+
NotableDomain.objects.get_or_create(domain='osf.io', note=NotableDomain.Note.IGNORED)
220+
user = mock_user(user_data)
221+
result, message = has_domain_in_user_fields_for_names(user)
222+
assert result is False
223+
assert message == SpamStatus.HAM
224+
225+
@pytest.mark.parametrize(
226+
'user_data',
227+
[
228+
{
229+
'fullname': 'Judith Sarah',
230+
'given_name': 'Judith',
231+
'family_name': 'Preuss',
232+
'middle_names': 'osf.io',
233+
'suffix' : 'M.Sc.',
234+
},
235+
]
236+
)
237+
def test_has_no_notable_domain_in_user_fields(user_data):
238+
NotableDomain.objects.get_or_create(domain='google.com', note=NotableDomain.Note.IGNORED)
239+
user = mock_user(user_data)
240+
result, message = has_domain_in_user_fields_for_names(user)
241+
assert result is True
242+
assert message == SpamStatus.SPAM

poetry.lock

Lines changed: 33 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ djangorestframework-csv = "3.0.2"
102102
gevent = "24.2.1"
103103
packaging = "^24.0"
104104
flask-cors = "6.0.1"
105+
urlextract = "^1.9.0"
105106

106107
[tool.poetry.group.dev.dependencies]
107108
pytest = "7.4.4"

0 commit comments

Comments
 (0)