This is an automated email from the ASF dual-hosted git repository. dill0wn pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/allura.git
commit 3178ea7374c7b0ac6ba758b49e28fe1f49444ba2 Author: Guillermo Cruz <[email protected]> AuthorDate: Tue Jan 3 17:05:41 2023 -0600 [#8484] improvements to validation and fixed project features duplication bug --- Allura/allura/controllers/auth.py | 19 +++++++- Allura/allura/ext/admin/widgets.py | 17 ++++--- .../user_profile/templates/sections/social.html | 2 +- Allura/allura/lib/validators.py | 55 +++++++++++++++------- Allura/allura/lib/widgets/forms.py | 8 +--- .../templates/widgets/sortable_repeated_field.html | 17 +++++-- Allura/allura/tests/functional/test_admin.py | 39 +++++++++++++++ Allura/development.ini | 1 - 8 files changed, 118 insertions(+), 40 deletions(-) diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py index 5086701d8..21b3fc346 100644 --- a/Allura/allura/controllers/auth.py +++ b/Allura/allura/controllers/auth.py @@ -24,6 +24,7 @@ import warnings from six.moves.urllib.parse import urlparse, urljoin import bson +import formencode as fe import tg from tg import expose, flash, redirect, validate, config, session from tg.decorators import with_trailing_slash, without_trailing_slash @@ -41,6 +42,7 @@ from allura import model as M from allura.lib.security import require_authenticated, has_access, is_site_admin from allura.lib import helpers as h from allura.lib import plugin +from allura.lib import validators as V from allura.lib.decorators import require_post, reconfirm_auth from allura.lib.exceptions import InvalidRecoveryCode, MultifactorRateLimitError from allura.lib.repository import RepositoryApp @@ -1053,8 +1055,21 @@ class UserContactsController(BaseController): def add_social_network(self, **kw): require_authenticated() - if kw['socialnetwork'] == 'Twitter' and not kw['accounturl'].startswith('http'): - kw['accounturl'] = 'http://twitter.com/%s' % kw['accounturl'].replace('@', '') + validator_map = { + 'Twitter': V.TwitterValidator(), + 'Instagram': V.InstagramValidator(), + 'Facebook': V.FacebookValidator(), + 'Mastodon': V.FediverseValidator(), + 'Linkedin': V.LinkedinValidator(), + } + + try: + Validator = validator_map.get(kw['socialnetwork']) + kw['accounturl'] = Validator().to_python(kw['accounturl']) + except fe.Invalid as e: + # c.form_errors['accounturl'] = e.msg + flash(e.msg, 'error') + redirect('.') c.user.add_multivalue_pref('socialnetworks', {'socialnetwork': kw['socialnetwork'], 'accounturl': kw['accounturl']}) diff --git a/Allura/allura/ext/admin/widgets.py b/Allura/allura/ext/admin/widgets.py index 7af817d0b..f20cfc9ac 100644 --- a/Allura/allura/ext/admin/widgets.py +++ b/Allura/allura/ext/admin/widgets.py @@ -164,9 +164,7 @@ class MetadataAdmin(ff.AdminForm): defaults = dict( ff.AdminForm.defaults, enctype='multipart/form-data') - allowed_social_domains = aslist(tg.config.get('allowed_social_domains', - ['facebook', 'instagram', 'linkedin', 'twitter']), - ',') + class fields(ew_core.NameList): name = ew.InputField(field_type='text', label='Name', @@ -224,16 +222,17 @@ class MetadataAdmin(ff.AdminForm): field_type="text", label="Google Analytics ID", attrs=(dict(placeholder='UA-123456-0', pattern='UA-[0-9]+-[0-9]+'))) twitter_handle = ew.InputField( - field_type="text", label='Twitter Handle', validator=V.SocialDomainValidator('twitter.com')) + field_type="text", label='Twitter Handle', + validator=V.TwitterValidator) + facebook_page = ew.InputField(field_type="text", label='Facebook page', - validator=formencode.All(fev.URL(add_http=True), V.SocialDomainValidator('facebook.com')) ) + validator=V.FacebookValidator) instagram_page = ew.InputField( field_type="text", label='Instagram page', - validator=formencode.All(fev.URL(add_http=True), V.SocialDomainValidator('instagram.com'))) - fediverse_address = ew.InputField(field_type="text", label="Mastodon address", - validator=V.FediverseAddressValidator) - + validator=V.InstagramValidator) + fediverse_address = ew.InputField(field_type="text", label="Mastodon address", + validator=V.FediverseValidator) class AuditLog(ew_core.Widget): diff --git a/Allura/allura/ext/user_profile/templates/sections/social.html b/Allura/allura/ext/user_profile/templates/sections/social.html index d94f0bf48..ba9feb4a2 100644 --- a/Allura/allura/ext/user_profile/templates/sections/social.html +++ b/Allura/allura/ext/user_profile/templates/sections/social.html @@ -34,7 +34,7 @@ <dl> {% for contact in user.get_pref('socialnetworks') %} {% if contact.socialnetwork == 'Mastodon' %} - <dt>{{ contact.socialnetwork }}</dt><dd><a href="{{ h.parse_fediverse_address(contact.accounturl) }}" rel="me nofollow">{{ contact.accounturl }}</a></dd> + <dt>{{ contact.socialnetwork }}</dt><dd><a href="{{ h.parse_fediverse_address(contact.accounturl) }}" rel="me nofollow" target="_blank">{{ contact.accounturl }}</a></dd> {% else %} <dt>{{ contact.socialnetwork }}</dt><dd>{{ contact.accounturl|urlize(nofollow=True) }}</dd> {% endif %} diff --git a/Allura/allura/lib/validators.py b/Allura/allura/lib/validators.py index aa0488705..4d9368e38 100644 --- a/Allura/allura/lib/validators.py +++ b/Allura/allura/lib/validators.py @@ -488,33 +488,52 @@ class IconValidator(fev.FancyValidator): return value -FEDIVERSE_REGEX = r'^@[a-zA-Z_]*@[a-zA-Z_]*\.{1}[A-Za-z]{0,10}$' +FEDIVERSE_REGEX = r'^@[\w-]+@[\w-]+(\.[\w-]+)+$' -class FediverseAddressValidator(fev.FancyValidator): +class LinkedinValidator(fev.FancyValidator): + def _to_python(self, value, state): + if value.startswith('@') and not re.match(FEDIVERSE_REGEX, value): + value = f'https://linkedin.com/in/{value.replace("@", "")}/' + elif 'linkedin.com' not in value: + raise fe.Invalid('Invalid Linkedin address', value, state) + return value +class TwitterValidator(fev.FancyValidator): def _to_python(self, value, state): - match = re.match(FEDIVERSE_REGEX , value) - if not match: - raise fe.Invalid('Address format must be @your username@your server', value, state) + if value.startswith('@') and not re.match(FEDIVERSE_REGEX, value): + value = f'https://twitter.com/{value.replace("@", "")}' + elif 'twitter.com' not in value: + raise fe.Invalid('Invalid Twitter address', value, state) + return value + - return value.lower() +class InstagramValidator(fev.FancyValidator): + def _to_python(self, value, state): + if value.startswith('@') and not re.match(FEDIVERSE_REGEX, value): + value = f'https://instagram.com/{value.replace("@", "")}' + elif 'instagram.com' not in value: + raise fe.Invalid('Invalid Instagram address', value, state) + return value +class FacebookValidator(fev.FancyValidator): + def _to_python(self, value, state): + if value.startswith('@') and not re.match(FEDIVERSE_REGEX, value): + value = f'https://facebook.com/{value.replace("@", "")}' + elif 'facebook.com' not in value: + raise fe.Invalid('Invalid Facebook address', value, state) + return value -class SocialDomainValidator(fev.FancyValidator): - def __init__(self, domain='', **kw): - self.domain = domain - self.domains = kw.get('domains') +class FediverseValidator(fev.FancyValidator): def _to_python(self, value, state): - if value.startswith('@') and not re.match(FEDIVERSE_REGEX , value): - value = f'https://twitter.com/{value.replace("@","")}' - url = urlsplit(value) - if not re.match(FEDIVERSE_REGEX , value): - if self.domain and not self.domain == url.netloc.replace('www.',''): - raise fe.Invalid('Invalid domain for this field', value, state) - if self.domains and not any(domain == url.netloc.replace('www.','') for domain in self.domains): - raise fe.Invalid('Invalid domain for this field', value, state) + if value.startswith('http'): + url = urlsplit(value) + value = f'{url.path.replace("/", "")}@{url.netloc}' + if not re.match(FEDIVERSE_REGEX, value): + raise fe.Invalid('Invalid Mastodon address', value, state) + elif not re.match(FEDIVERSE_REGEX , value): + raise fe.Invalid('Invalid Mastodon address', value, state) return value diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py index 3444bf170..94b99871a 100644 --- a/Allura/allura/lib/widgets/forms.py +++ b/Allura/allura/lib/widgets/forms.py @@ -436,9 +436,6 @@ class AddSocialNetworkForm(ForgeForm): socialnetworks = aslist(tg.config.get('socialnetworks', ['Facebook', 'Linkedin', 'Twitter',]), ',') - allowed_social_domains = aslist(tg.config.get('allowed_social_domains', - ['facebook.com', 'instagram.com', 'linkedin.com', 'twitter.com']), - ',') return [ ew.SingleSelectField( @@ -450,9 +447,8 @@ class AddSocialNetworkForm(ForgeForm): ew.TextField( name='accounturl', label='Account url', - validator=formencode.All( - V.UnicodeString(not_empty=True), V.SocialDomainValidator(domains=allowed_social_domains) - )) + validator=V.UnicodeString(not_empty=True), + ) ] diff --git a/Allura/allura/templates/widgets/sortable_repeated_field.html b/Allura/allura/templates/widgets/sortable_repeated_field.html index e81249fb0..8390d0e22 100644 --- a/Allura/allura/templates/widgets/sortable_repeated_field.html +++ b/Allura/allura/templates/widgets/sortable_repeated_field.html @@ -26,13 +26,24 @@ {% if show_button %}{{ widget.button.display() }}{% endif %} <br style="clear:both"/> <div class="{{ flist_cls }}"> + {% set id = 0 %} {% for i in range(repetitions) %} {% set ctx = widget.context_for(i) %} - {{ widget.field.display(css_class=field_cls, **ctx) }} + {% if c.form_values %} + {% if 'features-' ~ i ~ '.feature' in c.form_values %} + {% set ctx = widget.context_for(i) %} + {% do ctx.update({'value': {'feature': c.form_values.get('features-' ~ i ~ '.feature')} }) %} + {{ widget.field.display(css_class=field_cls, **ctx) }} + {% endif %} + {% else %} + {{ widget.field.display(css_class=field_cls, **ctx) }} + {% endif %} {% endfor %} {% if extra_field_on_focus_name %} - {% set ctx = widget.context_for(repetitions) %} - {{ widget.field.display(css_class=field_cls, **ctx) }} + {% if not c.form_values %} + {% set ctx = widget.context_for(repetitions) %} + {{ widget.field.display(css_class=field_cls, **ctx) }} + {% endif %} {% endif %} {{ widget.field.display(name=name+'#', css_class=stub_cls) }} </div> diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py index c1d71bd72..11deb1ea0 100644 --- a/Allura/allura/tests/functional/test_admin.py +++ b/Allura/allura/tests/functional/test_admin.py @@ -956,6 +956,45 @@ class TestProjectAdmin(TestController): r = self.app.get('/admin/invitations') r.mustcontain('Neighborhood Invitation(s) for test') + def test_social_networks(self): + #Invalid Twitter + resp = self.app.post('/admin/update', params={'twitter_handle':'https://twit.com/tests'}) + assert resp.status_int == 200 + resp = self.app.post('/admin/update', params={'twitter_handle': 'https://google.com'}) + assert resp.status_int == 200 + #invalid Facebook + resp = self.app.post('/admin/update', params={'facebook_page': 'https://facebok.com'}) + assert resp.status_int == 200 + resp = self.app.post('/admin/update', params={'facebook_page': 'https://spam.com'}) + assert resp.status_int == 200 + assert 'Invalid Facebook address' in resp + #invalid instagram + resp = self.app.post('/admin/update', params={'instagram_page': 'https://instagrams.com'}) + assert resp.status_int == 200 + #invalid fediverse + resp = self.app.post('/admin/update', params={'fediverse_address': '@[email protected]'}) + assert resp.status_int == 200 + + #valid Twitter + resp = self.app.post('/admin/update', params={'twitter_handle': 'https://twitter.com/sourceforge'}) + assert resp.status_int == 302 + resp = self.app.post('/admin/update', params={'twitter_handle': '@sourceforge'}) + assert resp.status_int == 302 + #valid Facebook + resp = self.app.post('/admin/update', params={'facebook_page': 'https://www.facebook.com/sourceforgenet/'}) + assert resp.status_int == 302 + #valid instagram + resp = self.app.post('/admin/update', params={'instagram_page': 'https://instagram.com/test'}) + assert resp.status_int == 302 + resp = self.app.post('/admin/update', params={'instagram_page': '@test'}) + assert resp.status_int == 302 + # valid fediverse + resp = self.app.post('/admin/update', params={'fediverse_address': '@[email protected]'}) + assert resp.status_int == 302 + resp = self.app.post('/admin/update', params={'fediverse_address': 'https://indieweb.social/@test'}) + assert resp.status_int == 302 + + class TestExport(TestController): diff --git a/Allura/development.ini b/Allura/development.ini index 8499ef58a..7872c3d92 100644 --- a/Allura/development.ini +++ b/Allura/development.ini @@ -199,7 +199,6 @@ auth.allow_non_primary_email_password_reset = true auth.require_email_addr = true ; List of social network options to use on user account settings socialnetworks = Facebook, Linkedin, Twitter, Instagram, Mastodon -allowed_social_domains = facebook.com, instagram.com, linkedin.com, twitter.com ; Allow uploading ssh key, optionally set ssh preferences url auth.allow_upload_ssh_key = false
