You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by di...@apache.org on 2023/01/12 20:42:14 UTC

[allura] branch master updated (c690b1f6c -> 3178ea737)

This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/allura.git


    from c690b1f6c [#8488] applied patch and updated docs
     new 8294faf5a [#8484] added fediverse and instagram form fields for projects
     new 3178ea737 [#8484] improvements to validation and fixed project features duplication bug

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 Allura/allura/controllers/auth.py                  | 19 +++++-
 Allura/allura/ext/admin/admin_main.py              | 11 ++++
 Allura/allura/ext/admin/widgets.py                 | 15 ++++-
 .../user_profile/templates/sections/social.html    |  4 ++
 Allura/allura/lib/helpers.py                       |  4 ++
 Allura/allura/lib/validators.py                    | 54 +++++++++++++++-
 Allura/allura/lib/widgets/forms.py                 |  6 +-
 Allura/allura/model/project.py                     | 13 ++++
 Allura/allura/templates/widgets/forge_form.html    |  2 +-
 .../templates/widgets/sortable_repeated_field.html | 17 +++++-
 .../templates/admin_widgets/metadata_admin.html    |  8 +++
 Allura/allura/tests/functional/test_admin.py       | 39 ++++++++++++
 Allura/allura/tests/functional/test_auth.py        | 71 +++++++++++++++++++---
 Allura/development.ini                             |  2 +-
 14 files changed, 241 insertions(+), 24 deletions(-)


[allura] 01/02: [#8484] added fediverse and instagram form fields for projects

Posted by di...@apache.org.
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 8294faf5a1004486d2a202939f03f6bd3dcc7266
Author: Guillermo Cruz <gu...@slashdotmedia.com>
AuthorDate: Fri Dec 9 16:35:56 2022 -0600

    [#8484] added fediverse and instagram form fields for projects
---
 Allura/allura/ext/admin/admin_main.py              | 11 ++++
 Allura/allura/ext/admin/widgets.py                 | 18 ++++--
 .../user_profile/templates/sections/social.html    |  4 ++
 Allura/allura/lib/helpers.py                       |  4 ++
 Allura/allura/lib/validators.py                    | 35 ++++++++++-
 Allura/allura/lib/widgets/forms.py                 | 10 ++-
 Allura/allura/model/project.py                     | 13 ++++
 Allura/allura/templates/widgets/forge_form.html    |  2 +-
 .../templates/admin_widgets/metadata_admin.html    |  8 +++
 Allura/allura/tests/functional/test_auth.py        | 71 +++++++++++++++++++---
 Allura/development.ini                             |  3 +-
 11 files changed, 159 insertions(+), 20 deletions(-)

diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index d40d9b9b0..0a9ef9a61 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -319,6 +319,8 @@ class ProjectAdminController(BaseController):
                support_page_url='',
                twitter_handle='',
                facebook_page='',
+               fediverse_address='',
+               instagram_page='',
                removal='',
                moved_to_url='',
                tracking_id='',
@@ -385,6 +387,15 @@ class ProjectAdminController(BaseController):
                 M.AuditLog.log(
                     'change project facebook page to %s', facebook_page)
                 c.project.set_social_account('Facebook', facebook_page)
+        old_fediverse = c.project.social_account('Fediverse')
+        if not old_fediverse or fediverse_address != old_fediverse.accounturl:
+            M.AuditLog.log('change project fediverse username to %s',
+                           fediverse_address)
+            c.project.set_social_account('Fediverse', fediverse_address)
+        old_instagram = c.project.social_account('Instagram')
+        if not old_instagram or instagram_page != old_instagram:
+            M.AuditLog.log('change project instagram page to %s', instagram_page)
+            c.project.set_social_account('Instagram', instagram_page)
         if support_page_url != c.project.support_page_url:
             M.AuditLog.log('change project support page url to %s',
                            support_page_url)
diff --git a/Allura/allura/ext/admin/widgets.py b/Allura/allura/ext/admin/widgets.py
index 5a2e0e3cb..7af817d0b 100644
--- a/Allura/allura/ext/admin/widgets.py
+++ b/Allura/allura/ext/admin/widgets.py
@@ -14,7 +14,7 @@
 #       KIND, either express or implied.  See the License for the
 #       specific language governing permissions and limitations
 #       under the License.
-
+import tg
 from tg import tmpl_context as c
 
 import ew as ew_core
@@ -28,6 +28,7 @@ from allura.lib.widgets import forms as ff
 from allura.lib.widgets import form_fields as ffw
 
 from bson import ObjectId
+from paste.deploy.converters import aslist
 
 
 class CardField(ew._Jinja2Widget):
@@ -163,7 +164,9 @@ 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',
@@ -221,9 +224,16 @@ 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')
+            field_type="text", label='Twitter Handle', validator=V.SocialDomainValidator('twitter.com'))
         facebook_page = ew.InputField(field_type="text", label='Facebook page',
-                                      validator=fev.URL(add_http=True))
+                                      validator=formencode.All(fev.URL(add_http=True), V.SocialDomainValidator('facebook.com')) )
+        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)
+
+
 
 
 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 fe7eb970f..d94f0bf48 100644
--- a/Allura/allura/ext/user_profile/templates/sections/social.html
+++ b/Allura/allura/ext/user_profile/templates/sections/social.html
@@ -33,7 +33,11 @@
 {% block content %}
     <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>
+        {% else %}
         <dt>{{ contact.socialnetwork }}</dt><dd>{{ contact.accounturl|urlize(nofollow=True) }}</dd>
+        {% endif %}
     {% else %}
         <dd class="empty">No social networks entered.</dd>
     {% endfor %}
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 6aefc5627..7e7600a3d 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -1334,3 +1334,7 @@ def pluralize_tool_name(tool_name: string, count: int):
     if tool_name is not None and tool_name in pluralize_tools:
         return f"{tool_name}{'s'[:count^1]}"
     return tool_name
+
+def parse_fediverse_address(username: str):
+    pieces = username.split('@')
+    return f'https://{pieces[-1]}/@{pieces[1]}'
diff --git a/Allura/allura/lib/validators.py b/Allura/allura/lib/validators.py
index b7e56d7ba..aa0488705 100644
--- a/Allura/allura/lib/validators.py
+++ b/Allura/allura/lib/validators.py
@@ -24,7 +24,7 @@ from tg import tmpl_context as c
 from . import helpers as h
 from datetime import datetime
 import six
-from urllib.parse import urlparse
+from urllib.parse import urlsplit
 from ipaddress import ip_address
 import socket
 
@@ -52,7 +52,7 @@ class URLIsPrivate(URL):
 
     def _to_python(self, value, state):
         value = super(URLIsPrivate, self)._to_python(value, state)
-        url_components = urlparse(value)
+        url_components = urlsplit(value)
         try:
             host_ip = socket.gethostbyname(url_components.netloc)
         except socket.gaierror:
@@ -487,3 +487,34 @@ class IconValidator(fev.FancyValidator):
                 value, state)
 
         return value
+
+FEDIVERSE_REGEX = r'^@[a-zA-Z_]*@[a-zA-Z_]*\.{1}[A-Za-z]{0,10}$'
+
+class FediverseAddressValidator(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)
+
+        return value.lower()
+
+
+
+class SocialDomainValidator(fev.FancyValidator):
+    def __init__(self, domain='', **kw):
+        self.domain = domain
+        self.domains = kw.get('domains')
+
+    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)
+        return value
+
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index 69bf853cd..3444bf170 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -434,8 +434,12 @@ class AddSocialNetworkForm(ForgeForm):
     @property
     def fields(self):
         socialnetworks = aslist(tg.config.get('socialnetworks',
-                                              ['Facebook', 'Linkedin', 'Twitter', 'Google+']),
+                                              ['Facebook', 'Linkedin', 'Twitter',]),
                                 ',')
+        allowed_social_domains = aslist(tg.config.get('allowed_social_domains',
+                                              ['facebook.com', 'instagram.com', 'linkedin.com', 'twitter.com']),
+                                ',')
+
         return [
             ew.SingleSelectField(
                 name='socialnetwork',
@@ -446,7 +450,9 @@ class AddSocialNetworkForm(ForgeForm):
             ew.TextField(
                 name='accounturl',
                 label='Account url',
-                validator=V.UnicodeString(not_empty=True))
+                validator=formencode.All(
+                    V.UnicodeString(not_empty=True), V.SocialDomainValidator(domains=allowed_social_domains)
+                ))
         ]
 
 
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index fad47aefc..743384775 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -1134,6 +1134,19 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
     def facebook_page(self):
         return self.social_account('Facebook').accounturl
 
+    @property
+    def fediverse_address(self):
+        return self.social_account('Fediverse').accounturl
+
+    @classmethod
+    def fediverse_parse_address(cls, username):
+        pieces = username.split('@')
+        return f'https://{pieces[-1]}/@{pieces[1]}'
+
+    @property
+    def instagram_page(self):
+        return self.social_account('Instagram').accounturl
+
     def social_account(self, socialnetwork):
         try:
             account = next(
diff --git a/Allura/allura/templates/widgets/forge_form.html b/Allura/allura/templates/widgets/forge_form.html
index 4f182d1f8..47656c9bc 100644
--- a/Allura/allura/templates/widgets/forge_form.html
+++ b/Allura/allura/templates/widgets/forge_form.html
@@ -32,7 +32,7 @@
     {% set ctx=widget.context_for(field) %}
     {% if field.field_type != 'hidden' %}
       {% if ctx.errors and field.show_errors -%}
-      <div class="grid-{{19 + extra_width}}"><span {{widget.j2_attrs({'class':error_class})}}>{{ctx.errors|nl2br}}</span></div>
+      <div class="grid-{{19 + extra_width}}"><span {{widget.j2_attrs({'class':error_class})}}>{{ctx.errors|nl2br}}</span></div><br>
       {%- endif %}
       {% if field.show_label and field.label %}
       <label for="{{ctx.id}}" class="grid-4">{{field.label}}:</label>
diff --git a/Allura/allura/templates_responsive/override/allura/ext/admin/templates/admin_widgets/metadata_admin.html b/Allura/allura/templates_responsive/override/allura/ext/admin/templates/admin_widgets/metadata_admin.html
index b47b0cce3..68ac3c213 100644
--- a/Allura/allura/templates_responsive/override/allura/ext/admin/templates/admin_widgets/metadata_admin.html
+++ b/Allura/allura/templates_responsive/override/allura/ext/admin/templates/admin_widgets/metadata_admin.html
@@ -97,6 +97,14 @@
             {{ widget.display_label(widget.fields.facebook_page) }}
             {{widget.display_field(widget.fields.facebook_page) }}
             </div>
+            <div class="field">
+            {{ widget.display_label(widget.fields.instagram_page) }}
+            {{ widget.display_field(widget.fields.instagram_page) }}
+            </div>
+            <div class="field">
+            {{ widget.display_label(widget.fields.fediverse_address) }}
+            {{ widget.display_field(widget.fields.fediverse_address) }}
+            </div>
         </div>
 
         {% if c.project.neighborhood.features['google_analytics'] %}
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 5c459db65..941215623 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -1172,6 +1172,7 @@ class TestAuthRest(TestRestApiBase):
 
 
 class TestPreferences(TestController):
+
     @td.with_user_project('test-admin')
     def test_personal_data(self):
         from pytz import country_names
@@ -1179,7 +1180,6 @@ class TestPreferences(TestController):
         setsex, setbirthdate, setcountry, setcity, settimezone = \
             ('Male', '19/08/1988', 'IT', 'Milan', 'Europe/Rome')
         self.app.get('/auth/user_info/')
-
         # Check if personal data is properly set
         r = self.app.post('/auth/user_info/change_personal_data',
                           params=dict(
@@ -1231,6 +1231,40 @@ class TestPreferences(TestController):
         user = M.User.query.get(username='test-admin')
         assert user.birthdate is None
 
+    @td.with_user_project('test-admin')
+    def test_contacts_not_allowed(self):
+        self.app.get('/auth/user_info/')
+        socialnetwork = 'Facebook'
+        accounturl = 'http://www.faceboookk.com/test'
+        self.app.post('/auth/user_info/contacts/add_social_network',
+                      params=dict(socialnetwork=socialnetwork,
+                                  accounturl=accounturl,
+                                  _session_id=self.app.cookies['_session_id'],
+                                  ))
+        user = M.User.query.get(username='test-admin')
+        assert len(user.socialnetworks) == 0
+
+        socialnetwork = 'Instagram'
+        accounturl = 'http://www.insta.com/test'
+        self.app.post('/auth/user_info/contacts/add_social_network',
+                      params=dict(socialnetwork=socialnetwork,
+                                  accounturl=accounturl,
+                                  _session_id=self.app.cookies['_session_id'],
+                                  ))
+        user = M.User.query.get(username='test-admin')
+        assert len(user.socialnetworks) == 0
+
+        socialnetwork = 'Mastodon'
+        accounturl = '@username@server'
+        self.app.post('/auth/user_info/contacts/add_social_network',
+                      params=dict(socialnetwork=socialnetwork,
+                                  accounturl=accounturl,
+                                  _session_id=self.app.cookies['_session_id'],
+                                  ))
+        user = M.User.query.get(username='test-admin')
+        assert len(user.socialnetworks) == 0
+
+
     @td.with_user_project('test-admin')
     def test_contacts(self):
         # Add skype account
@@ -1256,7 +1290,7 @@ class TestPreferences(TestController):
 
         # Add second social network account
         socialnetwork2 = 'Twitter'
-        accounturl2 = 'http://twitter.com/test'
+        accounturl2 = 'https://twitter.com/test'
         self.app.post('/auth/user_info/contacts/add_social_network',
                       params=dict(socialnetwork=socialnetwork2,
                                   accounturl='@test',
@@ -1264,8 +1298,19 @@ class TestPreferences(TestController):
                                   ))
         user = M.User.query.get(username='test-admin')
         assert len(user.socialnetworks) == 2
-        assert {'socialnetwork': socialnetwork, 'accounturl': accounturl} in user.socialnetworks
-        assert {'socialnetwork': socialnetwork2, 'accounturl': accounturl2} in user.socialnetworks
+        expected = [{'socialnetwork': socialnetwork, 'accounturl': accounturl},
+                    {'socialnetwork': socialnetwork2, 'accounturl': accounturl2}]
+        assert all([social in expected for social in user.socialnetworks])
+
+        socialnetwork3 = 'Mastodon'
+        accounturl3 = '@username@server.social'
+        self.app.post('/auth/user_info/contacts/add_social_network',
+                      params=dict(socialnetwork=socialnetwork3,
+                                  accounturl=accounturl3,
+                                  _session_id=self.app.cookies['_session_id'],
+                                  ))
+        user = M.User.query.get(username='test-admin')
+        assert len(user.socialnetworks) == 3
 
         # Remove first social network account
         self.app.post('/auth/user_info/contacts/remove_social_network',
@@ -1274,8 +1319,10 @@ class TestPreferences(TestController):
                                   _session_id=self.app.cookies['_session_id'],
                                   ))
         user = M.User.query.get(username='test-admin')
-        assert len(user.socialnetworks) == 1
-        assert {'socialnetwork': socialnetwork2, 'accounturl': accounturl2} in user.socialnetworks
+        assert len(user.socialnetworks) == 2
+        expected = [{'socialnetwork': socialnetwork2, 'accounturl': accounturl2},
+               {'socialnetwork': socialnetwork3, 'accounturl': accounturl3}]
+        assert  all([social in expected for social in user.socialnetworks])
 
         # Add empty social network account
         self.app.post('/auth/user_info/contacts/add_social_network',
@@ -1283,8 +1330,10 @@ class TestPreferences(TestController):
                                   _session_id=self.app.cookies['_session_id'],
                                   ))
         user = M.User.query.get(username='test-admin')
-        assert len(user.socialnetworks) == 1
-        assert {'socialnetwork': socialnetwork2, 'accounturl': accounturl2} in user.socialnetworks
+        assert len(user.socialnetworks) == 2
+        expected = [{'socialnetwork': socialnetwork2, 'accounturl': accounturl2},
+               {'socialnetwork': socialnetwork3, 'accounturl': accounturl3}]
+        assert all([social in expected for social in user.socialnetworks])
 
         # Add invalid social network account
         self.app.post('/auth/user_info/contacts/add_social_network',
@@ -1292,8 +1341,10 @@ class TestPreferences(TestController):
                                   _session_id=self.app.cookies['_session_id'],
                                   ))
         user = M.User.query.get(username='test-admin')
-        assert len(user.socialnetworks) == 1
-        assert {'socialnetwork': socialnetwork2, 'accounturl': accounturl2} in user.socialnetworks
+        assert len(user.socialnetworks) == 2
+        expected = [{'socialnetwork': socialnetwork2, 'accounturl': accounturl2},
+               {'socialnetwork': socialnetwork3, 'accounturl': accounturl3}]
+        assert all([social in expected for social in user.socialnetworks])
 
         # Add telephone number
         telnumber = '+3902123456'
diff --git a/Allura/development.ini b/Allura/development.ini
index 171240692..8499ef58a 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -198,7 +198,8 @@ auth.allow_birth_date = true
 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, Google+
+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


[allura] 02/02: [#8484] improvements to validation and fixed project features duplication bug

Posted by di...@apache.org.
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 <gu...@slashdotmedia.com>
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': '@test12@indieweb.social'})
+        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': '@test@indieweb.social'})
+        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