diff --git a/test/test_config_domain.py b/test/test_config_domain.py index da04235..9c5c16f 100644 --- a/test/test_config_domain.py +++ b/test/test_config_domain.py @@ -2,7 +2,7 @@ import autopath from py.test import raises from tiramisu.config import Config -from tiramisu.option import DomainnameOption, OptionDescription +from tiramisu.option import DomainnameOption, EmailOption, URLOption, OptionDescription def test_domainname(): @@ -14,11 +14,12 @@ def test_domainname(): c.d = 'toto.com' raises(ValueError, "c.d = 'toto'") c.d = 'toto3.com' - c.d = 'toto3.3la' + raises(ValueError, "c.d = 'toto3.3la'") raises(ValueError, "c.d = '3toto.com'") - c.d = 'toto.co3' + raises(ValueError, "c.d = 'toto.co3'") raises(ValueError, "c.d = 'toto_super.com'") c.d = 'toto-.com' + raises(ValueError, "c.d = 'toto..com'") def test_domainname_netbios(): @@ -41,3 +42,30 @@ def test_domainname_hostname(): raises(ValueError, "c.d = 'toto.com'") c.d = 'toto' c.d = 'domainnametoolong' + + +def test_email(): + e = EmailOption('e', '') + od = OptionDescription('a', '', [e]) + c = Config(od) + c.read_write() + c.e = 'root@foo.com' + raises(ValueError, "c.e = 'root'") + raises(ValueError, "c.e = 'root@domain'") + + +def test_url(): + u = URLOption('u', '') + od = OptionDescription('a', '', [u]) + c = Config(od) + c.read_write() + c.u = 'http://foo.com' + c.u = 'https://foo.com' + c.u = 'https://foo.com/' + raises(ValueError, "c.u = 'ftp://foo.com'") + c.u = 'https://foo.com/index.html' + c.u = 'https://foo.com/index.html?var=value&var2=val2' + raises(ValueError, "c.u = 'https://foo.com/index\\n.html'") + c.u = 'https://foo.com:8443' + c.u = 'https://foo.com:8443/' + c.u = 'https://foo.com:8443/index.html' diff --git a/tiramisu/option.py b/tiramisu/option.py index c7a28c2..dec7b02 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -968,20 +968,35 @@ class DomainnameOption(Option): domainname: fqdn: with tld, not supported yet """ - __slots__ = ('_type', '_allow_ip') + __slots__ = ('_type', '_allow_ip', '_allow_without_dot', '_domain_re') _opt_type = 'domainname' def __init__(self, name, doc, default=None, default_multi=None, requires=None, multi=False, callback=None, callback_params=None, validator=None, validator_params=None, properties=None, allow_ip=False, type_='domainname', - warnings_only=False): + warnings_only=False, allow_without_dot=False): if type_ not in ['netbios', 'hostname', 'domainname']: raise ValueError(_('unknown type_ {0} for hostname').format(type_)) self._type = type_ if allow_ip not in [True, False]: raise ValueError(_('allow_ip must be a boolean')) + if allow_without_dot not in [True, False]: + raise ValueError(_('allow_without_dot must be a boolean')) self._allow_ip = allow_ip + self._allow_without_dot = allow_without_dot + end = '' + extrachar = '' + if self._type == 'netbios': + length = 14 + elif self._type == 'hostname': + length = 62 + elif self._type == 'domainname': + length = 62 + extrachar = '\.' + end = '+[a-z]*' + self._domain_re = re.compile(r'^(?:[a-z][a-z\d\-]{{,{0}}}{1}){2}$' + ''.format(length, extrachar, end)) super(DomainnameOption, self).__init__(name, doc, default=default, default_multi=default_multi, callback=callback, @@ -1000,27 +1015,82 @@ class DomainnameOption(Option): return except ValueError: pass - if self._type == 'netbios': - length = 15 - extrachar = '' - elif self._type == 'hostname': - length = 63 - extrachar = '' - elif self._type == 'domainname': - length = 255 - extrachar = '\.' - if '.' not in value: - raise ValueError(_("invalid value for {0}, must have dot" - "").format(self._name)) - if len(value) > length: - raise ValueError(_("invalid domainname's length for" - " {0} (max {1})").format(self._name, length)) - if len(value) == 1: + if self._type == 'domainname' and not self._allow_without_dot and \ + '.' not in value: + raise ValueError(_("invalid domainname for {0}, must have dot" + "").format(self._name)) + if len(value) > 255: + raise ValueError(_("invalid domainname's length for" + " {0} (max 255)").format(self._name)) + if len(value) < 2: raise ValueError(_("invalid domainname's length for {0} (min 2)" "").format(self._name)) - regexp = r'^[a-z]([a-z\d{0}-])*[a-z\d]$'.format(extrachar) - if re.match(regexp, value) is None: - raise ValueError(_('invalid domainname')) + if not self._domain_re.search(value): + raise ValueError(_('invalid domainname: {0}'.format(self._name))) + + +class EmailOption(DomainnameOption): + __slots__ = tuple() + username_re = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$") + + def __init__(self, *args, **kwargs): + kwargs['type_'] = 'domainname' + kwargs['allow_ip'] = False + kwargs['allow_without_dot'] = False + super(EmailOption, self).__init__(*args, **kwargs) + + def _validate(self, value): + splitted = value.split('@', 1) + try: + username, domain = splitted + except ValueError: + raise ValueError(_('invalid email address, should contains one @ ' + 'for {0}').format(self._name)) + if not self.username_re.search(username): + raise ValueError(_('invalid username in email address for {0}').format(self._name)) + super(EmailOption, self)._validate(domain) + + +class URLOption(DomainnameOption): + __slots__ = tuple() + proto_re = re.compile(r'(http|https)://') + path_re = re.compile(r"^[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$") + + def __init__(self, *args, **kwargs): + kwargs['type_'] = 'domainname' + kwargs['allow_ip'] = False + kwargs['allow_without_dot'] = False + super(URLOption, self).__init__(*args, **kwargs) + + def _validate(self, value): + match = self.proto_re.search(value) + if not match: + raise ValueError(_('invalid url, should start with http:// or ' + 'https:// for {0}').format(self._name)) + value = value[len(match.group(0)):] + # get domain/files + splitted = value.split('/', 1) + try: + domain, files = splitted + except ValueError: + domain = value + files = None + # if port in domain + splitted = domain.split(':', 1) + try: + domain, port = splitted + + except ValueError: + domain = splitted[0] + port = 0 + if not 0 <= int(port) <= 65535: + raise ValueError(_('port must be an between 0 and 65536')) + # validate domainname + super(URLOption, self)._validate(domain) + # validate file + if files is not None and files != '' and not self.path_re.search(files): + raise ValueError(_('invalid url, should endswith with filename for' + ' {0}').format(self._name)) class OptionDescription(BaseOption):