Skip to content

Commit 022c1d9

Browse files
committed
Hotfix merge branch 'humitos/import/url-repo-validation' into relcorp
2 parents 45ed4d3 + 43de909 commit 022c1d9

File tree

5 files changed

+109
-16
lines changed

5 files changed

+109
-16
lines changed

readthedocs/core/validators.py

+40
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from __future__ import absolute_import
55
import re
66

7+
from django.conf import settings
78
from django.core.exceptions import ValidationError
89
from django.utils.deconstruct import deconstructible
910
from django.utils.translation import ugettext_lazy as _
1011
from django.core.validators import RegexValidator
12+
from future.backports.urllib.parse import urlparse
1113

1214

1315
domain_regex = (
@@ -45,3 +47,41 @@ def __call__(self, value):
4547
super(DomainNameValidator, self).__call__(idnavalue)
4648

4749
validate_domain_name = DomainNameValidator()
50+
51+
52+
@deconstructible
53+
class RepositoryURLValidator(object):
54+
55+
def __call__(self, value):
56+
allow_private_repos = getattr(settings, 'ALLOW_PRIVATE_REPOS', False)
57+
public_schemes = ['https', 'http', 'git', 'ftps', 'ftp']
58+
private_schemes = ['ssh', 'ssh+git']
59+
valid_schemes = public_schemes
60+
if allow_private_repos:
61+
valid_schemes += private_schemes
62+
url = urlparse(value)
63+
if (
64+
(
65+
url.scheme not in valid_schemes and \
66+
'@' not in value and \
67+
not value.startswith('lp:')
68+
) or \
69+
(
70+
value.startswith('/') or \
71+
value.startswith('file://') or \
72+
value.startswith('.')
73+
)
74+
):
75+
# Avoid ``/path/to/local/file`` and ``file://`` scheme but allow
76+
# ``[email protected]:user/project.git`` and ``lp:bazaar``
77+
raise ValidationError(_('Invalid scheme for URL'))
78+
elif '&&' in value or '|' in value:
79+
raise ValidationError(_('Invalid character in the URL'))
80+
elif (
81+
('@' in value or url.scheme in private_schemes) and
82+
not allow_private_repos
83+
):
84+
raise ValidationError('Clonning via SSH is not supported')
85+
return value
86+
87+
validate_repository_url = RepositoryURLValidator()

readthedocs/projects/forms.py

-11
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,6 @@ def clean_name(self):
119119
_('Invalid project name, a project already exists with that name')) # yapf: disable # noqa
120120
return name
121121

122-
def clean_repo(self):
123-
repo = self.cleaned_data.get('repo', '').strip()
124-
pvt_repos = getattr(settings, 'ALLOW_PRIVATE_REPOS', False)
125-
if '&&' in repo or '|' in repo:
126-
raise forms.ValidationError(_('Invalid character in repo name'))
127-
elif '@' in repo and not pvt_repos:
128-
raise forms.ValidationError(
129-
_('It looks like you entered a private repo - please use the '
130-
'public (http:// or git://) clone url')) # yapf: disable
131-
return repo
132-
133122
def clean_remote_repository(self):
134123
remote_repo = self.cleaned_data.get('remote_repository', None)
135124
if not remote_repo:

readthedocs/projects/models.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from readthedocs.builds.constants import LATEST, LATEST_VERBOSE_NAME, STABLE
2323
from readthedocs.core.resolver import resolve, resolve_domain
2424
from readthedocs.core.utils import broadcast, slugify
25-
from readthedocs.core.validators import validate_domain_name
25+
from readthedocs.core.validators import validate_domain_name, validate_repository_url
2626
from readthedocs.projects import constants
2727
from readthedocs.projects.exceptions import ProjectConfigurationError
2828
from readthedocs.projects.querysets import (
@@ -86,6 +86,7 @@ class Project(models.Model):
8686
help_text=_('The reStructuredText '
8787
'description of the project'))
8888
repo = models.CharField(_('Repository URL'), max_length=255,
89+
validators=[validate_repository_url],
8990
help_text=_('Hosted documentation repository URL'))
9091
repo_type = models.CharField(_('Repository type'), max_length=10,
9192
choices=constants.REPO_CHOICES, default='git')

readthedocs/rtd_tests/tests/test_doc_building.py

-1
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,6 @@ def test_is_obsolete_with_json_different_python_version(self):
896896
exists.return_value = True
897897
self.assertTrue(python_env.is_obsolete)
898898

899-
@pytest.mark.xfail(reason='build.image is not being considered yet')
900899
def test_is_obsolete_with_json_different_build_image(self):
901900
config_data = {
902901
'build': {

readthedocs/rtd_tests/tests/test_project_forms.py

+67-3
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1+
# -*- coding: utf-8 -*-
2+
13
from __future__ import (
24
absolute_import, division, print_function, unicode_literals)
35

46
import mock
57
from django.contrib.auth.models import User
68
from django.test import TestCase
9+
from django.test.utils import override_settings
710
from django_dynamic_fixture import get
811
from textclassifier.validators import ClassifierValidator
912

1013
from readthedocs.projects.exceptions import ProjectSpamError
11-
from readthedocs.projects.forms import ProjectExtraForm, TranslationForm
14+
from readthedocs.projects.forms import (
15+
ProjectBasicsForm, ProjectExtraForm, TranslationForm)
1216
from readthedocs.projects.models import Project
1317

1418

1519
class TestProjectForms(TestCase):
16-
1720
@mock.patch.object(ClassifierValidator, '__call__')
1821
def test_form_spam(self, mocked_validator):
19-
"""Form description field fails spam validation"""
22+
"""Form description field fails spam validation."""
2023
mocked_validator.side_effect = ProjectSpamError
2124

2225
data = {
@@ -28,6 +31,67 @@ def test_form_spam(self, mocked_validator):
2831
with self.assertRaises(ProjectSpamError):
2932
form.is_valid()
3033

34+
def test_import_repo_url(self):
35+
"""Validate different type of repository URLs on importing a Project."""
36+
37+
common_urls = [
38+
# Invalid
39+
('./path/to/relative/folder', False),
40+
('../../path/to/relative/folder', False),
41+
('../../path/to/@/folder', False),
42+
('/path/to/local/folder', False),
43+
('/path/to/@/folder', False),
44+
('file:///path/to/local/folder', False),
45+
('file:///path/to/@/folder', False),
46+
('github.com/humitos/foo', False),
47+
('https://github.com/|/foo', False),
48+
('git://github.com/&&/foo', False),
49+
# Valid
50+
('git://github.com/humitos/foo', True),
51+
('http://github.com/humitos/foo', True),
52+
('https://github.com/humitos/foo', True),
53+
('http://gitlab.com/humitos/foo', True),
54+
('http://bitbucket.com/humitos/foo', True),
55+
('ftp://ftpserver.com/humitos/foo', True),
56+
('ftps://ftpserver.com/humitos/foo', True),
57+
('lp:zaraza', True),
58+
]
59+
60+
public_urls = [
61+
('[email protected]:humitos/foo', False),
62+
('ssh://[email protected]/humitos/foo', False),
63+
('ssh+git://github.com/humitos/foo', False),
64+
('[email protected]:strangeuser/readthedocs.git', False),
65+
('[email protected]:22/_ssh/docs', False),
66+
] + common_urls
67+
68+
private_urls = [
69+
('[email protected]:humitos/foo', True),
70+
('ssh://[email protected]/humitos/foo', True),
71+
('ssh+git://github.com/humitos/foo', True),
72+
('[email protected]:strangeuser/readthedocs.git', True),
73+
('[email protected]:22/_ssh/docs', True),
74+
] + common_urls
75+
76+
for url, valid in public_urls:
77+
initial = {
78+
'name': 'foo',
79+
'repo_type': 'git',
80+
'repo': url,
81+
}
82+
form = ProjectBasicsForm(initial)
83+
self.assertEqual(form.is_valid(), valid, msg=url)
84+
85+
with override_settings(ALLOW_PRIVATE_REPOS=True):
86+
for url, valid in private_urls:
87+
initial = {
88+
'name': 'foo',
89+
'repo_type': 'git',
90+
'repo': url,
91+
}
92+
form = ProjectBasicsForm(initial)
93+
self.assertEqual(form.is_valid(), valid, msg=url)
94+
3195

3296
class TestTranslationForm(TestCase):
3397

0 commit comments

Comments
 (0)