diff --git a/gluon/html.py b/gluon/html.py index 44f4f3eb..6642cfc5 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -25,9 +25,9 @@ import marshal from gluon import decoder from gluon.storage import Storage -from gluon.validators import simple_hash from gluon.utils import web2py_uuid, compare from gluon.highlight import highlight +from gluon.validators import simple_hash def local_html_escape(data, quote=False): diff --git a/gluon/packages/dal b/gluon/packages/dal index 48adca66..e28ad146 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 48adca66cf3a26531e3ef7e999919533963be4ab +Subproject commit e28ad14614e075a6d8022abffffbd00da05b52be diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 60c0b91c..29df8993 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -10,7 +10,6 @@ from .test_html import * from .test_contribs import * from .test_routes import * from .test_router import * -from .test_validators import * from .test_authapi import * from .test_tools import * from .test_utils import * @@ -21,8 +20,7 @@ from .test_appadmin import * from .test_web import * from .test_sqlhtml import * from .test_cron import * -from .test_is_url import * -from .test_scheduler import * +# from .test_scheduler import * if sys.version[:3] == '2.7': from .test_old_doctests import * diff --git a/gluon/tests/test_is_url.py b/gluon/tests/test_is_url.py deleted file mode 100644 index babe4edf..00000000 --- a/gluon/tests/test_is_url.py +++ /dev/null @@ -1,691 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Unit tests for IS_URL() -""" - -import unittest - -from gluon.validators import IS_URL, IS_HTTP_URL, IS_GENERIC_URL -from gluon.validators import unicode_to_ascii_authority - - -class TestIsUrl(unittest.TestCase): - - def testModeHttp(self): - - # defaults to mode='http' - - x = IS_URL() - self.assertEqual(x('http://google.ca'), ('http://google.ca', - None)) - self.assertEqual(x('google.ca'), ('http://google.ca', None)) - self.assertEqual(x('google.ca:80'), ('http://google.ca:80', - None)) - self.assertEqual(x('unreal.blargg'), ('unreal.blargg', - 'Enter a valid URL')) - self.assertEqual(x('google..ca'), ('google..ca', 'Enter a valid URL')) - self.assertEqual( - x('google.ca..'), ('google.ca..', 'Enter a valid URL')) - - # explicit use of 'http' mode - - x = IS_URL(mode='http') - self.assertEqual(x('http://google.ca'), ('http://google.ca', - None)) - self.assertEqual(x('google.ca'), ('http://google.ca', None)) - self.assertEqual(x('google.ca:80'), ('http://google.ca:80', - None)) - self.assertEqual(x('unreal.blargg'), ('unreal.blargg', - 'Enter a valid URL')) - - # prepends 'https' instead of 'http' - - x = IS_URL(mode='http', prepend_scheme='https') - self.assertEqual(x('http://google.ca'), ('http://google.ca', - None)) - self.assertEqual(x('google.ca'), ('https://google.ca', None)) - self.assertEqual(x('google.ca:80'), ('https://google.ca:80', - None)) - self.assertEqual(x('unreal.blargg'), ('unreal.blargg', - 'Enter a valid URL')) - - # prepending disabled - - x = IS_URL(prepend_scheme=None) - self.assertEqual(x('http://google.ca'), ('http://google.ca', - None)) - self.assertEqual(x('google.ca'), ('google.ca', None)) - self.assertEqual(x('google.ca:80'), ('google.ca:80', None)) - self.assertEqual(x('unreal.blargg'), ('unreal.blargg', - 'Enter a valid URL')) - - # custom allowed_schemes - - x = IS_URL(mode='http', allowed_schemes=[None, 'http']) - self.assertEqual(x('http://google.ca'), ('http://google.ca', - None)) - self.assertEqual(x('https://google.ca'), ('https://google.ca', - 'Enter a valid URL')) - self.assertEqual(x('google.ca'), ('http://google.ca', None)) - self.assertEqual(x('google.ca:80'), ('http://google.ca:80', - None)) - self.assertEqual(x('unreal.blargg'), ('unreal.blargg', - 'Enter a valid URL')) - - # custom allowed_schemes, excluding None - - x = IS_URL(allowed_schemes=['http']) - self.assertEqual(x('http://google.ca'), ('http://google.ca', - None)) - self.assertEqual(x('https://google.ca'), ('https://google.ca', - 'Enter a valid URL')) - self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL')) - self.assertEqual(x('google.ca:80'), ('google.ca:80', - 'Enter a valid URL')) - self.assertEqual(x('unreal.blargg'), ('unreal.blargg', - 'Enter a valid URL')) - - # custom allowed_schemes and prepend_scheme - - x = IS_URL(allowed_schemes=[None, 'https'], - prepend_scheme='https') - self.assertEqual(x('http://google.ca'), ('http://google.ca', - 'Enter a valid URL')) - self.assertEqual(x('https://google.ca'), ('https://google.ca', - None)) - self.assertEqual(x('google.ca'), ('https://google.ca', None)) - self.assertEqual(x('google.ca:80'), ('https://google.ca:80', - None)) - self.assertEqual(x('unreal.blargg'), ('unreal.blargg', - 'Enter a valid URL')) - - # Now any URL requiring prepending will fail, but prepending is still - # enabled! - - x = IS_URL(allowed_schemes=['http']) - self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL')) - - def testModeGeneric(self): - - # 'generic' mode - - x = IS_URL(mode='generic') - self.assertEqual(x('http://google.ca'), ('http://google.ca', None)) - self.assertEqual(x('google.ca'), ('google.ca', None)) - self.assertEqual(x('google.ca:80'), ('http://google.ca:80', None)) - self.assertEqual(x('blargg://unreal'), ('blargg://unreal', - 'Enter a valid URL')) - - # 'generic' mode with custom allowed_schemes that still includes - # 'http' (the default for prepend_scheme) - - x = IS_URL(mode='generic', allowed_schemes=['http', 'blargg']) - self.assertEqual(x('http://google.ca'), ('http://google.ca', - None)) - self.assertEqual(x('ftp://google.ca'), ('ftp://google.ca', - 'Enter a valid URL')) - self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL')) - self.assertEqual(x('google.ca:80'), ('google.ca:80', - 'Enter a valid URL')) - self.assertEqual(x('blargg://unreal'), ('blargg://unreal', - None)) - - # 'generic' mode with overriden prepend_scheme - - x = IS_URL(mode='generic', prepend_scheme='ftp') - self.assertEqual(x('http://google.ca'), ('http://google.ca', - None)) - self.assertEqual(x('ftp://google.ca'), ('ftp://google.ca', - None)) - self.assertEqual(x('google.ca'), ('google.ca', None)) - self.assertEqual(x('google.ca:80'), ('ftp://google.ca:80', - None)) - self.assertEqual(x('blargg://unreal'), ('blargg://unreal', - 'Enter a valid URL')) - - # 'generic' mode with overriden allowed_schemes and prepend_scheme - - x = IS_URL(mode='generic', allowed_schemes=[None, 'ftp', 'ftps' - ], prepend_scheme='ftp') - self.assertEqual(x('http://google.ca'), ('http://google.ca', - 'Enter a valid URL')) - self.assertEqual(x('google.ca'), ('google.ca', None)) - self.assertEqual(x('ftp://google.ca'), ('ftp://google.ca', - None)) - self.assertEqual(x('google.ca:80'), ('ftp://google.ca:80', - None)) - self.assertEqual(x('blargg://unreal'), ('blargg://unreal', - 'Enter a valid URL')) - - # Now any URL requiring prepending will fail, but prepending is still - # enabled! - - x = IS_URL(mode='generic', allowed_schemes=['http']) - self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL')) - - def testExceptionalUse(self): - - # mode must be in set ['http', 'generic'] - - try: - x = IS_URL(mode='ftp') - x('http://www.google.ca') - except Exception as e: - if str(e) != "invalid mode 'ftp' in IS_URL": - self.fail('Wrong exception: ' + str(e)) - else: - self.fail("Accepted invalid mode: 'ftp'") - - # allowed_schemes in 'http' mode must be in set [None, 'http', 'https'] - - try: - x = IS_URL(allowed_schemes=[None, 'ftp', 'ftps'], - prepend_scheme='ftp') - x('http://www.benn.ca') # we can only reasonably know about the - # error at calling time - except Exception as e: - if str(e)\ - != "allowed_scheme value 'ftp' is not in [None, 'http', 'https']": - self.fail('Wrong exception: ' + str(e)) - else: - self.fail( - "Accepted invalid allowed_schemes: [None, 'ftp', 'ftps']") - - # prepend_scheme's value must be in allowed_schemes (default for 'http' - # mode is [None, 'http', 'https']) - - try: - x = IS_URL(prepend_scheme='ftp') - x('http://www.benn.ca') # we can only reasonably know about the - # error at calling time - except Exception as e: - if str(e)\ - != "prepend_scheme='ftp' is not in allowed_schemes=[None, 'http', 'https']": - self.fail('Wrong exception: ' + str(e)) - else: - self.fail("Accepted invalid prepend_scheme: 'ftp'") - - # custom allowed_schemes that excludes 'http', so prepend_scheme must be - # specified! - - try: - x = IS_URL(allowed_schemes=[None, 'https']) - except Exception as e: - if str(e)\ - != "prepend_scheme='http' is not in allowed_schemes=[None, 'https']": - self.fail('Wrong exception: ' + str(e)) - else: - self.fail("Accepted invalid prepend_scheme: 'http'") - - # prepend_scheme must be in allowed_schemes - - try: - x = IS_URL(allowed_schemes=[None, 'http'], - prepend_scheme='https') - except Exception as e: - if str(e)\ - != "prepend_scheme='https' is not in allowed_schemes=[None, 'http']": - self.fail('Wrong exception: ' + str(e)) - else: - self.fail("Accepted invalid prepend_scheme: 'https'") - - # prepend_scheme's value (default is 'http') must be in allowed_schemes - - try: - x = IS_URL(mode='generic', allowed_schemes=[None, 'ftp', - 'ftps']) - except Exception as e: - if str(e)\ - != "prepend_scheme='http' is not in allowed_schemes=[None, 'ftp', 'ftps']": - self.fail('Wrong exception: ' + str(e)) - else: - self.fail("Accepted invalid prepend_scheme: 'http'") - - # prepend_scheme's value must be in allowed_schemes, which by default - # is all schemes that really exist - - try: - x = IS_URL(mode='generic', prepend_scheme='blargg') - x('http://www.google.ca') - # we can only reasonably know about the error at calling time - except Exception as e: - if not str(e).startswith( - "prepend_scheme='blargg' is not in allowed_schemes="): - self.fail('Wrong exception: ' + str(e)) - else: - self.fail("Accepted invalid prepend_scheme: 'blargg'") - - # prepend_scheme's value must be in allowed_schemes - - try: - x = IS_URL(mode='generic', allowed_schemes=[None, 'http'], - prepend_scheme='blargg') - except Exception as e: - if str(e)\ - != "prepend_scheme='blargg' is not in allowed_schemes=[None, 'http']": - self.fail('Wrong exception: ' + str(e)) - else: - self.fail("Accepted invalid prepend_scheme: 'blargg'") - - # Not inluding None in the allowed_schemes essentially disabled - # prepending, so even though - # prepend_scheme has the invalid value 'http', we don't care! - - x = IS_URL(allowed_schemes=['https'], prepend_scheme='https') - self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL')) - - # Not inluding None in the allowed_schemes essentially disabled prepending, so even though - # prepend_scheme has the invalid value 'http', we don't care! - - x = IS_URL(mode='generic', allowed_schemes=['https'], - prepend_scheme='https') - self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL')) - - -# ############################################################################## - - -class TestIsGenericUrl(unittest.TestCase): - - x = IS_GENERIC_URL() - - def testInvalidUrls(self): - urlsToCheckA = [] - for i in list(range(0, 32)) + [127]: - - # Control characters are disallowed in any part of a URL - - urlsToCheckA.append('http://www.benn' + chr(i) + '.ca') - - urlsToCheckB = [ - None, - '', - 'http://www.no spaces allowed.com', - 'http://www.benn.ca/no spaces allowed/', - 'http://www.benn.ca/angle_bracket/', - 'http://www.benn.ca/invalid%character', - 'http://www.benn.ca/illegal%%20use', - 'http://www.benn.ca/illegaluse%', - 'http://www.benn.ca/illegaluse%0', - 'http://www.benn.ca/illegaluse%x', - 'http://www.benn.ca/ill%egaluse%x', - 'http://www.benn.ca/double"quote/', - 'http://www.curly{brace.com', - 'http://www.benn.ca/curly}brace/', - 'http://www.benn.ca/or|symbol/', - 'http://www.benn.ca/back\slash', - 'http://www.benn.ca/the^carat', - 'http://left[bracket.me', - 'http://www.benn.ca/right]bracket', - 'http://www.benn.ca/angle`quote', - '-ttp://www.benn.ca', - '+ttp://www.benn.ca', - '.ttp://www.benn.ca', - '9ttp://www.benn.ca', - 'ht;tp://www.benn.ca', - 'ht@tp://www.benn.ca', - 'ht&tp://www.benn.ca', - 'ht=tp://www.benn.ca', - 'ht$tp://www.benn.ca', - 'ht,tp://www.benn.ca', - 'ht:tp://www.benn.ca', - 'htp://invalid_scheme.com', - ] - - failures = [] - - for url in urlsToCheckA + urlsToCheckB: - if self.x(url)[1] is None: - failures.append('Incorrectly accepted: ' + str(url)) - - if len(failures) > 0: - self.fail(failures) - - def testValidUrls(self): - urlsToCheck = [ - 'ftp://ftp.is.co.za/rfc/rfc1808.txt', - 'gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles', - 'http://www.math.uio.no/faq/compression-faq/part1.html', - 'mailto:mduerst@ifi.unizh.ch', - 'news:comp.infosystems.www.servers.unix', - 'telnet://melvyl.ucop.edu/', - 'hTTp://www.benn.ca', - '%66%74%70://ftp.is.co.za/rfc/rfc1808.txt', - '%46%74%70://ftp.is.co.za/rfc/rfc1808.txt', - '/faq/compression-faq/part1.html', - 'google.com', - 'www.google.com:8080', - '128.127.123.250:8080', - 'blargg:ping', - 'http://www.benn.ca', - 'http://benn.ca', - 'http://amazon.com/books/', - 'https://amazon.com/movies', - 'rtsp://idontknowthisprotocol', - 'HTTP://allcaps.com', - 'http://localhost', - 'http://localhost#fragment', - 'http://localhost/hello', - 'http://localhost/hello?query=True', - 'http://localhost/hello/', - 'http://localhost:8080', - 'http://localhost:8080/', - 'http://localhost:8080/hello', - 'http://localhost:8080/hello/', - 'file:///C:/Documents%20and%20Settings/Jonathan/Desktop/view.py', - ] - - failures = [] - - for url in urlsToCheck: - if self.x(url)[1] is not None: - failures.append('Incorrectly rejected: ' + str(url)) - - if len(failures) > 0: - self.fail(failures) - - def testPrepending(self): - # Does not prepend scheme for abbreviated domains - self.assertEqual(self.x('google.ca'), ('google.ca', None)) - - # Does not prepend scheme for abbreviated domains - self.assertEqual(self.x('google.ca:8080'), ('google.ca:8080', None)) - - # Does not prepend when scheme already exists - self.assertEqual(self.x('https://google.ca'), - ('https://google.ca', None)) - - # Does not prepend if None type is not specified in allowed_scheme, - # because a scheme is required - - y = IS_GENERIC_URL(allowed_schemes=['http', 'blargg'], - prepend_scheme='http') - self.assertEqual(y('google.ca'), ('google.ca', 'Enter a valid URL')) - - -# ############################################################################## - - -class TestIsHttpUrl(unittest.TestCase): - - x = IS_HTTP_URL() - - def testInvalidUrls(self): - urlsToCheck = [ - None, - '', - 'http://invalid' + chr(2) + '.com', - 'htp://invalid_scheme.com', - 'blargg://invalid_scheme.com', - 'http://-123.com', - 'http://abcd-.ca', - 'http://-abc123-.me', - 'http://www.dom&ain.com/', - 'http://www.dom=ain.com/', - 'http://www.benn.ca&', - 'http://%62%65%6E%6E%2E%63%61/path', - 'http://.domain.com', - 'http://.domain.com./path', - 'http://domain..com', - 'http://domain...at..com', - 'http://domain.com..', - 'http://domain.com../path', - 'http://domain.3m', - 'http://domain.-3m', - 'http://domain.3m-', - 'http://domain.-3m-', - 'http://domain.co&m', - 'http://domain.m3456', - 'http://domain.m-3/path#fragment', - 'http://domain.m---k/path?query=value', - 'http://23.32..', - 'http://23..32.56.0', - 'http://38997.222.999', - 'http://23.32.56.99.', - 'http://.23.32.56.99', - 'http://.23.32.56.99.', - 'http://w127.123.0.256:8080', - 'http://23.32.56.99:abcd', - 'http://23.32.56.99:23cd', - 'http://google.com:cd22', - 'http://23.32:1300.56.99', - 'http://www.yahoo:1600.com', - 'path/segment/without/starting/slash', - 'http://www.math.uio.no;param=3', - '://ABC.com:/%7esmith/home.html', - ] - - failures = [] - - for url in urlsToCheck: - if self.x(url)[1] is None: - failures.append('Incorrectly accepted: ' + str(url)) - - if len(failures) > 0: - self.fail(failures) - - def testValidUrls(self): - - urlsToCheck = [ - 'http://abc.com:80/~smith/home.html', - 'http://ABC.com/%7Esmith/home.html', - 'http://ABC.com:/%7esmith/home.html', - 'http://www.math.uio.no/faq/compression-faq/part1.html', - '//google.ca/faq/compression-faq/part1.html', - '//google.ca/faq;param=3', - '//google.ca/faq/index.html?query=5', - '//google.ca/faq/index.html;param=value?query=5', - '/faq/compression-faq/part1.html', - '/faq;param=3', - '/faq/index.html?query=5', - '/faq/index.html;param=value?query=5', - 'google.com', - 'benn.ca/init/default', - 'benn.ca/init;param=value/default?query=value', - 'http://host-name---with-dashes.me', - 'http://www.host-name---with-dashes.me', - 'http://a.com', - 'http://a.3.com', - 'http://a.bl-ck.com', - 'http://bl-e.b.com', - 'http://host123with456numbers.ca', - 'http://1234567890.com.', - 'http://1234567890.com./path', - 'http://google.com./path', - 'http://domain.xn--d1acj3b', - 'http://127.123.0.256', - 'http://127.123.0.256/document/drawer', - '127.123.0.256/document/', - '156.212.123.100', - 'http://www.google.com:180200', - 'http://www.google.com:8080/path', - 'http://www.google.com:8080', - '//www.google.com:8080', - 'www.google.com:8080', - 'http://127.123.0.256:8080/path', - '//127.123.0.256:8080', - '127.123.0.256:8080', - 'http://example.me??query=value?', - 'http://a.com', - 'http://3.com', - 'http://www.benn.ca', - 'http://benn.ca', - 'http://amazon.com/books/', - 'https://amazon.com/movies', - 'hTTp://allcaps.com', - 'http://localhost', - 'HTTPS://localhost.', - 'http://localhost#fragment', - 'http://localhost/hello;param=value', - 'http://localhost/hello;param=value/hi;param2=value2;param3=value3', - 'http://localhost/hello?query=True', - 'http://www.benn.ca/hello;param=value/hi;param2=value2;param3=value3/index.html?query=3', - 'http://localhost/hello/?query=1500&five=6', - 'http://localhost:8080', - 'http://localhost:8080/', - 'http://localhost:8080/hello', - 'http://localhost:8080/hello%20world/', - 'http://www.a.3.be-nn.5.ca', - 'http://www.amazon.COM', - ] - - failures = [] - - for url in urlsToCheck: - if self.x(url)[1] is not None: - failures.append('Incorrectly rejected: ' + str(url)) - - if len(failures) > 0: - self.fail(failures) - - def testPrepending(self): - # prepends scheme for abbreviated domains - self.assertEqual(self.x('google.ca'), ('http://google.ca', None)) - - # prepends scheme for abbreviated domains - self.assertEqual(self.x('google.ca:8080'), - ('http://google.ca:8080', None)) - - # does not prepend when scheme already exists - self.assertEqual(self.x('https://google.ca'), - ('https://google.ca', None)) - - y = IS_HTTP_URL( - prepend_scheme='https', allowed_schemes=[None, 'https']) - self.assertEqual(y('google.ca'), ( - 'https://google.ca', None)) # prepends https if asked - - z = IS_HTTP_URL(prepend_scheme=None) - self.assertEqual(z('google.ca:8080'), ('google.ca:8080', None)) # prepending disabled - - try: - IS_HTTP_URL(prepend_scheme='mailto') - except Exception as e: - if str(e)\ - != "prepend_scheme='mailto' is not in allowed_schemes=[None, 'http', 'https']": - self.fail('Wrong exception: ' + str(e)) - else: - self.fail("Got invalid prepend_scheme: 'mailto'") - - # Does not prepend if None type is not specified in allowed_scheme, because a scheme is required - - a = IS_HTTP_URL(allowed_schemes=['http']) - self.assertEqual(a('google.ca'), ('google.ca', 'Enter a valid URL')) - self.assertEqual(a('google.ca:80'), ('google.ca:80', - 'Enter a valid URL')) - - -class TestUnicode(unittest.TestCase): - x = IS_URL() - y = IS_URL(allowed_schemes=['https'], prepend_scheme='https') - #excludes the option for abbreviated URLs with no scheme - z = IS_URL(prepend_scheme=None) - # disables prepending the scheme in the return value - - def testUnicodeToAsciiUrl(self): - self.assertEqual(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu') - self.assertEqual( - unicode_to_ascii_authority(u'www.benn.ca'), 'www.benn.ca') - self.assertRaises(UnicodeError, unicode_to_ascii_authority, - u'\u4e2d' * 1000) # label is too long - - def testValidUrls(self): - self.assertEqual(self.x(u'www.Alliancefrancaise.nu'), ( - 'http://www.Alliancefrancaise.nu', None)) - self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu'), ( - 'http://www.xn--alliancefranaise-npb.nu', None)) - self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu:8080'), ( - 'http://www.xn--alliancefranaise-npb.nu:8080', None)) - self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu'), - ('http://www.xn--alliancefranaise-npb.nu', None)) - self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue', None)) - self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue#fragment', None)) - self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None)) - self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu:8080/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu:8080/parnaise/blue?query=value#fragment', None)) - self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None)) - self.assertEqual(self.x( - u'http://\u4e2d\u4fd4.com'), ('http://xn--fiq13b.com', None)) - self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86'), - ('http://xn--fiq13b.com/%4e%86', None)) - self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86', None)) - self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86#fragment'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86#fragment', None)) - self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com?query=\u4e86#fragment'), ('http://xn--fiq13b.com?query=%4e%86#fragment', None)) - self.assertEqual( - self.x(u'http://B\xfccher.ch'), ('http://xn--bcher-kva.ch', None)) - self.assertEqual(self.x(u'http://\xe4\xf6\xfc\xdf.com'), ( - 'http://xn--ss-uia6e4a.com', None)) - self.assertEqual(self.x( - u'http://visegr\xe1d.com'), ('http://xn--visegrd-mwa.com', None)) - self.assertEqual(self.x(u'http://h\xe1zipatika.com'), ( - 'http://xn--hzipatika-01a.com', None)) - self.assertEqual(self.x(u'http://www.\xe7ukurova.com'), ( - 'http://www.xn--ukurova-txa.com', None)) - self.assertEqual(self.x(u'http://nixier\xf6hre.nixieclock-tube.com'), ('http://xn--nixierhre-57a.nixieclock-tube.com', None)) - self.assertEqual(self.x(u'google.ca.'), ('http://google.ca.', None)) - - self.assertEqual( - self.y(u'https://google.ca'), ('https://google.ca', None)) - self.assertEqual(self.y( - u'https://\u4e2d\u4fd4.com'), ('https://xn--fiq13b.com', None)) - - self.assertEqual(self.z(u'google.ca'), ('google.ca', None)) - - def testInvalidUrls(self): - self.assertEqual( - self.x(u'://ABC.com'), (u'://ABC.com', 'Enter a valid URL')) - self.assertEqual(self.x(u'http://\u4e2d\u4fd4.dne'), ( - u'http://\u4e2d\u4fd4.dne', 'Enter a valid URL')) - self.assertEqual(self.x(u'https://google.dne'), ( - u'https://google.dne', 'Enter a valid URL')) - self.assertEqual(self.x(u'https://google..ca'), ( - u'https://google..ca', 'Enter a valid URL')) - self.assertEqual( - self.x(u'google..ca'), (u'google..ca', 'Enter a valid URL')) - self.assertEqual(self.x(u'http://' + u'\u4e2d' * 1000 + u'.com'), ( - u'http://' + u'\u4e2d' * 1000 + u'.com', 'Enter a valid URL')) - - self.assertEqual(self.x(u'http://google.com#fragment_\u4e86'), ( - u'http://google.com#fragment_\u4e86', 'Enter a valid URL')) - self.assertEqual(self.x(u'http\u4e86://google.com'), ( - u'http\u4e86://google.com', 'Enter a valid URL')) - self.assertEqual(self.x(u'http\u4e86://google.com#fragment_\u4e86'), ( - u'http\u4e86://google.com#fragment_\u4e86', 'Enter a valid URL')) - - self.assertEqual(self.y(u'http://\u4e2d\u4fd4.com/\u4e86'), ( - u'http://\u4e2d\u4fd4.com/\u4e86', 'Enter a valid URL')) - #self.assertEqual(self.y(u'google.ca'), (u'google.ca', 'Enter a valid URL')) - - self.assertEqual(self.z(u'invalid.domain..com'), ( - u'invalid.domain..com', 'Enter a valid URL')) - self.assertEqual(self.z(u'invalid.\u4e2d\u4fd4.blargg'), ( - u'invalid.\u4e2d\u4fd4.blargg', 'Enter a valid URL')) - -# ############################################################################## - - -class TestSimple(unittest.TestCase): - - def test_IS_URL(self): - rtn = IS_URL()('abc.com') - self.assertEqual(rtn, ('http://abc.com', None)) - rtn = IS_URL(mode='generic')('abc.com') - self.assertEqual(rtn, ('abc.com', None)) - rtn = IS_URL(allowed_schemes=['https'], prepend_scheme='https')('https://abc.com') - self.assertEqual(rtn, ('https://abc.com', None)) - rtn = IS_URL(prepend_scheme='https')('abc.com') - self.assertEqual(rtn, ('https://abc.com', None)) - rtn = IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], prepend_scheme='https')('https://abc.com') - self.assertEqual(rtn, ('https://abc.com', None)) - rtn = IS_URL(mode='generic', allowed_schemes=['ftps', 'https', None], prepend_scheme='https')('abc.com') - self.assertEqual(rtn, ('abc.com', None)) - # regression test for issue 773 - rtn = IS_URL()('domain.ninja') - self.assertEqual(rtn, ('http://domain.ninja', None)) - # addition of allowed_tlds - rtn = IS_URL(allowed_tlds=['com', 'net', 'org'])('domain.ninja') - self.assertEqual(rtn, ('domain.ninja', 'Enter a valid URL')) - # mode = 'generic' doesn't consider allowed_tlds - rtn = IS_URL(mode='generic', allowed_tlds=['com', 'net', 'org'])('domain.ninja') - self.assertEqual(rtn, ('domain.ninja', None)) diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py deleted file mode 100644 index 90f62c95..00000000 --- a/gluon/tests/test_validators.py +++ /dev/null @@ -1,1273 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""Unit tests for http.py """ - -import unittest -import datetime -import decimal -import re - -from gluon.validators import * -from gluon._compat import PY2, to_bytes - -class TestValidators(unittest.TestCase): - - def myassertRegex(self, *args, **kwargs): - if PY2: - return getattr(self, 'assertRegexpMatches')(*args, **kwargs) - return getattr(self, 'assertRegex')(*args, **kwargs) - - def test_MISC(self): - """ Test miscelaneous utility functions and some general behavior guarantees """ - from gluon.validators import options_sorter, Validator, UTC - self.assertEqual(options_sorter(('a', 'a'), ('a', 'a')), -1) - self.assertEqual(options_sorter(('A', 'A'), ('a', 'a')), -1) - self.assertEqual(options_sorter(('b', 'b'), ('a', 'a')), 1) - self.assertRaises(NotImplementedError, Validator(), 1) - utc = UTC() - dt = datetime.datetime.now() - self.assertEqual(utc.utcoffset(dt), UTC.ZERO) - self.assertEqual(utc.dst(dt), UTC.ZERO) - self.assertEqual(utc.tzname(dt), 'UTC') - - def test_IS_MATCH(self): - rtn = IS_MATCH('.+')('hello') - self.assertEqual(rtn, ('hello', None)) - rtn = IS_MATCH('hell')('hello') - self.assertEqual(rtn, ('hello', None)) - rtn = IS_MATCH('hell.*', strict=False)('hello') - self.assertEqual(rtn, ('hello', None)) - rtn = IS_MATCH('hello')('shello') - self.assertEqual(rtn, ('shello', 'Invalid expression')) - rtn = IS_MATCH('hello', search=True)('shello') - self.assertEqual(rtn, ('shello', None)) - rtn = IS_MATCH('hello', search=True, strict=False)('shellox') - self.assertEqual(rtn, ('shellox', None)) - rtn = IS_MATCH('.*hello.*', search=True, strict=False)('shellox') - self.assertEqual(rtn, ('shellox', None)) - rtn = IS_MATCH('.+')('') - self.assertEqual(rtn, ('', 'Invalid expression')) - rtn = IS_MATCH('hell', strict=True)('hellas') - self.assertEqual(rtn, ('hellas', 'Invalid expression')) - rtn = IS_MATCH('hell$', strict=True)('hellas') - self.assertEqual(rtn, ('hellas', 'Invalid expression')) - rtn = IS_MATCH('^.hell$', strict=True)('shell') - self.assertEqual(rtn, ('shell', None)) - rtn = IS_MATCH(u'hell', is_unicode=True)('àòè') - if PY2: - self.assertEqual(rtn, ('\xc3\xa0\xc3\xb2\xc3\xa8', 'Invalid expression')) - else: - self.assertEqual(rtn, ('àòè', 'Invalid expression')) - rtn = IS_MATCH(u'hell', is_unicode=True)(u'hell') - self.assertEqual(rtn, (u'hell', None)) - rtn = IS_MATCH('hell', is_unicode=True)(u'hell') - self.assertEqual(rtn, (u'hell', None)) - # regr test for #1044 - rtn = IS_MATCH('hello')(u'\xff') - self.assertEqual(rtn, (u'\xff', 'Invalid expression')) - - def test_IS_EQUAL_TO(self): - rtn = IS_EQUAL_TO('aaa')('aaa') - self.assertEqual(rtn, ('aaa', None)) - rtn = IS_EQUAL_TO('aaa')('aab') - self.assertEqual(rtn, ('aab', 'No match')) - - def test_IS_EXPR(self): - rtn = IS_EXPR('int(value) < 2')('1') - self.assertEqual(rtn, ('1', None)) - rtn = IS_EXPR('int(value) < 2')('2') - self.assertEqual(rtn, ('2', 'Invalid expression')) - rtn = IS_EXPR(lambda value: int(value))('1') - self.assertEqual(rtn, ('1', 1)) - rtn = IS_EXPR(lambda value: int(value) < 2 and 'invalid' or None)('2') - self.assertEqual(rtn, ('2', None)) - - def test_IS_LENGTH(self): - rtn = IS_LENGTH()('') - self.assertEqual(rtn, ('', None)) - rtn = IS_LENGTH()('1234567890') - self.assertEqual(rtn, ('1234567890', None)) - rtn = IS_LENGTH(maxsize=5, minsize=0)('1234567890') # too long - self.assertEqual(rtn, ('1234567890', 'Enter from 0 to 5 characters')) - rtn = IS_LENGTH(maxsize=50, minsize=20)('1234567890') # too short - self.assertEqual(rtn, ('1234567890', 'Enter from 20 to 50 characters')) - rtn = IS_LENGTH()(None) - self.assertEqual(rtn, (None, None)) - rtn = IS_LENGTH(minsize=0)(None) - self.assertEqual(rtn, (None, None)) - rtn = IS_LENGTH(minsize=1)(None) - self.assertEqual(rtn, (None, 'Enter from 1 to 255 characters')) - rtn = IS_LENGTH(minsize=1)([]) - self.assertEqual(rtn, ([], 'Enter from 1 to 255 characters')) - rtn = IS_LENGTH(minsize=1)([1, 2]) - self.assertEqual(rtn, ([1, 2], None)) - rtn = IS_LENGTH(minsize=1)([1]) - self.assertEqual(rtn, ([1], None)) - # test non utf-8 str - cpstr = u'lálá'.encode('cp1252') - rtn = IS_LENGTH(minsize=4)(cpstr) - self.assertEqual(rtn, (cpstr, None)) - rtn = IS_LENGTH(maxsize=4)(cpstr) - self.assertEqual(rtn, (cpstr, None)) - rtn = IS_LENGTH(minsize=0, maxsize=3)(cpstr) - self.assertEqual(rtn, (cpstr, 'Enter from 0 to 3 characters')) - # test unicode - rtn = IS_LENGTH(2)(u'°2') - if PY2: - self.assertEqual(rtn, ('\xc2\xb02', None)) - else: - self.assertEqual(rtn, (u'°2', None)) - rtn = IS_LENGTH(2)(u'°12') - if PY2: - self.assertEqual(rtn, (u'\xb012', 'Enter from 0 to 2 characters')) - else: - self.assertEqual(rtn, (u'°12', 'Enter from 0 to 2 characters')) - # test automatic str() - rtn = IS_LENGTH(minsize=1)(1) - self.assertEqual(rtn, ('1', None)) - rtn = IS_LENGTH(minsize=2)(1) - self.assertEqual(rtn, (1, 'Enter from 2 to 255 characters')) - # test FieldStorage - import cgi - from io import BytesIO - a = cgi.FieldStorage() - a.file = BytesIO(b'abc') - rtn = IS_LENGTH(minsize=4)(a) - self.assertEqual(rtn, (a, 'Enter from 4 to 255 characters')) - urlencode_data = b"key2=value2x&key3=value3&key4=value4" - urlencode_environ = { - 'CONTENT_LENGTH': str(len(urlencode_data)), - 'CONTENT_TYPE': 'application/x-www-form-urlencoded', - 'QUERY_STRING': 'key1=value1&key2=value2y', - 'REQUEST_METHOD': 'POST', - } - fake_stdin = BytesIO(urlencode_data) - fake_stdin.seek(0) - a = cgi.FieldStorage(fp=fake_stdin, environ=urlencode_environ) - rtn = IS_LENGTH(minsize=6)(a) - self.assertEqual(rtn, (a, 'Enter from 6 to 255 characters')) - a = cgi.FieldStorage() - rtn = IS_LENGTH(minsize=6)(a) - self.assertEqual(rtn, (a, 'Enter from 6 to 255 characters')) - rtn = IS_LENGTH(6)(a) - self.assertEqual(rtn, (a, None)) - - def test_IS_JSON(self): - rtn = IS_JSON()('{"a": 100}') - self.assertEqual(rtn, ({u'a': 100}, None)) - rtn = IS_JSON()('spam1234') - self.assertEqual(rtn, ('spam1234', 'Invalid json')) - rtn = IS_JSON(native_json=True)('{"a": 100}') - self.assertEqual(rtn, ('{"a": 100}', None)) - rtn = IS_JSON().formatter(None) - self.assertEqual(rtn, None) - rtn = IS_JSON().formatter({'a': 100}) - self.assertEqual(rtn, '{"a": 100}') - rtn = IS_JSON(native_json=True).formatter({'a': 100}) - self.assertEqual(rtn, {'a': 100}) - - def test_IS_IN_SET(self): - rtn = IS_IN_SET(['max', 'john'])('max') - self.assertEqual(rtn, ('max', None)) - rtn = IS_IN_SET(['max', 'john'])('massimo') - self.assertEqual(rtn, ('massimo', 'Value not allowed')) - rtn = IS_IN_SET(['max', 'john'], multiple=True)(('max', 'john')) - self.assertEqual(rtn, (('max', 'john'), None)) - rtn = IS_IN_SET(['max', 'john'], multiple=True)(('bill', 'john')) - self.assertEqual(rtn, (('bill', 'john'), 'Value not allowed')) - rtn = IS_IN_SET(('id1', 'id2'), ['first label', 'second label'])('id1') # Traditional way - self.assertEqual(rtn, ('id1', None)) - rtn = IS_IN_SET({'id1': 'first label', 'id2': 'second label'})('id1') - self.assertEqual(rtn, ('id1', None)) - rtn = IS_IN_SET(['id1', 'id2'], error_message='oops', multiple=True)(None) - self.assertEqual(rtn, ([], None)) - rtn = IS_IN_SET(['id1', 'id2'], error_message='oops', multiple=True)('') - self.assertEqual(rtn, ([], None)) - rtn = IS_IN_SET(['id1', 'id2'], error_message='oops', multiple=True)('id1') - self.assertEqual(rtn, (['id1'], None)) - rtn = IS_IN_SET(['id1', 'id2'], error_message='oops', multiple=(1, 2))(None) - self.assertEqual(rtn, (None, 'oops')) - import itertools - rtn = IS_IN_SET(itertools.chain(['1', '3', '5'], ['2', '4', '6']))('1') - self.assertEqual(rtn, ('1', None)) - rtn = IS_IN_SET([('id1', 'first label'), ('id2', 'second label')])('id1') # Redundant way - self.assertEqual(rtn, ('id1', None)) - rtn = IS_IN_SET([('id1', 'first label'), ('id2', 'second label')]).options(zero=False) - self.assertEqual(rtn, [('id1', 'first label'), ('id2', 'second label')]) - rtn = IS_IN_SET(['id1', 'id2']).options(zero=False) - self.assertEqual(rtn, [('id1', 'id1'), ('id2', 'id2')]) - rtn = IS_IN_SET(['id2', 'id1'], sort=True).options(zero=False) - self.assertEqual(rtn, [('id1', 'id1'), ('id2', 'id2')]) - - def test_IS_IN_DB(self): - from gluon.dal import DAL, Field - db = DAL('sqlite:memory') - db.define_table('person', Field('name')) - george_id = db.person.insert(name='george') - costanza_id = db.person.insert(name='costanza') - rtn = IS_IN_DB(db, 'person.id', '%(name)s')(george_id) - self.assertEqual(rtn, (george_id, None)) - rtn = IS_IN_DB(db, 'person.name', '%(name)s')('george') - self.assertEqual(rtn, ('george', None)) - rtn = IS_IN_DB(db, db.person, '%(name)s')(george_id) - self.assertEqual(rtn, (george_id, None)) - rtn = IS_IN_DB(db(db.person.id > 0), db.person, '%(name)s')(george_id) - self.assertEqual(rtn, (george_id, None)) - rtn = IS_IN_DB(db, 'person.id', '%(name)s', error_message='oops')(george_id + costanza_id) - self.assertEqual(rtn, (george_id + costanza_id, 'oops')) - rtn = IS_IN_DB(db, db.person.id, '%(name)s')(george_id) - self.assertEqual(rtn, (george_id, None)) - rtn = IS_IN_DB(db, db.person.id, '%(name)s', error_message='oops')(george_id + costanza_id) - self.assertEqual(rtn, (george_id + costanza_id, 'oops')) - rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=True)([george_id, costanza_id]) - self.assertEqual(rtn, ([george_id, costanza_id], None)) - rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=True, error_message='oops')("I'm not even an id") - self.assertEqual(rtn, ("I'm not even an id", 'oops')) - rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=True, delimiter=',')('%d,%d' % (george_id, costanza_id)) - self.assertEqual(rtn, (('%d,%d' % (george_id, costanza_id)).split(','), None)) - rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=(1, 3), delimiter=',')('%d,%d' % (george_id, costanza_id)) - self.assertEqual(rtn, (('%d,%d' % (george_id, costanza_id)).split(','), None)) - rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=(1, 2), delimiter=',', error_message='oops')('%d,%d' % (george_id, costanza_id)) - self.assertEqual(rtn, (('%d,%d' % (george_id, costanza_id)), 'oops')) - rtn = IS_IN_DB(db, db.person.id, '%(name)s', error_message='oops').options(zero=False) - self.assertEqual(sorted(rtn), [('%d' % george_id, 'george'), ('%d' % costanza_id, 'costanza')]) - rtn = IS_IN_DB(db, db.person.id, db.person.name, error_message='oops', sort=True).options(zero=True) - self.assertEqual(rtn, [('', ''), ('%d' % costanza_id, 'costanza'), ('%d' % george_id, 'george')]) - # Test None - rtn = IS_IN_DB(db, 'person.id', '%(name)s', error_message='oops')(None) - self.assertEqual(rtn, (None, 'oops')) - rtn = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops')(None) - self.assertEqual(rtn, (None, 'oops')) - # Test using the set it made for options - vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops') - vldtr.options() - rtn = vldtr('george') - self.assertEqual(rtn, ('george', None)) - rtn = vldtr('jerry') - self.assertEqual(rtn, ('jerry', 'oops')) - vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops', multiple=True) - vldtr.options() - rtn = vldtr(['george', 'costanza']) - self.assertEqual(rtn, (['george', 'costanza'], None)) - # Test it works with self reference - db.define_table('category', - Field('parent_id', 'reference category', requires=IS_EMPTY_OR(IS_IN_DB(db, 'category.id', '%(name)s'))), - Field('name') - ) - ret = db.category.validate_and_insert(name='seinfeld') - self.assertFalse(list(ret.errors)) - ret = db.category.validate_and_insert(name='characters', parent_id=ret.id) - self.assertFalse(list(ret.errors)) - rtn = IS_IN_DB(db, 'category.id', '%(name)s')(ret.id) - self.assertEqual(rtn, (ret.id, None)) - # Test _and - vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops', _and=IS_LENGTH(maxsize=7, error_message='bad')) - rtn = vldtr('george') - self.assertEqual(rtn, ('george', None)) - rtn = vldtr('costanza') - self.assertEqual(rtn, ('costanza', 'bad')) - rtn = vldtr('jerry') - self.assertEqual(rtn, ('jerry', 'oops')) - vldtr.options() # test theset with _and - rtn = vldtr('jerry') - self.assertEqual(rtn, ('jerry', 'oops')) - # Test auto_add - rtn = IS_IN_DB(db, 'person.id', '%(name)s', error_message='oops')('jerry') - self.assertEqual(rtn, ('jerry', 'oops')) - rtn = IS_IN_DB(db, 'person.id', '%(name)s', auto_add=True)('jerry') - self.assertEqual(rtn, (3, None)) - # Test it works with reference table - db.define_table('ref_table', - Field('name'), - Field('person_id', 'reference person') - ) - ret = db.ref_table.validate_and_insert(name='test reference table') - self.assertFalse(list(ret.errors)) - ret = db.ref_table.validate_and_insert(name='test reference table', person_id=george_id) - self.assertFalse(list(ret.errors)) - rtn = IS_IN_DB(db, 'ref_table.person_id', '%(name)s')(george_id) - self.assertEqual(rtn, (george_id, None)) - # Test it works with reference table.field and keyed table - db.define_table('person_keyed', - Field('name'), - primarykey=['name']) - db.person_keyed.insert(name='george') - db.person_keyed.insert(name='costanza') - rtn = IS_IN_DB(db, 'person_keyed.name')('george') - self.assertEqual(rtn, ('george', None)) - db.define_table('ref_table_field', - Field('name'), - Field('person_name', 'reference person_keyed.name') - ) - ret = db.ref_table_field.validate_and_insert(name='test reference table.field') - self.assertFalse(list(ret.errors)) - ret = db.ref_table_field.validate_and_insert(name='test reference table.field', person_name='george') - self.assertFalse(list(ret.errors)) - vldtr = IS_IN_DB(db, 'ref_table_field.person_name', '%(name)s') - vldtr.options() - rtn = vldtr('george') - self.assertEqual(rtn, ('george', None)) - # Test it works with list:reference table - db.define_table('list_ref_table', - Field('name'), - Field('person_list', 'list:reference person')) - ret = db.list_ref_table.validate_and_insert(name='test list:reference table') - self.assertFalse(list(ret.errors)) - ret = db.list_ref_table.validate_and_insert(name='test list:reference table', person_list=[george_id,costanza_id]) - self.assertFalse(list(ret.errors)) - vldtr = IS_IN_DB(db, 'list_ref_table.person_list') - vldtr.options() - rtn = vldtr([george_id,costanza_id]) - self.assertEqual(rtn, ([george_id,costanza_id], None)) - # Test it works with list:reference table.field and keyed table - #db.define_table('list_ref_table_field', - # Field('name'), - # Field('person_list', 'list:reference person_keyed.name')) - #ret = db.list_ref_table_field.validate_and_insert(name='test list:reference table.field') - #self.assertFalse(list(ret.errors)) - #ret = db.list_ref_table_field.validate_and_insert(name='test list:reference table.field', person_list=['george','costanza']) - #self.assertFalse(list(ret.errors)) - #vldtr = IS_IN_DB(db, 'list_ref_table_field.person_list') - #vldtr.options() - #rtn = vldtr(['george','costanza']) - #self.assertEqual(rtn, (['george','costanza'], None)) - db.person.drop() - db.category.drop() - db.person_keyed.drop() - db.ref_table.drop() - db.ref_table_field.drop() - db.list_ref_table.drop() - #db.list_ref_table_field.drop() - - def test_IS_NOT_IN_DB(self): - from gluon.dal import DAL, Field - db = DAL('sqlite:memory') - db.define_table('person', Field('name'), Field('nickname')) - db.person.insert(name='george') - db.person.insert(name='costanza', nickname='T Bone') - rtn = IS_NOT_IN_DB(db, 'person.name', error_message='oops')('george') - self.assertEqual(rtn, ('george', 'oops')) - rtn = IS_NOT_IN_DB(db, 'person.name', error_message='oops', allowed_override=['george'])('george') - self.assertEqual(rtn, ('george', None)) - rtn = IS_NOT_IN_DB(db, 'person.name', error_message='oops')(' ') - self.assertEqual(rtn, (' ', 'oops')) - rtn = IS_NOT_IN_DB(db, 'person.name')('jerry') - self.assertEqual(rtn, ('jerry', None)) - rtn = IS_NOT_IN_DB(db, 'person.name')(u'jerry') - self.assertEqual(rtn, ('jerry', None)) - rtn = IS_NOT_IN_DB(db(db.person.id > 0), 'person.name')(u'jerry') - self.assertEqual(rtn, ('jerry', None)) - rtn = IS_NOT_IN_DB(db, db.person, error_message='oops')(1) - self.assertEqual(rtn, (1, 'oops')) - vldtr = IS_NOT_IN_DB(db, 'person.name', error_message='oops') - vldtr.set_self_id({'name': 'costanza', 'nickname': 'T Bone'}) - rtn = vldtr('george') - self.assertEqual(rtn, ('george', 'oops')) - rtn = vldtr('costanza') - self.assertEqual(rtn, ('costanza', None)) - - db.person.drop() - - def test_IS_INT_IN_RANGE(self): - rtn = IS_INT_IN_RANGE(1, 5)('4') - self.assertEqual(rtn, (4, None)) - rtn = IS_INT_IN_RANGE(1, 5)(4) - self.assertEqual(rtn, (4, None)) - rtn = IS_INT_IN_RANGE(1, 5)(1) - self.assertEqual(rtn, (1, None)) - rtn = IS_INT_IN_RANGE(1, 5)(5) - self.assertEqual(rtn, (5, 'Enter an integer between 1 and 4')) - rtn = IS_INT_IN_RANGE(1, 5)(5) - self.assertEqual(rtn, (5, 'Enter an integer between 1 and 4')) - rtn = IS_INT_IN_RANGE(1, 5)(3.5) - self.assertEqual(rtn, (3.5, 'Enter an integer between 1 and 4')) - rtn = IS_INT_IN_RANGE(None, 5)('4') - self.assertEqual(rtn, (4, None)) - rtn = IS_INT_IN_RANGE(None, 5)('6') - self.assertEqual(rtn, ('6', 'Enter an integer less than or equal to 4')) - rtn = IS_INT_IN_RANGE(1, None)('4') - self.assertEqual(rtn, (4, None)) - rtn = IS_INT_IN_RANGE(1, None)('0') - self.assertEqual(rtn, ('0', 'Enter an integer greater than or equal to 1')) - rtn = IS_INT_IN_RANGE()(6) - self.assertEqual(rtn, (6, None)) - rtn = IS_INT_IN_RANGE()('abc') - self.assertEqual(rtn, ('abc', 'Enter an integer')) - - def test_IS_FLOAT_IN_RANGE(self): - # with None - rtn = IS_FLOAT_IN_RANGE(1, 5)(None) - self.assertEqual(rtn, (None, 'Enter a number between 1 and 5')) - rtn = IS_FLOAT_IN_RANGE(1, 5)('4') - self.assertEqual(rtn, (4.0, None)) - rtn = IS_FLOAT_IN_RANGE(1, 5)(4) - self.assertEqual(rtn, (4.0, None)) - rtn = IS_FLOAT_IN_RANGE(1, 5)(1) - self.assertEqual(rtn, (1.0, None)) - rtn = IS_FLOAT_IN_RANGE(1, 5)(5.25) - self.assertEqual(rtn, (5.25, 'Enter a number between 1 and 5')) - rtn = IS_FLOAT_IN_RANGE(1, 5)(6.0) - self.assertEqual(rtn, (6.0, 'Enter a number between 1 and 5')) - rtn = IS_FLOAT_IN_RANGE(1, 5)(3.5) - self.assertEqual(rtn, (3.5, None)) - rtn = IS_FLOAT_IN_RANGE(1, None)(3.5) - self.assertEqual(rtn, (3.5, None)) - rtn = IS_FLOAT_IN_RANGE(None, 5)(3.5) - self.assertEqual(rtn, (3.5, None)) - rtn = IS_FLOAT_IN_RANGE(1, None)(0.5) - self.assertEqual(rtn, (0.5, 'Enter a number greater than or equal to 1')) - rtn = IS_FLOAT_IN_RANGE(None, 5)(6.5) - self.assertEqual(rtn, (6.5, 'Enter a number less than or equal to 5')) - rtn = IS_FLOAT_IN_RANGE()(6.5) - self.assertEqual(rtn, (6.5, None)) - rtn = IS_FLOAT_IN_RANGE()('abc') - self.assertEqual(rtn, ('abc', 'Enter a number')) - rtn = IS_FLOAT_IN_RANGE()('6,5') - self.assertEqual(rtn, ('6,5', 'Enter a number')) - rtn = IS_FLOAT_IN_RANGE(dot=',')('6.5') - self.assertEqual(rtn, (6.5, None)) - # With .formatter(None) - rtn = IS_FLOAT_IN_RANGE(dot=',').formatter(None) - self.assertEqual(rtn, None) - rtn = IS_FLOAT_IN_RANGE(dot=',').formatter(0.25) - self.assertEqual(rtn, '0,25') - # To trigger str2dec "if not '.' in s:" line - rtn = IS_FLOAT_IN_RANGE(dot=',').formatter(1) - self.assertEqual(rtn, '1,00') - - def test_IS_DECIMAL_IN_RANGE(self): - # with None - rtn = IS_DECIMAL_IN_RANGE(1, 5)(None) - self.assertEqual(rtn, (None, 'Enter a number between 1 and 5')) - rtn = IS_DECIMAL_IN_RANGE(1, 5)('4') - self.assertEqual(rtn, (decimal.Decimal('4'), None)) - rtn = IS_DECIMAL_IN_RANGE(1, 5)(4) - self.assertEqual(rtn, (decimal.Decimal('4'), None)) - rtn = IS_DECIMAL_IN_RANGE(1, 5)(1) - self.assertEqual(rtn, (decimal.Decimal('1'), None)) - rtn = IS_DECIMAL_IN_RANGE(1, 5)(5.25) - self.assertEqual(rtn, (5.25, 'Enter a number between 1 and 5')) - rtn = IS_DECIMAL_IN_RANGE(5.25, 6)(5.25) - self.assertEqual(rtn, (decimal.Decimal('5.25'), None)) - rtn = IS_DECIMAL_IN_RANGE(5.25, 6)('5.25') - self.assertEqual(rtn, (decimal.Decimal('5.25'), None)) - rtn = IS_DECIMAL_IN_RANGE(1, 5)(6.0) - self.assertEqual(rtn, (6.0, 'Enter a number between 1 and 5')) - rtn = IS_DECIMAL_IN_RANGE(1, 5)(3.5) - self.assertEqual(rtn, (decimal.Decimal('3.5'), None)) - rtn = IS_DECIMAL_IN_RANGE(1.5, 5.5)(3.5) - self.assertEqual(rtn, (decimal.Decimal('3.5'), None)) - rtn = IS_DECIMAL_IN_RANGE(1.5, 5.5)(6.5) - self.assertEqual(rtn, (6.5, 'Enter a number between 1.5 and 5.5')) - rtn = IS_DECIMAL_IN_RANGE(1.5, None)(6.5) - self.assertEqual(rtn, (decimal.Decimal('6.5'), None)) - rtn = IS_DECIMAL_IN_RANGE(1.5, None)(0.5) - self.assertEqual(rtn, (0.5, 'Enter a number greater than or equal to 1.5')) - rtn = IS_DECIMAL_IN_RANGE(None, 5.5)(4.5) - self.assertEqual(rtn, (decimal.Decimal('4.5'), None)) - rtn = IS_DECIMAL_IN_RANGE(None, 5.5)(6.5) - self.assertEqual(rtn, (6.5, 'Enter a number less than or equal to 5.5')) - rtn = IS_DECIMAL_IN_RANGE()(6.5) - self.assertEqual(rtn, (decimal.Decimal('6.5'), None)) - rtn = IS_DECIMAL_IN_RANGE(0, 99)(123.123) - self.assertEqual(rtn, (123.123, 'Enter a number between 0 and 99')) - rtn = IS_DECIMAL_IN_RANGE(0, 99)('123.123') - self.assertEqual(rtn, ('123.123', 'Enter a number between 0 and 99')) - rtn = IS_DECIMAL_IN_RANGE(0, 99)('12.34') - self.assertEqual(rtn, (decimal.Decimal('12.34'), None)) - rtn = IS_DECIMAL_IN_RANGE()('abc') - self.assertEqual(rtn, ('abc', 'Enter a number')) - rtn = IS_DECIMAL_IN_RANGE()('6,5') - self.assertEqual(rtn, ('6,5', 'Enter a number')) - rtn = IS_DECIMAL_IN_RANGE(dot=',')('6.5') - self.assertEqual(rtn, (decimal.Decimal('6.5'), None)) - rtn = IS_DECIMAL_IN_RANGE(1, 5)(decimal.Decimal('4')) - self.assertEqual(rtn, (decimal.Decimal('4'), None)) - # With .formatter(None) - rtn = IS_DECIMAL_IN_RANGE(dot=',').formatter(None) - self.assertEqual(rtn, None) - rtn = IS_DECIMAL_IN_RANGE(dot=',').formatter(0.25) - self.assertEqual(rtn, '0,25') - - def test_IS_NOT_EMPTY(self): - rtn = IS_NOT_EMPTY()(1) - self.assertEqual(rtn, (1, None)) - rtn = IS_NOT_EMPTY()(0) - self.assertEqual(rtn, (0, None)) - rtn = IS_NOT_EMPTY()('x') - self.assertEqual(rtn, ('x', None)) - rtn = IS_NOT_EMPTY()(' x ') - self.assertEqual(rtn, (' x ', None)) - rtn = IS_NOT_EMPTY()(None) - self.assertEqual(rtn, (None, 'Enter a value')) - rtn = IS_NOT_EMPTY()('') - self.assertEqual(rtn, ('', 'Enter a value')) - rtn = IS_NOT_EMPTY()(b'') - self.assertEqual(rtn, (b'', 'Enter a value')) - rtn = IS_NOT_EMPTY()(' ') - self.assertEqual(rtn, (' ', 'Enter a value')) - rtn = IS_NOT_EMPTY()(' \n\t') - self.assertEqual(rtn, (' \n\t', 'Enter a value')) - rtn = IS_NOT_EMPTY()([]) - self.assertEqual(rtn, ([], 'Enter a value')) - rtn = IS_NOT_EMPTY(empty_regex='def')('def') - self.assertEqual(rtn, ('def', 'Enter a value')) - rtn = IS_NOT_EMPTY(empty_regex='de[fg]')('deg') - self.assertEqual(rtn, ('deg', 'Enter a value')) - rtn = IS_NOT_EMPTY(empty_regex='def')('abc') - self.assertEqual(rtn, ('abc', None)) - - def test_IS_ALPHANUMERIC(self): - rtn = IS_ALPHANUMERIC()('1') - self.assertEqual(rtn, ('1', None)) - rtn = IS_ALPHANUMERIC()('') - self.assertEqual(rtn, ('', None)) - rtn = IS_ALPHANUMERIC()('A_a') - self.assertEqual(rtn, ('A_a', None)) - rtn = IS_ALPHANUMERIC()('!') - self.assertEqual(rtn, ('!', 'Enter only letters, numbers, and underscore')) - - def test_IS_EMAIL(self): - rtn = IS_EMAIL()('a@b.com') - self.assertEqual(rtn, ('a@b.com', None)) - rtn = IS_EMAIL()('abc@def.com') - self.assertEqual(rtn, ('abc@def.com', None)) - rtn = IS_EMAIL()('abc@3def.com') - self.assertEqual(rtn, ('abc@3def.com', None)) - rtn = IS_EMAIL()('abc@def.us') - self.assertEqual(rtn, ('abc@def.us', None)) - rtn = IS_EMAIL()('abc@d_-f.us') - self.assertEqual(rtn, ('abc@d_-f.us', None)) - rtn = IS_EMAIL()('@def.com') # missing name - self.assertEqual(rtn, ('@def.com', 'Enter a valid email address')) - rtn = IS_EMAIL()('"abc@def".com') # quoted name - self.assertEqual(rtn, ('"abc@def".com', 'Enter a valid email address')) - rtn = IS_EMAIL()('abc+def.com') # no @ - self.assertEqual(rtn, ('abc+def.com', 'Enter a valid email address')) - rtn = IS_EMAIL()('abc@def.x') # one-char TLD - self.assertEqual(rtn, ('abc@def.x', 'Enter a valid email address')) - rtn = IS_EMAIL()('abc@def.12') # numeric TLD - self.assertEqual(rtn, ('abc@def.12', 'Enter a valid email address')) - rtn = IS_EMAIL()('abc@def..com') # double-dot in domain - self.assertEqual(rtn, ('abc@def..com', 'Enter a valid email address')) - rtn = IS_EMAIL()('abc@.def.com') # dot starts domain - self.assertEqual(rtn, ('abc@.def.com', 'Enter a valid email address')) - rtn = IS_EMAIL()('abc@def.c_m') # underscore in TLD - self.assertEqual(rtn, ('abc@def.c_m', 'Enter a valid email address')) - rtn = IS_EMAIL()('NotAnEmail') # missing @ - self.assertEqual(rtn, ('NotAnEmail', 'Enter a valid email address')) - rtn = IS_EMAIL()('abc@NotAnEmail') # missing TLD - self.assertEqual(rtn, ('abc@NotAnEmail', 'Enter a valid email address')) - rtn = IS_EMAIL()('customer/department@example.com') - self.assertEqual(rtn, ('customer/department@example.com', None)) - rtn = IS_EMAIL()('$A12345@example.com') - self.assertEqual(rtn, ('$A12345@example.com', None)) - rtn = IS_EMAIL()('!def!xyz%abc@example.com') - self.assertEqual(rtn, ('!def!xyz%abc@example.com', None)) - rtn = IS_EMAIL()('_Yosemite.Sam@example.com') - self.assertEqual(rtn, ('_Yosemite.Sam@example.com', None)) - rtn = IS_EMAIL()('~@example.com') - self.assertEqual(rtn, ('~@example.com', None)) - rtn = IS_EMAIL()('.wooly@example.com') # dot starts name - self.assertEqual(rtn, ('.wooly@example.com', 'Enter a valid email address')) - rtn = IS_EMAIL()('wo..oly@example.com') # adjacent dots in name - self.assertEqual(rtn, ('wo..oly@example.com', 'Enter a valid email address')) - rtn = IS_EMAIL()('pootietang.@example.com') # dot ends name - self.assertEqual(rtn, ('pootietang.@example.com', 'Enter a valid email address')) - rtn = IS_EMAIL()('.@example.com') # name is bare dot - self.assertEqual(rtn, ('.@example.com', 'Enter a valid email address')) - rtn = IS_EMAIL()('Ima.Fool@example.com') - self.assertEqual(rtn, ('Ima.Fool@example.com', None)) - rtn = IS_EMAIL()('Ima Fool@example.com') # space in name - self.assertEqual(rtn, ('Ima Fool@example.com', 'Enter a valid email address')) - rtn = IS_EMAIL()('localguy@localhost') # localhost as domain - self.assertEqual(rtn, ('localguy@localhost', None)) - # test for banned - rtn = IS_EMAIL(banned='^.*\.com(|\..*)$')('localguy@localhost') # localhost as domain - self.assertEqual(rtn, ('localguy@localhost', None)) - rtn = IS_EMAIL(banned='^.*\.com(|\..*)$')('abc@example.com') - self.assertEqual(rtn, ('abc@example.com', 'Enter a valid email address')) - # test for forced - rtn = IS_EMAIL(forced='^.*\.edu(|\..*)$')('localguy@localhost') - self.assertEqual(rtn, ('localguy@localhost', 'Enter a valid email address')) - rtn = IS_EMAIL(forced='^.*\.edu(|\..*)$')('localguy@example.edu') - self.assertEqual(rtn, ('localguy@example.edu', None)) - # test for not a string at all - rtn = IS_EMAIL(error_message='oops')(42) - self.assertEqual(rtn, (42, 'oops')) - - # test for Internationalized Domain Names, see https://docs.python.org/2/library/codecs.html#module-encodings.idna - rtn = IS_EMAIL()('web2py@Alliancefrançaise.nu') - self.assertEqual(rtn, ('web2py@Alliancefrançaise.nu', None)) - - - def test_IS_LIST_OF_EMAILS(self): - emails = ['localguy@localhost', '_Yosemite.Sam@example.com'] - rtn = IS_LIST_OF_EMAILS()(','.join(emails)) - self.assertEqual(rtn, (','.join(emails), None)) - rtn = IS_LIST_OF_EMAILS()(';'.join(emails)) - self.assertEqual(rtn, (';'.join(emails), None)) - rtn = IS_LIST_OF_EMAILS()(' '.join(emails)) - self.assertEqual(rtn, (' '.join(emails), None)) - emails.append('a') - rtn = IS_LIST_OF_EMAILS()(';'.join(emails)) - self.assertEqual(rtn, ('localguy@localhost;_Yosemite.Sam@example.com;a', 'Invalid emails: a')) - rtn = IS_LIST_OF_EMAILS().formatter(['test@example.com', 'dude@example.com']) - self.assertEqual(rtn, 'test@example.com, dude@example.com') - - def test_IS_URL(self): - rtn = IS_URL()('http://example.com') - self.assertEqual(rtn, ('http://example.com', None)) - rtn = IS_URL(error_message='oops')('http://example,com') - self.assertEqual(rtn, ('http://example,com', 'oops')) - rtn = IS_URL(error_message='oops')('http://www.example.com:8800/a/b/c/d/e/f/g/h') - self.assertEqual(rtn, ('http://www.example.com:8800/a/b/c/d/e/f/g/h', None)) - rtn = IS_URL(error_message='oops', prepend_scheme='http')('example.com') - self.assertEqual(rtn, ('http://example.com', None)) - rtn = IS_URL()('http://example.com?q=george&p=22') - self.assertEqual(rtn, ('http://example.com?q=george&p=22', None)) - rtn = IS_URL(mode='generic', prepend_scheme=None)('example.com') - self.assertEqual(rtn, ('example.com', None)) - - def test_IS_TIME(self): - rtn = IS_TIME()('21:30') - self.assertEqual(rtn, (datetime.time(21, 30), None)) - rtn = IS_TIME()('21-30') - self.assertEqual(rtn, (datetime.time(21, 30), None)) - rtn = IS_TIME()('21.30') - self.assertEqual(rtn, (datetime.time(21, 30), None)) - rtn = IS_TIME()('21:30:59') - self.assertEqual(rtn, (datetime.time(21, 30, 59), None)) - rtn = IS_TIME()('5:30') - self.assertEqual(rtn, (datetime.time(5, 30), None)) - rtn = IS_TIME()('5:30 am') - self.assertEqual(rtn, (datetime.time(5, 30), None)) - rtn = IS_TIME()('5:30 pm') - self.assertEqual(rtn, (datetime.time(17, 30), None)) - rtn = IS_TIME()('5:30 whatever') - self.assertEqual(rtn, ('5:30 whatever', 'Enter time as hh:mm:ss (seconds, am, pm optional)')) - rtn = IS_TIME()('5:30 20') - self.assertEqual(rtn, ('5:30 20', 'Enter time as hh:mm:ss (seconds, am, pm optional)')) - rtn = IS_TIME()('24:30') - self.assertEqual(rtn, ('24:30', 'Enter time as hh:mm:ss (seconds, am, pm optional)')) - rtn = IS_TIME()('21:60') - self.assertEqual(rtn, ('21:60', 'Enter time as hh:mm:ss (seconds, am, pm optional)')) - rtn = IS_TIME()('21:30::') - self.assertEqual(rtn, ('21:30::', 'Enter time as hh:mm:ss (seconds, am, pm optional)')) - rtn = IS_TIME()('') - self.assertEqual(rtn, ('', 'Enter time as hh:mm:ss (seconds, am, pm optional)')) - - def test_IS_DATE(self): - v = IS_DATE(format="%m/%d/%Y", error_message="oops") - rtn = v('03/03/2008') - self.assertEqual(rtn, (datetime.date(2008, 3, 3), None)) - rtn = v('31/03/2008') - self.assertEqual(rtn, ('31/03/2008', 'oops')) - rtn = IS_DATE(format="%m/%d/%Y", error_message="oops").formatter(datetime.date(1834, 12, 14)) - self.assertEqual(rtn, '12/14/1834') - - def test_IS_DATETIME(self): - v = IS_DATETIME(format="%m/%d/%Y %H:%M", error_message="oops") - rtn = v('03/03/2008 12:40') - self.assertEqual(rtn, (datetime.datetime(2008, 3, 3, 12, 40), None)) - rtn = v('31/03/2008 29:40') - self.assertEqual(rtn, ('31/03/2008 29:40', 'oops')) - # Test timezone is removed and value is properly converted - # - # https://github.com/web2py/web2py/issues/1094 - - class DummyTimezone(datetime.tzinfo): - - ONE = datetime.timedelta(hours=1) - - def utcoffset(self, dt): - return DummyTimezone.ONE - - def tzname(self, dt): - return "UTC+1" - - def dst(self, dt): - return DummyTimezone.ONE - - def localize(self, dt, is_dst=False): - return dt.replace(tzinfo=self) - v = IS_DATETIME(format="%Y-%m-%d %H:%M", error_message="oops", timezone=DummyTimezone()) - rtn = v('1982-12-14 08:00') - self.assertEqual(rtn, (datetime.datetime(1982, 12, 14, 7, 0), None)) - - def test_IS_DATE_IN_RANGE(self): - v = IS_DATE_IN_RANGE(minimum=datetime.date(2008, 1, 1), - maximum=datetime.date(2009, 12, 31), - format="%m/%d/%Y", error_message="oops") - - rtn = v('03/03/2008') - self.assertEqual(rtn, (datetime.date(2008, 3, 3), None)) - rtn = v('03/03/2010') - self.assertEqual(rtn, ('03/03/2010', 'oops')) - rtn = v(datetime.date(2008, 3, 3)) - self.assertEqual(rtn, (datetime.date(2008, 3, 3), None)) - rtn = v(datetime.date(2010, 3, 3)) - self.assertEqual(rtn, (datetime.date(2010, 3, 3), 'oops')) - v = IS_DATE_IN_RANGE(maximum=datetime.date(2009, 12, 31), - format="%m/%d/%Y") - rtn = v('03/03/2010') - self.assertEqual(rtn, ('03/03/2010', 'Enter date on or before 12/31/2009')) - v = IS_DATE_IN_RANGE(minimum=datetime.date(2008, 1, 1), - format="%m/%d/%Y") - rtn = v('03/03/2007') - self.assertEqual(rtn, ('03/03/2007', 'Enter date on or after 01/01/2008')) - v = IS_DATE_IN_RANGE(minimum=datetime.date(2008, 1, 1), - maximum=datetime.date(2009, 12, 31), - format="%m/%d/%Y") - rtn = v('03/03/2007') - self.assertEqual(rtn, ('03/03/2007', 'Enter date in range 01/01/2008 12/31/2009')) - - def test_IS_DATETIME_IN_RANGE(self): - 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") - rtn = v('03/03/2008 12:40') - self.assertEqual(rtn, (datetime.datetime(2008, 3, 3, 12, 40), None)) - rtn = v('03/03/2010 10:34') - self.assertEqual(rtn, ('03/03/2010 10:34', 'oops')) - rtn = v(datetime.datetime(2008, 3, 3, 0, 0)) - self.assertEqual(rtn, (datetime.datetime(2008, 3, 3, 0, 0), None)) - rtn = v(datetime.datetime(2010, 3, 3, 0, 0)) - self.assertEqual(rtn, (datetime.datetime(2010, 3, 3, 0, 0), 'oops')) - v = IS_DATETIME_IN_RANGE(maximum=datetime.datetime(2009, 12, 31, 12, 20), - format='%m/%d/%Y %H:%M:%S') - rtn = v('03/03/2010 12:20:00') - self.assertEqual(rtn, ('03/03/2010 12:20:00', 'Enter date and time on or before 12/31/2009 12:20:00')) - v = IS_DATETIME_IN_RANGE(minimum=datetime.datetime(2008, 1, 1, 12, 20), - format='%m/%d/%Y %H:%M:%S') - rtn = v('03/03/2007 12:20:00') - self.assertEqual(rtn, ('03/03/2007 12:20:00', 'Enter date and time on or after 01/01/2008 12:20:00')) - 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:%S') - rtn = v('03/03/2007 12:20:00') - self.assertEqual(rtn, ('03/03/2007 12:20:00', 'Enter date and time in range 01/01/2008 12:20:00 12/31/2009 12:20:00')) - v = IS_DATETIME_IN_RANGE(maximum=datetime.datetime(2009, 12, 31, 12, 20), - format='%Y-%m-%d %H:%M:%S', error_message='oops') - rtn = v('clearly not a date') - self.assertEqual(rtn, ('clearly not a date', 'oops')) - - def test_IS_LIST_OF(self): - values = [0, 1, 2, 3, 4] - rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))(values) - self.assertEqual(rtn, (values, None)) - values.append(11) - rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))(values) - self.assertEqual(rtn, (values, 'Enter an integer between 0 and 9')) - rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))(1) - self.assertEqual(rtn, ([1], None)) - rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10), minimum=10)([1, 2]) - self.assertEqual(rtn, ([1, 2], 'Minimum length is 10')) - rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10), maximum=2)([1, 2, 3]) - self.assertEqual(rtn, ([1, 2, 3], 'Maximum length is 2')) - # regression test for issue 742 - rtn = IS_LIST_OF(minimum=1)('') - self.assertEqual(rtn, ('', 'Minimum length is 1')) - - def test_IS_LOWER(self): - rtn = IS_LOWER()('ABC') - self.assertEqual(rtn, ('abc', None)) - rtn = IS_LOWER()(b'ABC') - self.assertEqual(rtn, (b'abc', None)) - rtn = IS_LOWER()('Ñ') - self.assertEqual(rtn, ('ñ', None)) - - def test_IS_UPPER(self): - rtn = IS_UPPER()('abc') - self.assertEqual(rtn, ('ABC', None)) - rtn = IS_UPPER()(b'abc') - self.assertEqual(rtn, (b'ABC', None)) - rtn = IS_UPPER()('ñ') - self.assertEqual(rtn, ('Ñ', None)) - - def test_IS_SLUG(self): - rtn = IS_SLUG()('abc123') - self.assertEqual(rtn, ('abc123', None)) - rtn = IS_SLUG()('ABC123') - self.assertEqual(rtn, ('abc123', None)) - rtn = IS_SLUG()('abc-123') - self.assertEqual(rtn, ('abc-123', None)) - rtn = IS_SLUG()('abc--123') - self.assertEqual(rtn, ('abc-123', None)) - rtn = IS_SLUG()('abc 123') - self.assertEqual(rtn, ('abc-123', None)) - rtn = IS_SLUG()('abc\t_123') - self.assertEqual(rtn, ('abc-123', None)) - rtn = IS_SLUG()('-abc-') - self.assertEqual(rtn, ('abc', None)) - rtn = IS_SLUG()('--a--b--_ -c--') - self.assertEqual(rtn, ('a-b-c', None)) - rtn = IS_SLUG()('abc&123') - self.assertEqual(rtn, ('abc123', None)) - rtn = IS_SLUG()('abc&123&def') - self.assertEqual(rtn, ('abc123def', None)) - rtn = IS_SLUG()('ñ') - self.assertEqual(rtn, ('n', None)) - rtn = IS_SLUG(maxlen=4)('abc123') - self.assertEqual(rtn, ('abc1', None)) - rtn = IS_SLUG()('abc_123') - self.assertEqual(rtn, ('abc-123', None)) - rtn = IS_SLUG(keep_underscores=False)('abc_123') - self.assertEqual(rtn, ('abc-123', None)) - rtn = IS_SLUG(keep_underscores=True)('abc_123') - self.assertEqual(rtn, ('abc_123', None)) - rtn = IS_SLUG(check=False)('abc') - self.assertEqual(rtn, ('abc', None)) - rtn = IS_SLUG(check=True)('abc') - self.assertEqual(rtn, ('abc', None)) - rtn = IS_SLUG(check=False)('a bc') - self.assertEqual(rtn, ('a-bc', None)) - rtn = IS_SLUG(check=True)('a bc') - self.assertEqual(rtn, ('a bc', 'Must be slug')) - - def test_ANY_OF(self): - rtn = ANY_OF([IS_EMAIL(), IS_ALPHANUMERIC()])('a@b.co') - self.assertEqual(rtn, ('a@b.co', None)) - rtn = ANY_OF([IS_EMAIL(), IS_ALPHANUMERIC()])('abco') - self.assertEqual(rtn, ('abco', None)) - rtn = ANY_OF([IS_EMAIL(), IS_ALPHANUMERIC()])('@ab.co') - self.assertEqual(rtn, ('@ab.co', 'Enter only letters, numbers, and underscore')) - rtn = ANY_OF([IS_ALPHANUMERIC(), IS_EMAIL()])('@ab.co') - self.assertEqual(rtn, ('@ab.co', 'Enter a valid email address')) - rtn = ANY_OF([IS_DATE(), IS_EMAIL()])('a@b.co') - self.assertEqual(rtn, ('a@b.co', None)) - rtn = ANY_OF([IS_DATE(), IS_EMAIL()])('1982-12-14') - self.assertEqual(rtn, (datetime.date(1982, 12, 14), None)) - rtn = ANY_OF([IS_DATE(format='%m/%d/%Y'), IS_EMAIL()]).formatter(datetime.date(1834, 12, 14)) - self.assertEqual(rtn, '12/14/1834') - - def test_IS_EMPTY_OR(self): - rtn = IS_EMPTY_OR(IS_EMAIL())('abc@def.com') - self.assertEqual(rtn, ('abc@def.com', None)) - rtn = IS_EMPTY_OR(IS_EMAIL())(' ') - self.assertEqual(rtn, (None, None)) - rtn = IS_EMPTY_OR(IS_EMAIL(), null='abc')(' ') - self.assertEqual(rtn, ('abc', None)) - rtn = IS_EMPTY_OR(IS_EMAIL(), null='abc', empty_regex='def')('def') - self.assertEqual(rtn, ('abc', None)) - rtn = IS_EMPTY_OR(IS_EMAIL())('abc') - self.assertEqual(rtn, ('abc', 'Enter a valid email address')) - rtn = IS_EMPTY_OR(IS_EMAIL())(' abc ') - self.assertEqual(rtn, (' abc ', 'Enter a valid email address')) - rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options(zero=False) - self.assertEqual(rtn, [('', ''), ('id1', 'first label'), ('id2', 'second label')]) - rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options() - self.assertEqual(rtn, [('', 'zero'), ('id1', 'first label'), ('id2', 'second label')]) - rtn = IS_EMPTY_OR((IS_LOWER(), IS_EMAIL()))('AAA') - self.assertEqual(rtn, ('AAA', 'Enter a valid email address')) - rtn = IS_EMPTY_OR([IS_LOWER(), IS_EMAIL()])('AAA') - self.assertEqual(rtn, ('AAA', 'Enter a valid email address')) - - def test_CLEANUP(self): - rtn = CLEANUP()('helloò') - self.assertEqual(rtn, ('hello', None)) - - def test_CRYPT(self): - rtn = str(CRYPT(digest_alg='md5', salt=True)('test')[0]) - self.myassertRegex(rtn, r'^md5\$.{16}\$.{32}$') - rtn = str(CRYPT(digest_alg='sha1', salt=True)('test')[0]) - self.myassertRegex(rtn, r'^sha1\$.{16}\$.{40}$') - rtn = str(CRYPT(digest_alg='sha256', salt=True)('test')[0]) - self.myassertRegex(rtn, r'^sha256\$.{16}\$.{64}$') - rtn = str(CRYPT(digest_alg='sha384', salt=True)('test')[0]) - self.myassertRegex(rtn, r'^sha384\$.{16}\$.{96}$') - rtn = str(CRYPT(digest_alg='sha512', salt=True)('test')[0]) - self.myassertRegex(rtn, r'^sha512\$.{16}\$.{128}$') - alg = 'pbkdf2(1000,20,sha512)' - rtn = str(CRYPT(digest_alg=alg, salt=True)('test')[0]) - self.myassertRegex(rtn, r'^pbkdf2\(1000,20,sha512\)\$.{16}\$.{40}$') - rtn = str(CRYPT(digest_alg='md5', key='mykey', salt=True)('test')[0]) - self.myassertRegex(rtn, r'^md5\$.{16}\$.{32}$') - a = str(CRYPT(digest_alg='sha1', salt=False)('test')[0]) - self.assertEqual(CRYPT(digest_alg='sha1', salt=False)('test')[0], a) - self.assertEqual(CRYPT(digest_alg='sha1', salt=False)('test')[0], a[6:]) - self.assertEqual(CRYPT(digest_alg='md5', salt=False)('test')[0], a) - self.assertEqual(CRYPT(digest_alg='md5', salt=False)('test')[0], a[6:]) - - def test_IS_STRONG(self): - rtn = IS_STRONG(es=True)('Abcd1234') - self.assertEqual(rtn, ('Abcd1234', - 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|')) - rtn = IS_STRONG(es=True)('Abcd1234!') - self.assertEqual(rtn, ('Abcd1234!', None)) - rtn = IS_STRONG(es=True, entropy=1)('a') - self.assertEqual(rtn, ('a', None)) - rtn = IS_STRONG(es=True, entropy=1, min=2)('a') - self.assertEqual(rtn, ('a', 'Minimum length is 2')) - rtn = IS_STRONG(es=True, entropy=100)('abc123') - self.assertEqual(rtn, ('abc123', 'Entropy (32.35) less than required (100)')) - rtn = IS_STRONG(es=True, entropy=100)('and') - self.assertEqual(rtn, ('and', 'Entropy (14.57) less than required (100)')) - rtn = IS_STRONG(es=True, entropy=100)('aaa') - self.assertEqual(rtn, ('aaa', 'Entropy (14.42) less than required (100)')) - rtn = IS_STRONG(es=True, entropy=100)('a1d') - self.assertEqual(rtn, ('a1d', 'Entropy (15.97) less than required (100)')) - rtn = IS_STRONG(es=True, entropy=100)('añd') - if PY2: - self.assertEqual(rtn, ('a\xc3\xb1d', 'Entropy (18.13) less than required (100)')) - else: - self.assertEqual(rtn, ('añd', 'Entropy (18.13) less than required (100)')) - rtn = IS_STRONG()('********') - self.assertEqual(rtn, ('********', None)) - rtn = IS_STRONG(es=True, max=4)('abcde') - self.assertEqual(rtn, - ('abcde', - '|'.join(['Minimum length is 8', - 'Maximum length is 4', - 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|', - 'Must include at least 1 uppercase', - 'Must include at least 1 number'])) - ) - rtn = IS_STRONG(es=True)('abcde') - self.assertEqual(rtn, - ('abcde', - '|'.join(['Minimum length is 8', - 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|', - 'Must include at least 1 uppercase', - 'Must include at least 1 number'])) - ) - rtn = IS_STRONG(upper=0, lower=0, number=0, es=True)('Abcde1') - self.assertEqual(rtn, - ('Abcde1', - '|'.join(['Minimum length is 8', - 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|', - 'May not include any uppercase letters', - 'May not include any lowercase letters', - 'May not include any numbers'])) - ) - rtn = IS_STRONG(special=0, es=True)('Abcde1!') - self.assertEqual(rtn, - ('Abcde1!', - '|'.join(['Minimum length is 8', - 'May not contain any of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|'])) - ) - rtn = IS_STRONG(upper=False, number=False, special=False, es=True)('Abcde1!') - self.assertEqual(rtn, ('Abcde1!', 'Minimum length is 8')) - - def test_IS_IMAGE(self): - class DummyImageFile(object): - - def __init__(self, filename, ext, width, height): - from io import BytesIO - import struct - self.filename = filename + '.' + ext - self.file = BytesIO() - if ext == 'bmp': - self.file.write(b'BM') - self.file.write(b' ' * 16) - self.file.write(struct.pack(' -| 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 math -import datetime -import time -import cgi -import uuid -import hashlib -import hmac -import json -import struct -import decimal -import binascii -import ipaddress -import unicodedata -import encodings.idna - -from pydal._compat import StringIO, integer_types, basestring, unicodeT, urllib_unquote, \ - unichr, to_bytes, PY2, to_unicode, to_native, string_types, urlparse -from pydal.objects import Field, FieldVirtual, FieldMethod, Table - - -regex_isint = re.compile(r'^[+-]?\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_FILE', - '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', -] - -def options_sorter(x, y): - return (str(x[1]).upper() > str(y[1]).upper() and 1) or -1 - -def translate(text): - return Validator.translator(text) - - -class ValidationError(Exception): - pass - - -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. - """ - - translator = staticmethod(lambda text: text) - - def formatter(self, value): - """ - For some validators returns a formatted version (matching the validator) - of value. Otherwise just returns the value. - """ - return value - - def validate(self, value): - raise NotImplementedError - - def __call__(self, value): - try: - return self.validate(value), None - except ValidationError as e: - return value, e.message - - -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 validate(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 - raise ValidationError(self.translator(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 validate(self, value): - if value != self.expression: - raise ValidationError(self.translator(self.error_message)) - return value - - -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 validate(self, value): - if callable(self.expression): - message = self.expression(value) - if message: - raise ValidationError(message) - return value - # for backward compatibility - self.environment.update(value=value) - exec('__ret__=' + self.expression, self.environment) - if self.environment['__ret__']: - return value - raise ValidationError(self.translator(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 validate(self, value): - if value is None: - length = 0 - elif isinstance(value, str): - try: - length = len(to_unicode(value)) - except: - length = len(value) - elif isinstance(value, unicodeT): - length = len(value) - value = value.encode('utf8') - elif isinstance(value, (bytes, bytearray, tuple, list)): - length = len(value) - 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 - else: - value = str(value) - length = len(str(value)) - if self.minsize <= length <= self.maxsize: - return value - raise ValidationError(self.translator(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 validate(self, value): - try: - if self.native_json: - json.loads(value) # raises error in case of malformed json - return value # the serialized value is not passed - else: - return json.loads(value) - except JSONErrors: - raise ValidationError(self.translator(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 = list(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 validate(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: - raise ValidationError(self.translator(self.error_message)) - if self.multiple: - if isinstance(self.multiple, (tuple, list)) and \ - not self.multiple[0] <= len(values) < self.multiple[1]: - raise ValidationError(self.translator(self.error_message)) - return values - return value - - -regex1 = re.compile(r'\w+\.\w+') -regex2 = re.compile(r'%\(([^\)]+)\)\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): - - 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 = [f for f in fields if not isinstance(f, ignore)] - 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 validate(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: - raise ValidationError(self.translator(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]: - raise ValidationError(self.translator(self.error_message)) - if self.theset: - if not [v for v in values if v not in self.theset]: - return values - else: - def count(values, s=self.dbset, f=field): - return s(f.belongs(list(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 - elif count(values) == len(values): - return values - 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: - raise ValidationError(self.translator(self.error_message)) - - try: - value = int(value) - except TypeError: - raise ValidationError(self.translator(self.error_message)) - - if self.theset: - if str(value) in self.theset: - if self._and: - return self._and.validate(value) - return value - else: - if self.dbset(field == value).count(): - if self._and: - return self._and.validate(value) - return value - raise ValidationError(self.translator(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): - - 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 validate(self, value): - value = to_native(str(value)) - if not value.strip(): - raise ValidationError(self.translator(self.error_message)) - if value in self.allowed_override: - return value - (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): - raise ValidationError(self.translator(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): - raise ValidationError(self.translator(self.error_message)) - return value - - -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 validate(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 - raise ValidationError(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 validate(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 - except (ValueError, TypeError): - pass - raise ValidationError(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 validate(self, value): - try: - if not isinstance(value, decimal.Decimal): - value = decimal.Decimal(str(value).replace(self.dot, '.')) - if ((self.minimum is None or value >= self.minimum) and - (self.maximum is None or value <= self.maximum)): - return value - except (ValueError, TypeError, decimal.InvalidOperation): - pass - raise ValidationError(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 validate(self, value): - value, empty = is_empty(value, empty_regex=self.empty_regex) - if empty: - raise ValidationError(self.translator(self.error_message)) - return value - - -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, r'^[\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(r''' - ^(?!\.) # name may not begin with a dot - ( - [-a-z0-9!\#$%&'*+/=?^_`{|}~] # all legal characters except dot - | - (? 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 = [] - 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 validate(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): - raise ValidationError(self.translator(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 not self.GENERIC_URL_VALID.match(url) or (fragment_part and not self.URL_FRAGMENT_VALID.match(fragment_part)): - raise ValidationError(self.translator(self.error_message)) - # Then parse the URL into its components and check on - try: - components = urlparse.urlparse(urllib_unquote(value))._asdict() - except ValueError: - raise ValidationError(self.translator(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' - new_value = self.validate(schemeToUse + '://' + value) - return new_value if self.prepend_scheme else value - raise ValidationError(self.translator(self.error_message)) - return value - -# 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( - r"([\w.!~*'|;:&=+$,-]+@)?\d+\.\d+\.\d+\.\d+(:\d*)*$") - GENERIC_VALID_DOMAIN = re.compile(r"([\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 validate(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 - 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 - 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 not path.startswith('/'): - # 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' - new_value = self.validate(schemeToUse + '://' + value) - return new_value if self.prepend_scheme else value - return value - except: - pass - raise ValidationError(self.translator(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 validate(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 isinstance(value, unicodeT): - try: - value = 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 - raise ValidationError(self.translator(self.error_message)) - return subMethod.validate(value) - - -regex_time = re.compile( - '((?P[0-9]+))([^0-9 ]+(?P[0-9 ]+))?([^0-9ap ]+(?P[0-9]*))?((?P[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 validate(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 - except Exception: - raise ValidationError(self.translator(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 = self.translator(format) - self.error_message = str(error_message) - self.extremes = {} - - def validate(self, value): - if isinstance(value, datetime.date): - return value - try: - (y, m, d, hh, mm, ss, t0, t1, t2) = \ - time.strptime(value, str(self.format)) - value = datetime.date(y, m, d) - return value - except: - self.extremes.update(IS_DATETIME.nice(self.format)) - raise ValidationError(self.translator(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 = self.translator(format) - self.error_message = str(error_message) - self.extremes = {} - self.timezone = timezone - - def validate(self, value): - if isinstance(value, datetime.datetime): - return value - 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 - except: - self.extremes.update(IS_DATETIME.nice(self.format)) - raise ValidationError(self.translator(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 validate(self, value): - value = IS_DATE.validate(self, value) - if self.minimum and self.minimum > value: - raise ValidationError(self.translator(self.error_message) % self.extremes) - if self.maximum and value > self.maximum: - raise ValidationError(self.translator(self.error_message) % self.extremes) - return value - - -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 validate(self, value): - value = IS_DATETIME.validate(self, value) - if self.minimum and self.minimum > value: - raise ValidationError(self.translator(self.error_message) % self.extremes) - if self.maximum and value > self.maximum: - raise ValidationError(self.translator(self.error_message) % self.extremes) - return value - - -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 validate(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: - raise ValidationError(self.translator(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: - raise ValidationError(self.translator(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 = validator.validate(v) - new_value.append(v) - ivalue = new_value - return ivalue - - -class IS_LOWER(Validator): - """ - Converts to lowercase:: - - >>> IS_LOWER()('ABC') - ('abc', None) - >>> IS_LOWER()('Ñ') - ('\\xc3\\xb1', None) - - """ - - def validate(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) - - -class IS_UPPER(Validator): - """ - Converts to uppercase:: - - >>> IS_UPPER()('abc') - ('ABC', None) - >>> IS_UPPER()('ñ') - ('\\xc3\\x91', None) - - """ - - def validate(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) - - -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(r'&\w+?;', '', s) # strip html entities - if keep_underscores: - s = re.sub(r'\s+', '-', s) # whitespace to hyphens - s = re.sub(r'[^\w\-]', '', s) - # strip all but alphanumeric/underscore/hyphen - else: - s = re.sub(r'[\s_]+', '-', s) # whitespace & underscores to hyphens - s = re.sub(r'[^a-z0-9\-]', '', s) # strip all but alphanumeric/hyphen - s = re.sub(r'[-_][-_]+', '-', 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 validate(self, value): - if self.check and value != urlify(value, self.maxlen, self.keep_underscores): - raise ValidationError(self.translator(self.error_message)) - return urlify(value, self.maxlen, self.keep_underscores) - - -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 validate(self, value): - for validator in self.subs: - v, e = validator(value) - if not e: - return v - raise ValidationError(e) - - 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 validate(self, value): - value, empty = is_empty(value, empty_regex=self.empty_regex) - if empty: - return self.null - if isinstance(self.other, (list, tuple)): - for item in self.other: - value = item.validate(value) - return value - return self.other.validate(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 validate(self, value): - v = self.regex.sub('', str(value).strip()) - return v - - -def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None): - hashfunc = hashfunc or sha1 - hmac = hashlib.pbkdf2_hmac(hashfunc().name, to_bytes(data), - to_bytes(salt), iterations, keylen) - return binascii.hexlify(hmac) - - -def simple_hash(text, key='', salt='', digest_alg='md5'): - """Generate hash with the given text using the specified digest algorithm.""" - text = to_bytes(text) - key = to_bytes(key) - salt = to_bytes(salt) - if not digest_alg: - raise RuntimeError("simple_hash with digest_alg=None") - elif not isinstance(digest_alg, str): # manual approach - h = digest_alg(text + key + salt) - elif digest_alg.startswith('pbkdf2'): # latest and coolest! - iterations, keylen, alg = digest_alg[7:-1].split(',') - return to_native(pbkdf2_hex(text, salt, int(iterations), - int(keylen), get_digest(alg))) - elif key: # use hmac - digest_alg = get_digest(digest_alg) - h = hmac.new(key + salt, text, digest_alg) - else: # compatible with third party systems - h = get_digest(digest_alg)() - h.update(text + salt) - return h.hexdigest() - - -def get_digest(value): - """Return a hashlib digest algorithm from a string.""" - if isinstance(value, str): - value = value.lower() - if value not in ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'): - raise ValueError("Invalid digest algorithm: %s" % value) - value = getattr(hashlib, value) - return value - -DIGEST_ALG_BY_SIZE = { - 128 // 4: 'md5', - 160 // 4: 'sha1', - 224 // 4: 'sha224', - 256 // 4: 'sha256', - 384 // 4: 'sha384', - 512 // 4: 'sha512', -} - - -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 $$ - - 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(uuid.uuid4()).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(Validator): - """ - 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: - - $$ - - 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 validate(self, value): - v = value and str(value)[:self.max_length] - if not v or len(v) < self.min_length: - raise ValidationError(self.translator(self.error_message)) - if isinstance(value, LazyCrypt): - return value - return LazyCrypt(self, value) - -# 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 """ - 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(Validator): - """ - 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 validate(self, value): - failures = [] - if value and len(value) == value.count('*') > 4: - return value - if self.entropy is not None: - entropy = calc_entropy(value) - if entropy < self.entropy: - failures.append(self.translator("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(self.translator("Minimum length is %s") % self.min) - if isinstance(self.max, int) and self.max > 0: - if not len(value) <= self.max: - failures.append(self.translator("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(self.translator("Must include at least %s of the following: %s") - % (self.special, self.specials)) - elif self.special is 0: - if len(all_special) > 0: - failures.append(self.translator("May not contain any of the following: %s") - % self.specials) - if self.invalid: - all_invalid = [ch in value for ch in self.invalid] - if all_invalid.count(True) > 0: - failures.append(self.translator("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(self.translator("Must include at least %s uppercase") - % str(self.upper)) - elif self.upper is 0: - if len(all_upper) > 0: - failures.append( - self.translator("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(self.translator("Must include at least %s lowercase") - % str(self.lower)) - elif self.lower is 0: - if len(all_lower) > 0: - failures.append( - self.translator("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(self.translator("Must include at least %s %s") - % (str(self.number), numbers)) - elif self.number is 0: - if len(all_number) > 0: - failures.append(self.translator("May not include any numbers")) - if len(failures) == 0: - return value - if not self.error_message: - if self.estring: - raise ValidationError('|'.join(map(str, failures))) - raise ValidationError(', '.join(failures)) - else: - raise ValidationError(self.translator(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 - aspectratio: iterable containing target aspect ratio - - Use (-1, -1) as minsize to pass image size check. - Use (-1, -1) as aspectratio to pass aspect ratio 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))) - - Check if uploaded file has a 16:9 aspect ratio: - - INPUT(_type='file', _name='name', - requires=IS_IMAGE(aspectratio=(16, 9))) - """ - - def __init__(self, - extensions=('bmp', 'gif', 'jpeg', 'png'), - maxsize=(10000, 10000), - minsize=(0, 0), - aspectratio=(-1, -1), - error_message='Invalid image'): - - self.extensions = extensions - self.maxsize = maxsize - self.minsize = minsize - self.aspectratio = aspectratio - self.error_message = error_message - - def validate(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] - - if self.aspectratio > (-1, -1): - target_ratio = (1.0 * self.aspectratio[1]) / self.aspectratio[0] - actual_ratio = (1.0 * height) / width - - assert actual_ratio == target_ratio - - value.file.seek(0) - return value - except Exception as e: - raise ValidationError(self.translator(self.error_message)) - - def __bmp(self, stream): - if stream.read(2) == b'BM': - stream.read(16) - return struct.unpack("= 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_FILE(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: string/compiled regex or a list of strings/regex of valid filenames - extension: string/compiled regex or a list of strings/regex of valid extensions - lastdot: which dot should be used as a filename / extension separator: - True means last dot, eg. file.jpg.png -> file.jpg / 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_FILE(extension='pdf')) - - Check if file is called 'thumbnail' and has a jpg or png extension - (case insensitive): - - INPUT(_type='file', _name='name', - requires=IS_FILE(filename='thumbnail', - extension=['jpg', 'png'])) - - Check if file has a tar.gz extension and name starting with backup: - - INPUT(_type='file', _name='name', - requires=IS_FILE(filename=re.compile('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_FILE(filename='README', - extension='', case=0) - - """ - - def __init__(self, filename=None, extension=None, lastdot=True, case=1, - error_message='Enter valid filename'): - self.filename = filename - self.extension = extension - self.lastdot = lastdot - self.case = case - self.error_message = error_message - - def match(self, value1, value2): - if isinstance(value1, (list, tuple)): - for v in value1: - if self.match(v, value2): - return True - return False - elif isinstance(value1, type(regex_isint)): - return value1.match(value2) - elif isinstance(value1, str): - return value1 == value2 - - def validate(self, value): - try: - string = value.filename - except: - raise ValidationError(self.translator(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.match(self.filename, string[:dot]): - raise ValidationError(self.translator(self.error_message)) - elif self.extension and not self.match(self.extension, string[dot + 1:]): - raise ValidationError(self.translator(self.error_message)) - else: - return value - - -class IS_UPLOAD_FILENAME(Validator): - """ - For new applications, use IS_FILE(). - - 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 validate(self, value): - try: - string = value.filename - except: - raise ValidationError(self.translator(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]): - raise ValidationError(self.translator(self.error_message)) - elif self.extension and not self.extension.match(string[dot + 1:]): - raise ValidationError(self.translator(self.error_message)) - else: - return value - - -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( - r'^(([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([item for item in value if isinstance(item, int)]) == 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 validate(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 - - private = any([private_number[0] <= number <= private_number[1] - for private_number in self.private]) - if ok and self.is_private is not None and self.is_private != private: - ok = False - - automatic = self.automatic[0] <= number <= self.automatic[1] - if ok and self.is_automatic is not None and self.is_automatic != automatic: - ok = False - - if ok: - return value - - raise ValidationError(self.translator(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 validate(self, value): - try: - ip = ipaddress.IPv6Address(to_unicode(value)) - ok = True - except ipaddress.AddressValueError: - raise ValidationError(self.translator(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): - raise ValidationError(self.translator('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 - - raise ValidationError(self.translator(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 validate(self, value): - IPAddress = ipaddress.ip_address - IPv6Address = ipaddress.IPv6Address - IPv4Address = ipaddress.IPv4Address - - try: - ip = IPAddress(to_unicode(value)) - except ValueError: - raise ValidationError(self.translator(self.error_message)) - - if self.is_ipv4 and isinstance(ip, IPv6Address): - raise ValidationError(self.translator(self.error_message)) - elif self.is_ipv6 and isinstance(ip, IPv4Address): - raise ValidationError(self.translator(self.error_message)) - elif self.is_ipv4 or isinstance(ip, IPv4Address): - return 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 - ).validate(value) - elif self.is_ipv6 or isinstance(ip, IPv6Address): - return 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 - ).validate(value) - else: - raise ValidationError(self.translator(self.error_message))