3920 lines
144 KiB
Python
3920 lines
144 KiB
Python
#!/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
| This file is part of the web2py Web Framework
|
|
| Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
|
|
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
|
|
| Thanks to ga2arch for help with IS_IN_DB and IS_NOT_IN_DB on GAE
|
|
|
|
Validators
|
|
-----------
|
|
"""
|
|
import os
|
|
import re
|
|
import datetime
|
|
import time
|
|
import cgi
|
|
import json
|
|
import urllib
|
|
import struct
|
|
import decimal
|
|
import unicodedata
|
|
|
|
from gluon._compat import StringIO, integer_types, basestring, unicodeT, urllib_unquote, unichr, to_bytes, PY2, \
|
|
to_unicode, to_native, string_types, urlparse
|
|
from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE
|
|
from pydal.objects import Field, FieldVirtual, FieldMethod
|
|
from functools import reduce
|
|
|
|
regex_isint = re.compile('^[+-]?\d+$')
|
|
|
|
JSONErrors = (NameError, TypeError, ValueError, AttributeError,
|
|
KeyError)
|
|
|
|
__all__ = [
|
|
'ANY_OF',
|
|
'CLEANUP',
|
|
'CRYPT',
|
|
'IS_ALPHANUMERIC',
|
|
'IS_DATE_IN_RANGE',
|
|
'IS_DATE',
|
|
'IS_DATETIME_IN_RANGE',
|
|
'IS_DATETIME',
|
|
'IS_DECIMAL_IN_RANGE',
|
|
'IS_EMAIL',
|
|
'IS_LIST_OF_EMAILS',
|
|
'IS_EMPTY_OR',
|
|
'IS_EXPR',
|
|
'IS_FLOAT_IN_RANGE',
|
|
'IS_IMAGE',
|
|
'IS_IN_DB',
|
|
'IS_IN_SET',
|
|
'IS_INT_IN_RANGE',
|
|
'IS_IPV4',
|
|
'IS_IPV6',
|
|
'IS_IPADDRESS',
|
|
'IS_LENGTH',
|
|
'IS_LIST_OF',
|
|
'IS_LOWER',
|
|
'IS_MATCH',
|
|
'IS_EQUAL_TO',
|
|
'IS_NOT_EMPTY',
|
|
'IS_NOT_IN_DB',
|
|
'IS_NULL_OR',
|
|
'IS_SLUG',
|
|
'IS_STRONG',
|
|
'IS_TIME',
|
|
'IS_UPLOAD_FILENAME',
|
|
'IS_UPPER',
|
|
'IS_URL',
|
|
'IS_JSON',
|
|
]
|
|
|
|
try:
|
|
from gluon.globals import current
|
|
have_current = True
|
|
except ImportError:
|
|
have_current = False
|
|
|
|
|
|
def translate(text):
|
|
if text is None:
|
|
return None
|
|
elif isinstance(text, (str, unicodeT)) and have_current:
|
|
if hasattr(current, 'T'):
|
|
return str(current.T(text))
|
|
return str(text)
|
|
|
|
|
|
def options_sorter(x, y):
|
|
return (str(x[1]).upper() > str(y[1]).upper() and 1) or -1
|
|
|
|
|
|
class Validator(object):
|
|
"""
|
|
Root for all validators, mainly for documentation purposes.
|
|
|
|
Validators are classes used to validate input fields (including forms
|
|
generated from database tables).
|
|
|
|
Here is an example of using a validator with a FORM::
|
|
|
|
INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10))
|
|
|
|
Here is an example of how to require a validator for a table field::
|
|
|
|
db.define_table('person', Field('name'))
|
|
db.person.name.requires=IS_NOT_EMPTY()
|
|
|
|
Validators are always assigned using the requires attribute of a field. A
|
|
field can have a single validator or multiple validators. Multiple
|
|
validators are made part of a list::
|
|
|
|
db.person.name.requires=[IS_NOT_EMPTY(), IS_NOT_IN_DB(db, 'person.id')]
|
|
|
|
Validators are called by the function accepts on a FORM or other HTML
|
|
helper object that contains a form. They are always called in the order in
|
|
which they are listed.
|
|
|
|
Built-in validators have constructors that take the optional argument error
|
|
message which allows you to change the default error message.
|
|
Here is an example of a validator on a database table::
|
|
|
|
db.person.name.requires=IS_NOT_EMPTY(error_message=T('Fill this'))
|
|
|
|
where we have used the translation operator T to allow for
|
|
internationalization.
|
|
|
|
Notice that default error messages are not translated.
|
|
"""
|
|
|
|
def formatter(self, value):
|
|
"""
|
|
For some validators returns a formatted version (matching the validator)
|
|
of value. Otherwise just returns the value.
|
|
"""
|
|
return value
|
|
|
|
def __call__(self, value):
|
|
raise NotImplementedError
|
|
|
|
|
|
class IS_MATCH(Validator):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_MATCH('.+'))
|
|
|
|
The argument of IS_MATCH is a regular expression::
|
|
|
|
>>> IS_MATCH('.+')('hello')
|
|
('hello', None)
|
|
|
|
>>> IS_MATCH('hell')('hello')
|
|
('hello', None)
|
|
|
|
>>> IS_MATCH('hell.*', strict=False)('hello')
|
|
('hello', None)
|
|
|
|
>>> IS_MATCH('hello')('shello')
|
|
('shello', 'invalid expression')
|
|
|
|
>>> IS_MATCH('hello', search=True)('shello')
|
|
('shello', None)
|
|
|
|
>>> IS_MATCH('hello', search=True, strict=False)('shellox')
|
|
('shellox', None)
|
|
|
|
>>> IS_MATCH('.*hello.*', search=True, strict=False)('shellox')
|
|
('shellox', None)
|
|
|
|
>>> IS_MATCH('.+')('')
|
|
('', 'invalid expression')
|
|
|
|
"""
|
|
|
|
def __init__(self, expression, error_message='Invalid expression',
|
|
strict=False, search=False, extract=False,
|
|
is_unicode=False):
|
|
|
|
if strict or not search:
|
|
if not expression.startswith('^'):
|
|
expression = '^(%s)' % expression
|
|
if strict:
|
|
if not expression.endswith('$'):
|
|
expression = '(%s)$' % expression
|
|
if is_unicode:
|
|
if not isinstance(expression, unicodeT):
|
|
expression = expression.decode('utf8')
|
|
self.regex = re.compile(expression, re.UNICODE)
|
|
else:
|
|
self.regex = re.compile(expression)
|
|
self.error_message = error_message
|
|
self.extract = extract
|
|
self.is_unicode = is_unicode or (not(PY2))
|
|
|
|
def __call__(self, value):
|
|
if not(PY2): # PY3 convert bytes to unicode
|
|
value = to_unicode(value)
|
|
|
|
if self.is_unicode or not(PY2):
|
|
if not isinstance(value, unicodeT):
|
|
match = self.regex.search(str(value).decode('utf8'))
|
|
else:
|
|
match = self.regex.search(value)
|
|
else:
|
|
if not isinstance(value, unicodeT):
|
|
match = self.regex.search(str(value))
|
|
else:
|
|
match = self.regex.search(value.encode('utf8'))
|
|
if match is not None:
|
|
return (self.extract and match.group() or value, None)
|
|
return (value, translate(self.error_message))
|
|
|
|
|
|
class IS_EQUAL_TO(Validator):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='password')
|
|
INPUT(_type='text', _name='password2',
|
|
requires=IS_EQUAL_TO(request.vars.password))
|
|
|
|
The argument of IS_EQUAL_TO is a string::
|
|
|
|
>>> IS_EQUAL_TO('aaa')('aaa')
|
|
('aaa', None)
|
|
|
|
>>> IS_EQUAL_TO('aaa')('aab')
|
|
('aab', 'no match')
|
|
|
|
"""
|
|
|
|
def __init__(self, expression, error_message='No match'):
|
|
self.expression = expression
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
if value == self.expression:
|
|
return (value, None)
|
|
return (value, translate(self.error_message))
|
|
|
|
|
|
class IS_EXPR(Validator):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_EXPR('5 < int(value) < 10'))
|
|
|
|
The argument of IS_EXPR must be python condition::
|
|
|
|
>>> IS_EXPR('int(value) < 2')('1')
|
|
('1', None)
|
|
|
|
>>> IS_EXPR('int(value) < 2')('2')
|
|
('2', 'invalid expression')
|
|
|
|
"""
|
|
|
|
def __init__(self, expression, error_message='Invalid expression', environment=None):
|
|
self.expression = expression
|
|
self.error_message = error_message
|
|
self.environment = environment or {}
|
|
|
|
def __call__(self, value):
|
|
if callable(self.expression):
|
|
return (value, self.expression(value))
|
|
# for backward compatibility
|
|
self.environment.update(value=value)
|
|
exec('__ret__=' + self.expression, self.environment)
|
|
if self.environment['__ret__']:
|
|
return (value, None)
|
|
return (value, translate(self.error_message))
|
|
|
|
|
|
class IS_LENGTH(Validator):
|
|
"""
|
|
Checks if length of field's value fits between given boundaries. Works
|
|
for both text and file inputs.
|
|
|
|
Args:
|
|
maxsize: maximum allowed length / size
|
|
minsize: minimum allowed length / size
|
|
|
|
Examples:
|
|
Check if text string is shorter than 33 characters::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_LENGTH(32))
|
|
|
|
Check if password string is longer than 5 characters::
|
|
|
|
INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6))
|
|
|
|
Check if uploaded file has size between 1KB and 1MB::
|
|
|
|
INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024))
|
|
|
|
Other examples::
|
|
|
|
>>> IS_LENGTH()('')
|
|
('', None)
|
|
>>> IS_LENGTH()('1234567890')
|
|
('1234567890', None)
|
|
>>> IS_LENGTH(maxsize=5, minsize=0)('1234567890') # too long
|
|
('1234567890', 'enter from 0 to 5 characters')
|
|
>>> IS_LENGTH(maxsize=50, minsize=20)('1234567890') # too short
|
|
('1234567890', 'enter from 20 to 50 characters')
|
|
"""
|
|
|
|
def __init__(self, maxsize=255, minsize=0,
|
|
error_message='Enter from %(min)g to %(max)g characters'):
|
|
self.maxsize = maxsize
|
|
self.minsize = minsize
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
if value is None:
|
|
length = 0
|
|
if self.minsize <= length <= self.maxsize:
|
|
return (value, None)
|
|
elif isinstance(value, cgi.FieldStorage):
|
|
if value.file:
|
|
value.file.seek(0, os.SEEK_END)
|
|
length = value.file.tell()
|
|
value.file.seek(0, os.SEEK_SET)
|
|
elif hasattr(value, 'value'):
|
|
val = value.value
|
|
if val:
|
|
length = len(val)
|
|
else:
|
|
length = 0
|
|
if self.minsize <= length <= self.maxsize:
|
|
return (value, None)
|
|
elif isinstance(value, str):
|
|
try:
|
|
lvalue = len(to_unicode(value))
|
|
except:
|
|
lvalue = len(value)
|
|
if self.minsize <= lvalue <= self.maxsize:
|
|
return (value, None)
|
|
elif isinstance(value, unicodeT):
|
|
if self.minsize <= len(value) <= self.maxsize:
|
|
return (value.encode('utf8'), None)
|
|
elif isinstance(value, (bytes, bytearray)):
|
|
if self.minsize <= len(value) <= self.maxsize:
|
|
return (value, None)
|
|
elif isinstance(value, (tuple, list)):
|
|
if self.minsize <= len(value) <= self.maxsize:
|
|
return (value, None)
|
|
elif self.minsize <= len(str(value)) <= self.maxsize:
|
|
return (str(value), None)
|
|
return (value, translate(self.error_message)
|
|
% dict(min=self.minsize, max=self.maxsize))
|
|
|
|
|
|
class IS_JSON(Validator):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_JSON(error_message="This is not a valid json input")
|
|
|
|
>>> IS_JSON()('{"a": 100}')
|
|
({u'a': 100}, None)
|
|
|
|
>>> IS_JSON()('spam1234')
|
|
('spam1234', 'invalid json')
|
|
"""
|
|
|
|
def __init__(self, error_message='Invalid json', native_json=False):
|
|
self.native_json = native_json
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
try:
|
|
if self.native_json:
|
|
json.loads(value) # raises error in case of malformed json
|
|
return (value, None) # the serialized value is not passed
|
|
else:
|
|
return (json.loads(value), None)
|
|
except JSONErrors:
|
|
return (value, translate(self.error_message))
|
|
|
|
def formatter(self, value):
|
|
if value is None:
|
|
return None
|
|
if self.native_json:
|
|
return value
|
|
else:
|
|
return json.dumps(value)
|
|
|
|
|
|
class IS_IN_SET(Validator):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_IN_SET(['max', 'john'],zero=''))
|
|
|
|
The argument of IS_IN_SET must be a list or set::
|
|
|
|
>>> IS_IN_SET(['max', 'john'])('max')
|
|
('max', None)
|
|
>>> IS_IN_SET(['max', 'john'])('massimo')
|
|
('massimo', 'value not allowed')
|
|
>>> IS_IN_SET(['max', 'john'], multiple=True)(('max', 'john'))
|
|
(('max', 'john'), None)
|
|
>>> IS_IN_SET(['max', 'john'], multiple=True)(('bill', 'john'))
|
|
(('bill', 'john'), 'value not allowed')
|
|
>>> IS_IN_SET(('id1','id2'), ['first label','second label'])('id1') # Traditional way
|
|
('id1', None)
|
|
>>> IS_IN_SET({'id1':'first label', 'id2':'second label'})('id1')
|
|
('id1', None)
|
|
>>> import itertools
|
|
>>> IS_IN_SET(itertools.chain(['1','3','5'],['2','4','6']))('1')
|
|
('1', None)
|
|
>>> IS_IN_SET([('id1','first label'), ('id2','second label')])('id1') # Redundant way
|
|
('id1', None)
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
theset,
|
|
labels=None,
|
|
error_message='Value not allowed',
|
|
multiple=False,
|
|
zero='',
|
|
sort=False,
|
|
):
|
|
self.multiple = multiple
|
|
if isinstance(theset, dict):
|
|
self.theset = [str(item) for item in theset]
|
|
self.labels = theset.values()
|
|
elif theset and isinstance(theset, (tuple, list)) \
|
|
and isinstance(theset[0], (tuple, list)) and len(theset[0]) == 2:
|
|
self.theset = [str(item) for item, label in theset]
|
|
self.labels = [str(label) for item, label in theset]
|
|
else:
|
|
self.theset = [str(item) for item in theset]
|
|
self.labels = labels
|
|
self.error_message = error_message
|
|
self.zero = zero
|
|
self.sort = sort
|
|
|
|
def options(self, zero=True):
|
|
if not self.labels:
|
|
items = [(k, k) for (i, k) in enumerate(self.theset)]
|
|
else:
|
|
items = [(k, list(self.labels)[i]) for (i, k) in enumerate(self.theset)]
|
|
if self.sort:
|
|
items.sort(key=lambda o: str(o[1]).upper())
|
|
if zero and self.zero is not None and not self.multiple:
|
|
items.insert(0, ('', self.zero))
|
|
return items
|
|
|
|
def __call__(self, value):
|
|
if self.multiple:
|
|
# if below was values = re.compile("[\w\-:]+").findall(str(value))
|
|
if not value:
|
|
values = []
|
|
elif isinstance(value, (tuple, list)):
|
|
values = value
|
|
else:
|
|
values = [value]
|
|
else:
|
|
values = [value]
|
|
thestrset = [str(x) for x in self.theset]
|
|
failures = [x for x in values if not str(x) in thestrset]
|
|
if failures and self.theset:
|
|
return (value, translate(self.error_message))
|
|
if self.multiple:
|
|
if isinstance(self.multiple, (tuple, list)) and \
|
|
not self.multiple[0] <= len(values) < self.multiple[1]:
|
|
return (values, translate(self.error_message))
|
|
return (values, None)
|
|
return (value, None)
|
|
|
|
|
|
regex1 = re.compile('\w+\.\w+')
|
|
regex2 = re.compile('%\(([^\)]+)\)\d*(?:\.\d+)?[a-zA-Z]')
|
|
|
|
|
|
class IS_IN_DB(Validator):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_IN_DB(db, db.mytable.myfield, zero=''))
|
|
|
|
used for reference fields, rendered as a dropbox
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
dbset,
|
|
field,
|
|
label=None,
|
|
error_message='Value not in database',
|
|
orderby=None,
|
|
groupby=None,
|
|
distinct=None,
|
|
cache=None,
|
|
multiple=False,
|
|
zero='',
|
|
sort=False,
|
|
_and=None,
|
|
left=None,
|
|
delimiter=None,
|
|
auto_add=False,
|
|
):
|
|
from pydal.objects import Table
|
|
if hasattr(dbset, 'define_table'):
|
|
self.dbset = dbset()
|
|
else:
|
|
self.dbset = dbset
|
|
|
|
if isinstance(field, Table):
|
|
field = field._id
|
|
elif isinstance(field, str):
|
|
items = field.split('.')
|
|
if len(items) == 1:
|
|
field = items[0] + '.id'
|
|
|
|
(ktable, kfield) = str(field).split('.')
|
|
if not label:
|
|
label = '%%(%s)s' % kfield
|
|
if isinstance(label, str):
|
|
if regex1.match(str(label)):
|
|
label = '%%(%s)s' % str(label).split('.')[-1]
|
|
fieldnames = regex2.findall(label)
|
|
if kfield not in fieldnames:
|
|
fieldnames.append(kfield) # kfield must be last
|
|
elif isinstance(label, Field):
|
|
fieldnames = [label.name, kfield] # kfield must be last
|
|
label = '%%(%s)s' % label.name
|
|
elif callable(label):
|
|
fieldnames = '*'
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
self.fieldnames = fieldnames # fields requires to build the formatting
|
|
self.label = label
|
|
self.ktable = ktable
|
|
self.kfield = kfield
|
|
self.error_message = error_message
|
|
self.theset = None
|
|
self.orderby = orderby
|
|
self.groupby = groupby
|
|
self.distinct = distinct
|
|
self.cache = cache
|
|
self.multiple = multiple
|
|
self.zero = zero
|
|
self.sort = sort
|
|
self._and = _and
|
|
self.left = left
|
|
self.delimiter = delimiter
|
|
self.auto_add = auto_add
|
|
|
|
def set_self_id(self, id):
|
|
if self._and:
|
|
self._and.record_id = id
|
|
|
|
def build_set(self):
|
|
table = self.dbset.db[self.ktable]
|
|
if self.fieldnames == '*':
|
|
fields = [f for f in table]
|
|
else:
|
|
fields = [table[k] for k in self.fieldnames]
|
|
ignore = (FieldVirtual, FieldMethod)
|
|
fields = filter(lambda f: not isinstance(f, ignore), fields)
|
|
if self.dbset.db._dbname != 'gae':
|
|
orderby = self.orderby or reduce(lambda a, b: a | b, fields)
|
|
groupby = self.groupby
|
|
distinct = self.distinct
|
|
left = self.left
|
|
dd = dict(orderby=orderby, groupby=groupby,
|
|
distinct=distinct, cache=self.cache,
|
|
cacheable=True, left=left)
|
|
records = self.dbset(table).select(*fields, **dd)
|
|
else:
|
|
orderby = self.orderby or \
|
|
reduce(lambda a, b: a | b, (
|
|
f for f in fields if not f.name == 'id'))
|
|
dd = dict(orderby=orderby, cache=self.cache, cacheable=True)
|
|
records = self.dbset(table).select(table.ALL, **dd)
|
|
self.theset = [str(r[self.kfield]) for r in records]
|
|
if isinstance(self.label, str):
|
|
self.labels = [self.label % r for r in records]
|
|
else:
|
|
self.labels = [self.label(r) for r in records]
|
|
|
|
def options(self, zero=True):
|
|
self.build_set()
|
|
items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)]
|
|
if self.sort:
|
|
items.sort(key=lambda o: str(o[1]).upper())
|
|
if zero and self.zero is not None and not self.multiple:
|
|
items.insert(0, ('', self.zero))
|
|
return items
|
|
|
|
def maybe_add(self, table, fieldname, value):
|
|
d = {fieldname: value}
|
|
record = table(**d)
|
|
if record:
|
|
return record.id
|
|
else:
|
|
return table.insert(**d)
|
|
|
|
def __call__(self, value):
|
|
table = self.dbset.db[self.ktable]
|
|
field = table[self.kfield]
|
|
|
|
if self.multiple:
|
|
if self._and:
|
|
raise NotImplementedError
|
|
if isinstance(value, list):
|
|
values = value
|
|
elif self.delimiter:
|
|
values = value.split(self.delimiter) # because of autocomplete
|
|
elif value:
|
|
values = [value]
|
|
else:
|
|
values = []
|
|
|
|
if field.type in ('id', 'integer'):
|
|
new_values = []
|
|
for value in values:
|
|
if not (isinstance(value, integer_types) or value.isdigit()):
|
|
if self.auto_add:
|
|
value = str(self.maybe_add(table, self.fieldnames[0], value))
|
|
else:
|
|
return (values, translate(self.error_message))
|
|
new_values.append(value)
|
|
values = new_values
|
|
|
|
if isinstance(self.multiple, (tuple, list)) and \
|
|
not self.multiple[0] <= len(values) < self.multiple[1]:
|
|
return (values, translate(self.error_message))
|
|
if self.theset:
|
|
if not [v for v in values if v not in self.theset]:
|
|
return (values, None)
|
|
else:
|
|
def count(values, s=self.dbset, f=field):
|
|
return s(f.belongs(map(int, values))).count()
|
|
|
|
if self.dbset.db._adapter.dbengine == "google:datastore":
|
|
range_ids = range(0, len(values), 30)
|
|
total = sum(count(values[i:i + 30]) for i in range_ids)
|
|
if total == len(values):
|
|
return (values, None)
|
|
elif count(values) == len(values):
|
|
return (values, None)
|
|
else:
|
|
if field.type in ('id', 'integer'):
|
|
if isinstance(value, integer_types) or (isinstance(value, string_types) and value.isdigit()):
|
|
value = int(value)
|
|
elif self.auto_add:
|
|
value = self.maybe_add(table, self.fieldnames[0], value)
|
|
else:
|
|
return (value, translate(self.error_message))
|
|
|
|
try:
|
|
value = int(value)
|
|
except TypeError:
|
|
return (value, translate(self.error_message))
|
|
|
|
if self.theset:
|
|
if str(value) in self.theset:
|
|
if self._and:
|
|
return self._and(value)
|
|
else:
|
|
return (value, None)
|
|
else:
|
|
if self.dbset(field == value).count():
|
|
if self._and:
|
|
return self._and(value)
|
|
else:
|
|
return (value, None)
|
|
return (value, translate(self.error_message))
|
|
|
|
|
|
class IS_NOT_IN_DB(Validator):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_NOT_IN_DB(db, db.table))
|
|
|
|
makes the field unique
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
dbset,
|
|
field,
|
|
error_message='Value already in database or empty',
|
|
allowed_override=[],
|
|
ignore_common_filters=False,
|
|
):
|
|
|
|
from pydal.objects import Table
|
|
if isinstance(field, Table):
|
|
field = field._id
|
|
|
|
if hasattr(dbset, 'define_table'):
|
|
self.dbset = dbset()
|
|
else:
|
|
self.dbset = dbset
|
|
self.field = field
|
|
self.error_message = error_message
|
|
self.record_id = 0
|
|
self.allowed_override = allowed_override
|
|
self.ignore_common_filters = ignore_common_filters
|
|
|
|
def set_self_id(self, id):
|
|
self.record_id = id
|
|
|
|
def __call__(self, value):
|
|
value = to_native(str(value))
|
|
if not value.strip():
|
|
return (value, translate(self.error_message))
|
|
if value in self.allowed_override:
|
|
return (value, None)
|
|
(tablename, fieldname) = str(self.field).split('.')
|
|
table = self.dbset.db[tablename]
|
|
field = table[fieldname]
|
|
subset = self.dbset(field == value,
|
|
ignore_common_filters=self.ignore_common_filters)
|
|
id = self.record_id
|
|
if isinstance(id, dict):
|
|
fields = [table[f] for f in id]
|
|
row = subset.select(*fields, **dict(limitby=(0, 1), orderby_on_limitby=False)).first()
|
|
if row and any(str(row[f]) != str(id[f]) for f in id):
|
|
return (value, translate(self.error_message))
|
|
else:
|
|
row = subset.select(table._id, field, limitby=(0, 1), orderby_on_limitby=False).first()
|
|
if row and str(row[table._id]) != str(id):
|
|
return (value, translate(self.error_message))
|
|
return (value, None)
|
|
|
|
|
|
def range_error_message(error_message, what_to_enter, minimum, maximum):
|
|
"""build the error message for the number range validators"""
|
|
if error_message is None:
|
|
error_message = 'Enter ' + what_to_enter
|
|
if minimum is not None and maximum is not None:
|
|
error_message += ' between %(min)g and %(max)g'
|
|
elif minimum is not None:
|
|
error_message += ' greater than or equal to %(min)g'
|
|
elif maximum is not None:
|
|
error_message += ' less than or equal to %(max)g'
|
|
if type(maximum) in integer_types:
|
|
maximum -= 1
|
|
return translate(error_message) % dict(min=minimum, max=maximum)
|
|
|
|
|
|
class IS_INT_IN_RANGE(Validator):
|
|
"""
|
|
Determines that the argument is (or can be represented as) an int,
|
|
and that it falls within the specified range. The range is interpreted
|
|
in the Pythonic way, so the test is: min <= value < max.
|
|
|
|
The minimum and maximum limits can be None, meaning no lower or upper limit,
|
|
respectively.
|
|
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_INT_IN_RANGE(0, 10))
|
|
|
|
>>> IS_INT_IN_RANGE(1,5)('4')
|
|
(4, None)
|
|
>>> IS_INT_IN_RANGE(1,5)(4)
|
|
(4, None)
|
|
>>> IS_INT_IN_RANGE(1,5)(1)
|
|
(1, None)
|
|
>>> IS_INT_IN_RANGE(1,5)(5)
|
|
(5, 'enter an integer between 1 and 4')
|
|
>>> IS_INT_IN_RANGE(1,5)(5)
|
|
(5, 'enter an integer between 1 and 4')
|
|
>>> IS_INT_IN_RANGE(1,5)(3.5)
|
|
(3.5, 'enter an integer between 1 and 4')
|
|
>>> IS_INT_IN_RANGE(None,5)('4')
|
|
(4, None)
|
|
>>> IS_INT_IN_RANGE(None,5)('6')
|
|
('6', 'enter an integer less than or equal to 4')
|
|
>>> IS_INT_IN_RANGE(1,None)('4')
|
|
(4, None)
|
|
>>> IS_INT_IN_RANGE(1,None)('0')
|
|
('0', 'enter an integer greater than or equal to 1')
|
|
>>> IS_INT_IN_RANGE()(6)
|
|
(6, None)
|
|
>>> IS_INT_IN_RANGE()('abc')
|
|
('abc', 'enter an integer')
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
minimum=None,
|
|
maximum=None,
|
|
error_message=None,
|
|
):
|
|
self.minimum = int(minimum) if minimum is not None else None
|
|
self.maximum = int(maximum) if maximum is not None else None
|
|
self.error_message = range_error_message(
|
|
error_message, 'an integer', self.minimum, self.maximum)
|
|
|
|
def __call__(self, value):
|
|
if regex_isint.match(str(value)):
|
|
v = int(value)
|
|
if ((self.minimum is None or v >= self.minimum) and
|
|
(self.maximum is None or v < self.maximum)):
|
|
return (v, None)
|
|
return (value, self.error_message)
|
|
|
|
|
|
def str2dec(number):
|
|
s = str(number)
|
|
if '.' not in s:
|
|
s += '.00'
|
|
else:
|
|
s += '0' * (2 - len(s.split('.')[1]))
|
|
return s
|
|
|
|
|
|
class IS_FLOAT_IN_RANGE(Validator):
|
|
"""
|
|
Determines that the argument is (or can be represented as) a float,
|
|
and that it falls within the specified inclusive range.
|
|
The comparison is made with native arithmetic.
|
|
|
|
The minimum and maximum limits can be None, meaning no lower or upper limit,
|
|
respectively.
|
|
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_FLOAT_IN_RANGE(0, 10))
|
|
|
|
>>> IS_FLOAT_IN_RANGE(1,5)('4')
|
|
(4.0, None)
|
|
>>> IS_FLOAT_IN_RANGE(1,5)(4)
|
|
(4.0, None)
|
|
>>> IS_FLOAT_IN_RANGE(1,5)(1)
|
|
(1.0, None)
|
|
>>> IS_FLOAT_IN_RANGE(1,5)(5.25)
|
|
(5.25, 'enter a number between 1 and 5')
|
|
>>> IS_FLOAT_IN_RANGE(1,5)(6.0)
|
|
(6.0, 'enter a number between 1 and 5')
|
|
>>> IS_FLOAT_IN_RANGE(1,5)(3.5)
|
|
(3.5, None)
|
|
>>> IS_FLOAT_IN_RANGE(1,None)(3.5)
|
|
(3.5, None)
|
|
>>> IS_FLOAT_IN_RANGE(None,5)(3.5)
|
|
(3.5, None)
|
|
>>> IS_FLOAT_IN_RANGE(1,None)(0.5)
|
|
(0.5, 'enter a number greater than or equal to 1')
|
|
>>> IS_FLOAT_IN_RANGE(None,5)(6.5)
|
|
(6.5, 'enter a number less than or equal to 5')
|
|
>>> IS_FLOAT_IN_RANGE()(6.5)
|
|
(6.5, None)
|
|
>>> IS_FLOAT_IN_RANGE()('abc')
|
|
('abc', 'enter a number')
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
minimum=None,
|
|
maximum=None,
|
|
error_message=None,
|
|
dot='.'
|
|
):
|
|
self.minimum = float(minimum) if minimum is not None else None
|
|
self.maximum = float(maximum) if maximum is not None else None
|
|
self.dot = str(dot)
|
|
self.error_message = range_error_message(
|
|
error_message, 'a number', self.minimum, self.maximum)
|
|
|
|
def __call__(self, value):
|
|
try:
|
|
if self.dot == '.':
|
|
v = float(value)
|
|
else:
|
|
v = float(str(value).replace(self.dot, '.'))
|
|
if ((self.minimum is None or v >= self.minimum) and
|
|
(self.maximum is None or v <= self.maximum)):
|
|
return (v, None)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
return (value, self.error_message)
|
|
|
|
def formatter(self, value):
|
|
if value is None:
|
|
return None
|
|
return str2dec(value).replace('.', self.dot)
|
|
|
|
|
|
class IS_DECIMAL_IN_RANGE(Validator):
|
|
"""
|
|
Determines that the argument is (or can be represented as) a Python Decimal,
|
|
and that it falls within the specified inclusive range.
|
|
The comparison is made with Python Decimal arithmetic.
|
|
|
|
The minimum and maximum limits can be None, meaning no lower or upper limit,
|
|
respectively.
|
|
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10))
|
|
|
|
>>> IS_DECIMAL_IN_RANGE(1,5)('4')
|
|
(Decimal('4'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(1,5)(4)
|
|
(Decimal('4'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(1,5)(1)
|
|
(Decimal('1'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(1,5)(5.25)
|
|
(5.25, 'enter a number between 1 and 5')
|
|
>>> IS_DECIMAL_IN_RANGE(5.25,6)(5.25)
|
|
(Decimal('5.25'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(5.25,6)('5.25')
|
|
(Decimal('5.25'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(1,5)(6.0)
|
|
(6.0, 'enter a number between 1 and 5')
|
|
>>> IS_DECIMAL_IN_RANGE(1,5)(3.5)
|
|
(Decimal('3.5'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(1.5,5.5)(3.5)
|
|
(Decimal('3.5'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(1.5,5.5)(6.5)
|
|
(6.5, 'enter a number between 1.5 and 5.5')
|
|
>>> IS_DECIMAL_IN_RANGE(1.5,None)(6.5)
|
|
(Decimal('6.5'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(1.5,None)(0.5)
|
|
(0.5, 'enter a number greater than or equal to 1.5')
|
|
>>> IS_DECIMAL_IN_RANGE(None,5.5)(4.5)
|
|
(Decimal('4.5'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(None,5.5)(6.5)
|
|
(6.5, 'enter a number less than or equal to 5.5')
|
|
>>> IS_DECIMAL_IN_RANGE()(6.5)
|
|
(Decimal('6.5'), None)
|
|
>>> IS_DECIMAL_IN_RANGE(0,99)(123.123)
|
|
(123.123, 'enter a number between 0 and 99')
|
|
>>> IS_DECIMAL_IN_RANGE(0,99)('123.123')
|
|
('123.123', 'enter a number between 0 and 99')
|
|
>>> IS_DECIMAL_IN_RANGE(0,99)('12.34')
|
|
(Decimal('12.34'), None)
|
|
>>> IS_DECIMAL_IN_RANGE()('abc')
|
|
('abc', 'enter a number')
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
minimum=None,
|
|
maximum=None,
|
|
error_message=None,
|
|
dot='.'
|
|
):
|
|
self.minimum = decimal.Decimal(str(minimum)) if minimum is not None else None
|
|
self.maximum = decimal.Decimal(str(maximum)) if maximum is not None else None
|
|
self.dot = str(dot)
|
|
self.error_message = range_error_message(
|
|
error_message, 'a number', self.minimum, self.maximum)
|
|
|
|
def __call__(self, value):
|
|
try:
|
|
if isinstance(value, decimal.Decimal):
|
|
v = value
|
|
else:
|
|
v = decimal.Decimal(str(value).replace(self.dot, '.'))
|
|
if ((self.minimum is None or v >= self.minimum) and
|
|
(self.maximum is None or v <= self.maximum)):
|
|
return (v, None)
|
|
except (ValueError, TypeError, decimal.InvalidOperation):
|
|
pass
|
|
return (value, self.error_message)
|
|
|
|
def formatter(self, value):
|
|
if value is None:
|
|
return None
|
|
return str2dec(value).replace('.', self.dot)
|
|
|
|
|
|
def is_empty(value, empty_regex=None):
|
|
_value = value
|
|
"""test empty field"""
|
|
if isinstance(value, (str, unicodeT)):
|
|
value = value.strip()
|
|
if empty_regex is not None and empty_regex.match(value):
|
|
value = ''
|
|
if value is None or value == '' or value == b'' or value == []:
|
|
return (_value, True)
|
|
return (_value, False)
|
|
|
|
|
|
class IS_NOT_EMPTY(Validator):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_NOT_EMPTY())
|
|
|
|
>>> IS_NOT_EMPTY()(1)
|
|
(1, None)
|
|
>>> IS_NOT_EMPTY()(0)
|
|
(0, None)
|
|
>>> IS_NOT_EMPTY()('x')
|
|
('x', None)
|
|
>>> IS_NOT_EMPTY()(' x ')
|
|
('x', None)
|
|
>>> IS_NOT_EMPTY()(None)
|
|
(None, 'enter a value')
|
|
>>> IS_NOT_EMPTY()('')
|
|
('', 'enter a value')
|
|
>>> IS_NOT_EMPTY()(' ')
|
|
('', 'enter a value')
|
|
>>> IS_NOT_EMPTY()(' \\n\\t')
|
|
('', 'enter a value')
|
|
>>> IS_NOT_EMPTY()([])
|
|
([], 'enter a value')
|
|
>>> IS_NOT_EMPTY(empty_regex='def')('def')
|
|
('', 'enter a value')
|
|
>>> IS_NOT_EMPTY(empty_regex='de[fg]')('deg')
|
|
('', 'enter a value')
|
|
>>> IS_NOT_EMPTY(empty_regex='def')('abc')
|
|
('abc', None)
|
|
"""
|
|
|
|
def __init__(self, error_message='Enter a value', empty_regex=None):
|
|
self.error_message = error_message
|
|
if empty_regex is not None:
|
|
self.empty_regex = re.compile(empty_regex)
|
|
else:
|
|
self.empty_regex = None
|
|
|
|
def __call__(self, value):
|
|
value, empty = is_empty(value, empty_regex=self.empty_regex)
|
|
if empty:
|
|
return (value, translate(self.error_message))
|
|
return (value, None)
|
|
|
|
|
|
class IS_ALPHANUMERIC(IS_MATCH):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_ALPHANUMERIC())
|
|
|
|
>>> IS_ALPHANUMERIC()('1')
|
|
('1', None)
|
|
>>> IS_ALPHANUMERIC()('')
|
|
('', None)
|
|
>>> IS_ALPHANUMERIC()('A_a')
|
|
('A_a', None)
|
|
>>> IS_ALPHANUMERIC()('!')
|
|
('!', 'enter only letters, numbers, and underscore')
|
|
"""
|
|
|
|
def __init__(self, error_message='Enter only letters, numbers, and underscore'):
|
|
IS_MATCH.__init__(self, '^[\w]*$', error_message)
|
|
|
|
|
|
class IS_EMAIL(Validator):
|
|
"""
|
|
Checks if field's value is a valid email address. Can be set to disallow
|
|
or force addresses from certain domain(s).
|
|
|
|
Email regex adapted from
|
|
http://haacked.com/archive/2007/08/21/i-knew-how-to-validate-an-email-address-until-i.aspx,
|
|
generally following the RFCs, except that we disallow quoted strings
|
|
and permit underscores and leading numerics in subdomain labels
|
|
|
|
Args:
|
|
banned: regex text for disallowed address domains
|
|
forced: regex text for required address domains
|
|
|
|
Both arguments can also be custom objects with a match(value) method.
|
|
|
|
Example:
|
|
Check for valid email address::
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_EMAIL())
|
|
|
|
Check for valid email address that can't be from a .com domain::
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_EMAIL(banned='^.*\.com(|\..*)$'))
|
|
|
|
Check for valid email address that must be from a .edu domain::
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_EMAIL(forced='^.*\.edu(|\..*)$'))
|
|
|
|
>>> IS_EMAIL()('a@b.com')
|
|
('a@b.com', None)
|
|
>>> IS_EMAIL()('abc@def.com')
|
|
('abc@def.com', None)
|
|
>>> IS_EMAIL()('abc@3def.com')
|
|
('abc@3def.com', None)
|
|
>>> IS_EMAIL()('abc@def.us')
|
|
('abc@def.us', None)
|
|
>>> IS_EMAIL()('abc@d_-f.us')
|
|
('abc@d_-f.us', None)
|
|
>>> IS_EMAIL()('@def.com') # missing name
|
|
('@def.com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('"abc@def".com') # quoted name
|
|
('"abc@def".com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('abc+def.com') # no @
|
|
('abc+def.com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('abc@def.x') # one-char TLD
|
|
('abc@def.x', 'enter a valid email address')
|
|
>>> IS_EMAIL()('abc@def.12') # numeric TLD
|
|
('abc@def.12', 'enter a valid email address')
|
|
>>> IS_EMAIL()('abc@def..com') # double-dot in domain
|
|
('abc@def..com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('abc@.def.com') # dot starts domain
|
|
('abc@.def.com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('abc@def.c_m') # underscore in TLD
|
|
('abc@def.c_m', 'enter a valid email address')
|
|
>>> IS_EMAIL()('NotAnEmail') # missing @
|
|
('NotAnEmail', 'enter a valid email address')
|
|
>>> IS_EMAIL()('abc@NotAnEmail') # missing TLD
|
|
('abc@NotAnEmail', 'enter a valid email address')
|
|
>>> IS_EMAIL()('customer/department@example.com')
|
|
('customer/department@example.com', None)
|
|
>>> IS_EMAIL()('$A12345@example.com')
|
|
('$A12345@example.com', None)
|
|
>>> IS_EMAIL()('!def!xyz%abc@example.com')
|
|
('!def!xyz%abc@example.com', None)
|
|
>>> IS_EMAIL()('_Yosemite.Sam@example.com')
|
|
('_Yosemite.Sam@example.com', None)
|
|
>>> IS_EMAIL()('~@example.com')
|
|
('~@example.com', None)
|
|
>>> IS_EMAIL()('.wooly@example.com') # dot starts name
|
|
('.wooly@example.com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('wo..oly@example.com') # adjacent dots in name
|
|
('wo..oly@example.com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('pootietang.@example.com') # dot ends name
|
|
('pootietang.@example.com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('.@example.com') # name is bare dot
|
|
('.@example.com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('Ima.Fool@example.com')
|
|
('Ima.Fool@example.com', None)
|
|
>>> IS_EMAIL()('Ima Fool@example.com') # space in name
|
|
('Ima Fool@example.com', 'enter a valid email address')
|
|
>>> IS_EMAIL()('localguy@localhost') # localhost as domain
|
|
('localguy@localhost', None)
|
|
|
|
"""
|
|
|
|
body_regex = re.compile('''
|
|
^(?!\.) # name may not begin with a dot
|
|
(
|
|
[-a-z0-9!\#$%&'*+/=?^_`{|}~] # all legal characters except dot
|
|
|
|
|
(?<!\.)\. # single dots only
|
|
)+
|
|
(?<!\.)$ # name may not end with a dot
|
|
''', re.VERBOSE | re.IGNORECASE)
|
|
domain_regex = re.compile('''
|
|
(
|
|
localhost
|
|
|
|
|
(
|
|
[a-z0-9]
|
|
# [sub]domain begins with alphanumeric
|
|
(
|
|
[-\w]* # alphanumeric, underscore, dot, hyphen
|
|
[a-z0-9] # ending alphanumeric
|
|
)?
|
|
\. # ending dot
|
|
)+
|
|
[a-z]{2,} # TLD alpha-only
|
|
)$
|
|
''', re.VERBOSE | re.IGNORECASE)
|
|
|
|
regex_proposed_but_failed = re.compile('^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$', re.VERBOSE | re.IGNORECASE)
|
|
|
|
def __init__(self,
|
|
banned=None,
|
|
forced=None,
|
|
error_message='Enter a valid email address'):
|
|
if isinstance(banned, str):
|
|
banned = re.compile(banned)
|
|
if isinstance(forced, str):
|
|
forced = re.compile(forced)
|
|
self.banned = banned
|
|
self.forced = forced
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
if not(isinstance(value, (basestring, unicodeT))) or not value or '@' not in value:
|
|
return (value, translate(self.error_message))
|
|
|
|
body, domain = value.rsplit('@', 1)
|
|
|
|
try:
|
|
match_body = self.body_regex.match(body)
|
|
match_domain = self.domain_regex.match(domain)
|
|
|
|
if not match_domain:
|
|
# check for Internationalized Domain Names
|
|
# see https://docs.python.org/2/library/codecs.html#module-encodings.idna
|
|
domain_encoded = to_unicode(domain).encode('idna').decode('ascii')
|
|
match_domain = self.domain_regex.match(domain_encoded)
|
|
|
|
match = (match_body is not None) and (match_domain is not None)
|
|
except (TypeError, UnicodeError):
|
|
# Value may not be a string where we can look for matches.
|
|
# Example: we're calling ANY_OF formatter and IS_EMAIL is asked to validate a date.
|
|
match = None
|
|
if match:
|
|
if (not self.banned or not self.banned.match(domain)) \
|
|
and (not self.forced or self.forced.match(domain)):
|
|
return (value, None)
|
|
return (value, translate(self.error_message))
|
|
|
|
|
|
class IS_LIST_OF_EMAILS(object):
|
|
"""
|
|
Example:
|
|
Used as::
|
|
|
|
Field('emails', 'list:string',
|
|
widget=SQLFORM.widgets.text.widget,
|
|
requires=IS_LIST_OF_EMAILS(),
|
|
represent=lambda v, r: \
|
|
XML(', '.join([A(x, _href='mailto:'+x).xml() for x in (v or [])]))
|
|
)
|
|
"""
|
|
split_emails = re.compile('[^,;\s]+')
|
|
|
|
def __init__(self, error_message='Invalid emails: %s'):
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
bad_emails = []
|
|
f = IS_EMAIL()
|
|
for email in self.split_emails.findall(value):
|
|
error = f(email)[1]
|
|
if error and email not in bad_emails:
|
|
bad_emails.append(email)
|
|
if not bad_emails:
|
|
return (value, None)
|
|
else:
|
|
return (value,
|
|
translate(self.error_message) % ', '.join(bad_emails))
|
|
|
|
def formatter(self, value, row=None):
|
|
return ', '.join(value or [])
|
|
|
|
|
|
# URL scheme source:
|
|
# <http://en.wikipedia.org/wiki/URI_scheme> obtained on 2008-Nov-10
|
|
|
|
official_url_schemes = [
|
|
'aaa',
|
|
'aaas',
|
|
'acap',
|
|
'cap',
|
|
'cid',
|
|
'crid',
|
|
'data',
|
|
'dav',
|
|
'dict',
|
|
'dns',
|
|
'fax',
|
|
'file',
|
|
'ftp',
|
|
'go',
|
|
'gopher',
|
|
'h323',
|
|
'http',
|
|
'https',
|
|
'icap',
|
|
'im',
|
|
'imap',
|
|
'info',
|
|
'ipp',
|
|
'iris',
|
|
'iris.beep',
|
|
'iris.xpc',
|
|
'iris.xpcs',
|
|
'iris.lws',
|
|
'ldap',
|
|
'mailto',
|
|
'mid',
|
|
'modem',
|
|
'msrp',
|
|
'msrps',
|
|
'mtqp',
|
|
'mupdate',
|
|
'news',
|
|
'nfs',
|
|
'nntp',
|
|
'opaquelocktoken',
|
|
'pop',
|
|
'pres',
|
|
'prospero',
|
|
'rtsp',
|
|
'service',
|
|
'shttp',
|
|
'sip',
|
|
'sips',
|
|
'snmp',
|
|
'soap.beep',
|
|
'soap.beeps',
|
|
'tag',
|
|
'tel',
|
|
'telnet',
|
|
'tftp',
|
|
'thismessage',
|
|
'tip',
|
|
'tv',
|
|
'urn',
|
|
'vemmi',
|
|
'wais',
|
|
'xmlrpc.beep',
|
|
'xmlrpc.beep',
|
|
'xmpp',
|
|
'z39.50r',
|
|
'z39.50s',
|
|
]
|
|
unofficial_url_schemes = [
|
|
'about',
|
|
'adiumxtra',
|
|
'aim',
|
|
'afp',
|
|
'aw',
|
|
'callto',
|
|
'chrome',
|
|
'cvs',
|
|
'ed2k',
|
|
'feed',
|
|
'fish',
|
|
'gg',
|
|
'gizmoproject',
|
|
'iax2',
|
|
'irc',
|
|
'ircs',
|
|
'itms',
|
|
'jar',
|
|
'javascript',
|
|
'keyparc',
|
|
'lastfm',
|
|
'ldaps',
|
|
'magnet',
|
|
'mms',
|
|
'msnim',
|
|
'mvn',
|
|
'notes',
|
|
'nsfw',
|
|
'psyc',
|
|
'paparazzi:http',
|
|
'rmi',
|
|
'rsync',
|
|
'secondlife',
|
|
'sgn',
|
|
'skype',
|
|
'ssh',
|
|
'sftp',
|
|
'smb',
|
|
'sms',
|
|
'soldat',
|
|
'steam',
|
|
'svn',
|
|
'teamspeak',
|
|
'unreal',
|
|
'ut2004',
|
|
'ventrilo',
|
|
'view-source',
|
|
'webcal',
|
|
'wyciwyg',
|
|
'xfire',
|
|
'xri',
|
|
'ymsgr',
|
|
]
|
|
all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes
|
|
http_schemes = [None, 'http', 'https']
|
|
|
|
# Defined in RFC 3490, Section 3.1, Requirement #1
|
|
# Use this regex to split the authority component of a unicode URL into
|
|
# its component labels
|
|
label_split_regex = re.compile(u'[\u002e\u3002\uff0e\uff61]')
|
|
|
|
|
|
def escape_unicode(string):
|
|
"""
|
|
Converts a unicode string into US-ASCII, using a simple conversion scheme.
|
|
Each unicode character that does not have a US-ASCII equivalent is
|
|
converted into a URL escaped form based on its hexadecimal value.
|
|
For example, the unicode character '\u4e86' will become the string '%4e%86'
|
|
|
|
Args:
|
|
string: unicode string, the unicode string to convert into an
|
|
escaped US-ASCII form
|
|
|
|
Returns:
|
|
string: the US-ASCII escaped form of the inputted string
|
|
|
|
@author: Jonathan Benn
|
|
"""
|
|
returnValue = StringIO()
|
|
|
|
for character in string:
|
|
code = ord(character)
|
|
if code > 0x7F:
|
|
hexCode = hex(code)
|
|
returnValue.write('%' + hexCode[2:4] + '%' + hexCode[4:6])
|
|
else:
|
|
returnValue.write(character)
|
|
|
|
return returnValue.getvalue()
|
|
|
|
|
|
def unicode_to_ascii_authority(authority):
|
|
"""
|
|
Follows the steps in RFC 3490, Section 4 to convert a unicode authority
|
|
string into its ASCII equivalent.
|
|
For example, u'www.Alliancefran\xe7aise.nu' will be converted into
|
|
'www.xn--alliancefranaise-npb.nu'
|
|
|
|
Args:
|
|
authority: unicode string, the URL authority component to convert,
|
|
e.g. u'www.Alliancefran\xe7aise.nu'
|
|
|
|
Returns:
|
|
string: the US-ASCII character equivalent to the inputed authority,
|
|
e.g. 'www.xn--alliancefranaise-npb.nu'
|
|
|
|
Raises:
|
|
Exception: if the function is not able to convert the inputed
|
|
authority
|
|
|
|
@author: Jonathan Benn
|
|
"""
|
|
# RFC 3490, Section 4, Step 1
|
|
# The encodings.idna Python module assumes that AllowUnassigned == True
|
|
|
|
# RFC 3490, Section 4, Step 2
|
|
labels = label_split_regex.split(authority)
|
|
|
|
# RFC 3490, Section 4, Step 3
|
|
# The encodings.idna Python module assumes that UseSTD3ASCIIRules == False
|
|
|
|
# RFC 3490, Section 4, Step 4
|
|
# We use the ToASCII operation because we are about to put the authority
|
|
# into an IDN-unaware slot
|
|
asciiLabels = []
|
|
import encodings.idna
|
|
for label in labels:
|
|
if label:
|
|
asciiLabels.append(to_native(encodings.idna.ToASCII(label)))
|
|
else:
|
|
# encodings.idna.ToASCII does not accept an empty string, but
|
|
# it is necessary for us to allow for empty labels so that we
|
|
# don't modify the URL
|
|
asciiLabels.append('')
|
|
# RFC 3490, Section 4, Step 5
|
|
return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels))
|
|
|
|
|
|
def unicode_to_ascii_url(url, prepend_scheme):
|
|
"""
|
|
Converts the inputed unicode url into a US-ASCII equivalent. This function
|
|
goes a little beyond RFC 3490, which is limited in scope to the domain name
|
|
(authority) only. Here, the functionality is expanded to what was observed
|
|
on Wikipedia on 2009-Jan-22:
|
|
|
|
Component Can Use Unicode?
|
|
--------- ----------------
|
|
scheme No
|
|
authority Yes
|
|
path Yes
|
|
query Yes
|
|
fragment No
|
|
|
|
The authority component gets converted to punycode, but occurrences of
|
|
unicode in other components get converted into a pair of URI escapes (we
|
|
assume 4-byte unicode). E.g. the unicode character U+4E2D will be
|
|
converted into '%4E%2D'. Testing with Firefox v3.0.5 has shown that it can
|
|
understand this kind of URI encoding.
|
|
|
|
Args:
|
|
url: unicode string, the URL to convert from unicode into US-ASCII
|
|
prepend_scheme: string, a protocol scheme to prepend to the URL if
|
|
we're having trouble parsing it.
|
|
e.g. "http". Input None to disable this functionality
|
|
|
|
Returns:
|
|
string: a US-ASCII equivalent of the inputed url
|
|
|
|
@author: Jonathan Benn
|
|
"""
|
|
# convert the authority component of the URL into an ASCII punycode string,
|
|
# but encode the rest using the regular URI character encoding
|
|
components = urlparse.urlparse(url)
|
|
prepended = False
|
|
# If no authority was found
|
|
if not components.netloc:
|
|
# Try appending a scheme to see if that fixes the problem
|
|
scheme_to_prepend = prepend_scheme or 'http'
|
|
components = urlparse.urlparse(to_unicode(scheme_to_prepend) + u'://' + url)
|
|
prepended = True
|
|
|
|
# if we still can't find the authority
|
|
if not components.netloc:
|
|
raise Exception('No authority component found, ' +
|
|
'could not decode unicode to US-ASCII')
|
|
|
|
# We're here if we found an authority, let's rebuild the URL
|
|
scheme = components.scheme
|
|
authority = components.netloc
|
|
path = components.path
|
|
query = components.query
|
|
fragment = components.fragment
|
|
|
|
if prepended:
|
|
scheme = ''
|
|
|
|
unparsed = urlparse.urlunparse((scheme,
|
|
unicode_to_ascii_authority(authority),
|
|
escape_unicode(path),
|
|
'',
|
|
escape_unicode(query),
|
|
str(fragment)))
|
|
if unparsed.startswith('//'):
|
|
unparsed = unparsed[2:] # Remove the // urlunparse puts in the beginning
|
|
return unparsed
|
|
|
|
|
|
class IS_GENERIC_URL(Validator):
|
|
"""
|
|
Rejects a URL string if any of the following is true:
|
|
* The string is empty or None
|
|
* The string uses characters that are not allowed in a URL
|
|
* The URL scheme specified (if one is specified) is not valid
|
|
|
|
Based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html
|
|
|
|
This function only checks the URL's syntax. It does not check that the URL
|
|
points to a real document, for example, or that it otherwise makes sense
|
|
semantically. This function does automatically prepend 'http://' in front
|
|
of a URL if and only if that's necessary to successfully parse the URL.
|
|
Please note that a scheme will be prepended only for rare cases
|
|
(e.g. 'google.ca:80')
|
|
|
|
The list of allowed schemes is customizable with the allowed_schemes
|
|
parameter. If you exclude None from the list, then abbreviated URLs
|
|
(lacking a scheme such as 'http') will be rejected.
|
|
|
|
The default prepended scheme is customizable with the prepend_scheme
|
|
parameter. If you set prepend_scheme to None then prepending will be
|
|
disabled. URLs that require prepending to parse will still be accepted,
|
|
but the return value will not be modified.
|
|
|
|
@author: Jonathan Benn
|
|
|
|
>>> IS_GENERIC_URL()('http://user@abc.com')
|
|
('http://user@abc.com', None)
|
|
|
|
Args:
|
|
error_message: a string, the error message to give the end user
|
|
if the URL does not validate
|
|
allowed_schemes: a list containing strings or None. Each element
|
|
is a scheme the inputed URL is allowed to use
|
|
prepend_scheme: a string, this scheme is prepended if it's
|
|
necessary to make the URL valid
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
error_message='Enter a valid URL',
|
|
allowed_schemes=None,
|
|
prepend_scheme=None,
|
|
):
|
|
|
|
self.error_message = error_message
|
|
if allowed_schemes is None:
|
|
self.allowed_schemes = all_url_schemes
|
|
else:
|
|
self.allowed_schemes = allowed_schemes
|
|
self.prepend_scheme = prepend_scheme
|
|
if self.prepend_scheme not in self.allowed_schemes:
|
|
raise SyntaxError("prepend_scheme='%s' is not in allowed_schemes=%s"
|
|
% (self.prepend_scheme, self.allowed_schemes))
|
|
|
|
GENERIC_URL = re.compile(r"%[^0-9A-Fa-f]{2}|%[^0-9A-Fa-f][0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]|%$|%[0-9A-Fa-f]$|%[^0-9A-Fa-f]$")
|
|
GENERIC_URL_VALID = re.compile(r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$")
|
|
URL_FRAGMENT_VALID = re.compile(r"[|A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$")
|
|
|
|
def __call__(self, value):
|
|
"""
|
|
Args:
|
|
value: a string, the URL to validate
|
|
|
|
Returns:
|
|
a tuple, where tuple[0] is the inputed value (possible
|
|
prepended with prepend_scheme), and tuple[1] is either
|
|
None (success!) or the string error_message
|
|
"""
|
|
|
|
# if we dont have anything or the URL misuses the '%' character
|
|
|
|
if not value or self.GENERIC_URL.search(value):
|
|
return (value, translate(self.error_message))
|
|
|
|
if '#' in value:
|
|
url, fragment_part = value.split('#')
|
|
else:
|
|
url, fragment_part = value, ''
|
|
# if the URL is only composed of valid characters
|
|
if self.GENERIC_URL_VALID.match(url) and (not fragment_part or self.URL_FRAGMENT_VALID.match(fragment_part)):
|
|
# Then parse the URL into its components and check on
|
|
try:
|
|
components = urlparse.urlparse(urllib_unquote(value))._asdict()
|
|
except ValueError:
|
|
return (value, translate(self.error_message))
|
|
|
|
# Clean up the scheme before we check it
|
|
scheme = components['scheme']
|
|
if len(scheme) == 0:
|
|
scheme = None
|
|
else:
|
|
scheme = components['scheme'].lower()
|
|
# If the scheme doesn't really exists
|
|
if scheme not in self.allowed_schemes or not scheme and ':' in components['path']:
|
|
# for the possible case of abbreviated URLs with
|
|
# ports, check to see if adding a valid scheme fixes
|
|
# the problem (but only do this if it doesn't have
|
|
# one already!)
|
|
if '://' not in value and None in self.allowed_schemes:
|
|
schemeToUse = self.prepend_scheme or 'http'
|
|
prependTest = self.__call__(
|
|
schemeToUse + '://' + value)
|
|
# if the prepend test succeeded
|
|
if prependTest[1] is None:
|
|
# if prepending in the output is enabled
|
|
if self.prepend_scheme:
|
|
return prependTest
|
|
else:
|
|
return (value, None)
|
|
else:
|
|
return (value, None)
|
|
# else the URL is not valid
|
|
return (value, translate(self.error_message))
|
|
|
|
# Sources (obtained 2017-Nov-11):
|
|
# http://data.iana.org/TLD/tlds-alpha-by-domain.txt
|
|
# see scripts/parse_top_level_domains.py for an easy update
|
|
|
|
official_top_level_domains = [
|
|
# a
|
|
'aaa', 'aarp', 'abarth', 'abb', 'abbott', 'abbvie', 'abc',
|
|
'able', 'abogado', 'abudhabi', 'ac', 'academy', 'accenture',
|
|
'accountant', 'accountants', 'aco', 'active', 'actor', 'ad',
|
|
'adac', 'ads', 'adult', 'ae', 'aeg', 'aero', 'aetna', 'af',
|
|
'afamilycompany', 'afl', 'africa', 'ag', 'agakhan', 'agency',
|
|
'ai', 'aig', 'aigo', 'airbus', 'airforce', 'airtel', 'akdn',
|
|
'al', 'alfaromeo', 'alibaba', 'alipay', 'allfinanz', 'allstate',
|
|
'ally', 'alsace', 'alstom', 'am', 'americanexpress',
|
|
'americanfamily', 'amex', 'amfam', 'amica', 'amsterdam',
|
|
'analytics', 'android', 'anquan', 'anz', 'ao', 'aol',
|
|
'apartments', 'app', 'apple', 'aq', 'aquarelle', 'ar', 'arab',
|
|
'aramco', 'archi', 'army', 'arpa', 'art', 'arte', 'as', 'asda',
|
|
'asia', 'associates', 'at', 'athleta', 'attorney', 'au',
|
|
'auction', 'audi', 'audible', 'audio', 'auspost', 'author',
|
|
'auto', 'autos', 'avianca', 'aw', 'aws', 'ax', 'axa', 'az',
|
|
'azure',
|
|
# b
|
|
'ba', 'baby', 'baidu', 'banamex', 'bananarepublic', 'band',
|
|
'bank', 'bar', 'barcelona', 'barclaycard', 'barclays',
|
|
'barefoot', 'bargains', 'baseball', 'basketball', 'bauhaus',
|
|
'bayern', 'bb', 'bbc', 'bbt', 'bbva', 'bcg', 'bcn', 'bd', 'be',
|
|
'beats', 'beauty', 'beer', 'bentley', 'berlin', 'best',
|
|
'bestbuy', 'bet', 'bf', 'bg', 'bh', 'bharti', 'bi', 'bible',
|
|
'bid', 'bike', 'bing', 'bingo', 'bio', 'biz', 'bj', 'black',
|
|
'blackfriday', 'blanco', 'blockbuster', 'blog', 'bloomberg',
|
|
'blue', 'bm', 'bms', 'bmw', 'bn', 'bnl', 'bnpparibas', 'bo',
|
|
'boats', 'boehringer', 'bofa', 'bom', 'bond', 'boo', 'book',
|
|
'booking', 'boots', 'bosch', 'bostik', 'boston', 'bot',
|
|
'boutique', 'box', 'br', 'bradesco', 'bridgestone', 'broadway',
|
|
'broker', 'brother', 'brussels', 'bs', 'bt', 'budapest',
|
|
'bugatti', 'build', 'builders', 'business', 'buy', 'buzz', 'bv',
|
|
'bw', 'by', 'bz', 'bzh',
|
|
# c
|
|
'ca', 'cab', 'cafe', 'cal', 'call', 'calvinklein', 'cam',
|
|
'camera', 'camp', 'cancerresearch', 'canon', 'capetown',
|
|
'capital', 'capitalone', 'car', 'caravan', 'cards', 'care',
|
|
'career', 'careers', 'cars', 'cartier', 'casa', 'case', 'caseih',
|
|
'cash', 'casino', 'cat', 'catering', 'catholic', 'cba', 'cbn',
|
|
'cbre', 'cbs', 'cc', 'cd', 'ceb', 'center', 'ceo', 'cern', 'cf',
|
|
'cfa', 'cfd', 'cg', 'ch', 'chanel', 'channel', 'chase', 'chat',
|
|
'cheap', 'chintai', 'christmas', 'chrome', 'chrysler', 'church',
|
|
'ci', 'cipriani', 'circle', 'cisco', 'citadel', 'citi', 'citic',
|
|
'city', 'cityeats', 'ck', 'cl', 'claims', 'cleaning', 'click',
|
|
'clinic', 'clinique', 'clothing', 'cloud', 'club', 'clubmed',
|
|
'cm', 'cn', 'co', 'coach', 'codes', 'coffee', 'college',
|
|
'cologne', 'com', 'comcast', 'commbank', 'community', 'company',
|
|
'compare', 'computer', 'comsec', 'condos', 'construction',
|
|
'consulting', 'contact', 'contractors', 'cooking',
|
|
'cookingchannel', 'cool', 'coop', 'corsica', 'country', 'coupon',
|
|
'coupons', 'courses', 'cr', 'credit', 'creditcard',
|
|
'creditunion', 'cricket', 'crown', 'crs', 'cruise', 'cruises',
|
|
'csc', 'cu', 'cuisinella', 'cv', 'cw', 'cx', 'cy', 'cymru',
|
|
'cyou', 'cz',
|
|
# d
|
|
'dabur', 'dad', 'dance', 'data', 'date', 'dating', 'datsun',
|
|
'day', 'dclk', 'dds', 'de', 'deal', 'dealer', 'deals', 'degree',
|
|
'delivery', 'dell', 'deloitte', 'delta', 'democrat', 'dental',
|
|
'dentist', 'desi', 'design', 'dev', 'dhl', 'diamonds', 'diet',
|
|
'digital', 'direct', 'directory', 'discount', 'discover', 'dish',
|
|
'diy', 'dj', 'dk', 'dm', 'dnp', 'do', 'docs', 'doctor', 'dodge',
|
|
'dog', 'doha', 'domains', 'dot', 'download', 'drive', 'dtv',
|
|
'dubai', 'duck', 'dunlop', 'duns', 'dupont', 'durban', 'dvag',
|
|
'dvr', 'dz',
|
|
# e
|
|
'earth', 'eat', 'ec', 'eco', 'edeka', 'edu', 'education', 'ee',
|
|
'eg', 'email', 'emerck', 'energy', 'engineer', 'engineering',
|
|
'enterprises', 'epost', 'epson', 'equipment', 'er', 'ericsson',
|
|
'erni', 'es', 'esq', 'estate', 'esurance', 'et', 'etisalat',
|
|
'eu', 'eurovision', 'eus', 'events', 'everbank', 'exchange',
|
|
'expert', 'exposed', 'express', 'extraspace',
|
|
# f
|
|
'fage', 'fail', 'fairwinds', 'faith', 'family', 'fan', 'fans',
|
|
'farm', 'farmers', 'fashion', 'fast', 'fedex', 'feedback',
|
|
'ferrari', 'ferrero', 'fi', 'fiat', 'fidelity', 'fido', 'film',
|
|
'final', 'finance', 'financial', 'fire', 'firestone', 'firmdale',
|
|
'fish', 'fishing', 'fit', 'fitness', 'fj', 'fk', 'flickr',
|
|
'flights', 'flir', 'florist', 'flowers', 'fly', 'fm', 'fo',
|
|
'foo', 'food', 'foodnetwork', 'football', 'ford', 'forex',
|
|
'forsale', 'forum', 'foundation', 'fox', 'fr', 'free',
|
|
'fresenius', 'frl', 'frogans', 'frontdoor', 'frontier', 'ftr',
|
|
'fujitsu', 'fujixerox', 'fun', 'fund', 'furniture', 'futbol',
|
|
'fyi',
|
|
# g
|
|
'ga', 'gal', 'gallery', 'gallo', 'gallup', 'game', 'games',
|
|
'gap', 'garden', 'gb', 'gbiz', 'gd', 'gdn', 'ge', 'gea', 'gent',
|
|
'genting', 'george', 'gf', 'gg', 'ggee', 'gh', 'gi', 'gift',
|
|
'gifts', 'gives', 'giving', 'gl', 'glade', 'glass', 'gle',
|
|
'global', 'globo', 'gm', 'gmail', 'gmbh', 'gmo', 'gmx', 'gn',
|
|
'godaddy', 'gold', 'goldpoint', 'golf', 'goo', 'goodhands',
|
|
'goodyear', 'goog', 'google', 'gop', 'got', 'gov', 'gp', 'gq',
|
|
'gr', 'grainger', 'graphics', 'gratis', 'green', 'gripe',
|
|
'grocery', 'group', 'gs', 'gt', 'gu', 'guardian', 'gucci',
|
|
'guge', 'guide', 'guitars', 'guru', 'gw', 'gy',
|
|
# h
|
|
'hair', 'hamburg', 'hangout', 'haus', 'hbo', 'hdfc', 'hdfcbank',
|
|
'health', 'healthcare', 'help', 'helsinki', 'here', 'hermes',
|
|
'hgtv', 'hiphop', 'hisamitsu', 'hitachi', 'hiv', 'hk', 'hkt',
|
|
'hm', 'hn', 'hockey', 'holdings', 'holiday', 'homedepot',
|
|
'homegoods', 'homes', 'homesense', 'honda', 'honeywell', 'horse',
|
|
'hospital', 'host', 'hosting', 'hot', 'hoteles', 'hotels',
|
|
'hotmail', 'house', 'how', 'hr', 'hsbc', 'ht', 'hu', 'hughes',
|
|
'hyatt', 'hyundai',
|
|
# i
|
|
'ibm', 'icbc', 'ice', 'icu', 'id', 'ie', 'ieee', 'ifm', 'ikano',
|
|
'il', 'im', 'imamat', 'imdb', 'immo', 'immobilien', 'in',
|
|
'industries', 'infiniti', 'info', 'ing', 'ink', 'institute',
|
|
'insurance', 'insure', 'int', 'intel', 'international', 'intuit',
|
|
'investments', 'io', 'ipiranga', 'iq', 'ir', 'irish', 'is',
|
|
'iselect', 'ismaili', 'ist', 'istanbul', 'it', 'itau', 'itv',
|
|
'iveco', 'iwc',
|
|
# j
|
|
'jaguar', 'java', 'jcb', 'jcp', 'je', 'jeep', 'jetzt', 'jewelry',
|
|
'jio', 'jlc', 'jll', 'jm', 'jmp', 'jnj', 'jo', 'jobs', 'joburg',
|
|
'jot', 'joy', 'jp', 'jpmorgan', 'jprs', 'juegos', 'juniper',
|
|
# k
|
|
'kaufen', 'kddi', 'ke', 'kerryhotels', 'kerrylogistics',
|
|
'kerryproperties', 'kfh', 'kg', 'kh', 'ki', 'kia', 'kim',
|
|
'kinder', 'kindle', 'kitchen', 'kiwi', 'km', 'kn', 'koeln',
|
|
'komatsu', 'kosher', 'kp', 'kpmg', 'kpn', 'kr', 'krd', 'kred',
|
|
'kuokgroup', 'kw', 'ky', 'kyoto', 'kz',
|
|
# l
|
|
'la', 'lacaixa', 'ladbrokes', 'lamborghini', 'lamer',
|
|
'lancaster', 'lancia', 'lancome', 'land', 'landrover', 'lanxess',
|
|
'lasalle', 'lat', 'latino', 'latrobe', 'law', 'lawyer', 'lb',
|
|
'lc', 'lds', 'lease', 'leclerc', 'lefrak', 'legal', 'lego',
|
|
'lexus', 'lgbt', 'li', 'liaison', 'lidl', 'life',
|
|
'lifeinsurance', 'lifestyle', 'lighting', 'like', 'lilly',
|
|
'limited', 'limo', 'lincoln', 'linde', 'link', 'lipsy', 'live',
|
|
'living', 'lixil', 'lk', 'loan', 'loans', 'localhost', 'locker',
|
|
'locus', 'loft', 'lol', 'london', 'lotte', 'lotto', 'love',
|
|
'lpl', 'lplfinancial', 'lr', 'ls', 'lt', 'ltd', 'ltda', 'lu',
|
|
'lundbeck', 'lupin', 'luxe', 'luxury', 'lv', 'ly',
|
|
# m
|
|
'ma', 'macys', 'madrid', 'maif', 'maison', 'makeup', 'man',
|
|
'management', 'mango', 'map', 'market', 'marketing', 'markets',
|
|
'marriott', 'marshalls', 'maserati', 'mattel', 'mba', 'mc',
|
|
'mckinsey', 'md', 'me', 'med', 'media', 'meet', 'melbourne',
|
|
'meme', 'memorial', 'men', 'menu', 'meo', 'merckmsd', 'metlife',
|
|
'mg', 'mh', 'miami', 'microsoft', 'mil', 'mini', 'mint', 'mit',
|
|
'mitsubishi', 'mk', 'ml', 'mlb', 'mls', 'mm', 'mma', 'mn', 'mo',
|
|
'mobi', 'mobile', 'mobily', 'moda', 'moe', 'moi', 'mom',
|
|
'monash', 'money', 'monster', 'mopar', 'mormon', 'mortgage',
|
|
'moscow', 'moto', 'motorcycles', 'mov', 'movie', 'movistar',
|
|
'mp', 'mq', 'mr', 'ms', 'msd', 'mt', 'mtn', 'mtr', 'mu',
|
|
'museum', 'mutual', 'mv', 'mw', 'mx', 'my', 'mz',
|
|
# n
|
|
'na', 'nab', 'nadex', 'nagoya', 'name', 'nationwide', 'natura',
|
|
'navy', 'nba', 'nc', 'ne', 'nec', 'net', 'netbank', 'netflix',
|
|
'network', 'neustar', 'new', 'newholland', 'news', 'next',
|
|
'nextdirect', 'nexus', 'nf', 'nfl', 'ng', 'ngo', 'nhk', 'ni',
|
|
'nico', 'nike', 'nikon', 'ninja', 'nissan', 'nissay', 'nl', 'no',
|
|
'nokia', 'northwesternmutual', 'norton', 'now', 'nowruz',
|
|
'nowtv', 'np', 'nr', 'nra', 'nrw', 'ntt', 'nu', 'nyc', 'nz',
|
|
# o
|
|
'obi', 'observer', 'off', 'office', 'okinawa', 'olayan',
|
|
'olayangroup', 'oldnavy', 'ollo', 'om', 'omega', 'one', 'ong',
|
|
'onl', 'online', 'onyourside', 'ooo', 'open', 'oracle', 'orange',
|
|
'org', 'organic', 'origins', 'osaka', 'otsuka', 'ott', 'ovh',
|
|
# p
|
|
'pa', 'page', 'panasonic', 'panerai', 'paris', 'pars',
|
|
'partners', 'parts', 'party', 'passagens', 'pay', 'pccw', 'pe',
|
|
'pet', 'pf', 'pfizer', 'pg', 'ph', 'pharmacy', 'phd', 'philips',
|
|
'phone', 'photo', 'photography', 'photos', 'physio', 'piaget',
|
|
'pics', 'pictet', 'pictures', 'pid', 'pin', 'ping', 'pink',
|
|
'pioneer', 'pizza', 'pk', 'pl', 'place', 'play', 'playstation',
|
|
'plumbing', 'plus', 'pm', 'pn', 'pnc', 'pohl', 'poker',
|
|
'politie', 'porn', 'post', 'pr', 'pramerica', 'praxi', 'press',
|
|
'prime', 'pro', 'prod', 'productions', 'prof', 'progressive',
|
|
'promo', 'properties', 'property', 'protection', 'pru',
|
|
'prudential', 'ps', 'pt', 'pub', 'pw', 'pwc', 'py',
|
|
# q
|
|
'qa', 'qpon', 'quebec', 'quest', 'qvc',
|
|
# r
|
|
'racing', 'radio', 'raid', 're', 'read', 'realestate', 'realtor',
|
|
'realty', 'recipes', 'red', 'redstone', 'redumbrella', 'rehab',
|
|
'reise', 'reisen', 'reit', 'reliance', 'ren', 'rent', 'rentals',
|
|
'repair', 'report', 'republican', 'rest', 'restaurant', 'review',
|
|
'reviews', 'rexroth', 'rich', 'richardli', 'ricoh',
|
|
'rightathome', 'ril', 'rio', 'rip', 'rmit', 'ro', 'rocher',
|
|
'rocks', 'rodeo', 'rogers', 'room', 'rs', 'rsvp', 'ru', 'rugby',
|
|
'ruhr', 'run', 'rw', 'rwe', 'ryukyu',
|
|
# s
|
|
'sa', 'saarland', 'safe', 'safety', 'sakura', 'sale', 'salon',
|
|
'samsclub', 'samsung', 'sandvik', 'sandvikcoromant', 'sanofi',
|
|
'sap', 'sapo', 'sarl', 'sas', 'save', 'saxo', 'sb', 'sbi', 'sbs',
|
|
'sc', 'sca', 'scb', 'schaeffler', 'schmidt', 'scholarships',
|
|
'school', 'schule', 'schwarz', 'science', 'scjohnson', 'scor',
|
|
'scot', 'sd', 'se', 'search', 'seat', 'secure', 'security',
|
|
'seek', 'select', 'sener', 'services', 'ses', 'seven', 'sew',
|
|
'sex', 'sexy', 'sfr', 'sg', 'sh', 'shangrila', 'sharp', 'shaw',
|
|
'shell', 'shia', 'shiksha', 'shoes', 'shop', 'shopping',
|
|
'shouji', 'show', 'showtime', 'shriram', 'si', 'silk', 'sina',
|
|
'singles', 'site', 'sj', 'sk', 'ski', 'skin', 'sky', 'skype',
|
|
'sl', 'sling', 'sm', 'smart', 'smile', 'sn', 'sncf', 'so',
|
|
'soccer', 'social', 'softbank', 'software', 'sohu', 'solar',
|
|
'solutions', 'song', 'sony', 'soy', 'space', 'spiegel', 'spot',
|
|
'spreadbetting', 'sr', 'srl', 'srt', 'st', 'stada', 'staples',
|
|
'star', 'starhub', 'statebank', 'statefarm', 'statoil', 'stc',
|
|
'stcgroup', 'stockholm', 'storage', 'store', 'stream', 'studio',
|
|
'study', 'style', 'su', 'sucks', 'supplies', 'supply', 'support',
|
|
'surf', 'surgery', 'suzuki', 'sv', 'swatch', 'swiftcover',
|
|
'swiss', 'sx', 'sy', 'sydney', 'symantec', 'systems', 'sz',
|
|
# t
|
|
'tab', 'taipei', 'talk', 'taobao', 'target', 'tatamotors',
|
|
'tatar', 'tattoo', 'tax', 'taxi', 'tc', 'tci', 'td', 'tdk',
|
|
'team', 'tech', 'technology', 'tel', 'telecity', 'telefonica',
|
|
'temasek', 'tennis', 'teva', 'tf', 'tg', 'th', 'thd', 'theater',
|
|
'theatre', 'tiaa', 'tickets', 'tienda', 'tiffany', 'tips',
|
|
'tires', 'tirol', 'tj', 'tjmaxx', 'tjx', 'tk', 'tkmaxx', 'tl',
|
|
'tm', 'tmall', 'tn', 'to', 'today', 'tokyo', 'tools', 'top',
|
|
'toray', 'toshiba', 'total', 'tours', 'town', 'toyota', 'toys',
|
|
'tr', 'trade', 'trading', 'training', 'travel', 'travelchannel',
|
|
'travelers', 'travelersinsurance', 'trust', 'trv', 'tt', 'tube',
|
|
'tui', 'tunes', 'tushu', 'tv', 'tvs', 'tw', 'tz',
|
|
# u
|
|
'ua', 'ubank', 'ubs', 'uconnect', 'ug', 'uk', 'unicom',
|
|
'university', 'uno', 'uol', 'ups', 'us', 'uy', 'uz',
|
|
# v
|
|
'va', 'vacations', 'vana', 'vanguard', 'vc', 've', 'vegas',
|
|
'ventures', 'verisign', 'versicherung', 'vet', 'vg', 'vi',
|
|
'viajes', 'video', 'vig', 'viking', 'villas', 'vin', 'vip',
|
|
'virgin', 'visa', 'vision', 'vista', 'vistaprint', 'viva',
|
|
'vivo', 'vlaanderen', 'vn', 'vodka', 'volkswagen', 'volvo',
|
|
'vote', 'voting', 'voto', 'voyage', 'vu', 'vuelos',
|
|
# w
|
|
'wales', 'walmart', 'walter', 'wang', 'wanggou', 'warman',
|
|
'watch', 'watches', 'weather', 'weatherchannel', 'webcam',
|
|
'weber', 'website', 'wed', 'wedding', 'weibo', 'weir', 'wf',
|
|
'whoswho', 'wien', 'wiki', 'williamhill', 'win', 'windows',
|
|
'wine', 'winners', 'wme', 'wolterskluwer', 'woodside', 'work',
|
|
'works', 'world', 'wow', 'ws', 'wtc', 'wtf',
|
|
# x
|
|
'xbox', 'xerox', 'xfinity', 'xihuan', 'xin', 'xn--11b4c3d',
|
|
'xn--1ck2e1b', 'xn--1qqw23a', 'xn--2scrj9c', 'xn--30rr7y',
|
|
'xn--3bst00m', 'xn--3ds443g', 'xn--3e0b707e', 'xn--3hcrj9c',
|
|
'xn--3oq18vl8pn36a', 'xn--3pxu8k', 'xn--42c2d9a', 'xn--45br5cyl',
|
|
'xn--45brj9c', 'xn--45q11c', 'xn--4gbrim', 'xn--54b7fta0cc',
|
|
'xn--55qw42g', 'xn--55qx5d', 'xn--5su34j936bgsg', 'xn--5tzm5g',
|
|
'xn--6frz82g', 'xn--6qq986b3xl', 'xn--80adxhks', 'xn--80ao21a',
|
|
'xn--80aqecdr1a', 'xn--80asehdb', 'xn--80aswg', 'xn--8y0a063a',
|
|
'xn--90a3ac', 'xn--90ae', 'xn--90ais', 'xn--9dbq2a',
|
|
'xn--9et52u', 'xn--9krt00a', 'xn--b4w605ferd',
|
|
'xn--bck1b9a5dre4c', 'xn--c1avg', 'xn--c2br7g', 'xn--cck2b3b',
|
|
'xn--cg4bki', 'xn--clchc0ea0b2g2a9gcd', 'xn--czr694b',
|
|
'xn--czrs0t', 'xn--czru2d', 'xn--d1acj3b', 'xn--d1alf',
|
|
'xn--e1a4c', 'xn--eckvdtc9d', 'xn--efvy88h', 'xn--estv75g',
|
|
'xn--fct429k', 'xn--fhbei', 'xn--fiq228c5hs', 'xn--fiq64b',
|
|
'xn--fiqs8s', 'xn--fiqz9s', 'xn--fjq720a', 'xn--flw351e',
|
|
'xn--fpcrj9c3d', 'xn--fzc2c9e2c', 'xn--fzys8d69uvgm',
|
|
'xn--g2xx48c', 'xn--gckr3f0f', 'xn--gecrj9c', 'xn--gk3at1e',
|
|
'xn--h2breg3eve', 'xn--h2brj9c', 'xn--h2brj9c8c', 'xn--hxt814e',
|
|
'xn--i1b6b1a6a2e', 'xn--imr513n', 'xn--io0a7i', 'xn--j1aef',
|
|
'xn--j1amh', 'xn--j6w193g', 'xn--jlq61u9w7b', 'xn--jvr189m',
|
|
'xn--kcrx77d1x4a', 'xn--kprw13d', 'xn--kpry57d', 'xn--kpu716f',
|
|
'xn--kput3i', 'xn--l1acc', 'xn--lgbbat1ad8j', 'xn--mgb9awbf',
|
|
'xn--mgba3a3ejt', 'xn--mgba3a4f16a', 'xn--mgba7c0bbn0a',
|
|
'xn--mgbaakc7dvf', 'xn--mgbaam7a8h', 'xn--mgbab2bd',
|
|
'xn--mgbai9azgqp6j', 'xn--mgbayh7gpa', 'xn--mgbb9fbpob',
|
|
'xn--mgbbh1a', 'xn--mgbbh1a71e', 'xn--mgbc0a9azcg',
|
|
'xn--mgbca7dzdo', 'xn--mgberp4a5d4ar', 'xn--mgbgu82a',
|
|
'xn--mgbi4ecexp', 'xn--mgbpl2fh', 'xn--mgbt3dhd', 'xn--mgbtx2b',
|
|
'xn--mgbx4cd0ab', 'xn--mix891f', 'xn--mk1bu44c', 'xn--mxtq1m',
|
|
'xn--ngbc5azd', 'xn--ngbe9e0a', 'xn--ngbrx', 'xn--node',
|
|
'xn--nqv7f', 'xn--nqv7fs00ema', 'xn--nyqy26a', 'xn--o3cw4h',
|
|
'xn--ogbpf8fl', 'xn--p1acf', 'xn--p1ai', 'xn--pbt977c',
|
|
'xn--pgbs0dh', 'xn--pssy2u', 'xn--q9jyb4c', 'xn--qcka1pmc',
|
|
'xn--qxam', 'xn--rhqv96g', 'xn--rovu88b', 'xn--rvc1e0am3e',
|
|
'xn--s9brj9c', 'xn--ses554g', 'xn--t60b56a', 'xn--tckwe',
|
|
'xn--tiq49xqyj', 'xn--unup4y', 'xn--vermgensberater-ctb',
|
|
'xn--vermgensberatung-pwb', 'xn--vhquv', 'xn--vuq861b',
|
|
'xn--w4r85el8fhu5dnra', 'xn--w4rs40l', 'xn--wgbh1c',
|
|
'xn--wgbl6a', 'xn--xhq521b', 'xn--xkc2al3hye2a',
|
|
'xn--xkc2dl3a5ee0h', 'xn--y9a3aq', 'xn--yfro4i67o',
|
|
'xn--ygbi2ammx', 'xn--zfr164b', 'xperia', 'xxx', 'xyz',
|
|
# y
|
|
'yachts', 'yahoo', 'yamaxun', 'yandex', 'ye', 'yodobashi',
|
|
'yoga', 'yokohama', 'you', 'youtube', 'yt', 'yun',
|
|
# z
|
|
'za', 'zappos', 'zara', 'zero', 'zip', 'zippo', 'zm', 'zone',
|
|
'zuerich', 'zw'
|
|
]
|
|
|
|
|
|
class IS_HTTP_URL(Validator):
|
|
"""
|
|
Rejects a URL string if any of the following is true:
|
|
* The string is empty or None
|
|
* The string uses characters that are not allowed in a URL
|
|
* The string breaks any of the HTTP syntactic rules
|
|
* The URL scheme specified (if one is specified) is not 'http' or 'https'
|
|
* The top-level domain (if a host name is specified) does not exist
|
|
|
|
Based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html
|
|
|
|
This function only checks the URL's syntax. It does not check that the URL
|
|
points to a real document, for example, or that it otherwise makes sense
|
|
semantically. This function does automatically prepend 'http://' in front
|
|
of a URL in the case of an abbreviated URL (e.g. 'google.ca').
|
|
|
|
The list of allowed schemes is customizable with the allowed_schemes
|
|
parameter. If you exclude None from the list, then abbreviated URLs
|
|
(lacking a scheme such as 'http') will be rejected.
|
|
|
|
The default prepended scheme is customizable with the prepend_scheme
|
|
parameter. If you set prepend_scheme to None then prepending will be
|
|
disabled. URLs that require prepending to parse will still be accepted,
|
|
but the return value will not be modified.
|
|
|
|
@author: Jonathan Benn
|
|
|
|
>>> IS_HTTP_URL()('http://1.2.3.4')
|
|
('http://1.2.3.4', None)
|
|
>>> IS_HTTP_URL()('http://abc.com')
|
|
('http://abc.com', None)
|
|
>>> IS_HTTP_URL()('https://abc.com')
|
|
('https://abc.com', None)
|
|
>>> IS_HTTP_URL()('httpx://abc.com')
|
|
('httpx://abc.com', 'enter a valid URL')
|
|
>>> IS_HTTP_URL()('http://abc.com:80')
|
|
('http://abc.com:80', None)
|
|
>>> IS_HTTP_URL()('http://user@abc.com')
|
|
('http://user@abc.com', None)
|
|
>>> IS_HTTP_URL()('http://user@1.2.3.4')
|
|
('http://user@1.2.3.4', None)
|
|
|
|
Args:
|
|
error_message: a string, the error message to give the end user
|
|
if the URL does not validate
|
|
allowed_schemes: a list containing strings or None. Each element
|
|
is a scheme the inputed URL is allowed to use
|
|
prepend_scheme: a string, this scheme is prepended if it's
|
|
necessary to make the URL valid
|
|
"""
|
|
|
|
GENERIC_VALID_IP = re.compile(
|
|
"([\w.!~*'|;:&=+$,-]+@)?\d+\.\d+\.\d+\.\d+(:\d*)*$")
|
|
GENERIC_VALID_DOMAIN = re.compile("([\w.!~*'|;:&=+$,-]+@)?(([A-Za-z0-9]+[A-Za-z0-9\-]*[A-Za-z0-9]+\.)*([A-Za-z0-9]+\.)*)*([A-Za-z]+[A-Za-z0-9\-]*[A-Za-z0-9]+)\.?(:\d*)*$")
|
|
|
|
def __init__(
|
|
self,
|
|
error_message='Enter a valid URL',
|
|
allowed_schemes=None,
|
|
prepend_scheme='http',
|
|
allowed_tlds=None
|
|
):
|
|
|
|
self.error_message = error_message
|
|
if allowed_schemes is None:
|
|
self.allowed_schemes = http_schemes
|
|
else:
|
|
self.allowed_schemes = allowed_schemes
|
|
if allowed_tlds is None:
|
|
self.allowed_tlds = official_top_level_domains
|
|
else:
|
|
self.allowed_tlds = allowed_tlds
|
|
self.prepend_scheme = prepend_scheme
|
|
|
|
for i in self.allowed_schemes:
|
|
if i not in http_schemes:
|
|
raise SyntaxError("allowed_scheme value '%s' is not in %s" %
|
|
(i, http_schemes))
|
|
|
|
if self.prepend_scheme not in self.allowed_schemes:
|
|
raise SyntaxError("prepend_scheme='%s' is not in allowed_schemes=%s" %
|
|
(self.prepend_scheme, self.allowed_schemes))
|
|
|
|
def __call__(self, value):
|
|
"""
|
|
Args:
|
|
value: a string, the URL to validate
|
|
|
|
Returns:
|
|
a tuple, where tuple[0] is the inputed value
|
|
(possible prepended with prepend_scheme), and tuple[1] is either
|
|
None (success!) or the string error_message
|
|
"""
|
|
try:
|
|
# if the URL passes generic validation
|
|
x = IS_GENERIC_URL(error_message=self.error_message,
|
|
allowed_schemes=self.allowed_schemes,
|
|
prepend_scheme=self.prepend_scheme)
|
|
if x(value)[1] is None:
|
|
components = urlparse.urlparse(value)
|
|
authority = components.netloc
|
|
# if there is an authority component
|
|
if authority:
|
|
# if authority is a valid IP address
|
|
if self.GENERIC_VALID_IP.match(authority):
|
|
# Then this HTTP URL is valid
|
|
return (value, None)
|
|
else:
|
|
# else if authority is a valid domain name
|
|
domainMatch = self.GENERIC_VALID_DOMAIN.match(
|
|
authority)
|
|
if domainMatch:
|
|
# if the top-level domain really exists
|
|
if domainMatch.group(5).lower()\
|
|
in self.allowed_tlds:
|
|
# Then this HTTP URL is valid
|
|
return (value, None)
|
|
else:
|
|
# else this is a relative/abbreviated URL, which will parse
|
|
# into the URL's path component
|
|
path = components.path
|
|
# relative case: if this is a valid path (if it starts with
|
|
# a slash)
|
|
if path.startswith('/'):
|
|
# Then this HTTP URL is valid
|
|
return (value, None)
|
|
else:
|
|
# abbreviated case: if we haven't already, prepend a
|
|
# scheme and see if it fixes the problem
|
|
if '://' not in value and None in self.allowed_schemes:
|
|
schemeToUse = self.prepend_scheme or 'http'
|
|
prependTest = self.__call__(schemeToUse
|
|
+ '://' + value)
|
|
# if the prepend test succeeded
|
|
if prependTest[1] is None:
|
|
# if prepending in the output is enabled
|
|
if self.prepend_scheme:
|
|
return prependTest
|
|
else:
|
|
# else return the original, non-prepended
|
|
# value
|
|
return (value, None)
|
|
except:
|
|
pass
|
|
# else the HTTP URL is not valid
|
|
return (value, translate(self.error_message))
|
|
|
|
|
|
class IS_URL(Validator):
|
|
"""
|
|
Rejects a URL string if any of the following is true:
|
|
|
|
* The string is empty or None
|
|
* The string uses characters that are not allowed in a URL
|
|
* The string breaks any of the HTTP syntactic rules
|
|
* The URL scheme specified (if one is specified) is not 'http' or 'https'
|
|
* The top-level domain (if a host name is specified) does not exist
|
|
|
|
(These rules are based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html)
|
|
|
|
This function only checks the URL's syntax. It does not check that the URL
|
|
points to a real document, for example, or that it otherwise makes sense
|
|
semantically. This function does automatically prepend 'http://' in front
|
|
of a URL in the case of an abbreviated URL (e.g. 'google.ca').
|
|
|
|
If the parameter mode='generic' is used, then this function's behavior
|
|
changes. It then rejects a URL string if any of the following is true:
|
|
|
|
* The string is empty or None
|
|
* The string uses characters that are not allowed in a URL
|
|
* The URL scheme specified (if one is specified) is not valid
|
|
|
|
(These rules are based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html)
|
|
|
|
The list of allowed schemes is customizable with the allowed_schemes
|
|
parameter. If you exclude None from the list, then abbreviated URLs
|
|
(lacking a scheme such as 'http') will be rejected.
|
|
|
|
The default prepended scheme is customizable with the prepend_scheme
|
|
parameter. If you set prepend_scheme to None then prepending will be
|
|
disabled. URLs that require prepending to parse will still be accepted,
|
|
but the return value will not be modified.
|
|
|
|
IS_URL is compatible with the Internationalized Domain Name (IDN) standard
|
|
specified in RFC 3490 (http://tools.ietf.org/html/rfc3490). As a result,
|
|
URLs can be regular strings or unicode strings.
|
|
If the URL's domain component (e.g. google.ca) contains non-US-ASCII
|
|
letters, then the domain will be converted into Punycode (defined in
|
|
RFC 3492, http://tools.ietf.org/html/rfc3492). IS_URL goes a bit beyond
|
|
the standards, and allows non-US-ASCII characters to be present in the path
|
|
and query components of the URL as well. These non-US-ASCII characters will
|
|
be escaped using the standard '%20' type syntax. e.g. the unicode
|
|
character with hex code 0x4e86 will become '%4e%86'
|
|
|
|
Args:
|
|
error_message: a string, the error message to give the end user
|
|
if the URL does not validate
|
|
allowed_schemes: a list containing strings or None. Each element
|
|
is a scheme the inputed URL is allowed to use
|
|
prepend_scheme: a string, this scheme is prepended if it's
|
|
necessary to make the URL valid
|
|
|
|
Code Examples::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_URL())
|
|
>>> IS_URL()('abc.com')
|
|
('http://abc.com', None)
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_URL(mode='generic'))
|
|
>>> IS_URL(mode='generic')('abc.com')
|
|
('abc.com', None)
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_URL(allowed_schemes=['https'], prepend_scheme='https'))
|
|
>>> IS_URL(allowed_schemes=['https'], prepend_scheme='https')('https://abc.com')
|
|
('https://abc.com', None)
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_URL(prepend_scheme='https'))
|
|
>>> IS_URL(prepend_scheme='https')('abc.com')
|
|
('https://abc.com', None)
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_URL(mode='generic', allowed_schemes=['ftps', 'https'],
|
|
prepend_scheme='https'))
|
|
>>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], prepend_scheme='https')('https://abc.com')
|
|
('https://abc.com', None)
|
|
>>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https', None], prepend_scheme='https')('abc.com')
|
|
('abc.com', None)
|
|
|
|
@author: Jonathan Benn
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
error_message='Enter a valid URL',
|
|
mode='http',
|
|
allowed_schemes=None,
|
|
prepend_scheme='http',
|
|
allowed_tlds=None
|
|
):
|
|
|
|
self.error_message = error_message
|
|
self.mode = mode.lower()
|
|
if self.mode not in ['generic', 'http']:
|
|
raise SyntaxError("invalid mode '%s' in IS_URL" % self.mode)
|
|
self.allowed_schemes = allowed_schemes
|
|
if allowed_tlds is None:
|
|
self.allowed_tlds = official_top_level_domains
|
|
else:
|
|
self.allowed_tlds = allowed_tlds
|
|
|
|
if self.allowed_schemes:
|
|
if prepend_scheme not in self.allowed_schemes:
|
|
raise SyntaxError("prepend_scheme='%s' is not in allowed_schemes=%s"
|
|
% (prepend_scheme, self.allowed_schemes))
|
|
|
|
# if allowed_schemes is None, then we will defer testing
|
|
# prepend_scheme's validity to a sub-method
|
|
|
|
self.prepend_scheme = prepend_scheme
|
|
|
|
def __call__(self, value):
|
|
"""
|
|
Args:
|
|
value: a unicode or regular string, the URL to validate
|
|
|
|
Returns:
|
|
a (string, string) tuple, where tuple[0] is the modified
|
|
input value and tuple[1] is either None (success!) or the
|
|
string error_message. The input value will never be modified in the
|
|
case of an error. However, if there is success then the input URL
|
|
may be modified to (1) prepend a scheme, and/or (2) convert a
|
|
non-compliant unicode URL into a compliant US-ASCII version.
|
|
"""
|
|
if self.mode == 'generic':
|
|
subMethod = IS_GENERIC_URL(error_message=self.error_message,
|
|
allowed_schemes=self.allowed_schemes,
|
|
prepend_scheme=self.prepend_scheme)
|
|
elif self.mode == 'http':
|
|
subMethod = IS_HTTP_URL(error_message=self.error_message,
|
|
allowed_schemes=self.allowed_schemes,
|
|
prepend_scheme=self.prepend_scheme,
|
|
allowed_tlds=self.allowed_tlds)
|
|
else:
|
|
raise SyntaxError("invalid mode '%s' in IS_URL" % self.mode)
|
|
|
|
if not isinstance(value, unicodeT):
|
|
return subMethod(value)
|
|
else:
|
|
try:
|
|
asciiValue = unicode_to_ascii_url(value, self.prepend_scheme)
|
|
except Exception as e:
|
|
# If we are not able to convert the unicode url into a
|
|
# US-ASCII URL, then the URL is not valid
|
|
return (value, translate(self.error_message))
|
|
methodResult = subMethod(asciiValue)
|
|
# if the validation of the US-ASCII version of the value failed
|
|
if not methodResult[1] is None:
|
|
# then return the original input value, not the US-ASCII version
|
|
return (value, methodResult[1])
|
|
else:
|
|
return methodResult
|
|
|
|
|
|
regex_time = re.compile(
|
|
'((?P<h>[0-9]+))([^0-9 ]+(?P<m>[0-9 ]+))?([^0-9ap ]+(?P<s>[0-9]*))?((?P<d>[ap]m))?')
|
|
|
|
|
|
class IS_TIME(Validator):
|
|
"""
|
|
Example:
|
|
Use as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_TIME())
|
|
|
|
understands the following formats
|
|
hh:mm:ss [am/pm]
|
|
hh:mm [am/pm]
|
|
hh [am/pm]
|
|
|
|
[am/pm] is optional, ':' can be replaced by any other non-space non-digit::
|
|
|
|
>>> IS_TIME()('21:30')
|
|
(datetime.time(21, 30), None)
|
|
>>> IS_TIME()('21-30')
|
|
(datetime.time(21, 30), None)
|
|
>>> IS_TIME()('21.30')
|
|
(datetime.time(21, 30), None)
|
|
>>> IS_TIME()('21:30:59')
|
|
(datetime.time(21, 30, 59), None)
|
|
>>> IS_TIME()('5:30')
|
|
(datetime.time(5, 30), None)
|
|
>>> IS_TIME()('5:30 am')
|
|
(datetime.time(5, 30), None)
|
|
>>> IS_TIME()('5:30 pm')
|
|
(datetime.time(17, 30), None)
|
|
>>> IS_TIME()('5:30 whatever')
|
|
('5:30 whatever', 'enter time as hh:mm:ss (seconds, am, pm optional)')
|
|
>>> IS_TIME()('5:30 20')
|
|
('5:30 20', 'enter time as hh:mm:ss (seconds, am, pm optional)')
|
|
>>> IS_TIME()('24:30')
|
|
('24:30', 'enter time as hh:mm:ss (seconds, am, pm optional)')
|
|
>>> IS_TIME()('21:60')
|
|
('21:60', 'enter time as hh:mm:ss (seconds, am, pm optional)')
|
|
>>> IS_TIME()('21:30::')
|
|
('21:30::', 'enter time as hh:mm:ss (seconds, am, pm optional)')
|
|
>>> IS_TIME()('')
|
|
('', 'enter time as hh:mm:ss (seconds, am, pm optional)')ù
|
|
|
|
"""
|
|
|
|
def __init__(self, error_message='Enter time as hh:mm:ss (seconds, am, pm optional)'):
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
try:
|
|
ivalue = value
|
|
value = regex_time.match(value.lower())
|
|
(h, m, s) = (int(value.group('h')), 0, 0)
|
|
if not value.group('m') is None:
|
|
m = int(value.group('m'))
|
|
if not value.group('s') is None:
|
|
s = int(value.group('s'))
|
|
if value.group('d') == 'pm' and 0 < h < 12:
|
|
h += 12
|
|
if value.group('d') == 'am' and h == 12:
|
|
h = 0
|
|
if not (h in range(24) and m in range(60) and s
|
|
in range(60)):
|
|
raise ValueError('Hours or minutes or seconds are outside of allowed range')
|
|
value = datetime.time(h, m, s)
|
|
return (value, None)
|
|
except AttributeError:
|
|
pass
|
|
except ValueError:
|
|
pass
|
|
return (ivalue, translate(self.error_message))
|
|
|
|
|
|
# A UTC class.
|
|
class UTC(datetime.tzinfo):
|
|
"""UTC"""
|
|
ZERO = datetime.timedelta(0)
|
|
|
|
def utcoffset(self, dt):
|
|
return UTC.ZERO
|
|
|
|
def tzname(self, dt):
|
|
return "UTC"
|
|
|
|
def dst(self, dt):
|
|
return UTC.ZERO
|
|
utc = UTC()
|
|
|
|
|
|
class IS_DATE(Validator):
|
|
"""
|
|
Examples:
|
|
Use as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_DATE())
|
|
|
|
date has to be in the ISO8960 format YYYY-MM-DD
|
|
"""
|
|
|
|
def __init__(self, format='%Y-%m-%d',
|
|
error_message='Enter date as %(format)s'):
|
|
self.format = translate(format)
|
|
self.error_message = str(error_message)
|
|
self.extremes = {}
|
|
|
|
def __call__(self, value):
|
|
ovalue = value
|
|
if isinstance(value, datetime.date):
|
|
return (value, None)
|
|
try:
|
|
(y, m, d, hh, mm, ss, t0, t1, t2) = \
|
|
time.strptime(value, str(self.format))
|
|
value = datetime.date(y, m, d)
|
|
return (value, None)
|
|
except:
|
|
self.extremes.update(IS_DATETIME.nice(self.format))
|
|
return (ovalue, translate(self.error_message) % self.extremes)
|
|
|
|
def formatter(self, value):
|
|
if value is None:
|
|
return None
|
|
format = self.format
|
|
year = value.year
|
|
y = '%.4i' % year
|
|
format = format.replace('%y', y[-2:])
|
|
format = format.replace('%Y', y)
|
|
if year < 1900:
|
|
year = 2000
|
|
d = datetime.date(year, value.month, value.day)
|
|
return d.strftime(format)
|
|
|
|
|
|
class IS_DATETIME(Validator):
|
|
"""
|
|
Examples:
|
|
Use as::
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_DATETIME())
|
|
|
|
datetime has to be in the ISO8960 format YYYY-MM-DD hh:mm:ss
|
|
timezome must be None or a pytz.timezone("America/Chicago") object
|
|
"""
|
|
|
|
isodatetime = '%Y-%m-%d %H:%M:%S'
|
|
|
|
@staticmethod
|
|
def nice(format):
|
|
code = (('%Y', '1963'),
|
|
('%y', '63'),
|
|
('%d', '28'),
|
|
('%m', '08'),
|
|
('%b', 'Aug'),
|
|
('%B', 'August'),
|
|
('%H', '14'),
|
|
('%I', '02'),
|
|
('%p', 'PM'),
|
|
('%M', '30'),
|
|
('%S', '59'))
|
|
for (a, b) in code:
|
|
format = format.replace(a, b)
|
|
return dict(format=format)
|
|
|
|
def __init__(self, format='%Y-%m-%d %H:%M:%S',
|
|
error_message='Enter date and time as %(format)s',
|
|
timezone=None):
|
|
self.format = translate(format)
|
|
self.error_message = str(error_message)
|
|
self.extremes = {}
|
|
self.timezone = timezone
|
|
|
|
def __call__(self, value):
|
|
ovalue = value
|
|
if isinstance(value, datetime.datetime):
|
|
return (value, None)
|
|
try:
|
|
(y, m, d, hh, mm, ss, t0, t1, t2) = \
|
|
time.strptime(value, str(self.format))
|
|
value = datetime.datetime(y, m, d, hh, mm, ss)
|
|
if self.timezone is not None:
|
|
# TODO: https://github.com/web2py/web2py/issues/1094 (temporary solution)
|
|
value = self.timezone.localize(value).astimezone(utc).replace(tzinfo=None)
|
|
return (value, None)
|
|
except:
|
|
self.extremes.update(IS_DATETIME.nice(self.format))
|
|
return (ovalue, translate(self.error_message) % self.extremes)
|
|
|
|
def formatter(self, value):
|
|
if value is None:
|
|
return None
|
|
format = self.format
|
|
year = value.year
|
|
y = '%.4i' % year
|
|
format = format.replace('%y', y[-2:])
|
|
format = format.replace('%Y', y)
|
|
if year < 1900:
|
|
year = 2000
|
|
d = datetime.datetime(year, value.month, value.day,
|
|
value.hour, value.minute, value.second)
|
|
if self.timezone is not None:
|
|
d = d.replace(tzinfo=utc).astimezone(self.timezone)
|
|
return d.strftime(format)
|
|
|
|
|
|
class IS_DATE_IN_RANGE(IS_DATE):
|
|
"""
|
|
Examples:
|
|
Use as::
|
|
|
|
>>> v = IS_DATE_IN_RANGE(minimum=datetime.date(2008,1,1), \
|
|
maximum=datetime.date(2009,12,31), \
|
|
format="%m/%d/%Y",error_message="Oops")
|
|
|
|
>>> v('03/03/2008')
|
|
(datetime.date(2008, 3, 3), None)
|
|
|
|
>>> v('03/03/2010')
|
|
('03/03/2010', 'oops')
|
|
|
|
>>> v(datetime.date(2008,3,3))
|
|
(datetime.date(2008, 3, 3), None)
|
|
|
|
>>> v(datetime.date(2010,3,3))
|
|
(datetime.date(2010, 3, 3), 'oops')
|
|
|
|
"""
|
|
|
|
def __init__(self,
|
|
minimum=None,
|
|
maximum=None,
|
|
format='%Y-%m-%d',
|
|
error_message=None):
|
|
self.minimum = minimum
|
|
self.maximum = maximum
|
|
if error_message is None:
|
|
if minimum is None:
|
|
error_message = "Enter date on or before %(max)s"
|
|
elif maximum is None:
|
|
error_message = "Enter date on or after %(min)s"
|
|
else:
|
|
error_message = "Enter date in range %(min)s %(max)s"
|
|
IS_DATE.__init__(self,
|
|
format=format,
|
|
error_message=error_message)
|
|
self.extremes = dict(min=self.formatter(minimum),
|
|
max=self.formatter(maximum))
|
|
|
|
def __call__(self, value):
|
|
ovalue = value
|
|
(value, msg) = IS_DATE.__call__(self, value)
|
|
if msg is not None:
|
|
return (value, msg)
|
|
if self.minimum and self.minimum > value:
|
|
return (ovalue, translate(self.error_message) % self.extremes)
|
|
if self.maximum and value > self.maximum:
|
|
return (ovalue, translate(self.error_message) % self.extremes)
|
|
return (value, None)
|
|
|
|
|
|
class IS_DATETIME_IN_RANGE(IS_DATETIME):
|
|
"""
|
|
Examples:
|
|
Use as::
|
|
>>> v = IS_DATETIME_IN_RANGE(\
|
|
minimum=datetime.datetime(2008,1,1,12,20), \
|
|
maximum=datetime.datetime(2009,12,31,12,20), \
|
|
format="%m/%d/%Y %H:%M",error_message="Oops")
|
|
>>> v('03/03/2008 12:40')
|
|
(datetime.datetime(2008, 3, 3, 12, 40), None)
|
|
|
|
>>> v('03/03/2010 10:34')
|
|
('03/03/2010 10:34', 'oops')
|
|
|
|
>>> v(datetime.datetime(2008,3,3,0,0))
|
|
(datetime.datetime(2008, 3, 3, 0, 0), None)
|
|
|
|
>>> v(datetime.datetime(2010,3,3,0,0))
|
|
(datetime.datetime(2010, 3, 3, 0, 0), 'oops')
|
|
|
|
"""
|
|
|
|
def __init__(self,
|
|
minimum=None,
|
|
maximum=None,
|
|
format='%Y-%m-%d %H:%M:%S',
|
|
error_message=None,
|
|
timezone=None):
|
|
self.minimum = minimum
|
|
self.maximum = maximum
|
|
if error_message is None:
|
|
if minimum is None:
|
|
error_message = "Enter date and time on or before %(max)s"
|
|
elif maximum is None:
|
|
error_message = "Enter date and time on or after %(min)s"
|
|
else:
|
|
error_message = "Enter date and time in range %(min)s %(max)s"
|
|
IS_DATETIME.__init__(self,
|
|
format=format,
|
|
error_message=error_message,
|
|
timezone=timezone)
|
|
self.extremes = dict(min=self.formatter(minimum),
|
|
max=self.formatter(maximum))
|
|
|
|
def __call__(self, value):
|
|
ovalue = value
|
|
(value, msg) = IS_DATETIME.__call__(self, value)
|
|
if msg is not None:
|
|
return (value, msg)
|
|
if self.minimum and self.minimum > value:
|
|
return (ovalue, translate(self.error_message) % self.extremes)
|
|
if self.maximum and value > self.maximum:
|
|
return (ovalue, translate(self.error_message) % self.extremes)
|
|
return (value, None)
|
|
|
|
|
|
class IS_LIST_OF(Validator):
|
|
|
|
def __init__(self, other=None, minimum=None, maximum=None, error_message=None):
|
|
self.other = other
|
|
self.minimum = minimum
|
|
self.maximum = maximum
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
ivalue = value
|
|
if not isinstance(value, list):
|
|
ivalue = [ivalue]
|
|
ivalue = [i for i in ivalue if str(i).strip()]
|
|
if self.minimum is not None and len(ivalue) < self.minimum:
|
|
return (ivalue, translate(self.error_message or
|
|
'Minimum length is %(min)s') % dict(min=self.minimum, max=self.maximum))
|
|
if self.maximum is not None and len(ivalue) > self.maximum:
|
|
return (ivalue, translate(self.error_message or
|
|
'Maximum length is %(max)s') % dict(min=self.minimum, max=self.maximum))
|
|
new_value = []
|
|
other = self.other
|
|
if self.other:
|
|
if not isinstance(other, (list, tuple)):
|
|
other = [other]
|
|
for item in ivalue:
|
|
v = item
|
|
for validator in other:
|
|
(v, e) = validator(v)
|
|
if e:
|
|
return (ivalue, e)
|
|
new_value.append(v)
|
|
ivalue = new_value
|
|
return (ivalue, None)
|
|
|
|
|
|
class IS_LOWER(Validator):
|
|
"""
|
|
Converts to lowercase::
|
|
|
|
>>> IS_LOWER()('ABC')
|
|
('abc', None)
|
|
>>> IS_LOWER()('Ñ')
|
|
('\\xc3\\xb1', None)
|
|
|
|
"""
|
|
|
|
def __call__(self, value):
|
|
cast_back = lambda x: x
|
|
if isinstance(value, str):
|
|
cast_back = to_native
|
|
elif isinstance(value, bytes):
|
|
cast_back = to_bytes
|
|
value = to_unicode(value).lower()
|
|
return (cast_back(value), None)
|
|
|
|
|
|
class IS_UPPER(Validator):
|
|
"""
|
|
Converts to uppercase::
|
|
|
|
>>> IS_UPPER()('abc')
|
|
('ABC', None)
|
|
>>> IS_UPPER()('ñ')
|
|
('\\xc3\\x91', None)
|
|
|
|
"""
|
|
|
|
def __call__(self, value):
|
|
cast_back = lambda x: x
|
|
if isinstance(value, str):
|
|
cast_back = to_native
|
|
elif isinstance(value, bytes):
|
|
cast_back = to_bytes
|
|
value = to_unicode(value).upper()
|
|
return (cast_back(value), None)
|
|
|
|
|
|
def urlify(s, maxlen=80, keep_underscores=False):
|
|
"""
|
|
Converts incoming string to a simplified ASCII subset.
|
|
if (keep_underscores): underscores are retained in the string
|
|
else: underscores are translated to hyphens (default)
|
|
"""
|
|
s = to_unicode(s) # to unicode
|
|
s = s.lower() # to lowercase
|
|
s = unicodedata.normalize('NFKD', s) # replace special characters
|
|
s = to_native(s, charset='ascii', errors='ignore') # encode as ASCII
|
|
s = re.sub('&\w+?;', '', s) # strip html entities
|
|
if keep_underscores:
|
|
s = re.sub('\s+', '-', s) # whitespace to hyphens
|
|
s = re.sub('[^\w\-]', '', s)
|
|
# strip all but alphanumeric/underscore/hyphen
|
|
else:
|
|
s = re.sub('[\s_]+', '-', s) # whitespace & underscores to hyphens
|
|
s = re.sub('[^a-z0-9\-]', '', s) # strip all but alphanumeric/hyphen
|
|
s = re.sub('[-_][-_]+', '-', s) # collapse strings of hyphens
|
|
s = s.strip('-') # remove leading and trailing hyphens
|
|
return s[:maxlen] # enforce maximum length
|
|
|
|
|
|
class IS_SLUG(Validator):
|
|
"""
|
|
converts arbitrary text string to a slug::
|
|
|
|
>>> IS_SLUG()('abc123')
|
|
('abc123', None)
|
|
>>> IS_SLUG()('ABC123')
|
|
('abc123', None)
|
|
>>> IS_SLUG()('abc-123')
|
|
('abc-123', None)
|
|
>>> IS_SLUG()('abc--123')
|
|
('abc-123', None)
|
|
>>> IS_SLUG()('abc 123')
|
|
('abc-123', None)
|
|
>>> IS_SLUG()('abc\t_123')
|
|
('abc-123', None)
|
|
>>> IS_SLUG()('-abc-')
|
|
('abc', None)
|
|
>>> IS_SLUG()('--a--b--_ -c--')
|
|
('a-b-c', None)
|
|
>>> IS_SLUG()('abc&123')
|
|
('abc123', None)
|
|
>>> IS_SLUG()('abc&123&def')
|
|
('abc123def', None)
|
|
>>> IS_SLUG()('ñ')
|
|
('n', None)
|
|
>>> IS_SLUG(maxlen=4)('abc123')
|
|
('abc1', None)
|
|
>>> IS_SLUG()('abc_123')
|
|
('abc-123', None)
|
|
>>> IS_SLUG(keep_underscores=False)('abc_123')
|
|
('abc-123', None)
|
|
>>> IS_SLUG(keep_underscores=True)('abc_123')
|
|
('abc_123', None)
|
|
>>> IS_SLUG(check=False)('abc')
|
|
('abc', None)
|
|
>>> IS_SLUG(check=True)('abc')
|
|
('abc', None)
|
|
>>> IS_SLUG(check=False)('a bc')
|
|
('a-bc', None)
|
|
>>> IS_SLUG(check=True)('a bc')
|
|
('a bc', 'must be slug')
|
|
"""
|
|
|
|
@staticmethod
|
|
def urlify(value, maxlen=80, keep_underscores=False):
|
|
return urlify(value, maxlen, keep_underscores)
|
|
|
|
def __init__(self, maxlen=80, check=False, error_message='Must be slug', keep_underscores=False):
|
|
self.maxlen = maxlen
|
|
self.check = check
|
|
self.error_message = error_message
|
|
self.keep_underscores = keep_underscores
|
|
|
|
def __call__(self, value):
|
|
if self.check and value != urlify(value, self.maxlen, self.keep_underscores):
|
|
return (value, translate(self.error_message))
|
|
return (urlify(value, self.maxlen, self.keep_underscores), None)
|
|
|
|
|
|
class ANY_OF(Validator):
|
|
"""
|
|
Tests if any of the validators in a list returns successfully::
|
|
|
|
>>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('a@b.co')
|
|
('a@b.co', None)
|
|
>>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('abco')
|
|
('abco', None)
|
|
>>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('@ab.co')
|
|
('@ab.co', 'enter only letters, numbers, and underscore')
|
|
>>> ANY_OF([IS_ALPHANUMERIC(),IS_EMAIL()])('@ab.co')
|
|
('@ab.co', 'enter a valid email address')
|
|
|
|
"""
|
|
|
|
def __init__(self, subs, error_message=None):
|
|
self.subs = subs
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
for validator in self.subs:
|
|
value, error = validator(value)
|
|
if error is None:
|
|
break
|
|
if error is not None and self.error_message is not None:
|
|
error = translate(self.error_message)
|
|
return value, error
|
|
|
|
def formatter(self, value):
|
|
# Use the formatter of the first subvalidator
|
|
# that validates the value and has a formatter
|
|
for validator in self.subs:
|
|
if hasattr(validator, 'formatter') and validator(value)[1] is None:
|
|
return validator.formatter(value)
|
|
|
|
|
|
class IS_EMPTY_OR(Validator):
|
|
"""
|
|
Dummy class for testing IS_EMPTY_OR::
|
|
|
|
>>> IS_EMPTY_OR(IS_EMAIL())('abc@def.com')
|
|
('abc@def.com', None)
|
|
>>> IS_EMPTY_OR(IS_EMAIL())(' ')
|
|
(None, None)
|
|
>>> IS_EMPTY_OR(IS_EMAIL(), null='abc')(' ')
|
|
('abc', None)
|
|
>>> IS_EMPTY_OR(IS_EMAIL(), null='abc', empty_regex='def')('def')
|
|
('abc', None)
|
|
>>> IS_EMPTY_OR(IS_EMAIL())('abc')
|
|
('abc', 'enter a valid email address')
|
|
>>> IS_EMPTY_OR(IS_EMAIL())(' abc ')
|
|
('abc', 'enter a valid email address')
|
|
"""
|
|
|
|
def __init__(self, other, null=None, empty_regex=None):
|
|
(self.other, self.null) = (other, null)
|
|
if empty_regex is not None:
|
|
self.empty_regex = re.compile(empty_regex)
|
|
else:
|
|
self.empty_regex = None
|
|
if hasattr(other, 'multiple'):
|
|
self.multiple = other.multiple
|
|
if hasattr(other, 'options'):
|
|
self.options = self._options
|
|
|
|
def _options(self, *args, **kwargs):
|
|
options = self.other.options(*args, **kwargs)
|
|
if (not options or options[0][0] != '') and not self.multiple:
|
|
options.insert(0, ('', ''))
|
|
return options
|
|
|
|
def set_self_id(self, id):
|
|
if isinstance(self.other, (list, tuple)):
|
|
for item in self.other:
|
|
if hasattr(item, 'set_self_id'):
|
|
item.set_self_id(id)
|
|
else:
|
|
if hasattr(self.other, 'set_self_id'):
|
|
self.other.set_self_id(id)
|
|
|
|
def __call__(self, value):
|
|
value, empty = is_empty(value, empty_regex=self.empty_regex)
|
|
if empty:
|
|
return (self.null, None)
|
|
if isinstance(self.other, (list, tuple)):
|
|
error = None
|
|
for item in self.other:
|
|
value, error = item(value)
|
|
if error:
|
|
break
|
|
return value, error
|
|
else:
|
|
return self.other(value)
|
|
|
|
def formatter(self, value):
|
|
if hasattr(self.other, 'formatter'):
|
|
return self.other.formatter(value)
|
|
return value
|
|
|
|
IS_NULL_OR = IS_EMPTY_OR # for backward compatibility
|
|
|
|
|
|
class CLEANUP(Validator):
|
|
"""
|
|
Examples:
|
|
Use as::
|
|
|
|
INPUT(_type='text', _name='name', requires=CLEANUP())
|
|
|
|
removes special characters on validation
|
|
"""
|
|
REGEX_CLEANUP = re.compile('[^\x09\x0a\x0d\x20-\x7e]')
|
|
|
|
def __init__(self, regex=None):
|
|
self.regex = self.REGEX_CLEANUP if regex is None \
|
|
else re.compile(regex)
|
|
|
|
def __call__(self, value):
|
|
v = self.regex.sub('', str(value).strip())
|
|
return (v, None)
|
|
|
|
|
|
class LazyCrypt(object):
|
|
"""
|
|
Stores a lazy password hash
|
|
"""
|
|
|
|
def __init__(self, crypt, password):
|
|
"""
|
|
crypt is an instance of the CRYPT validator,
|
|
password is the password as inserted by the user
|
|
"""
|
|
self.crypt = crypt
|
|
self.password = password
|
|
self.crypted = None
|
|
|
|
def __str__(self):
|
|
"""
|
|
Encrypted self.password and caches it in self.crypted.
|
|
If self.crypt.salt the output is in the format <algorithm>$<salt>$<hash>
|
|
|
|
Try get the digest_alg from the key (if it exists)
|
|
else assume the default digest_alg. If not key at all, set key=''
|
|
|
|
If a salt is specified use it, if salt is True, set salt to uuid
|
|
(this should all be backward compatible)
|
|
|
|
Options:
|
|
key = 'uuid'
|
|
key = 'md5:uuid'
|
|
key = 'sha512:uuid'
|
|
...
|
|
key = 'pbkdf2(1000,64,sha512):uuid' 1000 iterations and 64 chars length
|
|
"""
|
|
if self.crypted:
|
|
return self.crypted
|
|
if self.crypt.key:
|
|
if ':' in self.crypt.key:
|
|
digest_alg, key = self.crypt.key.split(':', 1)
|
|
else:
|
|
digest_alg, key = self.crypt.digest_alg, self.crypt.key
|
|
else:
|
|
digest_alg, key = self.crypt.digest_alg, ''
|
|
if self.crypt.salt:
|
|
if self.crypt.salt is True:
|
|
salt = str(web2py_uuid()).replace('-', '')[-16:]
|
|
else:
|
|
salt = self.crypt.salt
|
|
else:
|
|
salt = ''
|
|
hashed = simple_hash(self.password, key, salt, digest_alg)
|
|
self.crypted = '%s$%s$%s' % (digest_alg, salt, hashed)
|
|
return self.crypted
|
|
|
|
def __eq__(self, stored_password):
|
|
"""
|
|
compares the current lazy crypted password with a stored password
|
|
"""
|
|
|
|
# LazyCrypt objects comparison
|
|
if isinstance(stored_password, self.__class__):
|
|
return ((self is stored_password) or
|
|
((self.crypt.key == stored_password.crypt.key) and
|
|
(self.password == stored_password.password)))
|
|
|
|
if self.crypt.key:
|
|
if ':' in self.crypt.key:
|
|
key = self.crypt.key.split(':')[1]
|
|
else:
|
|
key = self.crypt.key
|
|
else:
|
|
key = ''
|
|
if stored_password is None:
|
|
return False
|
|
elif stored_password.count('$') == 2:
|
|
(digest_alg, salt, hash) = stored_password.split('$')
|
|
h = simple_hash(self.password, key, salt, digest_alg)
|
|
temp_pass = '%s$%s$%s' % (digest_alg, salt, h)
|
|
else: # no salting
|
|
# guess digest_alg
|
|
digest_alg = DIGEST_ALG_BY_SIZE.get(len(stored_password), None)
|
|
if not digest_alg:
|
|
return False
|
|
else:
|
|
temp_pass = simple_hash(self.password, key, '', digest_alg)
|
|
return temp_pass == stored_password
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
|
|
class CRYPT(object):
|
|
"""
|
|
Examples:
|
|
Use as::
|
|
|
|
INPUT(_type='text', _name='name', requires=CRYPT())
|
|
|
|
encodes the value on validation with a digest.
|
|
|
|
If no arguments are provided CRYPT uses the MD5 algorithm.
|
|
If the key argument is provided the HMAC+MD5 algorithm is used.
|
|
If the digest_alg is specified this is used to replace the
|
|
MD5 with, for example, SHA512. The digest_alg can be
|
|
the name of a hashlib algorithm as a string or the algorithm itself.
|
|
|
|
min_length is the minimal password length (default 4) - IS_STRONG for serious security
|
|
error_message is the message if password is too short
|
|
|
|
Notice that an empty password is accepted but invalid. It will not allow login back.
|
|
Stores junk as hashed password.
|
|
|
|
Specify an algorithm or by default we will use sha512.
|
|
|
|
Typical available algorithms:
|
|
md5, sha1, sha224, sha256, sha384, sha512
|
|
|
|
If salt, it hashes a password with a salt.
|
|
If salt is True, this method will automatically generate one.
|
|
Either case it returns an encrypted password string in the following format:
|
|
|
|
<algorithm>$<salt>$<hash>
|
|
|
|
Important: hashed password is returned as a LazyCrypt object and computed only if needed.
|
|
The LasyCrypt object also knows how to compare itself with an existing salted password
|
|
|
|
Supports standard algorithms
|
|
|
|
>>> for alg in ('md5','sha1','sha256','sha384','sha512'):
|
|
... print(str(CRYPT(digest_alg=alg,salt=True)('test')[0]))
|
|
md5$...$...
|
|
sha1$...$...
|
|
sha256$...$...
|
|
sha384$...$...
|
|
sha512$...$...
|
|
|
|
The syntax is always alg$salt$hash
|
|
|
|
Supports for pbkdf2
|
|
|
|
>>> alg = 'pbkdf2(1000,20,sha512)'
|
|
>>> print(str(CRYPT(digest_alg=alg,salt=True)('test')[0]))
|
|
pbkdf2(1000,20,sha512)$...$...
|
|
|
|
An optional hmac_key can be specified and it is used as salt prefix
|
|
|
|
>>> a = str(CRYPT(digest_alg='md5',key='mykey',salt=True)('test')[0])
|
|
>>> print(a)
|
|
md5$...$...
|
|
|
|
Even if the algorithm changes the hash can still be validated
|
|
|
|
>>> CRYPT(digest_alg='sha1',key='mykey',salt=True)('test')[0] == a
|
|
True
|
|
|
|
If no salt is specified CRYPT can guess the algorithms from length:
|
|
|
|
>>> a = str(CRYPT(digest_alg='sha1',salt=False)('test')[0])
|
|
>>> a
|
|
'sha1$$a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
|
|
>>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a
|
|
True
|
|
>>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a[6:]
|
|
True
|
|
>>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a
|
|
True
|
|
>>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a[6:]
|
|
True
|
|
"""
|
|
|
|
def __init__(self,
|
|
key=None,
|
|
digest_alg='pbkdf2(1000,20,sha512)',
|
|
min_length=0,
|
|
error_message='Too short', salt=True,
|
|
max_length=1024):
|
|
"""
|
|
important, digest_alg='md5' is not the default hashing algorithm for
|
|
web2py. This is only an example of usage of this function.
|
|
|
|
The actual hash algorithm is determined from the key which is
|
|
generated by web2py in tools.py. This defaults to hmac+sha512.
|
|
"""
|
|
self.key = key
|
|
self.digest_alg = digest_alg
|
|
self.min_length = min_length
|
|
self.max_length = max_length
|
|
self.error_message = error_message
|
|
self.salt = salt
|
|
|
|
def __call__(self, value):
|
|
v = value and str(value)[:self.max_length]
|
|
if not v or len(v) < self.min_length:
|
|
return ('', translate(self.error_message))
|
|
if isinstance(value, LazyCrypt):
|
|
return (value, None)
|
|
return (LazyCrypt(self, value), None)
|
|
|
|
# entropy calculator for IS_STRONG
|
|
#
|
|
lowerset = frozenset(u'abcdefghijklmnopqrstuvwxyz')
|
|
upperset = frozenset(u'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
|
numberset = frozenset(u'0123456789')
|
|
sym1set = frozenset(u'!@#$%^&*()')
|
|
sym2set = frozenset(u'~`-_=+[]{}\\|;:\'",.<>?/')
|
|
otherset = frozenset(
|
|
u'0123456789abcdefghijklmnopqrstuvwxyz') # anything else
|
|
|
|
|
|
def calc_entropy(string):
|
|
""" calculates a simple entropy for a given string """
|
|
import math
|
|
alphabet = 0 # alphabet size
|
|
other = set()
|
|
seen = set()
|
|
lastset = None
|
|
string = to_unicode(string)
|
|
for c in string:
|
|
# classify this character
|
|
inset = otherset
|
|
for cset in (lowerset, upperset, numberset, sym1set, sym2set):
|
|
if c in cset:
|
|
inset = cset
|
|
break
|
|
# calculate effect of character on alphabet size
|
|
if inset not in seen:
|
|
seen.add(inset)
|
|
alphabet += len(inset) # credit for a new character set
|
|
elif c not in other:
|
|
alphabet += 1 # credit for unique characters
|
|
other.add(c)
|
|
if inset is not lastset:
|
|
alphabet += 1 # credit for set transitions
|
|
lastset = cset
|
|
entropy = len(
|
|
string) * math.log(alphabet) / 0.6931471805599453 # math.log(2)
|
|
return round(entropy, 2)
|
|
|
|
|
|
class IS_STRONG(object):
|
|
"""
|
|
Examples:
|
|
Use as::
|
|
|
|
INPUT(_type='password', _name='passwd',
|
|
requires=IS_STRONG(min=10, special=2, upper=2))
|
|
|
|
enforces complexity requirements on a field
|
|
|
|
>>> IS_STRONG(es=True)('Abcd1234')
|
|
('Abcd1234',
|
|
'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|')
|
|
>>> IS_STRONG(es=True)('Abcd1234!')
|
|
('Abcd1234!', None)
|
|
>>> IS_STRONG(es=True, entropy=1)('a')
|
|
('a', None)
|
|
>>> IS_STRONG(es=True, entropy=1, min=2)('a')
|
|
('a', 'Minimum length is 2')
|
|
>>> IS_STRONG(es=True, entropy=100)('abc123')
|
|
('abc123', 'Entropy (32.35) less than required (100)')
|
|
>>> IS_STRONG(es=True, entropy=100)('and')
|
|
('and', 'Entropy (14.57) less than required (100)')
|
|
>>> IS_STRONG(es=True, entropy=100)('aaa')
|
|
('aaa', 'Entropy (14.42) less than required (100)')
|
|
>>> IS_STRONG(es=True, entropy=100)('a1d')
|
|
('a1d', 'Entropy (15.97) less than required (100)')
|
|
>>> IS_STRONG(es=True, entropy=100)('añd')
|
|
('a\\xc3\\xb1d', 'Entropy (18.13) less than required (100)')
|
|
|
|
"""
|
|
|
|
def __init__(self, min=None, max=None, upper=None, lower=None, number=None,
|
|
entropy=None,
|
|
special=None, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|',
|
|
invalid=' "', error_message=None, es=False):
|
|
self.entropy = entropy
|
|
if entropy is None:
|
|
# enforce default requirements
|
|
self.min = 8 if min is None else min
|
|
self.max = max # was 20, but that doesn't make sense
|
|
self.upper = 1 if upper is None else upper
|
|
self.lower = 1 if lower is None else lower
|
|
self.number = 1 if number is None else number
|
|
self.special = 1 if special is None else special
|
|
else:
|
|
# by default, an entropy spec is exclusive
|
|
self.min = min
|
|
self.max = max
|
|
self.upper = upper
|
|
self.lower = lower
|
|
self.number = number
|
|
self.special = special
|
|
self.specials = specials
|
|
self.invalid = invalid
|
|
self.error_message = error_message
|
|
self.estring = es # return error message as string (for doctest)
|
|
|
|
def __call__(self, value):
|
|
failures = []
|
|
if value and len(value) == value.count('*') > 4:
|
|
return (value, None)
|
|
if self.entropy is not None:
|
|
entropy = calc_entropy(value)
|
|
if entropy < self.entropy:
|
|
failures.append(translate("Entropy (%(have)s) less than required (%(need)s)")
|
|
% dict(have=entropy, need=self.entropy))
|
|
if isinstance(self.min, int) and self.min > 0:
|
|
if not len(value) >= self.min:
|
|
failures.append(translate("Minimum length is %s") % self.min)
|
|
if isinstance(self.max, int) and self.max > 0:
|
|
if not len(value) <= self.max:
|
|
failures.append(translate("Maximum length is %s") % self.max)
|
|
if isinstance(self.special, int):
|
|
all_special = [ch in value for ch in self.specials]
|
|
if self.special > 0:
|
|
if not all_special.count(True) >= self.special:
|
|
failures.append(translate("Must include at least %s of the following: %s")
|
|
% (self.special, self.specials))
|
|
if self.invalid:
|
|
all_invalid = [ch in value for ch in self.invalid]
|
|
if all_invalid.count(True) > 0:
|
|
failures.append(translate("May not contain any of the following: %s")
|
|
% self.invalid)
|
|
if isinstance(self.upper, int):
|
|
all_upper = re.findall("[A-Z]", value)
|
|
if self.upper > 0:
|
|
if not len(all_upper) >= self.upper:
|
|
failures.append(translate("Must include at least %s uppercase")
|
|
% str(self.upper))
|
|
else:
|
|
if len(all_upper) > 0:
|
|
failures.append(
|
|
translate("May not include any uppercase letters"))
|
|
if isinstance(self.lower, int):
|
|
all_lower = re.findall("[a-z]", value)
|
|
if self.lower > 0:
|
|
if not len(all_lower) >= self.lower:
|
|
failures.append(translate("Must include at least %s lowercase")
|
|
% str(self.lower))
|
|
else:
|
|
if len(all_lower) > 0:
|
|
failures.append(
|
|
translate("May not include any lowercase letters"))
|
|
if isinstance(self.number, int):
|
|
all_number = re.findall("[0-9]", value)
|
|
if self.number > 0:
|
|
numbers = "number"
|
|
if self.number > 1:
|
|
numbers = "numbers"
|
|
if not len(all_number) >= self.number:
|
|
failures.append(translate("Must include at least %s %s")
|
|
% (str(self.number), numbers))
|
|
else:
|
|
if len(all_number) > 0:
|
|
failures.append(translate("May not include any numbers"))
|
|
if len(failures) == 0:
|
|
return (value, None)
|
|
if not self.error_message:
|
|
if self.estring:
|
|
return (value, '|'.join(failures))
|
|
from gluon.html import XML
|
|
return (value, XML('<br />'.join(failures)))
|
|
else:
|
|
return (value, translate(self.error_message))
|
|
|
|
|
|
class IS_IMAGE(Validator):
|
|
"""
|
|
Checks if file uploaded through file input was saved in one of selected
|
|
image formats and has dimensions (width and height) within given boundaries.
|
|
|
|
Does *not* check for maximum file size (use IS_LENGTH for that). Returns
|
|
validation failure if no data was uploaded.
|
|
|
|
Supported file formats: BMP, GIF, JPEG, PNG.
|
|
|
|
Code parts taken from
|
|
http://mail.python.org/pipermail/python-list/2007-June/617126.html
|
|
|
|
Args:
|
|
extensions: iterable containing allowed *lowercase* image file extensions
|
|
('jpg' extension of uploaded file counts as 'jpeg')
|
|
maxsize: iterable containing maximum width and height of the image
|
|
minsize: iterable containing minimum width and height of the image
|
|
|
|
Use (-1, -1) as minsize to pass image size check.
|
|
|
|
Examples:
|
|
Check if uploaded file is in any of supported image formats:
|
|
|
|
INPUT(_type='file', _name='name', requires=IS_IMAGE())
|
|
|
|
Check if uploaded file is either JPEG or PNG:
|
|
|
|
INPUT(_type='file', _name='name',
|
|
requires=IS_IMAGE(extensions=('jpeg', 'png')))
|
|
|
|
Check if uploaded file is PNG with maximum size of 200x200 pixels:
|
|
|
|
INPUT(_type='file', _name='name',
|
|
requires=IS_IMAGE(extensions=('png'), maxsize=(200, 200)))
|
|
"""
|
|
|
|
def __init__(self,
|
|
extensions=('bmp', 'gif', 'jpeg', 'png'),
|
|
maxsize=(10000, 10000),
|
|
minsize=(0, 0),
|
|
error_message='Invalid image'):
|
|
|
|
self.extensions = extensions
|
|
self.maxsize = maxsize
|
|
self.minsize = minsize
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
try:
|
|
extension = value.filename.rfind('.')
|
|
assert extension >= 0
|
|
extension = value.filename[extension + 1:].lower()
|
|
if extension == 'jpg':
|
|
extension = 'jpeg'
|
|
assert extension in self.extensions
|
|
if extension == 'bmp':
|
|
width, height = self.__bmp(value.file)
|
|
elif extension == 'gif':
|
|
width, height = self.__gif(value.file)
|
|
elif extension == 'jpeg':
|
|
width, height = self.__jpeg(value.file)
|
|
elif extension == 'png':
|
|
width, height = self.__png(value.file)
|
|
else:
|
|
width = -1
|
|
height = -1
|
|
assert self.minsize[0] <= width <= self.maxsize[0] \
|
|
and self.minsize[1] <= height <= self.maxsize[1]
|
|
value.file.seek(0)
|
|
return (value, None)
|
|
except Exception as e:
|
|
return (value, translate(self.error_message))
|
|
|
|
def __bmp(self, stream):
|
|
if stream.read(2) == b'BM':
|
|
stream.read(16)
|
|
return struct.unpack("<LL", stream.read(8))
|
|
return (-1, -1)
|
|
|
|
def __gif(self, stream):
|
|
if stream.read(6) in (b'GIF87a', b'GIF89a'):
|
|
stream = stream.read(5)
|
|
if len(stream) == 5:
|
|
return tuple(struct.unpack("<HHB", stream)[:-1])
|
|
return (-1, -1)
|
|
|
|
def __jpeg(self, stream):
|
|
if stream.read(2) == b'\xFF\xD8':
|
|
while True:
|
|
(marker, code, length) = struct.unpack("!BBH", stream.read(4))
|
|
if marker != 0xFF:
|
|
break
|
|
elif code >= 0xC0 and code <= 0xC3:
|
|
return tuple(reversed(
|
|
struct.unpack("!xHH", stream.read(5))))
|
|
else:
|
|
stream.read(length - 2)
|
|
return (-1, -1)
|
|
|
|
def __png(self, stream):
|
|
if stream.read(8) == b'\211PNG\r\n\032\n':
|
|
stream.read(4)
|
|
if stream.read(4) == b"IHDR":
|
|
return struct.unpack("!LL", stream.read(8))
|
|
return (-1, -1)
|
|
|
|
|
|
class IS_UPLOAD_FILENAME(Validator):
|
|
"""
|
|
Checks if name and extension of file uploaded through file input matches
|
|
given criteria.
|
|
|
|
Does *not* ensure the file type in any way. Returns validation failure
|
|
if no data was uploaded.
|
|
|
|
Args:
|
|
filename: filename (before dot) regex
|
|
extension: extension (after dot) regex
|
|
lastdot: which dot should be used as a filename / extension separator:
|
|
True means last dot, eg. file.png -> file / png
|
|
False means first dot, eg. file.tar.gz -> file / tar.gz
|
|
case: 0 - keep the case, 1 - transform the string into lowercase (default),
|
|
2 - transform the string into uppercase
|
|
|
|
If there is no dot present, extension checks will be done against empty
|
|
string and filename checks against whole value.
|
|
|
|
Examples:
|
|
Check if file has a pdf extension (case insensitive):
|
|
|
|
INPUT(_type='file', _name='name',
|
|
requires=IS_UPLOAD_FILENAME(extension='pdf'))
|
|
|
|
Check if file has a tar.gz extension and name starting with backup:
|
|
|
|
INPUT(_type='file', _name='name',
|
|
requires=IS_UPLOAD_FILENAME(filename='backup.*',
|
|
extension='tar.gz', lastdot=False))
|
|
|
|
Check if file has no extension and name matching README
|
|
(case sensitive):
|
|
|
|
INPUT(_type='file', _name='name',
|
|
requires=IS_UPLOAD_FILENAME(filename='^README$',
|
|
extension='^$', case=0)
|
|
|
|
"""
|
|
|
|
def __init__(self, filename=None, extension=None, lastdot=True, case=1,
|
|
error_message='Enter valid filename'):
|
|
if isinstance(filename, str):
|
|
filename = re.compile(filename)
|
|
if isinstance(extension, str):
|
|
extension = re.compile(extension)
|
|
self.filename = filename
|
|
self.extension = extension
|
|
self.lastdot = lastdot
|
|
self.case = case
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
try:
|
|
string = value.filename
|
|
except:
|
|
return (value, translate(self.error_message))
|
|
if self.case == 1:
|
|
string = string.lower()
|
|
elif self.case == 2:
|
|
string = string.upper()
|
|
if self.lastdot:
|
|
dot = string.rfind('.')
|
|
else:
|
|
dot = string.find('.')
|
|
if dot == -1:
|
|
dot = len(string)
|
|
if self.filename and not self.filename.match(string[:dot]):
|
|
return (value, translate(self.error_message))
|
|
elif self.extension and not self.extension.match(string[dot + 1:]):
|
|
return (value, translate(self.error_message))
|
|
else:
|
|
return (value, None)
|
|
|
|
|
|
class IS_IPV4(Validator):
|
|
"""
|
|
Checks if field's value is an IP version 4 address in decimal form. Can
|
|
be set to force addresses from certain range.
|
|
|
|
IPv4 regex taken from: http://regexlib.com/REDetails.aspx?regexp_id=1411
|
|
|
|
Args:
|
|
|
|
minip: lowest allowed address; accepts:
|
|
|
|
- str, eg. 192.168.0.1
|
|
- list or tuple of octets, eg. [192, 168, 0, 1]
|
|
maxip: highest allowed address; same as above
|
|
invert: True to allow addresses only from outside of given range; note
|
|
that range boundaries are not matched this way
|
|
is_localhost: localhost address treatment:
|
|
|
|
- None (default): indifferent
|
|
- True (enforce): query address must match localhost address (127.0.0.1)
|
|
- False (forbid): query address must not match localhost address
|
|
is_private: same as above, except that query address is checked against
|
|
two address ranges: 172.16.0.0 - 172.31.255.255 and
|
|
192.168.0.0 - 192.168.255.255
|
|
is_automatic: same as above, except that query address is checked against
|
|
one address range: 169.254.0.0 - 169.254.255.255
|
|
|
|
Minip and maxip may also be lists or tuples of addresses in all above
|
|
forms (str, int, list / tuple), allowing setup of multiple address ranges::
|
|
|
|
minip = (minip1, minip2, ... minipN)
|
|
| | |
|
|
| | |
|
|
maxip = (maxip1, maxip2, ... maxipN)
|
|
|
|
Longer iterable will be truncated to match length of shorter one.
|
|
|
|
Examples:
|
|
Check for valid IPv4 address:
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_IPV4())
|
|
|
|
Check for valid IPv4 address belonging to specific range:
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_IPV4(minip='100.200.0.0', maxip='100.200.255.255'))
|
|
|
|
Check for valid IPv4 address belonging to either 100.110.0.0 -
|
|
100.110.255.255 or 200.50.0.0 - 200.50.0.255 address range:
|
|
|
|
INPUT(_type='text', _name='name',
|
|
requires=IS_IPV4(minip=('100.110.0.0', '200.50.0.0'),
|
|
maxip=('100.110.255.255', '200.50.0.255')))
|
|
|
|
Check for valid IPv4 address belonging to private address space:
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_IPV4(is_private=True))
|
|
|
|
Check for valid IPv4 address that is not a localhost address:
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_IPV4(is_localhost=False))
|
|
|
|
>>> IS_IPV4()('1.2.3.4')
|
|
('1.2.3.4', None)
|
|
>>> IS_IPV4()('255.255.255.255')
|
|
('255.255.255.255', None)
|
|
>>> IS_IPV4()('1.2.3.4 ')
|
|
('1.2.3.4 ', 'enter valid IPv4 address')
|
|
>>> IS_IPV4()('1.2.3.4.5')
|
|
('1.2.3.4.5', 'enter valid IPv4 address')
|
|
>>> IS_IPV4()('123.123')
|
|
('123.123', 'enter valid IPv4 address')
|
|
>>> IS_IPV4()('1111.2.3.4')
|
|
('1111.2.3.4', 'enter valid IPv4 address')
|
|
>>> IS_IPV4()('0111.2.3.4')
|
|
('0111.2.3.4', 'enter valid IPv4 address')
|
|
>>> IS_IPV4()('256.2.3.4')
|
|
('256.2.3.4', 'enter valid IPv4 address')
|
|
>>> IS_IPV4()('300.2.3.4')
|
|
('300.2.3.4', 'enter valid IPv4 address')
|
|
>>> IS_IPV4(minip='1.2.3.4', maxip='1.2.3.4')('1.2.3.4')
|
|
('1.2.3.4', None)
|
|
>>> IS_IPV4(minip='1.2.3.5', maxip='1.2.3.9', error_message='Bad ip')('1.2.3.4')
|
|
('1.2.3.4', 'bad ip')
|
|
>>> IS_IPV4(maxip='1.2.3.4', invert=True)('127.0.0.1')
|
|
('127.0.0.1', None)
|
|
>>> IS_IPV4(maxip='1.2.3.4', invert=True)('1.2.3.4')
|
|
('1.2.3.4', 'enter valid IPv4 address')
|
|
>>> IS_IPV4(is_localhost=True)('127.0.0.1')
|
|
('127.0.0.1', None)
|
|
>>> IS_IPV4(is_localhost=True)('1.2.3.4')
|
|
('1.2.3.4', 'enter valid IPv4 address')
|
|
>>> IS_IPV4(is_localhost=False)('127.0.0.1')
|
|
('127.0.0.1', 'enter valid IPv4 address')
|
|
>>> IS_IPV4(maxip='100.0.0.0', is_localhost=True)('127.0.0.1')
|
|
('127.0.0.1', 'enter valid IPv4 address')
|
|
|
|
"""
|
|
|
|
regex = re.compile(
|
|
'^(([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.){3}([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$')
|
|
numbers = (16777216, 65536, 256, 1)
|
|
localhost = 2130706433
|
|
private = ((2886729728, 2886795263), (3232235520, 3232301055))
|
|
automatic = (2851995648, 2852061183)
|
|
|
|
def __init__(
|
|
self,
|
|
minip='0.0.0.0',
|
|
maxip='255.255.255.255',
|
|
invert=False,
|
|
is_localhost=None,
|
|
is_private=None,
|
|
is_automatic=None,
|
|
error_message='Enter valid IPv4 address'):
|
|
for n, value in enumerate((minip, maxip)):
|
|
temp = []
|
|
if isinstance(value, str):
|
|
temp.append(value.split('.'))
|
|
elif isinstance(value, (list, tuple)):
|
|
if len(value) == len(list(filter(lambda item: isinstance(item, int), value))) == 4:
|
|
temp.append(value)
|
|
else:
|
|
for item in value:
|
|
if isinstance(item, str):
|
|
temp.append(item.split('.'))
|
|
elif isinstance(item, (list, tuple)):
|
|
temp.append(item)
|
|
numbers = []
|
|
for item in temp:
|
|
number = 0
|
|
for i, j in zip(self.numbers, item):
|
|
number += i * int(j)
|
|
numbers.append(number)
|
|
if n == 0:
|
|
self.minip = numbers
|
|
else:
|
|
self.maxip = numbers
|
|
self.invert = invert
|
|
self.is_localhost = is_localhost
|
|
self.is_private = is_private
|
|
self.is_automatic = is_automatic
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
if self.regex.match(value):
|
|
number = 0
|
|
for i, j in zip(self.numbers, value.split('.')):
|
|
number += i * int(j)
|
|
ok = False
|
|
for bottom, top in zip(self.minip, self.maxip):
|
|
if self.invert != (bottom <= number <= top):
|
|
ok = True
|
|
if ok and self.is_localhost is not None and \
|
|
self.is_localhost != (number == self.localhost):
|
|
ok = False
|
|
if ok and self.is_private is not None and (self.is_private !=
|
|
any([private_number[0] <= number <= private_number[1]
|
|
for private_number in self.private])):
|
|
ok = False
|
|
if ok and self.is_automatic is not None and (self.is_automatic !=
|
|
(self.automatic[0] <= number <= self.automatic[1])):
|
|
ok = False
|
|
if ok:
|
|
return (value, None)
|
|
return (value, translate(self.error_message))
|
|
|
|
|
|
class IS_IPV6(Validator):
|
|
"""
|
|
Checks if field's value is an IP version 6 address.
|
|
|
|
Uses the ipaddress from the Python 3 standard library
|
|
and its Python 2 backport (in contrib/ipaddress.py).
|
|
|
|
Args:
|
|
is_private: None (default): indifferent
|
|
True (enforce): address must be in fc00::/7 range
|
|
False (forbid): address must NOT be in fc00::/7 range
|
|
is_link_local: Same as above but uses fe80::/10 range
|
|
is_reserved: Same as above but uses IETF reserved range
|
|
is_multicast: Same as above but uses ff00::/8 range
|
|
is_routeable: Similar to above but enforces not private, link_local,
|
|
reserved or multicast
|
|
is_6to4: Same as above but uses 2002::/16 range
|
|
is_teredo: Same as above but uses 2001::/32 range
|
|
subnets: value must be a member of at least one from list of subnets
|
|
|
|
Examples:
|
|
Check for valid IPv6 address:
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_IPV6())
|
|
|
|
Check for valid IPv6 address is a link_local address:
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_IPV6(is_link_local=True))
|
|
|
|
Check for valid IPv6 address that is Internet routeable:
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_IPV6(is_routeable=True))
|
|
|
|
Check for valid IPv6 address in specified subnet:
|
|
|
|
INPUT(_type='text', _name='name', requires=IS_IPV6(subnets=['2001::/32'])
|
|
|
|
>>> IS_IPV6()('fe80::126c:8ffa:fe22:b3af')
|
|
('fe80::126c:8ffa:fe22:b3af', None)
|
|
>>> IS_IPV6()('192.168.1.1')
|
|
('192.168.1.1', 'enter valid IPv6 address')
|
|
>>> IS_IPV6(error_message='Bad ip')('192.168.1.1')
|
|
('192.168.1.1', 'bad ip')
|
|
>>> IS_IPV6(is_link_local=True)('fe80::126c:8ffa:fe22:b3af')
|
|
('fe80::126c:8ffa:fe22:b3af', None)
|
|
>>> IS_IPV6(is_link_local=False)('fe80::126c:8ffa:fe22:b3af')
|
|
('fe80::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
|
|
>>> IS_IPV6(is_link_local=True)('2001::126c:8ffa:fe22:b3af')
|
|
('2001::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
|
|
>>> IS_IPV6(is_multicast=True)('2001::126c:8ffa:fe22:b3af')
|
|
('2001::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
|
|
>>> IS_IPV6(is_multicast=True)('ff00::126c:8ffa:fe22:b3af')
|
|
('ff00::126c:8ffa:fe22:b3af', None)
|
|
>>> IS_IPV6(is_routeable=True)('2001::126c:8ffa:fe22:b3af')
|
|
('2001::126c:8ffa:fe22:b3af', None)
|
|
>>> IS_IPV6(is_routeable=True)('ff00::126c:8ffa:fe22:b3af')
|
|
('ff00::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
|
|
>>> IS_IPV6(subnets='2001::/32')('2001::8ffa:fe22:b3af')
|
|
('2001::8ffa:fe22:b3af', None)
|
|
>>> IS_IPV6(subnets='fb00::/8')('2001::8ffa:fe22:b3af')
|
|
('2001::8ffa:fe22:b3af', 'enter valid IPv6 address')
|
|
>>> IS_IPV6(subnets=['fc00::/8','2001::/32'])('2001::8ffa:fe22:b3af')
|
|
('2001::8ffa:fe22:b3af', None)
|
|
>>> IS_IPV6(subnets='invalidsubnet')('2001::8ffa:fe22:b3af')
|
|
('2001::8ffa:fe22:b3af', 'invalid subnet provided')
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
is_private=None,
|
|
is_link_local=None,
|
|
is_reserved=None,
|
|
is_multicast=None,
|
|
is_routeable=None,
|
|
is_6to4=None,
|
|
is_teredo=None,
|
|
subnets=None,
|
|
error_message='Enter valid IPv6 address'):
|
|
self.is_private = is_private
|
|
self.is_link_local = is_link_local
|
|
self.is_reserved = is_reserved
|
|
self.is_multicast = is_multicast
|
|
self.is_routeable = is_routeable
|
|
self.is_6to4 = is_6to4
|
|
self.is_teredo = is_teredo
|
|
self.subnets = subnets
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
from gluon._compat import ipaddress
|
|
|
|
try:
|
|
ip = ipaddress.IPv6Address(to_unicode(value))
|
|
ok = True
|
|
except ipaddress.AddressValueError:
|
|
return (value, translate(self.error_message))
|
|
|
|
if self.subnets:
|
|
# iterate through self.subnets to see if value is a member
|
|
ok = False
|
|
if isinstance(self.subnets, str):
|
|
self.subnets = [self.subnets]
|
|
for network in self.subnets:
|
|
try:
|
|
ipnet = ipaddress.IPv6Network(to_unicode(network))
|
|
except (ipaddress.NetmaskValueError, ipaddress.AddressValueError):
|
|
return (value, translate('invalid subnet provided'))
|
|
if ip in ipnet:
|
|
ok = True
|
|
|
|
if self.is_routeable:
|
|
self.is_private = False
|
|
self.is_reserved = False
|
|
self.is_multicast = False
|
|
|
|
if ok and self.is_private is not None and \
|
|
self.is_private != ip.is_private:
|
|
ok = False
|
|
if ok and self.is_link_local is not None and \
|
|
self.is_link_local != ip.is_link_local:
|
|
ok = False
|
|
if ok and self.is_reserved is not None and \
|
|
self.is_reserved != ip.is_reserved:
|
|
ok = False
|
|
if ok and self.is_multicast is not None and \
|
|
self.is_multicast != ip.is_multicast:
|
|
ok = False
|
|
if ok and self.is_6to4 is not None and \
|
|
self.is_6to4 != bool(ip.sixtofour):
|
|
ok = False
|
|
if ok and self.is_teredo is not None and \
|
|
self.is_teredo != bool(ip.teredo):
|
|
ok = False
|
|
|
|
if ok:
|
|
return (value, None)
|
|
|
|
return (value, translate(self.error_message))
|
|
|
|
|
|
class IS_IPADDRESS(Validator):
|
|
"""
|
|
Checks if field's value is an IP Address (v4 or v6). Can be set to force
|
|
addresses from within a specific range. Checks are done with the correct
|
|
IS_IPV4 and IS_IPV6 validators.
|
|
|
|
Uses the ipaddress from the Python 3 standard library
|
|
and its Python 2 backport (in contrib/ipaddress.py).
|
|
|
|
Args:
|
|
minip: lowest allowed address; accepts:
|
|
str, eg. 192.168.0.1
|
|
list or tuple of octets, eg. [192, 168, 0, 1]
|
|
maxip: highest allowed address; same as above
|
|
invert: True to allow addresses only from outside of given range; note
|
|
that range boundaries are not matched this way
|
|
|
|
IPv4 specific arguments:
|
|
|
|
- is_localhost: localhost address treatment:
|
|
|
|
- None (default): indifferent
|
|
- True (enforce): query address must match localhost address
|
|
(127.0.0.1)
|
|
- False (forbid): query address must not match localhost address
|
|
- is_private: same as above, except that query address is checked against
|
|
two address ranges: 172.16.0.0 - 172.31.255.255 and
|
|
192.168.0.0 - 192.168.255.255
|
|
- is_automatic: same as above, except that query address is checked against
|
|
one address range: 169.254.0.0 - 169.254.255.255
|
|
- is_ipv4: either:
|
|
|
|
- None (default): indifferent
|
|
- True (enforce): must be an IPv4 address
|
|
- False (forbid): must NOT be an IPv4 address
|
|
|
|
IPv6 specific arguments:
|
|
|
|
- is_link_local: Same as above but uses fe80::/10 range
|
|
- is_reserved: Same as above but uses IETF reserved range
|
|
- is_multicast: Same as above but uses ff00::/8 range
|
|
- is_routeable: Similar to above but enforces not private, link_local,
|
|
reserved or multicast
|
|
- is_6to4: Same as above but uses 2002::/16 range
|
|
- is_teredo: Same as above but uses 2001::/32 range
|
|
- subnets: value must be a member of at least one from list of subnets
|
|
- is_ipv6: either:
|
|
|
|
- None (default): indifferent
|
|
- True (enforce): must be an IPv6 address
|
|
- False (forbid): must NOT be an IPv6 address
|
|
|
|
Minip and maxip may also be lists or tuples of addresses in all above
|
|
forms (str, int, list / tuple), allowing setup of multiple address ranges::
|
|
|
|
minip = (minip1, minip2, ... minipN)
|
|
| | |
|
|
| | |
|
|
maxip = (maxip1, maxip2, ... maxipN)
|
|
|
|
Longer iterable will be truncated to match length of shorter one.
|
|
|
|
>>> IS_IPADDRESS()('192.168.1.5')
|
|
('192.168.1.5', None)
|
|
>>> IS_IPADDRESS(is_ipv6=False)('192.168.1.5')
|
|
('192.168.1.5', None)
|
|
>>> IS_IPADDRESS()('255.255.255.255')
|
|
('255.255.255.255', None)
|
|
>>> IS_IPADDRESS()('192.168.1.5 ')
|
|
('192.168.1.5 ', 'enter valid IP address')
|
|
>>> IS_IPADDRESS()('192.168.1.1.5')
|
|
('192.168.1.1.5', 'enter valid IP address')
|
|
>>> IS_IPADDRESS()('123.123')
|
|
('123.123', 'enter valid IP address')
|
|
>>> IS_IPADDRESS()('1111.2.3.4')
|
|
('1111.2.3.4', 'enter valid IP address')
|
|
>>> IS_IPADDRESS()('0111.2.3.4')
|
|
('0111.2.3.4', 'enter valid IP address')
|
|
>>> IS_IPADDRESS()('256.2.3.4')
|
|
('256.2.3.4', 'enter valid IP address')
|
|
>>> IS_IPADDRESS()('300.2.3.4')
|
|
('300.2.3.4', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(minip='192.168.1.0', maxip='192.168.1.255')('192.168.1.100')
|
|
('192.168.1.100', None)
|
|
>>> IS_IPADDRESS(minip='1.2.3.5', maxip='1.2.3.9', error_message='Bad ip')('1.2.3.4')
|
|
('1.2.3.4', 'bad ip')
|
|
>>> IS_IPADDRESS(maxip='1.2.3.4', invert=True)('127.0.0.1')
|
|
('127.0.0.1', None)
|
|
>>> IS_IPADDRESS(maxip='192.168.1.4', invert=True)('192.168.1.4')
|
|
('192.168.1.4', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(is_localhost=True)('127.0.0.1')
|
|
('127.0.0.1', None)
|
|
>>> IS_IPADDRESS(is_localhost=True)('192.168.1.10')
|
|
('192.168.1.10', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(is_localhost=False)('127.0.0.1')
|
|
('127.0.0.1', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(maxip='100.0.0.0', is_localhost=True)('127.0.0.1')
|
|
('127.0.0.1', 'enter valid IP address')
|
|
|
|
>>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af')
|
|
('fe80::126c:8ffa:fe22:b3af', None)
|
|
>>> IS_IPADDRESS(is_ipv4=False)('fe80::126c:8ffa:fe22:b3af')
|
|
('fe80::126c:8ffa:fe22:b3af', None)
|
|
>>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af ')
|
|
('fe80::126c:8ffa:fe22:b3af ', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(is_ipv4=True)('fe80::126c:8ffa:fe22:b3af')
|
|
('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(is_ipv6=True)('192.168.1.1')
|
|
('192.168.1.1', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(is_ipv6=True, error_message='Bad ip')('192.168.1.1')
|
|
('192.168.1.1', 'bad ip')
|
|
>>> IS_IPADDRESS(is_link_local=True)('fe80::126c:8ffa:fe22:b3af')
|
|
('fe80::126c:8ffa:fe22:b3af', None)
|
|
>>> IS_IPADDRESS(is_link_local=False)('fe80::126c:8ffa:fe22:b3af')
|
|
('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(is_link_local=True)('2001::126c:8ffa:fe22:b3af')
|
|
('2001::126c:8ffa:fe22:b3af', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(is_multicast=True)('2001::126c:8ffa:fe22:b3af')
|
|
('2001::126c:8ffa:fe22:b3af', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(is_multicast=True)('ff00::126c:8ffa:fe22:b3af')
|
|
('ff00::126c:8ffa:fe22:b3af', None)
|
|
>>> IS_IPADDRESS(is_routeable=True)('2001::126c:8ffa:fe22:b3af')
|
|
('2001::126c:8ffa:fe22:b3af', None)
|
|
>>> IS_IPADDRESS(is_routeable=True)('ff00::126c:8ffa:fe22:b3af')
|
|
('ff00::126c:8ffa:fe22:b3af', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(subnets='2001::/32')('2001::8ffa:fe22:b3af')
|
|
('2001::8ffa:fe22:b3af', None)
|
|
>>> IS_IPADDRESS(subnets='fb00::/8')('2001::8ffa:fe22:b3af')
|
|
('2001::8ffa:fe22:b3af', 'enter valid IP address')
|
|
>>> IS_IPADDRESS(subnets=['fc00::/8','2001::/32'])('2001::8ffa:fe22:b3af')
|
|
('2001::8ffa:fe22:b3af', None)
|
|
>>> IS_IPADDRESS(subnets='invalidsubnet')('2001::8ffa:fe22:b3af')
|
|
('2001::8ffa:fe22:b3af', 'invalid subnet provided')
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
minip='0.0.0.0',
|
|
maxip='255.255.255.255',
|
|
invert=False,
|
|
is_localhost=None,
|
|
is_private=None,
|
|
is_automatic=None,
|
|
is_ipv4=None,
|
|
is_link_local=None,
|
|
is_reserved=None,
|
|
is_multicast=None,
|
|
is_routeable=None,
|
|
is_6to4=None,
|
|
is_teredo=None,
|
|
subnets=None,
|
|
is_ipv6=None,
|
|
error_message='Enter valid IP address'):
|
|
self.minip = minip,
|
|
self.maxip = maxip,
|
|
self.invert = invert
|
|
self.is_localhost = is_localhost
|
|
self.is_private = is_private
|
|
self.is_automatic = is_automatic
|
|
self.is_ipv4 = is_ipv4 or is_ipv6 is False
|
|
self.is_private = is_private
|
|
self.is_link_local = is_link_local
|
|
self.is_reserved = is_reserved
|
|
self.is_multicast = is_multicast
|
|
self.is_routeable = is_routeable
|
|
self.is_6to4 = is_6to4
|
|
self.is_teredo = is_teredo
|
|
self.subnets = subnets
|
|
self.is_ipv6 = is_ipv6 or is_ipv4 is False
|
|
self.error_message = error_message
|
|
|
|
def __call__(self, value):
|
|
from gluon._compat import ipaddress
|
|
IPAddress = ipaddress.ip_address
|
|
IPv6Address = ipaddress.IPv6Address
|
|
IPv4Address = ipaddress.IPv4Address
|
|
|
|
try:
|
|
ip = IPAddress(to_unicode(value))
|
|
except ValueError:
|
|
return (value, translate(self.error_message))
|
|
|
|
if self.is_ipv4 and isinstance(ip, IPv6Address):
|
|
retval = (value, translate(self.error_message))
|
|
elif self.is_ipv6 and isinstance(ip, IPv4Address):
|
|
retval = (value, translate(self.error_message))
|
|
elif self.is_ipv4 or isinstance(ip, IPv4Address):
|
|
retval = IS_IPV4(
|
|
minip=self.minip,
|
|
maxip=self.maxip,
|
|
invert=self.invert,
|
|
is_localhost=self.is_localhost,
|
|
is_private=self.is_private,
|
|
is_automatic=self.is_automatic,
|
|
error_message=self.error_message
|
|
)(value)
|
|
elif self.is_ipv6 or isinstance(ip, IPv6Address):
|
|
retval = IS_IPV6(
|
|
is_private=self.is_private,
|
|
is_link_local=self.is_link_local,
|
|
is_reserved=self.is_reserved,
|
|
is_multicast=self.is_multicast,
|
|
is_routeable=self.is_routeable,
|
|
is_6to4=self.is_6to4,
|
|
is_teredo=self.is_teredo,
|
|
subnets=self.subnets,
|
|
error_message=self.error_message
|
|
)(value)
|
|
else:
|
|
retval = (value, translate(self.error_message))
|
|
|
|
return retval
|