source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/packages/dal/pydal/validators.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 154.9 KB
Line 
1#!/bin/env python
2# -*- coding: utf-8 -*-
3# pylint: disable=line-too-long,invalid-name
4
5"""
6| This file is part of the web2py Web Framework
7| Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
8| License: BSD
9| Thanks to ga2arch for help with IS_IN_DB and IS_NOT_IN_DB on GAE
10
11Validators
12-----------
13"""
14import os
15import re
16import math
17import datetime
18import time
19import cgi
20import uuid
21import hashlib
22import hmac
23import json
24import struct
25import decimal
26import binascii
27import unicodedata
28import encodings.idna
29from functools import reduce
30
31from ._compat import (
32    StringIO,
33    integer_types,
34    basestring,
35    unicodeT,
36    urllib_unquote,
37    unichr,
38    to_bytes,
39    PY2,
40    to_unicode,
41    to_native,
42    string_types,
43    urlparse,
44    ipaddress,
45)
46from .objects import Field, FieldVirtual, FieldMethod, Table
47
48
49JSONErrors = (NameError, TypeError, ValueError, AttributeError, KeyError)
50
51__all__ = [
52    "ANY_OF",
53    "CLEANUP",
54    "CRYPT",
55    "IS_ALPHANUMERIC",
56    "IS_DATE_IN_RANGE",
57    "IS_DATE",
58    "IS_DATETIME_IN_RANGE",
59    "IS_DATETIME",
60    "IS_DECIMAL_IN_RANGE",
61    "IS_EMAIL",
62    "IS_LIST_OF_EMAILS",
63    "IS_EMPTY_OR",
64    "IS_EXPR",
65    "IS_FILE",
66    "IS_FLOAT_IN_RANGE",
67    "IS_IMAGE",
68    "IS_IN_DB",
69    "IS_IN_SET",
70    "IS_INT_IN_RANGE",
71    "IS_IPV4",
72    "IS_IPV6",
73    "IS_IPADDRESS",
74    "IS_LENGTH",
75    "IS_LIST_OF",
76    "IS_LOWER",
77    "IS_MATCH",
78    "IS_EQUAL_TO",
79    "IS_NOT_EMPTY",
80    "IS_NOT_IN_DB",
81    "IS_NULL_OR",
82    "IS_SLUG",
83    "IS_STRONG",
84    "IS_TIME",
85    "IS_UPLOAD_FILENAME",
86    "IS_UPPER",
87    "IS_URL",
88    "IS_JSON",
89]
90
91
92def options_sorter(x, y):
93    return (str(x[1]).upper() > str(y[1]).upper() and 1) or -1
94
95
96def translate(text):
97    return Validator.translator(text)
98
99
100class ValidationError(Exception):
101    def __init__(self, message):
102        Exception.__init__(self, message)
103        self.message = message
104
105
106class Validator(object):
107    """
108    Root for all validators, mainly for documentation purposes.
109
110    Validators are classes used to validate input fields (including forms
111    generated from database tables).
112
113    Here is an example of using a validator with a FORM::
114
115        INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10))
116
117    Here is an example of how to require a validator for a table field::
118
119        db.define_table('person', Field('name'))
120        db.person.name.requires=IS_NOT_EMPTY()
121
122    Validators are always assigned using the requires attribute of a field. A
123    field can have a single validator or multiple validators. Multiple
124    validators are made part of a list::
125
126        db.person.name.requires=[IS_NOT_EMPTY(), IS_NOT_IN_DB(db, 'person.id')]
127
128    Validators are called by the function accepts on a FORM or other HTML
129    helper object that contains a form. They are always called in the order in
130    which they are listed.
131
132    Built-in validators have constructors that take the optional argument error
133    message which allows you to change the default error message.
134    Here is an example of a validator on a database table::
135
136        db.person.name.requires=IS_NOT_EMPTY(error_message=T('Fill this'))
137
138    where we have used the translation operator T to allow for
139    internationalization.
140
141    Notice that default error messages are not translated.
142    """
143
144    translator = staticmethod(lambda text: text)
145
146    def formatter(self, value):
147        """
148        For some validators returns a formatted version (matching the validator)
149        of value. Otherwise just returns the value.
150        """
151        return value
152
153    @staticmethod
154    def validate(value, record_id=None):
155        raise NotImplementedError
156
157    def __call__(self, value, record_id=None):
158        try:
159            return self.validate(value, record_id), None
160        except ValidationError as e:
161            return value, e.message
162
163
164def validator_caller(func, value, record_id=None):
165    validate = getattr(func, "validate", None)
166    if validate and validate is not Validator.validate:
167        return validate(value, record_id)
168    value, error = func(value)
169    if error is not None:
170        raise ValidationError(error)
171    return value
172
173
174class IS_MATCH(Validator):
175    """
176    Example:
177        Used as::
178
179            INPUT(_type='text', _name='name', requires=IS_MATCH('.+'))
180
181    The argument of IS_MATCH is a regular expression::
182
183        >>> IS_MATCH('.+')('hello')
184        ('hello', None)
185
186        >>> IS_MATCH('hell')('hello')
187        ('hello', None)
188
189        >>> IS_MATCH('hell.*', strict=False)('hello')
190        ('hello', None)
191
192        >>> IS_MATCH('hello')('shello')
193        ('shello', 'invalid expression')
194
195        >>> IS_MATCH('hello', search=True)('shello')
196        ('shello', None)
197
198        >>> IS_MATCH('hello', search=True, strict=False)('shellox')
199        ('shellox', None)
200
201        >>> IS_MATCH('.*hello.*', search=True, strict=False)('shellox')
202        ('shellox', None)
203
204        >>> IS_MATCH('.+')('')
205        ('', 'invalid expression')
206
207    """
208
209    def __init__(
210        self,
211        expression,
212        error_message="Invalid expression",
213        strict=False,
214        search=False,
215        extract=False,
216        is_unicode=False,
217    ):
218
219        if strict or not search:
220            if not expression.startswith("^"):
221                expression = "^(%s)" % expression
222        if strict:
223            if not expression.endswith("$"):
224                expression = "(%s)$" % expression
225        if is_unicode:
226            if not isinstance(expression, unicodeT):
227                expression = expression.decode("utf8")
228            self.regex = re.compile(expression, re.UNICODE)
229        else:
230            self.regex = re.compile(expression)
231        self.error_message = error_message
232        self.extract = extract
233        self.is_unicode = is_unicode or not PY2
234
235    def validate(self, value, record_id=None):
236        if not PY2:  # PY3 convert bytes to unicode
237            value = to_unicode(value)
238
239        if self.is_unicode or not PY2:
240            if not isinstance(value, unicodeT):
241                match = self.regex.search(str(value).decode("utf8"))
242            else:
243                match = self.regex.search(value)
244        else:
245            if not isinstance(value, unicodeT):
246                match = self.regex.search(str(value))
247            else:
248                match = self.regex.search(value.encode("utf8"))
249        if match is not None:
250            return self.extract and match.group() or value
251        raise ValidationError(self.translator(self.error_message))
252
253
254class IS_EQUAL_TO(Validator):
255    """
256    Example:
257        Used as::
258
259            INPUT(_type='text', _name='password')
260            INPUT(_type='text', _name='password2',
261                  requires=IS_EQUAL_TO(request.vars.password))
262
263    The argument of IS_EQUAL_TO is a string::
264
265        >>> IS_EQUAL_TO('aaa')('aaa')
266        ('aaa', None)
267
268        >>> IS_EQUAL_TO('aaa')('aab')
269        ('aab', 'no match')
270
271    """
272
273    def __init__(self, expression, error_message="No match"):
274        self.expression = expression
275        self.error_message = error_message
276
277    def validate(self, value, record_id=None):
278        if value != self.expression:
279            raise ValidationError(self.translator(self.error_message))
280        return value
281
282
283class IS_EXPR(Validator):
284    """
285    Example:
286        Used as::
287
288            INPUT(_type='text', _name='name',
289                requires=IS_EXPR('5 < int(value) < 10'))
290
291    The argument of IS_EXPR must be python condition::
292
293        >>> IS_EXPR('int(value) < 2')('1')
294        ('1', None)
295
296        >>> IS_EXPR('int(value) < 2')('2')
297        ('2', 'invalid expression')
298
299    """
300
301    def __init__(
302        self, expression, error_message="Invalid expression", environment=None
303    ):
304        self.expression = expression
305        self.error_message = error_message
306        self.environment = environment or {}
307
308    def validate(self, value, record_id=None):
309        if callable(self.expression):
310            message = self.expression(value)
311            if message:
312                raise ValidationError(message)
313            return value
314        # for backward compatibility
315        self.environment.update(value=value)
316        exec("__ret__=" + self.expression, self.environment)
317        if self.environment["__ret__"]:
318            return value
319        raise ValidationError(self.translator(self.error_message))
320
321
322class IS_LENGTH(Validator):
323    """
324    Checks if length of field's value fits between given boundaries. Works
325    for both text and file inputs.
326
327    Args:
328        maxsize: maximum allowed length / size
329        minsize: minimum allowed length / size
330
331    Examples:
332        Check if text string is shorter than 33 characters::
333
334            INPUT(_type='text', _name='name', requires=IS_LENGTH(32))
335
336        Check if password string is longer than 5 characters::
337
338            INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6))
339
340        Check if uploaded file has size between 1KB and 1MB::
341
342            INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024))
343
344        Other examples::
345
346            >>> IS_LENGTH()('')
347            ('', None)
348            >>> IS_LENGTH()('1234567890')
349            ('1234567890', None)
350            >>> IS_LENGTH(maxsize=5, minsize=0)('1234567890')  # too long
351            ('1234567890', 'enter from 0 to 5 characters')
352            >>> IS_LENGTH(maxsize=50, minsize=20)('1234567890')  # too short
353            ('1234567890', 'enter from 20 to 50 characters')
354    """
355
356    def __init__(
357        self,
358        maxsize=255,
359        minsize=0,
360        error_message="Enter from %(min)g to %(max)g characters",
361    ):
362        self.maxsize = maxsize
363        self.minsize = minsize
364        self.error_message = error_message
365
366    def validate(self, value, record_id=None):
367        if value is None:
368            length = 0
369        elif isinstance(value, str):
370            try:
371                length = len(to_unicode(value))
372            except:
373                length = len(value)
374        elif isinstance(value, unicodeT):
375            length = len(value)
376            value = value.encode("utf8")
377        elif isinstance(value, (bytes, bytearray, tuple, list)):
378            length = len(value)
379        elif isinstance(value, cgi.FieldStorage):
380            if value.file:
381                value.file.seek(0, os.SEEK_END)
382                length = value.file.tell()
383                value.file.seek(0, os.SEEK_SET)
384            elif hasattr(value, "value"):
385                val = value.value
386                if val:
387                    length = len(val)
388                else:
389                    length = 0
390        else:
391            value = str(value)
392            length = len(str(value))
393        if self.minsize <= length <= self.maxsize:
394            return value
395        raise ValidationError(
396            self.translator(self.error_message)
397            % dict(min=self.minsize, max=self.maxsize)
398        )
399
400
401class IS_JSON(Validator):
402    """
403    Example:
404        Used as::
405
406            INPUT(_type='text', _name='name',
407                requires=IS_JSON(error_message="This is not a valid json input")
408
409            >>> IS_JSON()('{"a": 100}')
410            ({u'a': 100}, None)
411
412            >>> IS_JSON()('spam1234')
413            ('spam1234', 'invalid json')
414    """
415
416    def __init__(self, error_message="Invalid json", native_json=False):
417        self.native_json = native_json
418        self.error_message = error_message
419
420    def validate(self, value, record_id=None):
421        if isinstance(value, (str, bytes)):
422            try:
423                if self.native_json:
424                    json.loads(value)  # raises error in case of malformed json
425                    return value  # the serialized value is not passed
426                else:
427                    return json.loads(value)
428            except JSONErrors:
429                raise ValidationError(self.translator(self.error_message))
430        else:
431            return value
432
433    def formatter(self, value):
434        if value is None:
435            return None
436        if self.native_json:
437            return value
438        else:
439            return json.dumps(value)
440
441
442class IS_IN_SET(Validator):
443    """
444    Example:
445        Used as::
446
447            INPUT(_type='text', _name='name',
448                  requires=IS_IN_SET(['max', 'john'],zero=''))
449
450    The argument of IS_IN_SET must be a list or set::
451
452        >>> IS_IN_SET(['max', 'john'])('max')
453        ('max', None)
454        >>> IS_IN_SET(['max', 'john'])('massimo')
455        ('massimo', 'value not allowed')
456        >>> IS_IN_SET(['max', 'john'], multiple=True)(('max', 'john'))
457        (('max', 'john'), None)
458        >>> IS_IN_SET(['max', 'john'], multiple=True)(('bill', 'john'))
459        (('bill', 'john'), 'value not allowed')
460        >>> IS_IN_SET(('id1','id2'), ['first label','second label'])('id1') # Traditional way
461        ('id1', None)
462        >>> IS_IN_SET({'id1':'first label', 'id2':'second label'})('id1')
463        ('id1', None)
464        >>> import itertools
465        >>> IS_IN_SET(itertools.chain(['1','3','5'],['2','4','6']))('1')
466        ('1', None)
467        >>> IS_IN_SET([('id1','first label'), ('id2','second label')])('id1') # Redundant way
468        ('id1', None)
469
470    """
471
472    def __init__(
473        self,
474        theset,
475        labels=None,
476        error_message="Value not allowed",
477        multiple=False,
478        zero="",
479        sort=False,
480    ):
481
482        self.multiple = multiple
483        if isinstance(theset, dict):
484            self.theset = [str(item) for item in theset]
485            self.labels = list(theset.values())
486        elif (
487            theset
488            and isinstance(theset, (tuple, list))
489            and isinstance(theset[0], (tuple, list))
490            and len(theset[0]) == 2
491        ):
492            self.theset = [str(item) for item, label in theset]
493            self.labels = [str(label) for item, label in theset]
494        else:
495            self.theset = [str(item) for item in theset]
496            self.labels = labels
497        self.error_message = error_message
498        self.zero = zero
499        self.sort = sort
500
501    def options(self, zero=True):
502        if not self.labels:
503            items = [(k, k) for (i, k) in enumerate(self.theset)]
504        else:
505            items = [(k, list(self.labels)[i]) for (i, k) in enumerate(self.theset)]
506        if self.sort:
507            items.sort(key=lambda o: str(o[1]).upper())
508        if zero and self.zero is not None and not self.multiple:
509            items.insert(0, ("", self.zero))
510        return items
511
512    def validate(self, value, record_id=None):
513        if self.multiple:
514            # if below was values = re.compile("[\w\-:]+").findall(str(value))
515            if not value:
516                values = []
517            elif isinstance(value, (tuple, list)):
518                values = value
519            else:
520                values = [value]
521        else:
522            values = [value]
523        thestrset = [str(x) for x in self.theset]
524        failures = [x for x in values if not str(x) in thestrset]
525        if failures and self.theset:
526            raise ValidationError(self.translator(self.error_message))
527        if self.multiple:
528            if (
529                isinstance(self.multiple, (tuple, list))
530                and not self.multiple[0] <= len(values) < self.multiple[1]
531            ):
532                raise ValidationError(self.translator(self.error_message))
533            return values
534        return value
535
536
537class IS_IN_DB(Validator):
538    """
539    Example:
540        Used as::
541
542            INPUT(_type='text', _name='name',
543                  requires=IS_IN_DB(db, db.mytable.myfield, zero=''))
544
545    used for reference fields, rendered as a dropbox
546    """
547
548    REGEX_TABLE_DOT_FIELD = r"^(\w+)\.(\w+)$"
549    REGEX_INTERP_CONV_SPECIFIER = r"%\((\w+)\)\d*(?:\.\d+)?[a-zA-Z]"
550
551    def __init__(
552        self,
553        dbset,
554        field,
555        label=None,
556        error_message="Value not in database",
557        orderby=None,
558        groupby=None,
559        distinct=None,
560        cache=None,
561        multiple=False,
562        zero="",
563        sort=False,
564        _and=None,
565        left=None,
566        delimiter=None,
567        auto_add=False,
568    ):
569
570        if hasattr(dbset, "define_table"):
571            self.dbset = dbset()
572        else:
573            self.dbset = dbset
574
575        if isinstance(field, Table):
576            field = field._id
577        elif isinstance(field, str):
578            items = field.split(".")
579            if len(items) == 1:
580                field = items[0] + ".id"
581
582        (ktable, kfield) = str(field).split(".")
583        if not label:
584            label = "%%(%s)s" % kfield
585        if isinstance(label, str):
586            m = re.match(self.REGEX_TABLE_DOT_FIELD, label)
587            if m:
588                label = "%%(%s)s" % m.group(2)
589            fieldnames = re.findall(self.REGEX_INTERP_CONV_SPECIFIER, label)
590            if kfield not in fieldnames:
591                fieldnames.append(kfield)  # kfield must be last
592        elif isinstance(label, Field):
593            fieldnames = [label.name, kfield]  # kfield must be last
594            label = "%%(%s)s" % label.name
595        elif callable(label):
596            fieldnames = "*"
597        else:
598            raise NotImplementedError
599
600        self.fieldnames = fieldnames  # fields requires to build the formatting
601        self.label = label
602        self.ktable = ktable
603        self.kfield = kfield
604        self.error_message = error_message
605        self.theset = None
606        self.orderby = orderby
607        self.groupby = groupby
608        self.distinct = distinct
609        self.cache = cache
610        self.multiple = multiple
611        self.zero = zero
612        self.sort = sort
613        self._and = _and
614        self.left = left
615        self.delimiter = delimiter
616        self.auto_add = auto_add
617
618    def set_self_id(self, id):
619        if self._and:
620            self._and.record_id = id
621
622    def build_set(self):
623        table = self.dbset.db[self.ktable]
624        if self.fieldnames == "*":
625            fields = [f for f in table]
626        else:
627            fields = [table[k] for k in self.fieldnames]
628        ignore = (FieldVirtual, FieldMethod)
629        fields = [f for f in fields if not isinstance(f, ignore)]
630        if self.dbset.db._dbname != "gae":
631            orderby = self.orderby or reduce(lambda a, b: a | b, fields)
632            groupby = self.groupby
633            distinct = self.distinct
634            left = self.left
635            dd = dict(
636                orderby=orderby,
637                groupby=groupby,
638                distinct=distinct,
639                cache=self.cache,
640                cacheable=True,
641                left=left,
642            )
643            records = self.dbset(table).select(*fields, **dd)
644        else:
645            orderby = self.orderby or reduce(
646                lambda a, b: a | b, (f for f in fields if not f.name == "id")
647            )
648            dd = dict(orderby=orderby, cache=self.cache, cacheable=True)
649            records = self.dbset(table).select(table.ALL, **dd)
650        self.theset = [str(r[self.kfield]) for r in records]
651        if isinstance(self.label, str):
652            self.labels = [self.label % r for r in records]
653        else:
654            self.labels = [self.label(r) for r in records]
655
656    def options(self, zero=True):
657        self.build_set()
658        items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)]
659        if self.sort:
660            items.sort(key=lambda o: str(o[1]).upper())
661        if zero and self.zero is not None and not self.multiple:
662            items.insert(0, ("", self.zero))
663        return items
664
665    def maybe_add(self, table, fieldname, value):
666        d = {fieldname: value}
667        record = table(**d)
668        if record:
669            return record.id
670        else:
671            return table.insert(**d)
672
673    def validate(self, value, record_id=None):
674        table = self.dbset.db[self.ktable]
675        field = table[self.kfield]
676
677        if self.multiple:
678            if self._and:
679                raise NotImplementedError
680            if isinstance(value, list):
681                values = value
682            elif self.delimiter:
683                values = value.split(self.delimiter)  # because of autocomplete
684            elif value:
685                values = [value]
686            else:
687                values = []
688
689            if field.type in ("id", "integer"):
690                new_values = []
691                for value in values:
692                    if not (isinstance(value, integer_types) or value.isdigit()):
693                        if self.auto_add:
694                            value = str(
695                                self.maybe_add(table, self.fieldnames[0], value)
696                            )
697                        else:
698                            raise ValidationError(self.translator(self.error_message))
699                    new_values.append(value)
700                values = new_values
701
702            if (
703                isinstance(self.multiple, (tuple, list))
704                and not self.multiple[0] <= len(values) < self.multiple[1]
705            ):
706                raise ValidationError(self.translator(self.error_message))
707            if self.theset:
708                if not [v for v in values if v not in self.theset]:
709                    return values
710            else:
711
712                def count(values, s=self.dbset, f=field):
713                    return s(f.belongs(list(map(int, values)))).count()
714
715                if self.dbset.db._adapter.dbengine == "google:datastore":
716                    range_ids = range(0, len(values), 30)
717                    total = sum(count(values[i : i + 30]) for i in range_ids)
718                    if total == len(values):
719                        return values
720                elif count(values) == len(values):
721                    return values
722        else:
723            if field.type in ("id", "integer"):
724                if isinstance(value, integer_types) or (
725                    isinstance(value, string_types) and value.isdigit()
726                ):
727                    value = int(value)
728                elif self.auto_add:
729                    value = self.maybe_add(table, self.fieldnames[0], value)
730                else:
731                    raise ValidationError(self.translator(self.error_message))
732
733                try:
734                    value = int(value)
735                except TypeError:
736                    raise ValidationError(self.translator(self.error_message))
737
738            if self.theset:
739                if str(value) in self.theset:
740                    if self._and:
741                        return validator_caller(self._and, value, record_id)
742                    return value
743            else:
744                if self.dbset(field == value).count():
745                    if self._and:
746                        return validator_caller(self._and, value, record_id)
747                    return value
748        raise ValidationError(self.translator(self.error_message))
749
750
751class IS_NOT_IN_DB(Validator):
752    """
753    Example:
754        Used as::
755
756            INPUT(_type='text', _name='name', requires=IS_NOT_IN_DB(db, db.table))
757
758    makes the field unique
759    """
760
761    def __init__(
762        self,
763        dbset,
764        field,
765        error_message="Value already in database or empty",
766        allowed_override=[],
767        ignore_common_filters=False,
768    ):
769
770        if isinstance(field, Table):
771            field = field._id
772
773        if hasattr(dbset, "define_table"):
774            self.dbset = dbset()
775        else:
776            self.dbset = dbset
777        self.field = field
778        self.error_message = error_message
779        self.record_id = 0
780        self.allowed_override = allowed_override
781        self.ignore_common_filters = ignore_common_filters
782
783    def set_self_id(self, id):
784        # this is legacy  - web2py uses but nobody else should
785        # it is not safe if the object is recycled
786        self.record_id = id
787
788    def validate(self, value, record_id=None):
789        value = to_native(str(value))
790        if not value.strip():
791            raise ValidationError(self.translator(self.error_message))
792        if value in self.allowed_override:
793            return value
794        (tablename, fieldname) = str(self.field).split(".")
795        table = self.dbset.db[tablename]
796        field = table[fieldname]
797        query = field == value
798
799        # make sure exclude the record_id
800        id = record_id or self.record_id
801        if isinstance(id, dict):
802            id = table(**id)
803        if not id is None:
804            query &= table._id != id
805        subset = self.dbset(query, ignore_common_filters=self.ignore_common_filters)
806        if subset.select(limitby=(0, 1)):
807            raise ValidationError(self.translator(self.error_message))
808        return value
809
810
811def range_error_message(error_message, what_to_enter, minimum, maximum):
812    """build the error message for the number range validators"""
813    if error_message is None:
814        error_message = "Enter " + what_to_enter
815        if minimum is not None and maximum is not None:
816            error_message += " between %(min)g and %(max)g"
817        elif minimum is not None:
818            error_message += " greater than or equal to %(min)g"
819        elif maximum is not None:
820            error_message += " less than or equal to %(max)g"
821    if type(maximum) in integer_types:
822        maximum -= 1
823    return translate(error_message) % dict(min=minimum, max=maximum)
824
825
826class IS_INT_IN_RANGE(Validator):
827    """
828    Determines that the argument is (or can be represented as) an int,
829    and that it falls within the specified range. The range is interpreted
830    in the Pythonic way, so the test is: min <= value < max.
831
832    The minimum and maximum limits can be None, meaning no lower or upper limit,
833    respectively.
834
835    Example:
836        Used as::
837
838            INPUT(_type='text', _name='name', requires=IS_INT_IN_RANGE(0, 10))
839
840            >>> IS_INT_IN_RANGE(1,5)('4')
841            (4, None)
842            >>> IS_INT_IN_RANGE(1,5)(4)
843            (4, None)
844            >>> IS_INT_IN_RANGE(1,5)(1)
845            (1, None)
846            >>> IS_INT_IN_RANGE(1,5)(5)
847            (5, 'enter an integer between 1 and 4')
848            >>> IS_INT_IN_RANGE(1,5)(5)
849            (5, 'enter an integer between 1 and 4')
850            >>> IS_INT_IN_RANGE(1,5)(3.5)
851            (3.5, 'enter an integer between 1 and 4')
852            >>> IS_INT_IN_RANGE(None,5)('4')
853            (4, None)
854            >>> IS_INT_IN_RANGE(None,5)('6')
855            ('6', 'enter an integer less than or equal to 4')
856            >>> IS_INT_IN_RANGE(1,None)('4')
857            (4, None)
858            >>> IS_INT_IN_RANGE(1,None)('0')
859            ('0', 'enter an integer greater than or equal to 1')
860            >>> IS_INT_IN_RANGE()(6)
861            (6, None)
862            >>> IS_INT_IN_RANGE()('abc')
863            ('abc', 'enter an integer')
864    """
865
866    REGEX_INT = r"^[+-]?\d+$"
867
868    def __init__(self, minimum=None, maximum=None, error_message=None):
869
870        self.minimum = int(minimum) if minimum is not None else None
871        self.maximum = int(maximum) if maximum is not None else None
872        self.error_message = error_message
873
874    def validate(self, value, record_id=None):
875        if re.match(self.REGEX_INT, str(value)):
876            v = int(value)
877            if (self.minimum is None or v >= self.minimum) and (
878                self.maximum is None or v < self.maximum
879            ):
880                return v
881        raise ValidationError(
882            range_error_message(
883                self.error_message, "an integer", self.minimum, self.maximum
884            )
885        )
886
887
888def str2dec(number):
889    s = str(number)
890    if "." not in s:
891        s += ".00"
892    else:
893        s += "0" * (2 - len(s.split(".")[1]))
894    return s
895
896
897class IS_FLOAT_IN_RANGE(Validator):
898    """
899    Determines that the argument is (or can be represented as) a float,
900    and that it falls within the specified inclusive range.
901    The comparison is made with native arithmetic.
902
903    The minimum and maximum limits can be None, meaning no lower or upper limit,
904    respectively.
905
906    Example:
907        Used as::
908
909            INPUT(_type='text', _name='name', requires=IS_FLOAT_IN_RANGE(0, 10))
910
911            >>> IS_FLOAT_IN_RANGE(1,5)('4')
912            (4.0, None)
913            >>> IS_FLOAT_IN_RANGE(1,5)(4)
914            (4.0, None)
915            >>> IS_FLOAT_IN_RANGE(1,5)(1)
916            (1.0, None)
917            >>> IS_FLOAT_IN_RANGE(1,5)(5.25)
918            (5.25, 'enter a number between 1 and 5')
919            >>> IS_FLOAT_IN_RANGE(1,5)(6.0)
920            (6.0, 'enter a number between 1 and 5')
921            >>> IS_FLOAT_IN_RANGE(1,5)(3.5)
922            (3.5, None)
923            >>> IS_FLOAT_IN_RANGE(1,None)(3.5)
924            (3.5, None)
925            >>> IS_FLOAT_IN_RANGE(None,5)(3.5)
926            (3.5, None)
927            >>> IS_FLOAT_IN_RANGE(1,None)(0.5)
928            (0.5, 'enter a number greater than or equal to 1')
929            >>> IS_FLOAT_IN_RANGE(None,5)(6.5)
930            (6.5, 'enter a number less than or equal to 5')
931            >>> IS_FLOAT_IN_RANGE()(6.5)
932            (6.5, None)
933            >>> IS_FLOAT_IN_RANGE()('abc')
934            ('abc', 'enter a number')
935    """
936
937    def __init__(self, minimum=None, maximum=None, error_message=None, dot="."):
938
939        self.minimum = float(minimum) if minimum is not None else None
940        self.maximum = float(maximum) if maximum is not None else None
941        self.dot = str(dot)
942        self.error_message = error_message
943
944    def validate(self, value, record_id=None):
945        try:
946            if self.dot == ".":
947                v = float(value)
948            else:
949                v = float(str(value).replace(self.dot, "."))
950            if (self.minimum is None or v >= self.minimum) and (
951                self.maximum is None or v <= self.maximum
952            ):
953                return v
954        except (ValueError, TypeError):
955            pass
956        raise ValidationError(
957            range_error_message(
958                self.error_message, "a number", self.minimum, self.maximum
959            )
960        )
961
962    def formatter(self, value):
963        if value is None:
964            return None
965        return str2dec(value).replace(".", self.dot)
966
967
968class IS_DECIMAL_IN_RANGE(Validator):
969    """
970    Determines that the argument is (or can be represented as) a Python Decimal,
971    and that it falls within the specified inclusive range.
972    The comparison is made with Python Decimal arithmetic.
973
974    The minimum and maximum limits can be None, meaning no lower or upper limit,
975    respectively.
976
977    Example:
978        Used as::
979
980            INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10))
981
982            >>> IS_DECIMAL_IN_RANGE(1,5)('4')
983            (Decimal('4'), None)
984            >>> IS_DECIMAL_IN_RANGE(1,5)(4)
985            (Decimal('4'), None)
986            >>> IS_DECIMAL_IN_RANGE(1,5)(1)
987            (Decimal('1'), None)
988            >>> IS_DECIMAL_IN_RANGE(1,5)(5.25)
989            (5.25, 'enter a number between 1 and 5')
990            >>> IS_DECIMAL_IN_RANGE(5.25,6)(5.25)
991            (Decimal('5.25'), None)
992            >>> IS_DECIMAL_IN_RANGE(5.25,6)('5.25')
993            (Decimal('5.25'), None)
994            >>> IS_DECIMAL_IN_RANGE(1,5)(6.0)
995            (6.0, 'enter a number between 1 and 5')
996            >>> IS_DECIMAL_IN_RANGE(1,5)(3.5)
997            (Decimal('3.5'), None)
998            >>> IS_DECIMAL_IN_RANGE(1.5,5.5)(3.5)
999            (Decimal('3.5'), None)
1000            >>> IS_DECIMAL_IN_RANGE(1.5,5.5)(6.5)
1001            (6.5, 'enter a number between 1.5 and 5.5')
1002            >>> IS_DECIMAL_IN_RANGE(1.5,None)(6.5)
1003            (Decimal('6.5'), None)
1004            >>> IS_DECIMAL_IN_RANGE(1.5,None)(0.5)
1005            (0.5, 'enter a number greater than or equal to 1.5')
1006            >>> IS_DECIMAL_IN_RANGE(None,5.5)(4.5)
1007            (Decimal('4.5'), None)
1008            >>> IS_DECIMAL_IN_RANGE(None,5.5)(6.5)
1009            (6.5, 'enter a number less than or equal to 5.5')
1010            >>> IS_DECIMAL_IN_RANGE()(6.5)
1011            (Decimal('6.5'), None)
1012            >>> IS_DECIMAL_IN_RANGE(0,99)(123.123)
1013            (123.123, 'enter a number between 0 and 99')
1014            >>> IS_DECIMAL_IN_RANGE(0,99)('123.123')
1015            ('123.123', 'enter a number between 0 and 99')
1016            >>> IS_DECIMAL_IN_RANGE(0,99)('12.34')
1017            (Decimal('12.34'), None)
1018            >>> IS_DECIMAL_IN_RANGE()('abc')
1019            ('abc', 'enter a number')
1020    """
1021
1022    def __init__(self, minimum=None, maximum=None, error_message=None, dot="."):
1023
1024        self.minimum = decimal.Decimal(str(minimum)) if minimum is not None else None
1025        self.maximum = decimal.Decimal(str(maximum)) if maximum is not None else None
1026        self.dot = str(dot)
1027        self.error_message = error_message
1028
1029    def validate(self, value, record_id=None):
1030        try:
1031            if not isinstance(value, decimal.Decimal):
1032                value = decimal.Decimal(str(value).replace(self.dot, "."))
1033            if (self.minimum is None or value >= self.minimum) and (
1034                self.maximum is None or value <= self.maximum
1035            ):
1036                return value
1037        except (ValueError, TypeError, decimal.InvalidOperation):
1038            pass
1039        raise ValidationError(
1040            range_error_message(
1041                self.error_message, "a number", self.minimum, self.maximum
1042            )
1043        )
1044
1045    def formatter(self, value):
1046        if value is None:
1047            return None
1048        return str2dec(value).replace(".", self.dot)
1049
1050
1051def is_empty(value, empty_regex=None):
1052    _value = value
1053    """test empty field"""
1054    if isinstance(value, (str, unicodeT)):
1055        value = value.strip()
1056        if empty_regex is not None and empty_regex.match(value):
1057            value = ""
1058    if value is None or value == "" or value == b"" or value == []:
1059        return (_value, True)
1060    return (_value, False)
1061
1062
1063class IS_NOT_EMPTY(Validator):
1064    """
1065    Example:
1066        Used as::
1067
1068            INPUT(_type='text', _name='name', requires=IS_NOT_EMPTY())
1069
1070            >>> IS_NOT_EMPTY()(1)
1071            (1, None)
1072            >>> IS_NOT_EMPTY()(0)
1073            (0, None)
1074            >>> IS_NOT_EMPTY()('x')
1075            ('x', None)
1076            >>> IS_NOT_EMPTY()(' x ')
1077            ('x', None)
1078            >>> IS_NOT_EMPTY()(None)
1079            (None, 'enter a value')
1080            >>> IS_NOT_EMPTY()('')
1081            ('', 'enter a value')
1082            >>> IS_NOT_EMPTY()('  ')
1083            ('', 'enter a value')
1084            >>> IS_NOT_EMPTY()(' \\n\\t')
1085            ('', 'enter a value')
1086            >>> IS_NOT_EMPTY()([])
1087            ([], 'enter a value')
1088            >>> IS_NOT_EMPTY(empty_regex='def')('def')
1089            ('', 'enter a value')
1090            >>> IS_NOT_EMPTY(empty_regex='de[fg]')('deg')
1091            ('', 'enter a value')
1092            >>> IS_NOT_EMPTY(empty_regex='def')('abc')
1093            ('abc', None)
1094    """
1095
1096    def __init__(self, error_message="Enter a value", empty_regex=None):
1097        self.error_message = error_message
1098        if empty_regex is not None:
1099            self.empty_regex = re.compile(empty_regex)
1100        else:
1101            self.empty_regex = None
1102
1103    def validate(self, value, record_id=None):
1104        value, empty = is_empty(value, empty_regex=self.empty_regex)
1105        if empty:
1106            raise ValidationError(self.translator(self.error_message))
1107        return value
1108
1109
1110class IS_ALPHANUMERIC(IS_MATCH):
1111    """
1112    Example:
1113        Used as::
1114
1115            INPUT(_type='text', _name='name', requires=IS_ALPHANUMERIC())
1116
1117            >>> IS_ALPHANUMERIC()('1')
1118            ('1', None)
1119            >>> IS_ALPHANUMERIC()('')
1120            ('', None)
1121            >>> IS_ALPHANUMERIC()('A_a')
1122            ('A_a', None)
1123            >>> IS_ALPHANUMERIC()('!')
1124            ('!', 'enter only letters, numbers, and underscore')
1125    """
1126
1127    def __init__(self, error_message="Enter only letters, numbers, and underscore"):
1128        IS_MATCH.__init__(self, r"^[\w]*$", error_message)
1129
1130
1131class IS_EMAIL(Validator):
1132
1133    """
1134    Checks if field's value is a valid email address. Can be set to disallow
1135    or force addresses from certain domain(s).
1136
1137    Email regex adapted from
1138    http://haacked.com/archive/2007/08/21/i-knew-how-to-validate-an-email-address-until-i.aspx,
1139    generally following the RFCs, except that we disallow quoted strings
1140    and permit underscores and leading numerics in subdomain labels
1141
1142    Args:
1143        banned: regex text for disallowed address domains
1144        forced: regex text for required address domains
1145
1146    Both arguments can also be custom objects with a match(value) method.
1147
1148    Example:
1149        Check for valid email address::
1150
1151            INPUT(_type='text', _name='name',
1152                requires=IS_EMAIL())
1153
1154        Check for valid email address that can't be from a .com domain::
1155
1156            INPUT(_type='text', _name='name',
1157                requires=IS_EMAIL(banned='^.*\\.com(|\\..*)$'))
1158
1159        Check for valid email address that must be from a .edu domain::
1160
1161            INPUT(_type='text', _name='name',
1162                requires=IS_EMAIL(forced='^.*\\.edu(|\\..*)$'))
1163
1164            >>> IS_EMAIL()('a@b.com')
1165            ('a@b.com', None)
1166            >>> IS_EMAIL()('abc@def.com')
1167            ('abc@def.com', None)
1168            >>> IS_EMAIL()('abc@3def.com')
1169            ('abc@3def.com', None)
1170            >>> IS_EMAIL()('abc@def.us')
1171            ('abc@def.us', None)
1172            >>> IS_EMAIL()('abc@d_-f.us')
1173            ('abc@d_-f.us', None)
1174            >>> IS_EMAIL()('@def.com')           # missing name
1175            ('@def.com', 'enter a valid email address')
1176            >>> IS_EMAIL()('"abc@def".com')      # quoted name
1177            ('"abc@def".com', 'enter a valid email address')
1178            >>> IS_EMAIL()('abc+def.com')        # no @
1179            ('abc+def.com', 'enter a valid email address')
1180            >>> IS_EMAIL()('abc@def.x')          # one-char TLD
1181            ('abc@def.x', 'enter a valid email address')
1182            >>> IS_EMAIL()('abc@def.12')         # numeric TLD
1183            ('abc@def.12', 'enter a valid email address')
1184            >>> IS_EMAIL()('abc@def..com')       # double-dot in domain
1185            ('abc@def..com', 'enter a valid email address')
1186            >>> IS_EMAIL()('abc@.def.com')       # dot starts domain
1187            ('abc@.def.com', 'enter a valid email address')
1188            >>> IS_EMAIL()('abc@def.c_m')        # underscore in TLD
1189            ('abc@def.c_m', 'enter a valid email address')
1190            >>> IS_EMAIL()('NotAnEmail')         # missing @
1191            ('NotAnEmail', 'enter a valid email address')
1192            >>> IS_EMAIL()('abc@NotAnEmail')     # missing TLD
1193            ('abc@NotAnEmail', 'enter a valid email address')
1194            >>> IS_EMAIL()('customer/department@example.com')
1195            ('customer/department@example.com', None)
1196            >>> IS_EMAIL()('$A12345@example.com')
1197            ('$A12345@example.com', None)
1198            >>> IS_EMAIL()('!def!xyz%abc@example.com')
1199            ('!def!xyz%abc@example.com', None)
1200            >>> IS_EMAIL()('_Yosemite.Sam@example.com')
1201            ('_Yosemite.Sam@example.com', None)
1202            >>> IS_EMAIL()('~@example.com')
1203            ('~@example.com', None)
1204            >>> IS_EMAIL()('.wooly@example.com')       # dot starts name
1205            ('.wooly@example.com', 'enter a valid email address')
1206            >>> IS_EMAIL()('wo..oly@example.com')      # adjacent dots in name
1207            ('wo..oly@example.com', 'enter a valid email address')
1208            >>> IS_EMAIL()('pootietang.@example.com')  # dot ends name
1209            ('pootietang.@example.com', 'enter a valid email address')
1210            >>> IS_EMAIL()('.@example.com')            # name is bare dot
1211            ('.@example.com', 'enter a valid email address')
1212            >>> IS_EMAIL()('Ima.Fool@example.com')
1213            ('Ima.Fool@example.com', None)
1214            >>> IS_EMAIL()('Ima Fool@example.com')     # space in name
1215            ('Ima Fool@example.com', 'enter a valid email address')
1216            >>> IS_EMAIL()('localguy@localhost')       # localhost as domain
1217            ('localguy@localhost', None)
1218
1219    """
1220
1221    # NOTE: use these with flags = re.VERBOSE | re.IGNORECASE
1222    REGEX_BODY = r"""
1223        ^(?!\.)                           # name may not begin with a dot
1224        (
1225          [-a-z0-9!\#$%&'*+/=?^_`{|}~]    # all legal characters except dot
1226          |
1227          (?<!\.)\.                       # single dots only
1228        )+
1229        (?<!\.)$                          # name may not end with a dot
1230    """
1231    REGEX_DOMAIN = r"""
1232        (
1233          localhost
1234          |
1235          (
1236            [a-z0-9]      # [sub]domain begins with alphanumeric
1237            (
1238              [-\w]*      # alphanumeric, underscore, dot, hyphen
1239              [a-z0-9]    # ending alphanumeric
1240            )?
1241          \.              # ending dot
1242          )+
1243          [a-z]{2,}       # TLD alpha-only
1244       )$
1245    """
1246    # regex_proposed_but_failed = re.compile(r'^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$', re.VERBOSE | re.IGNORECASE)
1247
1248    def __init__(
1249        self, banned=None, forced=None, error_message="Enter a valid email address"
1250    ):
1251        if isinstance(banned, str):
1252            banned = re.compile(banned)
1253        if isinstance(forced, str):
1254            forced = re.compile(forced)
1255        self.banned = banned
1256        self.forced = forced
1257        self.error_message = error_message
1258
1259    def validate(self, value, record_id=None):
1260        if (
1261            not (isinstance(value, (basestring, unicodeT)))
1262            or not value
1263            or "@" not in value
1264        ):
1265            raise ValidationError(self.translator(self.error_message))
1266
1267        body, domain = value.rsplit("@", 1)
1268
1269        try:
1270            regex_flags = re.VERBOSE | re.IGNORECASE
1271            match_body = re.match(self.REGEX_BODY, body, regex_flags)
1272            match_domain = re.match(self.REGEX_DOMAIN, domain, regex_flags)
1273
1274            if not match_domain:
1275                # check for Internationalized Domain Names
1276                # see https://docs.python.org/2/library/codecs.html#module-encodings.idna
1277                domain_encoded = to_unicode(domain).encode("idna").decode("ascii")
1278                match_domain = re.match(self.REGEX_DOMAIN, domain_encoded, regex_flags)
1279
1280            match = (match_body is not None) and (match_domain is not None)
1281        except (TypeError, UnicodeError):
1282            # Value may not be a string where we can look for matches.
1283            # Example: we're calling ANY_OF formatter and IS_EMAIL is asked to validate a date.
1284            match = None
1285        if match:
1286            if (not self.banned or not self.banned.match(domain)) and (
1287                not self.forced or self.forced.match(domain)
1288            ):
1289                return value
1290        raise ValidationError(self.translator(self.error_message))
1291
1292
1293class IS_LIST_OF_EMAILS(Validator):
1294    """
1295    Example:
1296        Used as::
1297
1298        Field('emails', 'list:string',
1299              widget=SQLFORM.widgets.text.widget,
1300              requires=IS_LIST_OF_EMAILS(),
1301              represent=lambda v, r: \
1302                XML(', '.join([A(x, _href='mailto:'+x).xml() for x in (v or [])]))
1303             )
1304    """
1305
1306    REGEX_NOT_EMAIL_SPLITTER = r"[^,;\s]+"
1307
1308    def __init__(self, error_message="Invalid emails: %s"):
1309        self.error_message = error_message
1310
1311    def validate(self, value, record_id=None):
1312        bad_emails = []
1313        f = IS_EMAIL()
1314        for email in re.findall(self.REGEX_NOT_EMAIL_SPLITTER, value):
1315            error = f(email)[1]
1316            if error and email not in bad_emails:
1317                bad_emails.append(email)
1318        if bad_emails:
1319            raise ValidationError(
1320                self.translator(self.error_message) % ", ".join(bad_emails)
1321            )
1322        return value
1323
1324    def formatter(self, value, row=None):
1325        return ", ".join(value or [])
1326
1327
1328# URL scheme source:
1329# <http://en.wikipedia.org/wiki/URI_scheme> obtained on 2008-Nov-10
1330
1331official_url_schemes = [
1332    "aaa",
1333    "aaas",
1334    "acap",
1335    "cap",
1336    "cid",
1337    "crid",
1338    "data",
1339    "dav",
1340    "dict",
1341    "dns",
1342    "fax",
1343    "file",
1344    "ftp",
1345    "go",
1346    "gopher",
1347    "h323",
1348    "http",
1349    "https",
1350    "icap",
1351    "im",
1352    "imap",
1353    "info",
1354    "ipp",
1355    "iris",
1356    "iris.beep",
1357    "iris.xpc",
1358    "iris.xpcs",
1359    "iris.lws",
1360    "ldap",
1361    "mailto",
1362    "mid",
1363    "modem",
1364    "msrp",
1365    "msrps",
1366    "mtqp",
1367    "mupdate",
1368    "news",
1369    "nfs",
1370    "nntp",
1371    "opaquelocktoken",
1372    "pop",
1373    "pres",
1374    "prospero",
1375    "rtsp",
1376    "service",
1377    "shttp",
1378    "sip",
1379    "sips",
1380    "snmp",
1381    "soap.beep",
1382    "soap.beeps",
1383    "tag",
1384    "tel",
1385    "telnet",
1386    "tftp",
1387    "thismessage",
1388    "tip",
1389    "tv",
1390    "urn",
1391    "vemmi",
1392    "wais",
1393    "xmlrpc.beep",
1394    "xmlrpc.beep",
1395    "xmpp",
1396    "z39.50r",
1397    "z39.50s",
1398]
1399unofficial_url_schemes = [
1400    "about",
1401    "adiumxtra",
1402    "aim",
1403    "afp",
1404    "aw",
1405    "callto",
1406    "chrome",
1407    "cvs",
1408    "ed2k",
1409    "feed",
1410    "fish",
1411    "gg",
1412    "gizmoproject",
1413    "iax2",
1414    "irc",
1415    "ircs",
1416    "itms",
1417    "jar",
1418    "javascript",
1419    "keyparc",
1420    "lastfm",
1421    "ldaps",
1422    "magnet",
1423    "mms",
1424    "msnim",
1425    "mvn",
1426    "notes",
1427    "nsfw",
1428    "psyc",
1429    "paparazzi:http",
1430    "rmi",
1431    "rsync",
1432    "secondlife",
1433    "sgn",
1434    "skype",
1435    "ssh",
1436    "sftp",
1437    "smb",
1438    "sms",
1439    "soldat",
1440    "steam",
1441    "svn",
1442    "teamspeak",
1443    "unreal",
1444    "ut2004",
1445    "ventrilo",
1446    "view-source",
1447    "webcal",
1448    "wyciwyg",
1449    "xfire",
1450    "xri",
1451    "ymsgr",
1452]
1453all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes
1454http_schemes = [None, "http", "https"]
1455
1456# Defined in RFC 3490, Section 3.1, Requirement #1
1457# Use this regex to split the authority component of a unicode URL into
1458# its component labels
1459REGEX_AUTHORITY_SPLITTER = u"[\u002e\u3002\uff0e\uff61]"
1460
1461
1462def escape_unicode(string):
1463
1464    """
1465    Converts a unicode string into US-ASCII, using a simple conversion scheme.
1466    Each unicode character that does not have a US-ASCII equivalent is
1467    converted into a URL escaped form based on its hexadecimal value.
1468    For example, the unicode character '\\u4e86' will become the string '%4e%86'
1469
1470    Args:
1471        string: unicode string, the unicode string to convert into an
1472            escaped US-ASCII form
1473
1474    Returns:
1475        string: the US-ASCII escaped form of the inputted string
1476
1477    @author: Jonathan Benn
1478    """
1479    returnValue = StringIO()
1480
1481    for character in string:
1482        code = ord(character)
1483        if code > 0x7F:
1484            hexCode = hex(code)
1485            returnValue.write("%" + hexCode[2:4] + "%" + hexCode[4:6])
1486        else:
1487            returnValue.write(character)
1488
1489    return returnValue.getvalue()
1490
1491
1492def unicode_to_ascii_authority(authority):
1493    """
1494    Follows the steps in RFC 3490, Section 4 to convert a unicode authority
1495    string into its ASCII equivalent.
1496    For example, u'www.Alliancefran\\xe7aise.nu' will be converted into
1497    'www.xn--alliancefranaise-npb.nu'
1498
1499    Args:
1500        authority: unicode string, the URL authority component to convert,
1501            e.g. u'www.Alliancefran\\xe7aise.nu'
1502
1503    Returns:
1504        string: the US-ASCII character equivalent to the inputed authority,
1505             e.g. 'www.xn--alliancefranaise-npb.nu'
1506
1507    Raises:
1508        Exception: if the function is not able to convert the inputed
1509            authority
1510
1511    @author: Jonathan Benn
1512    """
1513    # RFC 3490, Section 4, Step 1
1514    # The encodings.idna Python module assumes that AllowUnassigned == True
1515
1516    # RFC 3490, Section 4, Step 2
1517    labels = re.split(REGEX_AUTHORITY_SPLITTER, authority)
1518
1519    # RFC 3490, Section 4, Step 3
1520    # The encodings.idna Python module assumes that UseSTD3ASCIIRules == False
1521
1522    # RFC 3490, Section 4, Step 4
1523    # We use the ToASCII operation because we are about to put the authority
1524    # into an IDN-unaware slot
1525    asciiLabels = []
1526    for label in labels:
1527        if label:
1528            asciiLabels.append(to_native(encodings.idna.ToASCII(label)))
1529        else:
1530            # encodings.idna.ToASCII does not accept an empty string, but
1531            # it is necessary for us to allow for empty labels so that we
1532            # don't modify the URL
1533            asciiLabels.append("")
1534    # RFC 3490, Section 4, Step 5
1535    return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels))
1536
1537
1538def unicode_to_ascii_url(url, prepend_scheme):
1539    """
1540    Converts the inputed unicode url into a US-ASCII equivalent. This function
1541    goes a little beyond RFC 3490, which is limited in scope to the domain name
1542    (authority) only. Here, the functionality is expanded to what was observed
1543    on Wikipedia on 2009-Jan-22:
1544
1545       Component    Can Use Unicode?
1546       ---------    ----------------
1547       scheme       No
1548       authority    Yes
1549       path         Yes
1550       query        Yes
1551       fragment     No
1552
1553    The authority component gets converted to punycode, but occurrences of
1554    unicode in other components get converted into a pair of URI escapes (we
1555    assume 4-byte unicode). E.g. the unicode character U+4E2D will be
1556    converted into '%4E%2D'. Testing with Firefox v3.0.5 has shown that it can
1557    understand this kind of URI encoding.
1558
1559    Args:
1560        url: unicode string, the URL to convert from unicode into US-ASCII
1561        prepend_scheme: string, a protocol scheme to prepend to the URL if
1562            we're having trouble parsing it.
1563            e.g. "http". Input None to disable this functionality
1564
1565    Returns:
1566        string: a US-ASCII equivalent of the inputed url
1567
1568    @author: Jonathan Benn
1569    """
1570    # convert the authority component of the URL into an ASCII punycode string,
1571    # but encode the rest using the regular URI character encoding
1572    components = urlparse.urlparse(url)
1573    prepended = False
1574    # If no authority was found
1575    if not components.netloc:
1576        # Try appending a scheme to see if that fixes the problem
1577        scheme_to_prepend = prepend_scheme or "http"
1578        components = urlparse.urlparse(to_unicode(scheme_to_prepend) + u"://" + url)
1579        prepended = True
1580
1581    # if we still can't find the authority
1582    if not components.netloc:
1583        # And it's not because the url is a relative url
1584        if not url.startswith("/"):
1585            raise Exception(
1586                "No authority component found, "
1587                + "could not decode unicode to US-ASCII"
1588            )
1589
1590    # We're here if we found an authority, let's rebuild the URL
1591    scheme = components.scheme
1592    authority = components.netloc
1593    path = components.path
1594    query = components.query
1595    fragment = components.fragment
1596
1597    if prepended:
1598        scheme = ""
1599
1600    unparsed = urlparse.urlunparse(
1601        (
1602            scheme,
1603            unicode_to_ascii_authority(authority),
1604            escape_unicode(path),
1605            "",
1606            escape_unicode(query),
1607            str(fragment),
1608        )
1609    )
1610    if unparsed.startswith("//"):
1611        unparsed = unparsed[2:]  # Remove the // urlunparse puts in the beginning
1612    return unparsed
1613
1614
1615class IS_GENERIC_URL(Validator):
1616    """
1617    Rejects a URL string if any of the following is true:
1618       * The string is empty or None
1619       * The string uses characters that are not allowed in a URL
1620       * The URL scheme specified (if one is specified) is not valid
1621
1622    Based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html
1623
1624    This function only checks the URL's syntax. It does not check that the URL
1625    points to a real document, for example, or that it otherwise makes sense
1626    semantically. This function does automatically prepend 'http://' in front
1627    of a URL if and only if that's necessary to successfully parse the URL.
1628    Please note that a scheme will be prepended only for rare cases
1629    (e.g. 'google.ca:80')
1630
1631    The list of allowed schemes is customizable with the allowed_schemes
1632    parameter. If you exclude None from the list, then abbreviated URLs
1633    (lacking a scheme such as 'http') will be rejected.
1634
1635    The default prepended scheme is customizable with the prepend_scheme
1636    parameter. If you set prepend_scheme to None then prepending will be
1637    disabled. URLs that require prepending to parse will still be accepted,
1638    but the return value will not be modified.
1639
1640    @author: Jonathan Benn
1641
1642        >>> IS_GENERIC_URL()('http://user@abc.com')
1643        ('http://user@abc.com', None)
1644
1645    Args:
1646        error_message: a string, the error message to give the end user
1647            if the URL does not validate
1648        allowed_schemes: a list containing strings or None. Each element
1649            is a scheme the inputed URL is allowed to use
1650        prepend_scheme: a string, this scheme is prepended if it's
1651            necessary to make the URL valid
1652
1653    """
1654
1655    def __init__(
1656        self,
1657        error_message="Enter a valid URL",
1658        allowed_schemes=None,
1659        prepend_scheme=None,
1660    ):
1661
1662        self.error_message = error_message
1663        if allowed_schemes is None:
1664            self.allowed_schemes = all_url_schemes
1665        else:
1666            self.allowed_schemes = allowed_schemes
1667        self.prepend_scheme = prepend_scheme
1668        if self.prepend_scheme not in self.allowed_schemes:
1669            raise SyntaxError(
1670                "prepend_scheme='%s' is not in allowed_schemes=%s"
1671                % (self.prepend_scheme, self.allowed_schemes)
1672            )
1673
1674    REGEX_GENERIC_URL = 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]$"
1675    REGEX_GENERIC_URL_VALID = r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$"
1676    REGEX_URL_FRAGMENT_VALID = r"[|A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$"
1677
1678    def validate(self, value, record_id=None):
1679        """
1680        Args:
1681            value: a string, the URL to validate
1682
1683        Returns:
1684            a tuple, where tuple[0] is the inputed value (possible
1685            prepended with prepend_scheme), and tuple[1] is either
1686            None (success!) or the string error_message
1687        """
1688
1689        # if we dont have anything or the URL misuses the '%' character
1690
1691        if not value or re.search(self.REGEX_GENERIC_URL, value):
1692            raise ValidationError(self.translator(self.error_message))
1693
1694        if "#" in value:
1695            url, fragment_part = value.split("#")
1696        else:
1697            url, fragment_part = value, ""
1698        # if the URL is only composed of valid characters
1699        if not re.match(self.REGEX_GENERIC_URL_VALID, url) or (
1700            fragment_part and not re.match(self.REGEX_URL_FRAGMENT_VALID, fragment_part)
1701        ):
1702            raise ValidationError(self.translator(self.error_message))
1703        # Then parse the URL into its components and check on
1704        try:
1705            components = urlparse.urlparse(urllib_unquote(value))._asdict()
1706        except ValueError:
1707            raise ValidationError(self.translator(self.error_message))
1708
1709        # Clean up the scheme before we check it
1710        scheme = components["scheme"]
1711        if len(scheme) == 0:
1712            scheme = None
1713        else:
1714            scheme = components["scheme"].lower()
1715        # If the scheme doesn't really exists
1716        if (
1717            scheme not in self.allowed_schemes
1718            or not scheme
1719            and ":" in components["path"]
1720        ):
1721            # for the possible case of abbreviated URLs with
1722            # ports, check to see if adding a valid scheme fixes
1723            # the problem (but only do this if it doesn't have
1724            # one already!)
1725            if "://" not in value and None in self.allowed_schemes:
1726                schemeToUse = self.prepend_scheme or "http"
1727                new_value = self.validate(schemeToUse + "://" + value)
1728                return new_value if self.prepend_scheme else value
1729            raise ValidationError(self.translator(self.error_message))
1730        return value
1731
1732
1733# Sources (obtained 2017-Nov-11):
1734#    http://data.iana.org/TLD/tlds-alpha-by-domain.txt
1735# see scripts/parse_top_level_domains.py for an easy update
1736
1737official_top_level_domains = [
1738    # a
1739    "aaa",
1740    "aarp",
1741    "abarth",
1742    "abb",
1743    "abbott",
1744    "abbvie",
1745    "abc",
1746    "able",
1747    "abogado",
1748    "abudhabi",
1749    "ac",
1750    "academy",
1751    "accenture",
1752    "accountant",
1753    "accountants",
1754    "aco",
1755    "active",
1756    "actor",
1757    "ad",
1758    "adac",
1759    "ads",
1760    "adult",
1761    "ae",
1762    "aeg",
1763    "aero",
1764    "aetna",
1765    "af",
1766    "afamilycompany",
1767    "afl",
1768    "africa",
1769    "ag",
1770    "agakhan",
1771    "agency",
1772    "ai",
1773    "aig",
1774    "aigo",
1775    "airbus",
1776    "airforce",
1777    "airtel",
1778    "akdn",
1779    "al",
1780    "alfaromeo",
1781    "alibaba",
1782    "alipay",
1783    "allfinanz",
1784    "allstate",
1785    "ally",
1786    "alsace",
1787    "alstom",
1788    "am",
1789    "americanexpress",
1790    "americanfamily",
1791    "amex",
1792    "amfam",
1793    "amica",
1794    "amsterdam",
1795    "analytics",
1796    "android",
1797    "anquan",
1798    "anz",
1799    "ao",
1800    "aol",
1801    "apartments",
1802    "app",
1803    "apple",
1804    "aq",
1805    "aquarelle",
1806    "ar",
1807    "arab",
1808    "aramco",
1809    "archi",
1810    "army",
1811    "arpa",
1812    "art",
1813    "arte",
1814    "as",
1815    "asda",
1816    "asia",
1817    "associates",
1818    "at",
1819    "athleta",
1820    "attorney",
1821    "au",
1822    "auction",
1823    "audi",
1824    "audible",
1825    "audio",
1826    "auspost",
1827    "author",
1828    "auto",
1829    "autos",
1830    "avianca",
1831    "aw",
1832    "aws",
1833    "ax",
1834    "axa",
1835    "az",
1836    "azure",
1837    # b
1838    "ba",
1839    "baby",
1840    "baidu",
1841    "banamex",
1842    "bananarepublic",
1843    "band",
1844    "bank",
1845    "bar",
1846    "barcelona",
1847    "barclaycard",
1848    "barclays",
1849    "barefoot",
1850    "bargains",
1851    "baseball",
1852    "basketball",
1853    "bauhaus",
1854    "bayern",
1855    "bb",
1856    "bbc",
1857    "bbt",
1858    "bbva",
1859    "bcg",
1860    "bcn",
1861    "bd",
1862    "be",
1863    "beats",
1864    "beauty",
1865    "beer",
1866    "bentley",
1867    "berlin",
1868    "best",
1869    "bestbuy",
1870    "bet",
1871    "bf",
1872    "bg",
1873    "bh",
1874    "bharti",
1875    "bi",
1876    "bible",
1877    "bid",
1878    "bike",
1879    "bing",
1880    "bingo",
1881    "bio",
1882    "biz",
1883    "bj",
1884    "black",
1885    "blackfriday",
1886    "blanco",
1887    "blockbuster",
1888    "blog",
1889    "bloomberg",
1890    "blue",
1891    "bm",
1892    "bms",
1893    "bmw",
1894    "bn",
1895    "bnl",
1896    "bnpparibas",
1897    "bo",
1898    "boats",
1899    "boehringer",
1900    "bofa",
1901    "bom",
1902    "bond",
1903    "boo",
1904    "book",
1905    "booking",
1906    "boots",
1907    "bosch",
1908    "bostik",
1909    "boston",
1910    "bot",
1911    "boutique",
1912    "box",
1913    "br",
1914    "bradesco",
1915    "bridgestone",
1916    "broadway",
1917    "broker",
1918    "brother",
1919    "brussels",
1920    "bs",
1921    "bt",
1922    "budapest",
1923    "bugatti",
1924    "build",
1925    "builders",
1926    "business",
1927    "buy",
1928    "buzz",
1929    "bv",
1930    "bw",
1931    "by",
1932    "bz",
1933    "bzh",
1934    # c
1935    "ca",
1936    "cab",
1937    "cafe",
1938    "cal",
1939    "call",
1940    "calvinklein",
1941    "cam",
1942    "camera",
1943    "camp",
1944    "cancerresearch",
1945    "canon",
1946    "capetown",
1947    "capital",
1948    "capitalone",
1949    "car",
1950    "caravan",
1951    "cards",
1952    "care",
1953    "career",
1954    "careers",
1955    "cars",
1956    "cartier",
1957    "casa",
1958    "case",
1959    "caseih",
1960    "cash",
1961    "casino",
1962    "cat",
1963    "catering",
1964    "catholic",
1965    "cba",
1966    "cbn",
1967    "cbre",
1968    "cbs",
1969    "cc",
1970    "cd",
1971    "ceb",
1972    "center",
1973    "ceo",
1974    "cern",
1975    "cf",
1976    "cfa",
1977    "cfd",
1978    "cg",
1979    "ch",
1980    "chanel",
1981    "channel",
1982    "chase",
1983    "chat",
1984    "cheap",
1985    "chintai",
1986    "christmas",
1987    "chrome",
1988    "chrysler",
1989    "church",
1990    "ci",
1991    "cipriani",
1992    "circle",
1993    "cisco",
1994    "citadel",
1995    "citi",
1996    "citic",
1997    "city",
1998    "cityeats",
1999    "ck",
2000    "cl",
2001    "claims",
2002    "cleaning",
2003    "click",
2004    "clinic",
2005    "clinique",
2006    "clothing",
2007    "cloud",
2008    "club",
2009    "clubmed",
2010    "cm",
2011    "cn",
2012    "co",
2013    "coach",
2014    "codes",
2015    "coffee",
2016    "college",
2017    "cologne",
2018    "com",
2019    "comcast",
2020    "commbank",
2021    "community",
2022    "company",
2023    "compare",
2024    "computer",
2025    "comsec",
2026    "condos",
2027    "construction",
2028    "consulting",
2029    "contact",
2030    "contractors",
2031    "cooking",
2032    "cookingchannel",
2033    "cool",
2034    "coop",
2035    "corsica",
2036    "country",
2037    "coupon",
2038    "coupons",
2039    "courses",
2040    "cr",
2041    "credit",
2042    "creditcard",
2043    "creditunion",
2044    "cricket",
2045    "crown",
2046    "crs",
2047    "cruise",
2048    "cruises",
2049    "csc",
2050    "cu",
2051    "cuisinella",
2052    "cv",
2053    "cw",
2054    "cx",
2055    "cy",
2056    "cymru",
2057    "cyou",
2058    "cz",
2059    # d
2060    "dabur",
2061    "dad",
2062    "dance",
2063    "data",
2064    "date",
2065    "dating",
2066    "datsun",
2067    "day",
2068    "dclk",
2069    "dds",
2070    "de",
2071    "deal",
2072    "dealer",
2073    "deals",
2074    "degree",
2075    "delivery",
2076    "dell",
2077    "deloitte",
2078    "delta",
2079    "democrat",
2080    "dental",
2081    "dentist",
2082    "desi",
2083    "design",
2084    "dev",
2085    "dhl",
2086    "diamonds",
2087    "diet",
2088    "digital",
2089    "direct",
2090    "directory",
2091    "discount",
2092    "discover",
2093    "dish",
2094    "diy",
2095    "dj",
2096    "dk",
2097    "dm",
2098    "dnp",
2099    "do",
2100    "docs",
2101    "doctor",
2102    "dodge",
2103    "dog",
2104    "doha",
2105    "domains",
2106    "dot",
2107    "download",
2108    "drive",
2109    "dtv",
2110    "dubai",
2111    "duck",
2112    "dunlop",
2113    "duns",
2114    "dupont",
2115    "durban",
2116    "dvag",
2117    "dvr",
2118    "dz",
2119    # e
2120    "earth",
2121    "eat",
2122    "ec",
2123    "eco",
2124    "edeka",
2125    "edu",
2126    "education",
2127    "ee",
2128    "eg",
2129    "email",
2130    "emerck",
2131    "energy",
2132    "engineer",
2133    "engineering",
2134    "enterprises",
2135    "epost",
2136    "epson",
2137    "equipment",
2138    "er",
2139    "ericsson",
2140    "erni",
2141    "es",
2142    "esq",
2143    "estate",
2144    "esurance",
2145    "et",
2146    "etisalat",
2147    "eu",
2148    "eurovision",
2149    "eus",
2150    "events",
2151    "everbank",
2152    "exchange",
2153    "expert",
2154    "exposed",
2155    "express",
2156    "extraspace",
2157    # f
2158    "fage",
2159    "fail",
2160    "fairwinds",
2161    "faith",
2162    "family",
2163    "fan",
2164    "fans",
2165    "farm",
2166    "farmers",
2167    "fashion",
2168    "fast",
2169    "fedex",
2170    "feedback",
2171    "ferrari",
2172    "ferrero",
2173    "fi",
2174    "fiat",
2175    "fidelity",
2176    "fido",
2177    "film",
2178    "final",
2179    "finance",
2180    "financial",
2181    "fire",
2182    "firestone",
2183    "firmdale",
2184    "fish",
2185    "fishing",
2186    "fit",
2187    "fitness",
2188    "fj",
2189    "fk",
2190    "flickr",
2191    "flights",
2192    "flir",
2193    "florist",
2194    "flowers",
2195    "fly",
2196    "fm",
2197    "fo",
2198    "foo",
2199    "food",
2200    "foodnetwork",
2201    "football",
2202    "ford",
2203    "forex",
2204    "forsale",
2205    "forum",
2206    "foundation",
2207    "fox",
2208    "fr",
2209    "free",
2210    "fresenius",
2211    "frl",
2212    "frogans",
2213    "frontdoor",
2214    "frontier",
2215    "ftr",
2216    "fujitsu",
2217    "fujixerox",
2218    "fun",
2219    "fund",
2220    "furniture",
2221    "futbol",
2222    "fyi",
2223    # g
2224    "ga",
2225    "gal",
2226    "gallery",
2227    "gallo",
2228    "gallup",
2229    "game",
2230    "games",
2231    "gap",
2232    "garden",
2233    "gb",
2234    "gbiz",
2235    "gd",
2236    "gdn",
2237    "ge",
2238    "gea",
2239    "gent",
2240    "genting",
2241    "george",
2242    "gf",
2243    "gg",
2244    "ggee",
2245    "gh",
2246    "gi",
2247    "gift",
2248    "gifts",
2249    "gives",
2250    "giving",
2251    "gl",
2252    "glade",
2253    "glass",
2254    "gle",
2255    "global",
2256    "globo",
2257    "gm",
2258    "gmail",
2259    "gmbh",
2260    "gmo",
2261    "gmx",
2262    "gn",
2263    "godaddy",
2264    "gold",
2265    "goldpoint",
2266    "golf",
2267    "goo",
2268    "goodhands",
2269    "goodyear",
2270    "goog",
2271    "google",
2272    "gop",
2273    "got",
2274    "gov",
2275    "gp",
2276    "gq",
2277    "gr",
2278    "grainger",
2279    "graphics",
2280    "gratis",
2281    "green",
2282    "gripe",
2283    "grocery",
2284    "group",
2285    "gs",
2286    "gt",
2287    "gu",
2288    "guardian",
2289    "gucci",
2290    "guge",
2291    "guide",
2292    "guitars",
2293    "guru",
2294    "gw",
2295    "gy",
2296    # h
2297    "hair",
2298    "hamburg",
2299    "hangout",
2300    "haus",
2301    "hbo",
2302    "hdfc",
2303    "hdfcbank",
2304    "health",
2305    "healthcare",
2306    "help",
2307    "helsinki",
2308    "here",
2309    "hermes",
2310    "hgtv",
2311    "hiphop",
2312    "hisamitsu",
2313    "hitachi",
2314    "hiv",
2315    "hk",
2316    "hkt",
2317    "hm",
2318    "hn",
2319    "hockey",
2320    "holdings",
2321    "holiday",
2322    "homedepot",
2323    "homegoods",
2324    "homes",
2325    "homesense",
2326    "honda",
2327    "honeywell",
2328    "horse",
2329    "hospital",
2330    "host",
2331    "hosting",
2332    "hot",
2333    "hoteles",
2334    "hotels",
2335    "hotmail",
2336    "house",
2337    "how",
2338    "hr",
2339    "hsbc",
2340    "ht",
2341    "hu",
2342    "hughes",
2343    "hyatt",
2344    "hyundai",
2345    # i
2346    "ibm",
2347    "icbc",
2348    "ice",
2349    "icu",
2350    "id",
2351    "ie",
2352    "ieee",
2353    "ifm",
2354    "ikano",
2355    "il",
2356    "im",
2357    "imamat",
2358    "imdb",
2359    "immo",
2360    "immobilien",
2361    "in",
2362    "industries",
2363    "infiniti",
2364    "info",
2365    "ing",
2366    "ink",
2367    "institute",
2368    "insurance",
2369    "insure",
2370    "int",
2371    "intel",
2372    "international",
2373    "intuit",
2374    "investments",
2375    "io",
2376    "ipiranga",
2377    "iq",
2378    "ir",
2379    "irish",
2380    "is",
2381    "iselect",
2382    "ismaili",
2383    "ist",
2384    "istanbul",
2385    "it",
2386    "itau",
2387    "itv",
2388    "iveco",
2389    "iwc",
2390    # j
2391    "jaguar",
2392    "java",
2393    "jcb",
2394    "jcp",
2395    "je",
2396    "jeep",
2397    "jetzt",
2398    "jewelry",
2399    "jio",
2400    "jlc",
2401    "jll",
2402    "jm",
2403    "jmp",
2404    "jnj",
2405    "jo",
2406    "jobs",
2407    "joburg",
2408    "jot",
2409    "joy",
2410    "jp",
2411    "jpmorgan",
2412    "jprs",
2413    "juegos",
2414    "juniper",
2415    # k
2416    "kaufen",
2417    "kddi",
2418    "ke",
2419    "kerryhotels",
2420    "kerrylogistics",
2421    "kerryproperties",
2422    "kfh",
2423    "kg",
2424    "kh",
2425    "ki",
2426    "kia",
2427    "kim",
2428    "kinder",
2429    "kindle",
2430    "kitchen",
2431    "kiwi",
2432    "km",
2433    "kn",
2434    "koeln",
2435    "komatsu",
2436    "kosher",
2437    "kp",
2438    "kpmg",
2439    "kpn",
2440    "kr",
2441    "krd",
2442    "kred",
2443    "kuokgroup",
2444    "kw",
2445    "ky",
2446    "kyoto",
2447    "kz",
2448    # l
2449    "la",
2450    "lacaixa",
2451    "ladbrokes",
2452    "lamborghini",
2453    "lamer",
2454    "lancaster",
2455    "lancia",
2456    "lancome",
2457    "land",
2458    "landrover",
2459    "lanxess",
2460    "lasalle",
2461    "lat",
2462    "latino",
2463    "latrobe",
2464    "law",
2465    "lawyer",
2466    "lb",
2467    "lc",
2468    "lds",
2469    "lease",
2470    "leclerc",
2471    "lefrak",
2472    "legal",
2473    "lego",
2474    "lexus",
2475    "lgbt",
2476    "li",
2477    "liaison",
2478    "lidl",
2479    "life",
2480    "lifeinsurance",
2481    "lifestyle",
2482    "lighting",
2483    "like",
2484    "lilly",
2485    "limited",
2486    "limo",
2487    "lincoln",
2488    "linde",
2489    "link",
2490    "lipsy",
2491    "live",
2492    "living",
2493    "lixil",
2494    "lk",
2495    "loan",
2496    "loans",
2497    "localhost",
2498    "locker",
2499    "locus",
2500    "loft",
2501    "lol",
2502    "london",
2503    "lotte",
2504    "lotto",
2505    "love",
2506    "lpl",
2507    "lplfinancial",
2508    "lr",
2509    "ls",
2510    "lt",
2511    "ltd",
2512    "ltda",
2513    "lu",
2514    "lundbeck",
2515    "lupin",
2516    "luxe",
2517    "luxury",
2518    "lv",
2519    "ly",
2520    # m
2521    "ma",
2522    "macys",
2523    "madrid",
2524    "maif",
2525    "maison",
2526    "makeup",
2527    "man",
2528    "management",
2529    "mango",
2530    "map",
2531    "market",
2532    "marketing",
2533    "markets",
2534    "marriott",
2535    "marshalls",
2536    "maserati",
2537    "mattel",
2538    "mba",
2539    "mc",
2540    "mckinsey",
2541    "md",
2542    "me",
2543    "med",
2544    "media",
2545    "meet",
2546    "melbourne",
2547    "meme",
2548    "memorial",
2549    "men",
2550    "menu",
2551    "meo",
2552    "merckmsd",
2553    "metlife",
2554    "mg",
2555    "mh",
2556    "miami",
2557    "microsoft",
2558    "mil",
2559    "mini",
2560    "mint",
2561    "mit",
2562    "mitsubishi",
2563    "mk",
2564    "ml",
2565    "mlb",
2566    "mls",
2567    "mm",
2568    "mma",
2569    "mn",
2570    "mo",
2571    "mobi",
2572    "mobile",
2573    "mobily",
2574    "moda",
2575    "moe",
2576    "moi",
2577    "mom",
2578    "monash",
2579    "money",
2580    "monster",
2581    "mopar",
2582    "mormon",
2583    "mortgage",
2584    "moscow",
2585    "moto",
2586    "motorcycles",
2587    "mov",
2588    "movie",
2589    "movistar",
2590    "mp",
2591    "mq",
2592    "mr",
2593    "ms",
2594    "msd",
2595    "mt",
2596    "mtn",
2597    "mtr",
2598    "mu",
2599    "museum",
2600    "mutual",
2601    "mv",
2602    "mw",
2603    "mx",
2604    "my",
2605    "mz",
2606    # n
2607    "na",
2608    "nab",
2609    "nadex",
2610    "nagoya",
2611    "name",
2612    "nationwide",
2613    "natura",
2614    "navy",
2615    "nba",
2616    "nc",
2617    "ne",
2618    "nec",
2619    "net",
2620    "netbank",
2621    "netflix",
2622    "network",
2623    "neustar",
2624    "new",
2625    "newholland",
2626    "news",
2627    "next",
2628    "nextdirect",
2629    "nexus",
2630    "nf",
2631    "nfl",
2632    "ng",
2633    "ngo",
2634    "nhk",
2635    "ni",
2636    "nico",
2637    "nike",
2638    "nikon",
2639    "ninja",
2640    "nissan",
2641    "nissay",
2642    "nl",
2643    "no",
2644    "nokia",
2645    "northwesternmutual",
2646    "norton",
2647    "now",
2648    "nowruz",
2649    "nowtv",
2650    "np",
2651    "nr",
2652    "nra",
2653    "nrw",
2654    "ntt",
2655    "nu",
2656    "nyc",
2657    "nz",
2658    # o
2659    "obi",
2660    "observer",
2661    "off",
2662    "office",
2663    "okinawa",
2664    "olayan",
2665    "olayangroup",
2666    "oldnavy",
2667    "ollo",
2668    "om",
2669    "omega",
2670    "one",
2671    "ong",
2672    "onl",
2673    "online",
2674    "onyourside",
2675    "ooo",
2676    "open",
2677    "oracle",
2678    "orange",
2679    "org",
2680    "organic",
2681    "origins",
2682    "osaka",
2683    "otsuka",
2684    "ott",
2685    "ovh",
2686    # p
2687    "pa",
2688    "page",
2689    "panasonic",
2690    "panerai",
2691    "paris",
2692    "pars",
2693    "partners",
2694    "parts",
2695    "party",
2696    "passagens",
2697    "pay",
2698    "pccw",
2699    "pe",
2700    "pet",
2701    "pf",
2702    "pfizer",
2703    "pg",
2704    "ph",
2705    "pharmacy",
2706    "phd",
2707    "philips",
2708    "phone",
2709    "photo",
2710    "photography",
2711    "photos",
2712    "physio",
2713    "piaget",
2714    "pics",
2715    "pictet",
2716    "pictures",
2717    "pid",
2718    "pin",
2719    "ping",
2720    "pink",
2721    "pioneer",
2722    "pizza",
2723    "pk",
2724    "pl",
2725    "place",
2726    "play",
2727    "playstation",
2728    "plumbing",
2729    "plus",
2730    "pm",
2731    "pn",
2732    "pnc",
2733    "pohl",
2734    "poker",
2735    "politie",
2736    "porn",
2737    "post",
2738    "pr",
2739    "pramerica",
2740    "praxi",
2741    "press",
2742    "prime",
2743    "pro",
2744    "prod",
2745    "productions",
2746    "prof",
2747    "progressive",
2748    "promo",
2749    "properties",
2750    "property",
2751    "protection",
2752    "pru",
2753    "prudential",
2754    "ps",
2755    "pt",
2756    "pub",
2757    "pw",
2758    "pwc",
2759    "py",
2760    # q
2761    "qa",
2762    "qpon",
2763    "quebec",
2764    "quest",
2765    "qvc",
2766    # r
2767    "racing",
2768    "radio",
2769    "raid",
2770    "re",
2771    "read",
2772    "realestate",
2773    "realtor",
2774    "realty",
2775    "recipes",
2776    "red",
2777    "redstone",
2778    "redumbrella",
2779    "rehab",
2780    "reise",
2781    "reisen",
2782    "reit",
2783    "reliance",
2784    "ren",
2785    "rent",
2786    "rentals",
2787    "repair",
2788    "report",
2789    "republican",
2790    "rest",
2791    "restaurant",
2792    "review",
2793    "reviews",
2794    "rexroth",
2795    "rich",
2796    "richardli",
2797    "ricoh",
2798    "rightathome",
2799    "ril",
2800    "rio",
2801    "rip",
2802    "rmit",
2803    "ro",
2804    "rocher",
2805    "rocks",
2806    "rodeo",
2807    "rogers",
2808    "room",
2809    "rs",
2810    "rsvp",
2811    "ru",
2812    "rugby",
2813    "ruhr",
2814    "run",
2815    "rw",
2816    "rwe",
2817    "ryukyu",
2818    # s
2819    "sa",
2820    "saarland",
2821    "safe",
2822    "safety",
2823    "sakura",
2824    "sale",
2825    "salon",
2826    "samsclub",
2827    "samsung",
2828    "sandvik",
2829    "sandvikcoromant",
2830    "sanofi",
2831    "sap",
2832    "sapo",
2833    "sarl",
2834    "sas",
2835    "save",
2836    "saxo",
2837    "sb",
2838    "sbi",
2839    "sbs",
2840    "sc",
2841    "sca",
2842    "scb",
2843    "schaeffler",
2844    "schmidt",
2845    "scholarships",
2846    "school",
2847    "schule",
2848    "schwarz",
2849    "science",
2850    "scjohnson",
2851    "scor",
2852    "scot",
2853    "sd",
2854    "se",
2855    "search",
2856    "seat",
2857    "secure",
2858    "security",
2859    "seek",
2860    "select",
2861    "sener",
2862    "services",
2863    "ses",
2864    "seven",
2865    "sew",
2866    "sex",
2867    "sexy",
2868    "sfr",
2869    "sg",
2870    "sh",
2871    "shangrila",
2872    "sharp",
2873    "shaw",
2874    "shell",
2875    "shia",
2876    "shiksha",
2877    "shoes",
2878    "shop",
2879    "shopping",
2880    "shouji",
2881    "show",
2882    "showtime",
2883    "shriram",
2884    "si",
2885    "silk",
2886    "sina",
2887    "singles",
2888    "site",
2889    "sj",
2890    "sk",
2891    "ski",
2892    "skin",
2893    "sky",
2894    "skype",
2895    "sl",
2896    "sling",
2897    "sm",
2898    "smart",
2899    "smile",
2900    "sn",
2901    "sncf",
2902    "so",
2903    "soccer",
2904    "social",
2905    "softbank",
2906    "software",
2907    "sohu",
2908    "solar",
2909    "solutions",
2910    "song",
2911    "sony",
2912    "soy",
2913    "space",
2914    "spiegel",
2915    "spot",
2916    "spreadbetting",
2917    "sr",
2918    "srl",
2919    "srt",
2920    "st",
2921    "stada",
2922    "staples",
2923    "star",
2924    "starhub",
2925    "statebank",
2926    "statefarm",
2927    "statoil",
2928    "stc",
2929    "stcgroup",
2930    "stockholm",
2931    "storage",
2932    "store",
2933    "stream",
2934    "studio",
2935    "study",
2936    "style",
2937    "su",
2938    "sucks",
2939    "supplies",
2940    "supply",
2941    "support",
2942    "surf",
2943    "surgery",
2944    "suzuki",
2945    "sv",
2946    "swatch",
2947    "swiftcover",
2948    "swiss",
2949    "sx",
2950    "sy",
2951    "sydney",
2952    "symantec",
2953    "systems",
2954    "sz",
2955    # t
2956    "tab",
2957    "taipei",
2958    "talk",
2959    "taobao",
2960    "target",
2961    "tatamotors",
2962    "tatar",
2963    "tattoo",
2964    "tax",
2965    "taxi",
2966    "tc",
2967    "tci",
2968    "td",
2969    "tdk",
2970    "team",
2971    "tech",
2972    "technology",
2973    "tel",
2974    "telecity",
2975    "telefonica",
2976    "temasek",
2977    "tennis",
2978    "teva",
2979    "tf",
2980    "tg",
2981    "th",
2982    "thd",
2983    "theater",
2984    "theatre",
2985    "tiaa",
2986    "tickets",
2987    "tienda",
2988    "tiffany",
2989    "tips",
2990    "tires",
2991    "tirol",
2992    "tj",
2993    "tjmaxx",
2994    "tjx",
2995    "tk",
2996    "tkmaxx",
2997    "tl",
2998    "tm",
2999    "tmall",
3000    "tn",
3001    "to",
3002    "today",
3003    "tokyo",
3004    "tools",
3005    "top",
3006    "toray",
3007    "toshiba",
3008    "total",
3009    "tours",
3010    "town",
3011    "toyota",
3012    "toys",
3013    "tr",
3014    "trade",
3015    "trading",
3016    "training",
3017    "travel",
3018    "travelchannel",
3019    "travelers",
3020    "travelersinsurance",
3021    "trust",
3022    "trv",
3023    "tt",
3024    "tube",
3025    "tui",
3026    "tunes",
3027    "tushu",
3028    "tv",
3029    "tvs",
3030    "tw",
3031    "tz",
3032    # u
3033    "ua",
3034    "ubank",
3035    "ubs",
3036    "uconnect",
3037    "ug",
3038    "uk",
3039    "unicom",
3040    "university",
3041    "uno",
3042    "uol",
3043    "ups",
3044    "us",
3045    "uy",
3046    "uz",
3047    # v
3048    "va",
3049    "vacations",
3050    "vana",
3051    "vanguard",
3052    "vc",
3053    "ve",
3054    "vegas",
3055    "ventures",
3056    "verisign",
3057    "versicherung",
3058    "vet",
3059    "vg",
3060    "vi",
3061    "viajes",
3062    "video",
3063    "vig",
3064    "viking",
3065    "villas",
3066    "vin",
3067    "vip",
3068    "virgin",
3069    "visa",
3070    "vision",
3071    "vista",
3072    "vistaprint",
3073    "viva",
3074    "vivo",
3075    "vlaanderen",
3076    "vn",
3077    "vodka",
3078    "volkswagen",
3079    "volvo",
3080    "vote",
3081    "voting",
3082    "voto",
3083    "voyage",
3084    "vu",
3085    "vuelos",
3086    # w
3087    "wales",
3088    "walmart",
3089    "walter",
3090    "wang",
3091    "wanggou",
3092    "warman",
3093    "watch",
3094    "watches",
3095    "weather",
3096    "weatherchannel",
3097    "webcam",
3098    "weber",
3099    "website",
3100    "wed",
3101    "wedding",
3102    "weibo",
3103    "weir",
3104    "wf",
3105    "whoswho",
3106    "wien",
3107    "wiki",
3108    "williamhill",
3109    "win",
3110    "windows",
3111    "wine",
3112    "winners",
3113    "wme",
3114    "wolterskluwer",
3115    "woodside",
3116    "work",
3117    "works",
3118    "world",
3119    "wow",
3120    "ws",
3121    "wtc",
3122    "wtf",
3123    # x
3124    "xbox",
3125    "xerox",
3126    "xfinity",
3127    "xihuan",
3128    "xin",
3129    "xn--11b4c3d",
3130    "xn--1ck2e1b",
3131    "xn--1qqw23a",
3132    "xn--2scrj9c",
3133    "xn--30rr7y",
3134    "xn--3bst00m",
3135    "xn--3ds443g",
3136    "xn--3e0b707e",
3137    "xn--3hcrj9c",
3138    "xn--3oq18vl8pn36a",
3139    "xn--3pxu8k",
3140    "xn--42c2d9a",
3141    "xn--45br5cyl",
3142    "xn--45brj9c",
3143    "xn--45q11c",
3144    "xn--4gbrim",
3145    "xn--54b7fta0cc",
3146    "xn--55qw42g",
3147    "xn--55qx5d",
3148    "xn--5su34j936bgsg",
3149    "xn--5tzm5g",
3150    "xn--6frz82g",
3151    "xn--6qq986b3xl",
3152    "xn--80adxhks",
3153    "xn--80ao21a",
3154    "xn--80aqecdr1a",
3155    "xn--80asehdb",
3156    "xn--80aswg",
3157    "xn--8y0a063a",
3158    "xn--90a3ac",
3159    "xn--90ae",
3160    "xn--90ais",
3161    "xn--9dbq2a",
3162    "xn--9et52u",
3163    "xn--9krt00a",
3164    "xn--b4w605ferd",
3165    "xn--bck1b9a5dre4c",
3166    "xn--c1avg",
3167    "xn--c2br7g",
3168    "xn--cck2b3b",
3169    "xn--cg4bki",
3170    "xn--clchc0ea0b2g2a9gcd",
3171    "xn--czr694b",
3172    "xn--czrs0t",
3173    "xn--czru2d",
3174    "xn--d1acj3b",
3175    "xn--d1alf",
3176    "xn--e1a4c",
3177    "xn--eckvdtc9d",
3178    "xn--efvy88h",
3179    "xn--estv75g",
3180    "xn--fct429k",
3181    "xn--fhbei",
3182    "xn--fiq228c5hs",
3183    "xn--fiq64b",
3184    "xn--fiqs8s",
3185    "xn--fiqz9s",
3186    "xn--fjq720a",
3187    "xn--flw351e",
3188    "xn--fpcrj9c3d",
3189    "xn--fzc2c9e2c",
3190    "xn--fzys8d69uvgm",
3191    "xn--g2xx48c",
3192    "xn--gckr3f0f",
3193    "xn--gecrj9c",
3194    "xn--gk3at1e",
3195    "xn--h2breg3eve",
3196    "xn--h2brj9c",
3197    "xn--h2brj9c8c",
3198    "xn--hxt814e",
3199    "xn--i1b6b1a6a2e",
3200    "xn--imr513n",
3201    "xn--io0a7i",
3202    "xn--j1aef",
3203    "xn--j1amh",
3204    "xn--j6w193g",
3205    "xn--jlq61u9w7b",
3206    "xn--jvr189m",
3207    "xn--kcrx77d1x4a",
3208    "xn--kprw13d",
3209    "xn--kpry57d",
3210    "xn--kpu716f",
3211    "xn--kput3i",
3212    "xn--l1acc",
3213    "xn--lgbbat1ad8j",
3214    "xn--mgb9awbf",
3215    "xn--mgba3a3ejt",
3216    "xn--mgba3a4f16a",
3217    "xn--mgba7c0bbn0a",
3218    "xn--mgbaakc7dvf",
3219    "xn--mgbaam7a8h",
3220    "xn--mgbab2bd",
3221    "xn--mgbai9azgqp6j",
3222    "xn--mgbayh7gpa",
3223    "xn--mgbb9fbpob",
3224    "xn--mgbbh1a",
3225    "xn--mgbbh1a71e",
3226    "xn--mgbc0a9azcg",
3227    "xn--mgbca7dzdo",
3228    "xn--mgberp4a5d4ar",
3229    "xn--mgbgu82a",
3230    "xn--mgbi4ecexp",
3231    "xn--mgbpl2fh",
3232    "xn--mgbt3dhd",
3233    "xn--mgbtx2b",
3234    "xn--mgbx4cd0ab",
3235    "xn--mix891f",
3236    "xn--mk1bu44c",
3237    "xn--mxtq1m",
3238    "xn--ngbc5azd",
3239    "xn--ngbe9e0a",
3240    "xn--ngbrx",
3241    "xn--node",
3242    "xn--nqv7f",
3243    "xn--nqv7fs00ema",
3244    "xn--nyqy26a",
3245    "xn--o3cw4h",
3246    "xn--ogbpf8fl",
3247    "xn--p1acf",
3248    "xn--p1ai",
3249    "xn--pbt977c",
3250    "xn--pgbs0dh",
3251    "xn--pssy2u",
3252    "xn--q9jyb4c",
3253    "xn--qcka1pmc",
3254    "xn--qxam",
3255    "xn--rhqv96g",
3256    "xn--rovu88b",
3257    "xn--rvc1e0am3e",
3258    "xn--s9brj9c",
3259    "xn--ses554g",
3260    "xn--t60b56a",
3261    "xn--tckwe",
3262    "xn--tiq49xqyj",
3263    "xn--unup4y",
3264    "xn--vermgensberater-ctb",
3265    "xn--vermgensberatung-pwb",
3266    "xn--vhquv",
3267    "xn--vuq861b",
3268    "xn--w4r85el8fhu5dnra",
3269    "xn--w4rs40l",
3270    "xn--wgbh1c",
3271    "xn--wgbl6a",
3272    "xn--xhq521b",
3273    "xn--xkc2al3hye2a",
3274    "xn--xkc2dl3a5ee0h",
3275    "xn--y9a3aq",
3276    "xn--yfro4i67o",
3277    "xn--ygbi2ammx",
3278    "xn--zfr164b",
3279    "xperia",
3280    "xxx",
3281    "xyz",
3282    # y
3283    "yachts",
3284    "yahoo",
3285    "yamaxun",
3286    "yandex",
3287    "ye",
3288    "yodobashi",
3289    "yoga",
3290    "yokohama",
3291    "you",
3292    "youtube",
3293    "yt",
3294    "yun",
3295    # z
3296    "za",
3297    "zappos",
3298    "zara",
3299    "zero",
3300    "zip",
3301    "zippo",
3302    "zm",
3303    "zone",
3304    "zuerich",
3305    "zw",
3306]
3307
3308
3309class IS_HTTP_URL(Validator):
3310    """
3311    Rejects a URL string if any of the following is true:
3312       * The string is empty or None
3313       * The string uses characters that are not allowed in a URL
3314       * The string breaks any of the HTTP syntactic rules
3315       * The URL scheme specified (if one is specified) is not 'http' or 'https'
3316       * The top-level domain (if a host name is specified) does not exist
3317
3318    Based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html
3319
3320    This function only checks the URL's syntax. It does not check that the URL
3321    points to a real document, for example, or that it otherwise makes sense
3322    semantically. This function does automatically prepend 'http://' in front
3323    of a URL in the case of an abbreviated URL (e.g. 'google.ca').
3324
3325    The list of allowed schemes is customizable with the allowed_schemes
3326    parameter. If you exclude None from the list, then abbreviated URLs
3327    (lacking a scheme such as 'http') will be rejected.
3328
3329    The default prepended scheme is customizable with the prepend_scheme
3330    parameter. If you set prepend_scheme to None then prepending will be
3331    disabled. URLs that require prepending to parse will still be accepted,
3332    but the return value will not be modified.
3333
3334    @author: Jonathan Benn
3335
3336        >>> IS_HTTP_URL()('http://1.2.3.4')
3337        ('http://1.2.3.4', None)
3338        >>> IS_HTTP_URL()('http://abc.com')
3339        ('http://abc.com', None)
3340        >>> IS_HTTP_URL()('https://abc.com')
3341        ('https://abc.com', None)
3342        >>> IS_HTTP_URL()('httpx://abc.com')
3343        ('httpx://abc.com', 'enter a valid URL')
3344        >>> IS_HTTP_URL()('http://abc.com:80')
3345        ('http://abc.com:80', None)
3346        >>> IS_HTTP_URL()('http://user@abc.com')
3347        ('http://user@abc.com', None)
3348        >>> IS_HTTP_URL()('http://user@1.2.3.4')
3349        ('http://user@1.2.3.4', None)
3350
3351    Args:
3352        error_message: a string, the error message to give the end user
3353            if the URL does not validate
3354        allowed_schemes: a list containing strings or None. Each element
3355            is a scheme the inputed URL is allowed to use
3356        prepend_scheme: a string, this scheme is prepended if it's
3357            necessary to make the URL valid
3358    """
3359
3360    REGEX_GENERIC_VALID_IP = r"([\w.!~*'|;:&=+$,-]+@)?\d+\.\d+\.\d+\.\d+(:\d*)*$"
3361    REGEX_GENERIC_VALID_DOMAIN = 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*)*$"
3362
3363    def __init__(
3364        self,
3365        error_message="Enter a valid URL",
3366        allowed_schemes=None,
3367        prepend_scheme="http",
3368        allowed_tlds=None,
3369    ):
3370
3371        self.error_message = error_message
3372        if allowed_schemes is None:
3373            self.allowed_schemes = http_schemes
3374        else:
3375            self.allowed_schemes = allowed_schemes
3376        if allowed_tlds is None:
3377            self.allowed_tlds = official_top_level_domains
3378        else:
3379            self.allowed_tlds = allowed_tlds
3380        self.prepend_scheme = prepend_scheme
3381
3382        for i in self.allowed_schemes:
3383            if i not in http_schemes:
3384                raise SyntaxError(
3385                    "allowed_scheme value '%s' is not in %s" % (i, http_schemes)
3386                )
3387
3388        if self.prepend_scheme not in self.allowed_schemes:
3389            raise SyntaxError(
3390                "prepend_scheme='%s' is not in allowed_schemes=%s"
3391                % (self.prepend_scheme, self.allowed_schemes)
3392            )
3393
3394    def validate(self, value, record_id=None):
3395        """
3396        Args:
3397            value: a string, the URL to validate
3398
3399        Returns:
3400            a tuple, where tuple[0] is the inputed value
3401            (possible prepended with prepend_scheme), and tuple[1] is either
3402            None (success!) or the string error_message
3403        """
3404        try:
3405            # if the URL passes generic validation
3406            x = IS_GENERIC_URL(
3407                error_message=self.error_message,
3408                allowed_schemes=self.allowed_schemes,
3409                prepend_scheme=self.prepend_scheme,
3410            )
3411            if x(value)[1] is None:
3412                components = urlparse.urlparse(value)
3413                authority = components.netloc
3414                # if there is an authority component
3415                if authority:
3416                    # if authority is a valid IP address
3417                    if re.match(self.REGEX_GENERIC_VALID_IP, authority):
3418                        # Then this HTTP URL is valid
3419                        return value
3420                    else:
3421                        # else if authority is a valid domain name
3422                        domainMatch = re.match(
3423                            self.REGEX_GENERIC_VALID_DOMAIN, authority
3424                        )
3425                        if domainMatch:
3426                            # if the top-level domain really exists
3427                            if domainMatch.group(5).lower() in self.allowed_tlds:
3428                                # Then this HTTP URL is valid
3429                                return value
3430                else:
3431                    # else this is a relative/abbreviated URL, which will parse
3432                    # into the URL's path component
3433                    path = components.path
3434                    # relative case: if this is a valid path (if it starts with
3435                    # a slash)
3436                    if not path.startswith("/"):
3437                        # abbreviated case: if we haven't already, prepend a
3438                        # scheme and see if it fixes the problem
3439                        if "://" not in value and None in self.allowed_schemes:
3440                            schemeToUse = self.prepend_scheme or "http"
3441                            new_value = self.validate(schemeToUse + "://" + value)
3442                            return new_value if self.prepend_scheme else value
3443                    return value
3444        except:
3445            pass
3446        raise ValidationError(self.translator(self.error_message))
3447
3448
3449class IS_URL(Validator):
3450    """
3451    Rejects a URL string if any of the following is true:
3452
3453       * The string is empty or None
3454       * The string uses characters that are not allowed in a URL
3455       * The string breaks any of the HTTP syntactic rules
3456       * The URL scheme specified (if one is specified) is not 'http' or 'https'
3457       * The top-level domain (if a host name is specified) does not exist
3458
3459    (These rules are based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html)
3460
3461    This function only checks the URL's syntax. It does not check that the URL
3462    points to a real document, for example, or that it otherwise makes sense
3463    semantically. This function does automatically prepend 'http://' in front
3464    of a URL in the case of an abbreviated URL (e.g. 'google.ca').
3465
3466    If the parameter mode='generic' is used, then this function's behavior
3467    changes. It then rejects a URL string if any of the following is true:
3468
3469       * The string is empty or None
3470       * The string uses characters that are not allowed in a URL
3471       * The URL scheme specified (if one is specified) is not valid
3472
3473    (These rules are based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html)
3474
3475    The list of allowed schemes is customizable with the allowed_schemes
3476    parameter. If you exclude None from the list, then abbreviated URLs
3477    (lacking a scheme such as 'http') will be rejected.
3478
3479    The default prepended scheme is customizable with the prepend_scheme
3480    parameter. If you set prepend_scheme to None then prepending will be
3481    disabled. URLs that require prepending to parse will still be accepted,
3482    but the return value will not be modified.
3483
3484    IS_URL is compatible with the Internationalized Domain Name (IDN) standard
3485    specified in RFC 3490 (http://tools.ietf.org/html/rfc3490). As a result,
3486    URLs can be regular strings or unicode strings.
3487    If the URL's domain component (e.g. google.ca) contains non-US-ASCII
3488    letters, then the domain will be converted into Punycode (defined in
3489    RFC 3492, http://tools.ietf.org/html/rfc3492). IS_URL goes a bit beyond
3490    the standards, and allows non-US-ASCII characters to be present in the path
3491    and query components of the URL as well. These non-US-ASCII characters will
3492    be escaped using the standard '%20' type syntax. e.g. the unicode
3493    character with hex code 0x4e86 will become '%4e%86'
3494
3495    Args:
3496        error_message: a string, the error message to give the end user
3497            if the URL does not validate
3498        allowed_schemes: a list containing strings or None. Each element
3499            is a scheme the inputed URL is allowed to use
3500        prepend_scheme: a string, this scheme is prepended if it's
3501            necessary to make the URL valid
3502
3503    Code Examples::
3504
3505        INPUT(_type='text', _name='name', requires=IS_URL())
3506        >>> IS_URL()('abc.com')
3507        ('http://abc.com', None)
3508
3509        INPUT(_type='text', _name='name', requires=IS_URL(mode='generic'))
3510        >>> IS_URL(mode='generic')('abc.com')
3511        ('abc.com', None)
3512
3513        INPUT(_type='text', _name='name',
3514            requires=IS_URL(allowed_schemes=['https'], prepend_scheme='https'))
3515        >>> IS_URL(allowed_schemes=['https'], prepend_scheme='https')('https://abc.com')
3516        ('https://abc.com', None)
3517
3518        INPUT(_type='text', _name='name',
3519            requires=IS_URL(prepend_scheme='https'))
3520        >>> IS_URL(prepend_scheme='https')('abc.com')
3521        ('https://abc.com', None)
3522
3523        INPUT(_type='text', _name='name',
3524            requires=IS_URL(mode='generic', allowed_schemes=['ftps', 'https'],
3525                prepend_scheme='https'))
3526        >>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], prepend_scheme='https')('https://abc.com')
3527        ('https://abc.com', None)
3528        >>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https', None], prepend_scheme='https')('abc.com')
3529        ('abc.com', None)
3530
3531    @author: Jonathan Benn
3532    """
3533
3534    def __init__(
3535        self,
3536        error_message="Enter a valid URL",
3537        mode="http",
3538        allowed_schemes=None,
3539        prepend_scheme="http",
3540        allowed_tlds=None,
3541    ):
3542
3543        self.error_message = error_message
3544        self.mode = mode.lower()
3545        if self.mode not in ["generic", "http"]:
3546            raise SyntaxError("invalid mode '%s' in IS_URL" % self.mode)
3547        self.allowed_schemes = allowed_schemes
3548        if allowed_tlds is None:
3549            self.allowed_tlds = official_top_level_domains
3550        else:
3551            self.allowed_tlds = allowed_tlds
3552
3553        if self.allowed_schemes:
3554            if prepend_scheme not in self.allowed_schemes:
3555                raise SyntaxError(
3556                    "prepend_scheme='%s' is not in allowed_schemes=%s"
3557                    % (prepend_scheme, self.allowed_schemes)
3558                )
3559
3560        # if allowed_schemes is None, then we will defer testing
3561        # prepend_scheme's validity to a sub-method
3562
3563        self.prepend_scheme = prepend_scheme
3564
3565    def validate(self, value, record_id=None):
3566        """
3567        Args:
3568            value: a unicode or regular string, the URL to validate
3569
3570        Returns:
3571            a (string, string) tuple, where tuple[0] is the modified
3572            input value and tuple[1] is either None (success!) or the
3573            string error_message. The input value will never be modified in the
3574            case of an error. However, if there is success then the input URL
3575            may be modified to (1) prepend a scheme, and/or (2) convert a
3576            non-compliant unicode URL into a compliant US-ASCII version.
3577        """
3578        if self.mode == "generic":
3579            subMethod = IS_GENERIC_URL(
3580                error_message=self.error_message,
3581                allowed_schemes=self.allowed_schemes,
3582                prepend_scheme=self.prepend_scheme,
3583            )
3584        elif self.mode == "http":
3585            subMethod = IS_HTTP_URL(
3586                error_message=self.error_message,
3587                allowed_schemes=self.allowed_schemes,
3588                prepend_scheme=self.prepend_scheme,
3589                allowed_tlds=self.allowed_tlds,
3590            )
3591        else:
3592            raise SyntaxError("invalid mode '%s' in IS_URL" % self.mode)
3593
3594        if isinstance(value, unicodeT):
3595            try:
3596                value = unicode_to_ascii_url(value, self.prepend_scheme)
3597            except Exception as e:
3598                # If we are not able to convert the unicode url into a
3599                # US-ASCII URL, then the URL is not valid
3600                raise ValidationError(self.translator(self.error_message))
3601        return subMethod.validate(value, record_id)
3602
3603
3604class IS_TIME(Validator):
3605    """
3606    Example:
3607        Use as::
3608
3609            INPUT(_type='text', _name='name', requires=IS_TIME())
3610
3611    understands the following formats
3612    hh:mm:ss [am/pm]
3613    hh:mm [am/pm]
3614    hh [am/pm]
3615
3616    [am/pm] is optional, ':' can be replaced by any other non-space non-digit::
3617
3618        >>> IS_TIME()('21:30')
3619        (datetime.time(21, 30), None)
3620        >>> IS_TIME()('21-30')
3621        (datetime.time(21, 30), None)
3622        >>> IS_TIME()('21.30')
3623        (datetime.time(21, 30), None)
3624        >>> IS_TIME()('21:30:59')
3625        (datetime.time(21, 30, 59), None)
3626        >>> IS_TIME()('5:30')
3627        (datetime.time(5, 30), None)
3628        >>> IS_TIME()('5:30 am')
3629        (datetime.time(5, 30), None)
3630        >>> IS_TIME()('5:30 pm')
3631        (datetime.time(17, 30), None)
3632        >>> IS_TIME()('5:30 whatever')
3633        ('5:30 whatever', 'enter time as hh:mm:ss (seconds, am, pm optional)')
3634        >>> IS_TIME()('5:30 20')
3635        ('5:30 20', 'enter time as hh:mm:ss (seconds, am, pm optional)')
3636        >>> IS_TIME()('24:30')
3637        ('24:30', 'enter time as hh:mm:ss (seconds, am, pm optional)')
3638        >>> IS_TIME()('21:60')
3639        ('21:60', 'enter time as hh:mm:ss (seconds, am, pm optional)')
3640        >>> IS_TIME()('21:30::')
3641        ('21:30::', 'enter time as hh:mm:ss (seconds, am, pm optional)')
3642        >>> IS_TIME()('')
3643        ('', 'enter time as hh:mm:ss (seconds, am, pm optional)')ù
3644
3645    """
3646
3647    REGEX_TIME = "((?P<h>[0-9]+))([^0-9 ]+(?P<m>[0-9 ]+))?([^0-9ap ]+(?P<s>[0-9]*))?((?P<d>[ap]m))?"
3648
3649    def __init__(
3650        self, error_message="Enter time as hh:mm:ss (seconds, am, pm optional)"
3651    ):
3652        self.error_message = error_message
3653
3654    def validate(self, value, record_id=None):
3655        try:
3656            ivalue = value
3657            value = re.match(self.REGEX_TIME, value.lower())
3658            (h, m, s) = (int(value.group("h")), 0, 0)
3659            if not value.group("m") is None:
3660                m = int(value.group("m"))
3661            if not value.group("s") is None:
3662                s = int(value.group("s"))
3663            if value.group("d") == "pm" and 0 < h < 12:
3664                h += 12
3665            if value.group("d") == "am" and h == 12:
3666                h = 0
3667            if not (h in range(24) and m in range(60) and s in range(60)):
3668                raise ValueError(
3669                    "Hours or minutes or seconds are outside of allowed range"
3670                )
3671            value = datetime.time(h, m, s)
3672            return value
3673        except Exception:
3674            raise ValidationError(self.translator(self.error_message))
3675
3676
3677# A UTC class.
3678class UTC(datetime.tzinfo):
3679    """UTC"""
3680
3681    ZERO = datetime.timedelta(0)
3682
3683    def utcoffset(self, dt):
3684        return UTC.ZERO
3685
3686    def tzname(self, dt):
3687        return "UTC"
3688
3689    def dst(self, dt):
3690        return UTC.ZERO
3691
3692
3693utc = UTC()
3694
3695
3696class IS_DATE(Validator):
3697    """
3698    Examples:
3699        Use as::
3700
3701            INPUT(_type='text', _name='name', requires=IS_DATE())
3702
3703    date has to be in the ISO8960 format YYYY-MM-DD
3704    """
3705
3706    def __init__(self, format="%Y-%m-%d", error_message="Enter date as %(format)s"):
3707        self.format = self.translator(format)
3708        self.error_message = str(error_message)
3709        self.extremes = {}
3710
3711    def validate(self, value, record_id=None):
3712        if isinstance(value, datetime.date):
3713            return value
3714        try:
3715            (y, m, d, hh, mm, ss, t0, t1, t2) = time.strptime(value, str(self.format))
3716            value = datetime.date(y, m, d)
3717            return value
3718        except:
3719            self.extremes.update(IS_DATETIME.nice(self.format))
3720            raise ValidationError(self.translator(self.error_message) % self.extremes)
3721
3722    def formatter(self, value):
3723        if value is None or value == "":
3724            return None
3725        format = self.format
3726        year = value.year
3727        y = "%.4i" % year
3728        format = format.replace("%y", y[-2:])
3729        format = format.replace("%Y", y)
3730        if year < 1900:
3731            year = 2000
3732        d = datetime.date(year, value.month, value.day)
3733        return d.strftime(format)
3734
3735
3736class IS_DATETIME(Validator):
3737    """
3738    Examples:
3739        Use as::
3740
3741            INPUT(_type='text', _name='name', requires=IS_DATETIME())
3742
3743    datetime has to be in the ISO8960 format YYYY-MM-DD hh:mm:ss
3744    timezome must be None or a pytz.timezone("America/Chicago") object
3745    """
3746
3747    isodatetime = "%Y-%m-%d %H:%M:%S"
3748
3749    @staticmethod
3750    def nice(format):
3751        code = (
3752            ("%Y", "1963"),
3753            ("%y", "63"),
3754            ("%d", "28"),
3755            ("%m", "08"),
3756            ("%b", "Aug"),
3757            ("%B", "August"),
3758            ("%H", "14"),
3759            ("%I", "02"),
3760            ("%p", "PM"),
3761            ("%M", "30"),
3762            ("%S", "59"),
3763        )
3764        for (a, b) in code:
3765            format = format.replace(a, b)
3766        return dict(format=format)
3767
3768    def __init__(
3769        self,
3770        format="%Y-%m-%d %H:%M:%S",
3771        error_message="Enter date and time as %(format)s",
3772        timezone=None,
3773    ):
3774        self.format = self.translator(format)
3775        self.error_message = str(error_message)
3776        self.extremes = {}
3777        self.timezone = timezone
3778
3779    def validate(self, value, record_id=None):
3780        if isinstance(value, datetime.datetime):
3781            return value
3782        try:
3783            if self.format == self.isodatetime:
3784                value = value.replace('T', ' ')
3785                if len(value) == 16:
3786                    value += ':00'
3787            (y, m, d, hh, mm, ss, t0, t1, t2) = time.strptime(value, str(self.format))
3788            value = datetime.datetime(y, m, d, hh, mm, ss)
3789            if self.timezone is not None:
3790                # TODO: https://github.com/web2py/web2py/issues/1094 (temporary solution)
3791                value = (
3792                    self.timezone.localize(value).astimezone(utc).replace(tzinfo=None)
3793                )
3794            return value
3795        except:
3796            self.extremes.update(IS_DATETIME.nice(self.format))
3797            raise ValidationError(self.translator(self.error_message) % self.extremes)
3798
3799    def formatter(self, value):
3800        if value is None or value == "":
3801            return None
3802        format = self.format
3803        year = value.year
3804        y = "%.4i" % year
3805        format = format.replace("%y", y[-2:])
3806        format = format.replace("%Y", y)
3807        if year < 1900:
3808            year = 2000
3809        d = datetime.datetime(
3810            year, value.month, value.day, value.hour, value.minute, value.second
3811        )
3812        if self.timezone is not None:
3813            d = d.replace(tzinfo=utc).astimezone(self.timezone)
3814        return d.strftime(format)
3815
3816
3817class IS_DATE_IN_RANGE(IS_DATE):
3818    """
3819    Examples:
3820        Use as::
3821
3822            >>> v = IS_DATE_IN_RANGE(minimum=datetime.date(2008,1,1), \
3823                                     maximum=datetime.date(2009,12,31), \
3824                                     format="%m/%d/%Y",error_message="Oops")
3825
3826            >>> v('03/03/2008')
3827            (datetime.date(2008, 3, 3), None)
3828
3829            >>> v('03/03/2010')
3830            ('03/03/2010', 'oops')
3831
3832            >>> v(datetime.date(2008,3,3))
3833            (datetime.date(2008, 3, 3), None)
3834
3835            >>> v(datetime.date(2010,3,3))
3836            (datetime.date(2010, 3, 3), 'oops')
3837
3838    """
3839
3840    def __init__(
3841        self, minimum=None, maximum=None, format="%Y-%m-%d", error_message=None
3842    ):
3843        self.minimum = minimum
3844        self.maximum = maximum
3845        if error_message is None:
3846            if minimum is None:
3847                error_message = "Enter date on or before %(max)s"
3848            elif maximum is None:
3849                error_message = "Enter date on or after %(min)s"
3850            else:
3851                error_message = "Enter date in range %(min)s %(max)s"
3852        IS_DATE.__init__(self, format=format, error_message=error_message)
3853        self.extremes = dict(min=self.formatter(minimum), max=self.formatter(maximum))
3854
3855    def validate(self, value, record_id=None):
3856        value = IS_DATE.validate(self, value, record_id=None)
3857        if self.minimum and self.minimum > value:
3858            raise ValidationError(self.translator(self.error_message) % self.extremes)
3859        if self.maximum and value > self.maximum:
3860            raise ValidationError(self.translator(self.error_message) % self.extremes)
3861        return value
3862
3863
3864class IS_DATETIME_IN_RANGE(IS_DATETIME):
3865    """
3866    Examples:
3867        Use as::
3868            >>> v = IS_DATETIME_IN_RANGE(\
3869                    minimum=datetime.datetime(2008,1,1,12,20), \
3870                    maximum=datetime.datetime(2009,12,31,12,20), \
3871                    format="%m/%d/%Y %H:%M",error_message="Oops")
3872            >>> v('03/03/2008 12:40')
3873            (datetime.datetime(2008, 3, 3, 12, 40), None)
3874
3875            >>> v('03/03/2010 10:34')
3876            ('03/03/2010 10:34', 'oops')
3877
3878            >>> v(datetime.datetime(2008,3,3,0,0))
3879            (datetime.datetime(2008, 3, 3, 0, 0), None)
3880
3881            >>> v(datetime.datetime(2010,3,3,0,0))
3882            (datetime.datetime(2010, 3, 3, 0, 0), 'oops')
3883
3884    """
3885
3886    def __init__(
3887        self,
3888        minimum=None,
3889        maximum=None,
3890        format="%Y-%m-%d %H:%M:%S",
3891        error_message=None,
3892        timezone=None,
3893    ):
3894        self.minimum = minimum
3895        self.maximum = maximum
3896        if error_message is None:
3897            if minimum is None:
3898                error_message = "Enter date and time on or before %(max)s"
3899            elif maximum is None:
3900                error_message = "Enter date and time on or after %(min)s"
3901            else:
3902                error_message = "Enter date and time in range %(min)s %(max)s"
3903        IS_DATETIME.__init__(
3904            self, format=format, error_message=error_message, timezone=timezone
3905        )
3906        self.extremes = dict(min=self.formatter(minimum), max=self.formatter(maximum))
3907
3908    def validate(self, value, record_id=None):
3909        value = IS_DATETIME.validate(self, value, record_id=None)
3910        if self.minimum and self.minimum > value:
3911            raise ValidationError(self.translator(self.error_message) % self.extremes)
3912        if self.maximum and value > self.maximum:
3913            raise ValidationError(self.translator(self.error_message) % self.extremes)
3914        return value
3915
3916
3917class IS_LIST_OF(Validator):
3918    def __init__(self, other=None, minimum=None, maximum=None, error_message=None):
3919        self.other = other
3920        self.minimum = minimum
3921        self.maximum = maximum
3922        self.error_message = error_message
3923
3924    def validate(self, value, record_id=None):
3925        ivalue = value
3926        if not isinstance(value, list):
3927            ivalue = [ivalue]
3928        ivalue = [i for i in ivalue if str(i).strip()]
3929        if self.minimum is not None and len(ivalue) < self.minimum:
3930            raise ValidationError(
3931                self.translator(self.error_message or "Minimum length is %(min)s")
3932                % dict(min=self.minimum, max=self.maximum)
3933            )
3934        if self.maximum is not None and len(ivalue) > self.maximum:
3935            raise ValidationError(
3936                self.translator(self.error_message or "Maximum length is %(max)s")
3937                % dict(min=self.minimum, max=self.maximum)
3938            )
3939        new_value = []
3940        other = self.other
3941        if self.other:
3942            if not isinstance(other, (list, tuple)):
3943                other = [other]
3944            for item in ivalue:
3945                v = item
3946                for validator in other:
3947                    v = validator_caller(validator, v, record_id)
3948                new_value.append(v)
3949            ivalue = new_value
3950        return ivalue
3951
3952
3953class IS_LOWER(Validator):
3954    """
3955    Converts to lowercase::
3956
3957        >>> IS_LOWER()('ABC')
3958        ('abc', None)
3959        >>> IS_LOWER()('Ñ')
3960        ('\\xc3\\xb1', None)
3961
3962    """
3963
3964    def validate(self, value, record_id=None):
3965        cast_back = lambda x: x
3966        if isinstance(value, str):
3967            cast_back = to_native
3968        elif isinstance(value, bytes):
3969            cast_back = to_bytes
3970        value = to_unicode(value).lower()
3971        return cast_back(value)
3972
3973
3974class IS_UPPER(Validator):
3975    """
3976    Converts to uppercase::
3977
3978        >>> IS_UPPER()('abc')
3979        ('ABC', None)
3980        >>> IS_UPPER()('ñ')
3981        ('\\xc3\\x91', None)
3982
3983    """
3984
3985    def validate(self, value, record_id=None):
3986        cast_back = lambda x: x
3987        if isinstance(value, str):
3988            cast_back = to_native
3989        elif isinstance(value, bytes):
3990            cast_back = to_bytes
3991        value = to_unicode(value).upper()
3992        return cast_back(value)
3993
3994
3995def urlify(s, maxlen=80, keep_underscores=False):
3996    """
3997    Converts incoming string to a simplified ASCII subset.
3998    if (keep_underscores): underscores are retained in the string
3999    else: underscores are translated to hyphens (default)
4000    """
4001    s = to_unicode(s)  # to unicode
4002    s = s.lower()  # to lowercase
4003    s = unicodedata.normalize("NFKD", s)  # replace special characters
4004    s = to_native(s, charset="ascii", errors="ignore")  # encode as ASCII
4005    s = re.sub(r"&\w+?;", "", s)  # strip html entities
4006    if keep_underscores:
4007        s = re.sub(r"\s+", "-", s)  # whitespace to hyphens
4008        s = re.sub(r"[^\w\-]", "", s)
4009        # strip all but alphanumeric/underscore/hyphen
4010    else:
4011        s = re.sub(r"[\s_]+", "-", s)  # whitespace & underscores to hyphens
4012        s = re.sub(r"[^a-z0-9\-]", "", s)  # strip all but alphanumeric/hyphen
4013    s = re.sub(r"[-_][-_]+", "-", s)  # collapse strings of hyphens
4014    s = s.strip("-")  # remove leading and trailing hyphens
4015    return s[:maxlen]  # enforce maximum length
4016
4017
4018class IS_SLUG(Validator):
4019    """
4020    converts arbitrary text string to a slug::
4021
4022        >>> IS_SLUG()('abc123')
4023        ('abc123', None)
4024        >>> IS_SLUG()('ABC123')
4025        ('abc123', None)
4026        >>> IS_SLUG()('abc-123')
4027        ('abc-123', None)
4028        >>> IS_SLUG()('abc--123')
4029        ('abc-123', None)
4030        >>> IS_SLUG()('abc 123')
4031        ('abc-123', None)
4032        >>> IS_SLUG()('abc\t_123')
4033        ('abc-123', None)
4034        >>> IS_SLUG()('-abc-')
4035        ('abc', None)
4036        >>> IS_SLUG()('--a--b--_ -c--')
4037        ('a-b-c', None)
4038        >>> IS_SLUG()('abc&amp;123')
4039        ('abc123', None)
4040        >>> IS_SLUG()('abc&amp;123&amp;def')
4041        ('abc123def', None)
4042        >>> IS_SLUG()('ñ')
4043        ('n', None)
4044        >>> IS_SLUG(maxlen=4)('abc123')
4045        ('abc1', None)
4046        >>> IS_SLUG()('abc_123')
4047        ('abc-123', None)
4048        >>> IS_SLUG(keep_underscores=False)('abc_123')
4049        ('abc-123', None)
4050        >>> IS_SLUG(keep_underscores=True)('abc_123')
4051        ('abc_123', None)
4052        >>> IS_SLUG(check=False)('abc')
4053        ('abc', None)
4054        >>> IS_SLUG(check=True)('abc')
4055        ('abc', None)
4056        >>> IS_SLUG(check=False)('a bc')
4057        ('a-bc', None)
4058        >>> IS_SLUG(check=True)('a bc')
4059        ('a bc', 'must be slug')
4060    """
4061
4062    @staticmethod
4063    def urlify(value, maxlen=80, keep_underscores=False):
4064        return urlify(value, maxlen, keep_underscores)
4065
4066    def __init__(
4067        self,
4068        maxlen=80,
4069        check=False,
4070        error_message="Must be slug",
4071        keep_underscores=False,
4072    ):
4073        self.maxlen = maxlen
4074        self.check = check
4075        self.error_message = error_message
4076        self.keep_underscores = keep_underscores
4077
4078    def validate(self, value, record_id=None):
4079        if self.check and value != urlify(value, self.maxlen, self.keep_underscores):
4080            raise ValidationError(self.translator(self.error_message))
4081        return urlify(value, self.maxlen, self.keep_underscores)
4082
4083
4084class ANY_OF(Validator):
4085    """
4086    Tests if any of the validators in a list returns successfully::
4087
4088        >>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('a@b.co')
4089        ('a@b.co', None)
4090        >>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('abco')
4091        ('abco', None)
4092        >>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('@ab.co')
4093        ('@ab.co', 'enter only letters, numbers, and underscore')
4094        >>> ANY_OF([IS_ALPHANUMERIC(),IS_EMAIL()])('@ab.co')
4095        ('@ab.co', 'enter a valid email address')
4096
4097    """
4098
4099    def __init__(self, subs, error_message=None):
4100        self.subs = subs
4101        self.error_message = error_message
4102
4103    def validate(self, value, record_id=None):
4104        for validator in self.subs:
4105            v, e = validator(value)
4106            if not e:
4107                return v
4108        raise ValidationError(e)
4109
4110    def formatter(self, value):
4111        # Use the formatter of the first subvalidator
4112        # that validates the value and has a formatter
4113        for validator in self.subs:
4114            if hasattr(validator, "formatter") and validator(value)[1] is None:
4115                return validator.formatter(value)
4116
4117
4118class IS_EMPTY_OR(Validator):
4119    """
4120    Dummy class for testing IS_EMPTY_OR::
4121
4122        >>> IS_EMPTY_OR(IS_EMAIL())('abc@def.com')
4123        ('abc@def.com', None)
4124        >>> IS_EMPTY_OR(IS_EMAIL())('   ')
4125        (None, None)
4126        >>> IS_EMPTY_OR(IS_EMAIL(), null='abc')('   ')
4127        ('abc', None)
4128        >>> IS_EMPTY_OR(IS_EMAIL(), null='abc', empty_regex='def')('def')
4129        ('abc', None)
4130        >>> IS_EMPTY_OR(IS_EMAIL())('abc')
4131        ('abc', 'enter a valid email address')
4132        >>> IS_EMPTY_OR(IS_EMAIL())(' abc ')
4133        ('abc', 'enter a valid email address')
4134    """
4135
4136    def __init__(self, other, null=None, empty_regex=None):
4137        (self.other, self.null) = (other, null)
4138        if empty_regex is not None:
4139            self.empty_regex = re.compile(empty_regex)
4140        else:
4141            self.empty_regex = None
4142        if hasattr(other, "multiple"):
4143            self.multiple = other.multiple
4144        if hasattr(other, "options"):
4145            self.options = self._options
4146
4147    def _options(self, *args, **kwargs):
4148        options = self.other.options(*args, **kwargs)
4149        if (not options or options[0][0] != "") and not self.multiple:
4150            options.insert(0, ("", ""))
4151        return options
4152
4153    def set_self_id(self, id):
4154        if isinstance(self.other, (list, tuple)):
4155            for item in self.other:
4156                if hasattr(item, "set_self_id"):
4157                    item.set_self_id(id)
4158        else:
4159            if hasattr(self.other, "set_self_id"):
4160                self.other.set_self_id(id)
4161
4162    def validate(self, value, record_id=None):
4163        value, empty = is_empty(value, empty_regex=self.empty_regex)
4164        if empty:
4165            return self.null
4166        if isinstance(self.other, (list, tuple)):
4167            for item in self.other:
4168                value = validator_caller(item, value, record_id)
4169            return value
4170        return validator_caller(self.other, value, record_id)
4171
4172    def formatter(self, value):
4173        if hasattr(self.other, "formatter"):
4174            return self.other.formatter(value)
4175        return value
4176
4177
4178IS_NULL_OR = IS_EMPTY_OR  # for backward compatibility
4179
4180
4181class CLEANUP(Validator):
4182    """
4183    Examples:
4184        Use as::
4185
4186            INPUT(_type='text', _name='name', requires=CLEANUP())
4187
4188    removes special characters on validation
4189    """
4190
4191    REGEX_CLEANUP = "[^\x09\x0a\x0d\x20-\x7e]"
4192
4193    def __init__(self, regex=None):
4194        self.regex = (
4195            re.compile(self.REGEX_CLEANUP) if regex is None else re.compile(regex)
4196        )
4197
4198    def validate(self, value, record_id=None):
4199        v = self.regex.sub("", str(value).strip())
4200        return v
4201
4202
4203def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None):
4204    hashfunc = hashfunc or hashlib.sha1
4205    hmac = hashlib.pbkdf2_hmac(
4206        hashfunc().name, to_bytes(data), to_bytes(salt), iterations, keylen
4207    )
4208    return binascii.hexlify(hmac)
4209
4210
4211def simple_hash(text, key="", salt="", digest_alg="md5"):
4212    """Generate hash with the given text using the specified digest algorithm."""
4213    text = to_bytes(text)
4214    key = to_bytes(key)
4215    salt = to_bytes(salt)
4216    if not digest_alg:
4217        raise RuntimeError("simple_hash with digest_alg=None")
4218    elif not isinstance(digest_alg, str):  # manual approach
4219        h = digest_alg(text + key + salt)
4220    elif digest_alg.startswith("pbkdf2"):  # latest and coolest!
4221        iterations, keylen, alg = digest_alg[7:-1].split(",")
4222        return to_native(
4223            pbkdf2_hex(text, salt, int(iterations), int(keylen), get_digest(alg))
4224        )
4225    elif key:  # use hmac
4226        digest_alg = get_digest(digest_alg)
4227        h = hmac.new(key + salt, text, digest_alg)
4228    else:  # compatible with third party systems
4229        h = get_digest(digest_alg)()
4230        h.update(text + salt)
4231    return h.hexdigest()
4232
4233
4234def get_digest(value):
4235    """Return a hashlib digest algorithm from a string."""
4236    if isinstance(value, str):
4237        value = value.lower()
4238        if value not in ("md5", "sha1", "sha224", "sha256", "sha384", "sha512"):
4239            raise ValueError("Invalid digest algorithm: %s" % value)
4240        value = getattr(hashlib, value)
4241    return value
4242
4243
4244DIGEST_ALG_BY_SIZE = {
4245    128 // 4: "md5",
4246    160 // 4: "sha1",
4247    224 // 4: "sha224",
4248    256 // 4: "sha256",
4249    384 // 4: "sha384",
4250    512 // 4: "sha512",
4251}
4252
4253
4254class LazyCrypt(object):
4255    """
4256    Stores a lazy password hash
4257    """
4258
4259    def __init__(self, crypt, password):
4260        """
4261        crypt is an instance of the CRYPT validator,
4262        password is the password as inserted by the user
4263        """
4264        self.crypt = crypt
4265        self.password = password
4266        self.crypted = None
4267
4268    def __str__(self):
4269        """
4270        Encrypted self.password and caches it in self.crypted.
4271        If self.crypt.salt the output is in the format <algorithm>$<salt>$<hash>
4272
4273        Try get the digest_alg from the key (if it exists)
4274        else assume the default digest_alg. If not key at all, set key=''
4275
4276        If a salt is specified use it, if salt is True, set salt to uuid
4277        (this should all be backward compatible)
4278
4279        Options:
4280        key = 'uuid'
4281        key = 'md5:uuid'
4282        key = 'sha512:uuid'
4283        ...
4284        key = 'pbkdf2(1000,64,sha512):uuid' 1000 iterations and 64 chars length
4285        """
4286        if self.crypted:
4287            return self.crypted
4288        if self.crypt.key:
4289            if ":" in self.crypt.key:
4290                digest_alg, key = self.crypt.key.split(":", 1)
4291            else:
4292                digest_alg, key = self.crypt.digest_alg, self.crypt.key
4293        else:
4294            digest_alg, key = self.crypt.digest_alg, ""
4295        if self.crypt.salt:
4296            if self.crypt.salt is True:
4297                salt = str(uuid.uuid4()).replace("-", "")[-16:]
4298            else:
4299                salt = self.crypt.salt
4300        else:
4301            salt = ""
4302        hashed = simple_hash(self.password, key, salt, digest_alg)
4303        self.crypted = "%s$%s$%s" % (digest_alg, salt, hashed)
4304        return self.crypted
4305
4306    def __eq__(self, stored_password):
4307        """
4308        compares the current lazy crypted password with a stored password
4309        """
4310
4311        # LazyCrypt objects comparison
4312        if isinstance(stored_password, self.__class__):
4313            return (self is stored_password) or (
4314                (self.crypt.key == stored_password.crypt.key)
4315                and (self.password == stored_password.password)
4316            )
4317
4318        if self.crypt.key:
4319            if ":" in self.crypt.key:
4320                key = self.crypt.key.split(":")[1]
4321            else:
4322                key = self.crypt.key
4323        else:
4324            key = ""
4325        if stored_password is None:
4326            return False
4327        elif stored_password.count("$") == 2:
4328            (digest_alg, salt, hash) = stored_password.split("$")
4329            h = simple_hash(self.password, key, salt, digest_alg)
4330            temp_pass = "%s$%s$%s" % (digest_alg, salt, h)
4331        else:  # no salting
4332            # guess digest_alg
4333            digest_alg = DIGEST_ALG_BY_SIZE.get(len(stored_password), None)
4334            if not digest_alg:
4335                return False
4336            else:
4337                temp_pass = simple_hash(self.password, key, "", digest_alg)
4338        return temp_pass == stored_password
4339
4340    def __ne__(self, other):
4341        return not self.__eq__(other)
4342
4343
4344class CRYPT(Validator):
4345    """
4346    Examples:
4347        Use as::
4348
4349            INPUT(_type='text', _name='name', requires=CRYPT())
4350
4351    encodes the value on validation with a digest.
4352
4353    If no arguments are provided CRYPT uses the MD5 algorithm.
4354    If the key argument is provided the HMAC+MD5 algorithm is used.
4355    If the digest_alg is specified this is used to replace the
4356    MD5 with, for example, SHA512. The digest_alg can be
4357    the name of a hashlib algorithm as a string or the algorithm itself.
4358
4359    min_length is the minimal password length (default 4) - IS_STRONG for serious security
4360    error_message is the message if password is too short
4361
4362    Notice that an empty password is accepted but invalid. It will not allow login back.
4363    Stores junk as hashed password.
4364
4365    Specify an algorithm or by default we will use sha512.
4366
4367    Typical available algorithms:
4368      md5, sha1, sha224, sha256, sha384, sha512
4369
4370    If salt, it hashes a password with a salt.
4371    If salt is True, this method will automatically generate one.
4372    Either case it returns an encrypted password string in the following format:
4373
4374      <algorithm>$<salt>$<hash>
4375
4376    Important: hashed password is returned as a LazyCrypt object and computed only if needed.
4377    The LasyCrypt object also knows how to compare itself with an existing salted password
4378
4379    Supports standard algorithms
4380
4381        >>> for alg in ('md5','sha1','sha256','sha384','sha512'):
4382        ...     print(str(CRYPT(digest_alg=alg,salt=True)('test')[0]))
4383        md5$...$...
4384        sha1$...$...
4385        sha256$...$...
4386        sha384$...$...
4387        sha512$...$...
4388
4389    The syntax is always alg$salt$hash
4390
4391    Supports for pbkdf2
4392
4393        >>> alg = 'pbkdf2(1000,20,sha512)'
4394        >>> print(str(CRYPT(digest_alg=alg,salt=True)('test')[0]))
4395        pbkdf2(1000,20,sha512)$...$...
4396
4397    An optional hmac_key can be specified and it is used as salt prefix
4398
4399        >>> a = str(CRYPT(digest_alg='md5',key='mykey',salt=True)('test')[0])
4400        >>> print(a)
4401        md5$...$...
4402
4403    Even if the algorithm changes the hash can still be validated
4404
4405        >>> CRYPT(digest_alg='sha1',key='mykey',salt=True)('test')[0] == a
4406        True
4407
4408    If no salt is specified CRYPT can guess the algorithms from length:
4409
4410        >>> a = str(CRYPT(digest_alg='sha1',salt=False)('test')[0])
4411        >>> a
4412        'sha1$$a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
4413        >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a
4414        True
4415        >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a[6:]
4416        True
4417        >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a
4418        True
4419        >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a[6:]
4420        True
4421        """
4422
4423    def __init__(
4424        self,
4425        key=None,
4426        digest_alg="pbkdf2(1000,20,sha512)",
4427        min_length=0,
4428        error_message="Too short",
4429        salt=True,
4430        max_length=1024,
4431    ):
4432        """
4433        important, digest_alg='md5' is not the default hashing algorithm for
4434        web2py. This is only an example of usage of this function.
4435
4436        The actual hash algorithm is determined from the key which is
4437        generated by web2py in tools.py. This defaults to hmac+sha512.
4438        """
4439        self.key = key
4440        self.digest_alg = digest_alg
4441        self.min_length = min_length
4442        self.max_length = max_length
4443        self.error_message = error_message
4444        self.salt = salt
4445
4446    def validate(self, value, record_id=None):
4447        v = value and str(value)[: self.max_length]
4448        if not v or len(v) < self.min_length:
4449            raise ValidationError(self.translator(self.error_message))
4450        if isinstance(value, LazyCrypt):
4451            return value
4452        return LazyCrypt(self, value)
4453
4454
4455#  entropy calculator for IS_STRONG
4456#
4457lowerset = frozenset(b"abcdefghijklmnopqrstuvwxyz")
4458upperset = frozenset(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ")
4459numberset = frozenset(b"0123456789")
4460sym1set = frozenset(b"!@#$%^&*() ")
4461sym2set = frozenset(b"~`-_=+[]{}\\|;:'\",.<>?/")
4462otherset = frozenset(b"".join(chr(x) if PY2 else chr(x).encode() for x in range(256)))
4463
4464
4465def calc_entropy(string):
4466    """ calculates a simple entropy for a given string """
4467    alphabet = 0  # alphabet size
4468    other = set()
4469    seen = set()
4470    lastset = None
4471    string = to_bytes(string or "")
4472    for c in string:
4473        # classify this character
4474        inset = None
4475        for cset in (lowerset, upperset, numberset, sym1set, sym2set, otherset):
4476            if c in cset:
4477                inset = cset
4478                break
4479        assert inset is not None
4480        # calculate effect of character on alphabet size
4481        if inset not in seen:
4482            seen.add(inset)
4483            alphabet += len(inset)  # credit for a new character set
4484        elif c not in other:
4485            alphabet += 1  # credit for unique characters
4486            other.add(c)
4487        if inset is not lastset:
4488            alphabet += 1  # credit for set transitions
4489            lastset = cset
4490    entropy = len(string) * math.log(alphabet or 1) / 0.6931471805599453  # math.log(2)
4491    return round(entropy, 2)
4492
4493
4494class IS_STRONG(Validator):
4495    """
4496    Examples:
4497        Use as::
4498
4499            INPUT(_type='password', _name='passwd',
4500            requires=IS_STRONG(min=10, special=2, upper=2))
4501
4502    enforces complexity requirements on a field
4503
4504        >>> IS_STRONG(es=True)('Abcd1234')
4505        ('Abcd1234',
4506         'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|')
4507        >>> IS_STRONG(es=True)('Abcd1234!')
4508        ('Abcd1234!', None)
4509        >>> IS_STRONG(es=True, entropy=1)('a')
4510        ('a', None)
4511        >>> IS_STRONG(es=True, entropy=1, min=2)('a')
4512        ('a', 'Minimum length is 2')
4513        >>> IS_STRONG(es=True, entropy=100)('abc123')
4514        ('abc123', 'Password too simple (32.35/100)')
4515        >>> IS_STRONG(es=True, entropy=100)('and')
4516        ('and', 'Password too simple (14.57/100)')
4517        >>> IS_STRONG(es=True, entropy=100)('aaa')
4518        ('aaa', 'Password too simple (14.42/100)')
4519        >>> IS_STRONG(es=True, entropy=100)('a1d')
4520        ('a1d', 'Password too simple (15.97/100)')
4521        >>> IS_STRONG(es=True, entropy=100)('añd')
4522        ('a\\xc3\\xb1d', 'Password too simple (31.26/10)')
4523
4524    """
4525
4526    def __init__(
4527        self,
4528        min=None,
4529        max=None,
4530        upper=None,
4531        lower=None,
4532        number=None,
4533        entropy=None,
4534        special=None,
4535        specials=r"~!@#$%^&*()_+-=?<>,.:;{}[]|",
4536        invalid=' "',
4537        error_message=None,
4538        es=False,
4539    ):
4540        self.entropy = entropy
4541        if entropy is None:
4542            # enforce default requirements
4543            self.min = 8 if min is None else min
4544            self.max = max  # was 20, but that doesn't make sense
4545            self.upper = 1 if upper is None else upper
4546            self.lower = 1 if lower is None else lower
4547            self.number = 1 if number is None else number
4548            self.special = 1 if special is None else special
4549        else:
4550            # by default, an entropy spec is exclusive
4551            self.min = min
4552            self.max = max
4553            self.upper = upper
4554            self.lower = lower
4555            self.number = number
4556            self.special = special
4557        self.specials = specials
4558        self.invalid = invalid
4559        self.error_message = error_message
4560        self.estring = es  # return error message as string (for doctest)
4561
4562    def validate(self, value, record_id=None):
4563        failures = []
4564        if value is None:
4565            value = ""
4566        if value and len(value) == value.count("*") > 4:
4567            return value
4568        if self.entropy is not None:
4569            entropy = calc_entropy(value)
4570            if entropy < self.entropy:
4571                failures.append(
4572                    self.translator("Password too simple (%(have)s/%(need)s)")
4573                    % dict(have=entropy, need=self.entropy)
4574                )
4575        if isinstance(self.min, int) and self.min > 0:
4576            if not len(value) >= self.min:
4577                failures.append(self.translator("Minimum length is %s") % self.min)
4578        if isinstance(self.max, int) and self.max > 0:
4579            if not len(value) <= self.max:
4580                failures.append(self.translator("Maximum length is %s") % self.max)
4581        if isinstance(self.special, int):
4582            all_special = [ch in value for ch in self.specials]
4583            if self.special > 0:
4584                if not all_special.count(True) >= self.special:
4585                    failures.append(
4586                        self.translator("Must include at least %s of the following: %s")
4587                        % (self.special, self.specials)
4588                    )
4589            elif self.special == 0 and self.special is not False:
4590                if len([item for item in all_special if item]) > 0:
4591                    failures.append(
4592                        self.translator("May not contain any of the following: %s")
4593                        % self.specials
4594                    )
4595        if self.invalid:
4596            all_invalid = [ch in value for ch in self.invalid]
4597            if all_invalid.count(True) > 0:
4598                failures.append(
4599                    self.translator("May not contain any of the following: %s")
4600                    % self.invalid
4601                )
4602        if isinstance(self.upper, int):
4603            all_upper = re.findall("[A-Z]", value)
4604            if self.upper > 0:
4605                if not len(all_upper) >= self.upper:
4606                    failures.append(
4607                        self.translator("Must include at least %s uppercase")
4608                        % str(self.upper)
4609                    )
4610            elif self.upper == 0 and self.upper is not False:
4611                if len(all_upper) > 0:
4612                    failures.append(
4613                        self.translator("May not include any uppercase letters")
4614                    )
4615        if isinstance(self.lower, int):
4616            all_lower = re.findall("[a-z]", value)
4617            if self.lower > 0:
4618                if not len(all_lower) >= self.lower:
4619                    failures.append(
4620                        self.translator("Must include at least %s lowercase")
4621                        % str(self.lower)
4622                    )
4623            elif self.lower == 0 and self.lower is not False:
4624                if len(all_lower) > 0:
4625                    failures.append(
4626                        self.translator("May not include any lowercase letters")
4627                    )
4628        if isinstance(self.number, int):
4629            all_number = re.findall("[0-9]", value)
4630            if self.number > 0:
4631                numbers = "number"
4632                if self.number > 1:
4633                    numbers = "numbers"
4634                if not len(all_number) >= self.number:
4635                    failures.append(
4636                        self.translator("Must include at least %s %s")
4637                        % (str(self.number), numbers)
4638                    )
4639            elif self.number == 0 and self.number is not False:
4640                if len(all_number) > 0:
4641                    failures.append(self.translator("May not include any numbers"))
4642        if len(failures) == 0:
4643            return value
4644        if not self.error_message:
4645            if self.estring:
4646                raise ValidationError("|".join(map(str, failures)))
4647            raise ValidationError(", ".join(failures))
4648        else:
4649            raise ValidationError(self.translator(self.error_message))
4650
4651
4652class IS_IMAGE(Validator):
4653    """
4654    Checks if file uploaded through file input was saved in one of selected
4655    image formats and has dimensions (width and height) within given boundaries.
4656
4657    Does *not* check for maximum file size (use IS_LENGTH for that). Returns
4658    validation failure if no data was uploaded.
4659
4660    Supported file formats: BMP, GIF, JPEG, PNG.
4661
4662    Code parts taken from
4663    http://mail.python.org/pipermail/python-list/2007-June/617126.html
4664
4665    Args:
4666        extensions: iterable containing allowed *lowercase* image file extensions
4667        ('jpg' extension of uploaded file counts as 'jpeg')
4668        maxsize: iterable containing maximum width and height of the image
4669        minsize: iterable containing minimum width and height of the image
4670        aspectratio: iterable containing target aspect ratio
4671
4672    Use (-1, -1) as minsize to pass image size check.
4673    Use (-1, -1) as aspectratio to pass aspect ratio check.
4674
4675    Examples:
4676        Check if uploaded file is in any of supported image formats:
4677
4678            INPUT(_type='file', _name='name', requires=IS_IMAGE())
4679
4680        Check if uploaded file is either JPEG or PNG:
4681
4682            INPUT(_type='file', _name='name',
4683                requires=IS_IMAGE(extensions=('jpeg', 'png')))
4684
4685        Check if uploaded file is PNG with maximum size of 200x200 pixels:
4686
4687            INPUT(_type='file', _name='name',
4688                requires=IS_IMAGE(extensions=('png'), maxsize=(200, 200)))
4689
4690        Check if uploaded file has a 16:9 aspect ratio:
4691
4692            INPUT(_type='file', _name='name',
4693                requires=IS_IMAGE(aspectratio=(16, 9)))
4694    """
4695
4696    def __init__(
4697        self,
4698        extensions=("bmp", "gif", "jpeg", "png"),
4699        maxsize=(10000, 10000),
4700        minsize=(0, 0),
4701        aspectratio=(-1, -1),
4702        error_message="Invalid image",
4703    ):
4704
4705        self.extensions = extensions
4706        self.maxsize = maxsize
4707        self.minsize = minsize
4708        self.aspectratio = aspectratio
4709        self.error_message = error_message
4710
4711    def validate(self, value, record_id=None):
4712        try:
4713            extension = value.filename.rfind(".")
4714            assert extension >= 0
4715            extension = value.filename[extension + 1 :].lower()
4716            if extension == "jpg":
4717                extension = "jpeg"
4718            assert extension in self.extensions
4719            if extension == "bmp":
4720                width, height = self.__bmp(value.file)
4721            elif extension == "gif":
4722                width, height = self.__gif(value.file)
4723            elif extension == "jpeg":
4724                width, height = self.__jpeg(value.file)
4725            elif extension == "png":
4726                width, height = self.__png(value.file)
4727            else:
4728                width = -1
4729                height = -1
4730
4731            assert (
4732                self.minsize[0] <= width <= self.maxsize[0]
4733                and self.minsize[1] <= height <= self.maxsize[1]
4734            )
4735
4736            if self.aspectratio > (-1, -1):
4737                target_ratio = (1.0 * self.aspectratio[1]) / self.aspectratio[0]
4738                actual_ratio = (1.0 * height) / width
4739
4740                assert actual_ratio == target_ratio
4741
4742            value.file.seek(0)
4743            return value
4744        except Exception as e:
4745            raise ValidationError(self.translator(self.error_message))
4746
4747    def __bmp(self, stream):
4748        if stream.read(2) == b"BM":
4749            stream.read(16)
4750            return struct.unpack("<LL", stream.read(8))
4751        return (-1, -1)
4752
4753    def __gif(self, stream):
4754        if stream.read(6) in (b"GIF87a", b"GIF89a"):
4755            stream = stream.read(5)
4756            if len(stream) == 5:
4757                return tuple(struct.unpack("<HHB", stream)[:-1])
4758        return (-1, -1)
4759
4760    def __jpeg(self, stream):
4761        if stream.read(2) == b"\xFF\xD8":
4762            while True:
4763                (marker, code, length) = struct.unpack("!BBH", stream.read(4))
4764                if marker != 0xFF:
4765                    break
4766                elif code >= 0xC0 and code <= 0xC3:
4767                    return tuple(reversed(struct.unpack("!xHH", stream.read(5))))
4768                else:
4769                    stream.read(length - 2)
4770        return (-1, -1)
4771
4772    def __png(self, stream):
4773        if stream.read(8) == b"\211PNG\r\n\032\n":
4774            stream.read(4)
4775            if stream.read(4) == b"IHDR":
4776                return struct.unpack("!LL", stream.read(8))
4777        return (-1, -1)
4778
4779
4780class IS_FILE(Validator):
4781    """
4782    Checks if name and extension of file uploaded through file input matches
4783    given criteria.
4784
4785    Does *not* ensure the file type in any way. Returns validation failure
4786    if no data was uploaded.
4787
4788    Args:
4789        filename: string/compiled regex or a list of strings/regex of valid filenames
4790        extension: string/compiled regex or a list of strings/regex of valid extensions
4791        lastdot: which dot should be used as a filename / extension separator:
4792            True means last dot, eg. file.jpg.png -> file.jpg / png
4793            False means first dot, eg. file.tar.gz -> file / tar.gz
4794        case: 0 - keep the case, 1 - transform the string into lowercase (default),
4795            2 - transform the string into uppercase
4796
4797    If there is no dot present, extension checks will be done against empty
4798    string and filename checks against whole value.
4799
4800    Examples:
4801        Check if file has a pdf extension (case insensitive):
4802
4803        INPUT(_type='file', _name='name',
4804                requires=IS_FILE(extension='pdf'))
4805
4806        Check if file is called 'thumbnail' and has a jpg or png extension
4807        (case insensitive):
4808
4809        INPUT(_type='file', _name='name',
4810                requires=IS_FILE(filename='thumbnail',
4811                extension=['jpg', 'png']))
4812
4813        Check if file has a tar.gz extension and name starting with backup:
4814
4815        INPUT(_type='file', _name='name',
4816                requires=IS_FILE(filename=re.compile('backup.*'),
4817                extension='tar.gz', lastdot=False))
4818
4819        Check if file has no extension and name matching README
4820        (case sensitive):
4821
4822            INPUT(_type='file', _name='name',
4823                requires=IS_FILE(filename='README',
4824                extension='', case=0)
4825
4826    """
4827
4828    def __init__(
4829        self,
4830        filename=None,
4831        extension=None,
4832        lastdot=True,
4833        case=1,
4834        error_message="Enter valid filename",
4835    ):
4836        self.filename = filename
4837        self.extension = extension
4838        self.lastdot = lastdot
4839        self.case = case
4840        self.error_message = error_message
4841
4842    def match(self, value1, value2):
4843        if isinstance(value1, (list, tuple)):
4844            for v in value1:
4845                if self.match(v, value2):
4846                    return True
4847            return False
4848        elif callable(getattr(value1, "match", None)):
4849            return value1.match(value2)
4850        elif isinstance(value1, str):
4851            return value1 == value2
4852
4853    def validate(self, value, record_id=None):
4854        try:
4855            string = value.filename
4856        except:
4857            raise ValidationError(self.translator(self.error_message))
4858        if self.case == 1:
4859            string = string.lower()
4860        elif self.case == 2:
4861            string = string.upper()
4862        if self.lastdot:
4863            dot = string.rfind(".")
4864        else:
4865            dot = string.find(".")
4866        if dot == -1:
4867            dot = len(string)
4868        if self.filename and not self.match(self.filename, string[:dot]):
4869            raise ValidationError(self.translator(self.error_message))
4870        elif self.extension and not self.match(self.extension, string[dot + 1 :]):
4871            raise ValidationError(self.translator(self.error_message))
4872        else:
4873            return value
4874
4875
4876class IS_UPLOAD_FILENAME(Validator):
4877    """
4878    For new applications, use IS_FILE().
4879
4880    Checks if name and extension of file uploaded through file input matches
4881    given criteria.
4882
4883    Does *not* ensure the file type in any way. Returns validation failure
4884    if no data was uploaded.
4885
4886    Args:
4887        filename: filename (before dot) regex
4888        extension: extension (after dot) regex
4889        lastdot: which dot should be used as a filename / extension separator:
4890            True means last dot, eg. file.png -> file / png
4891            False means first dot, eg. file.tar.gz -> file / tar.gz
4892        case: 0 - keep the case, 1 - transform the string into lowercase (default),
4893            2 - transform the string into uppercase
4894
4895    If there is no dot present, extension checks will be done against empty
4896    string and filename checks against whole value.
4897
4898    Examples:
4899        Check if file has a pdf extension (case insensitive):
4900
4901        INPUT(_type='file', _name='name',
4902                requires=IS_UPLOAD_FILENAME(extension='pdf'))
4903
4904        Check if file has a tar.gz extension and name starting with backup:
4905
4906        INPUT(_type='file', _name='name',
4907                requires=IS_UPLOAD_FILENAME(filename='backup.*',
4908                extension='tar.gz', lastdot=False))
4909
4910        Check if file has no extension and name matching README
4911        (case sensitive):
4912
4913            INPUT(_type='file', _name='name',
4914                requires=IS_UPLOAD_FILENAME(filename='^README$',
4915                extension='^$', case=0)
4916
4917    """
4918
4919    def __init__(
4920        self,
4921        filename=None,
4922        extension=None,
4923        lastdot=True,
4924        case=1,
4925        error_message="Enter valid filename",
4926    ):
4927        if isinstance(filename, str):
4928            filename = re.compile(filename)
4929        if isinstance(extension, str):
4930            extension = re.compile(extension)
4931        self.filename = filename
4932        self.extension = extension
4933        self.lastdot = lastdot
4934        self.case = case
4935        self.error_message = error_message
4936
4937    def validate(self, value, record_id=None):
4938        try:
4939            string = value.filename
4940        except:
4941            raise ValidationError(self.translator(self.error_message))
4942        if self.case == 1:
4943            string = string.lower()
4944        elif self.case == 2:
4945            string = string.upper()
4946        if self.lastdot:
4947            dot = string.rfind(".")
4948        else:
4949            dot = string.find(".")
4950        if dot == -1:
4951            dot = len(string)
4952        if self.filename and not self.filename.match(string[:dot]):
4953            raise ValidationError(self.translator(self.error_message))
4954        elif self.extension and not self.extension.match(string[dot + 1 :]):
4955            raise ValidationError(self.translator(self.error_message))
4956        else:
4957            return value
4958
4959
4960class IS_IPV4(Validator):
4961    """
4962    Checks if field's value is an IP version 4 address in decimal form. Can
4963    be set to force addresses from certain range.
4964
4965    IPv4 regex taken from: http://regexlib.com/REDetails.aspx?regexp_id=1411
4966
4967    Args:
4968
4969        minip: lowest allowed address; accepts:
4970
4971            - str, eg. 192.168.0.1
4972            - list or tuple of octets, eg. [192, 168, 0, 1]
4973        maxip: highest allowed address; same as above
4974        invert: True to allow addresses only from outside of given range; note
4975            that range boundaries are not matched this way
4976        is_localhost: localhost address treatment:
4977
4978            - None (default): indifferent
4979            - True (enforce): query address must match localhost address (127.0.0.1)
4980            - False (forbid): query address must not match localhost address
4981        is_private: same as above, except that query address is checked against
4982            two address ranges: 172.16.0.0 - 172.31.255.255 and
4983            192.168.0.0 - 192.168.255.255
4984        is_automatic: same as above, except that query address is checked against
4985            one address range: 169.254.0.0 - 169.254.255.255
4986
4987    Minip and maxip may also be lists or tuples of addresses in all above
4988    forms (str, int, list / tuple), allowing setup of multiple address ranges::
4989
4990        minip = (minip1, minip2, ... minipN)
4991                   |       |           |
4992                   |       |           |
4993        maxip = (maxip1, maxip2, ... maxipN)
4994
4995    Longer iterable will be truncated to match length of shorter one.
4996
4997    Examples:
4998        Check for valid IPv4 address:
4999
5000            INPUT(_type='text', _name='name', requires=IS_IPV4())
5001
5002        Check for valid IPv4 address belonging to specific range:
5003
5004            INPUT(_type='text', _name='name',
5005                requires=IS_IPV4(minip='100.200.0.0', maxip='100.200.255.255'))
5006
5007        Check for valid IPv4 address belonging to either 100.110.0.0 -
5008        100.110.255.255 or 200.50.0.0 - 200.50.0.255 address range:
5009
5010            INPUT(_type='text', _name='name',
5011                requires=IS_IPV4(minip=('100.110.0.0', '200.50.0.0'),
5012                             maxip=('100.110.255.255', '200.50.0.255')))
5013
5014        Check for valid IPv4 address belonging to private address space:
5015
5016            INPUT(_type='text', _name='name', requires=IS_IPV4(is_private=True))
5017
5018        Check for valid IPv4 address that is not a localhost address:
5019
5020            INPUT(_type='text', _name='name', requires=IS_IPV4(is_localhost=False))
5021
5022            >>> IS_IPV4()('1.2.3.4')
5023            ('1.2.3.4', None)
5024            >>> IS_IPV4()('255.255.255.255')
5025            ('255.255.255.255', None)
5026            >>> IS_IPV4()('1.2.3.4 ')
5027            ('1.2.3.4 ', 'enter valid IPv4 address')
5028            >>> IS_IPV4()('1.2.3.4.5')
5029            ('1.2.3.4.5', 'enter valid IPv4 address')
5030            >>> IS_IPV4()('123.123')
5031            ('123.123', 'enter valid IPv4 address')
5032            >>> IS_IPV4()('1111.2.3.4')
5033            ('1111.2.3.4', 'enter valid IPv4 address')
5034            >>> IS_IPV4()('0111.2.3.4')
5035            ('0111.2.3.4', 'enter valid IPv4 address')
5036            >>> IS_IPV4()('256.2.3.4')
5037            ('256.2.3.4', 'enter valid IPv4 address')
5038            >>> IS_IPV4()('300.2.3.4')
5039            ('300.2.3.4', 'enter valid IPv4 address')
5040            >>> IS_IPV4(minip='1.2.3.4', maxip='1.2.3.4')('1.2.3.4')
5041            ('1.2.3.4', None)
5042            >>> IS_IPV4(minip='1.2.3.5', maxip='1.2.3.9', error_message='Bad ip')('1.2.3.4')
5043            ('1.2.3.4', 'bad ip')
5044            >>> IS_IPV4(maxip='1.2.3.4', invert=True)('127.0.0.1')
5045            ('127.0.0.1', None)
5046            >>> IS_IPV4(maxip='1.2.3.4', invert=True)('1.2.3.4')
5047            ('1.2.3.4', 'enter valid IPv4 address')
5048            >>> IS_IPV4(is_localhost=True)('127.0.0.1')
5049            ('127.0.0.1', None)
5050            >>> IS_IPV4(is_localhost=True)('1.2.3.4')
5051            ('1.2.3.4', 'enter valid IPv4 address')
5052            >>> IS_IPV4(is_localhost=False)('127.0.0.1')
5053            ('127.0.0.1', 'enter valid IPv4 address')
5054            >>> IS_IPV4(maxip='100.0.0.0', is_localhost=True)('127.0.0.1')
5055            ('127.0.0.1', 'enter valid IPv4 address')
5056
5057    """
5058
5059    REGEX_IPV4 = re.compile(
5060        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])$"
5061    )
5062    numbers = (16777216, 65536, 256, 1)
5063    localhost = 2130706433
5064    private = ((2886729728, 2886795263), (3232235520, 3232301055))
5065    automatic = (2851995648, 2852061183)
5066
5067    def __init__(
5068        self,
5069        minip="0.0.0.0",
5070        maxip="255.255.255.255",
5071        invert=False,
5072        is_localhost=None,
5073        is_private=None,
5074        is_automatic=None,
5075        error_message="Enter valid IPv4 address",
5076    ):
5077
5078        for n, value in enumerate((minip, maxip)):
5079            temp = []
5080            if isinstance(value, str):
5081                temp.append(value.split("."))
5082            elif isinstance(value, (list, tuple)):
5083                if (
5084                    len(value)
5085                    == len([item for item in value if isinstance(item, int)])
5086                    == 4
5087                ):
5088                    temp.append(value)
5089                else:
5090                    for item in value:
5091                        if isinstance(item, str):
5092                            temp.append(item.split("."))
5093                        elif isinstance(item, (list, tuple)):
5094                            temp.append(item)
5095            numbers = []
5096            for item in temp:
5097                number = 0
5098                for i, j in zip(self.numbers, item):
5099                    number += i * int(j)
5100                numbers.append(number)
5101            if n == 0:
5102                self.minip = numbers
5103            else:
5104                self.maxip = numbers
5105        self.invert = invert
5106        self.is_localhost = is_localhost
5107        self.is_private = is_private
5108        self.is_automatic = is_automatic
5109        self.error_message = error_message
5110
5111    def validate(self, value, record_id=None):
5112        if re.match(self.REGEX_IPV4, value):
5113            number = 0
5114            for i, j in zip(self.numbers, value.split(".")):
5115                number += i * int(j)
5116            ok = False
5117
5118            for bottom, top in zip(self.minip, self.maxip):
5119                if self.invert != (bottom <= number <= top):
5120                    ok = True
5121
5122            if (
5123                ok
5124                and self.is_localhost is not None
5125                and self.is_localhost != (number == self.localhost)
5126            ):
5127                ok = False
5128
5129            private = any(
5130                [
5131                    private_number[0] <= number <= private_number[1]
5132                    for private_number in self.private
5133                ]
5134            )
5135            if ok and self.is_private is not None and self.is_private != private:
5136                ok = False
5137
5138            automatic = self.automatic[0] <= number <= self.automatic[1]
5139            if ok and self.is_automatic is not None and self.is_automatic != automatic:
5140                ok = False
5141
5142            if ok:
5143                return value
5144
5145        raise ValidationError(self.translator(self.error_message))
5146
5147
5148class IS_IPV6(Validator):
5149    """
5150    Checks if field's value is an IP version 6 address.
5151
5152    Uses the ipaddress from the Python 3 standard library
5153    and its Python 2 backport (in contrib/ipaddress.py).
5154
5155    Args:
5156        is_private: None (default): indifferent
5157                    True (enforce): address must be in fc00::/7 range
5158                    False (forbid): address must NOT be in fc00::/7 range
5159        is_link_local: Same as above but uses fe80::/10 range
5160        is_reserved: Same as above but uses IETF reserved range
5161        is_multicast: Same as above but uses ff00::/8 range
5162        is_routeable: Similar to above but enforces not private, link_local,
5163                      reserved or multicast
5164        is_6to4: Same as above but uses 2002::/16 range
5165        is_teredo: Same as above but uses 2001::/32 range
5166        subnets: value must be a member of at least one from list of subnets
5167
5168    Examples:
5169        Check for valid IPv6 address:
5170
5171            INPUT(_type='text', _name='name', requires=IS_IPV6())
5172
5173        Check for valid IPv6 address is a link_local address:
5174
5175            INPUT(_type='text', _name='name', requires=IS_IPV6(is_link_local=True))
5176
5177        Check for valid IPv6 address that is Internet routeable:
5178
5179            INPUT(_type='text', _name='name', requires=IS_IPV6(is_routeable=True))
5180
5181        Check for valid IPv6 address in specified subnet:
5182
5183            INPUT(_type='text', _name='name', requires=IS_IPV6(subnets=['2001::/32'])
5184
5185            >>> IS_IPV6()('fe80::126c:8ffa:fe22:b3af')
5186            ('fe80::126c:8ffa:fe22:b3af', None)
5187            >>> IS_IPV6()('192.168.1.1')
5188            ('192.168.1.1', 'enter valid IPv6 address')
5189            >>> IS_IPV6(error_message='Bad ip')('192.168.1.1')
5190            ('192.168.1.1', 'bad ip')
5191            >>> IS_IPV6(is_link_local=True)('fe80::126c:8ffa:fe22:b3af')
5192            ('fe80::126c:8ffa:fe22:b3af', None)
5193            >>> IS_IPV6(is_link_local=False)('fe80::126c:8ffa:fe22:b3af')
5194            ('fe80::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
5195            >>> IS_IPV6(is_link_local=True)('2001::126c:8ffa:fe22:b3af')
5196            ('2001::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
5197            >>> IS_IPV6(is_multicast=True)('2001::126c:8ffa:fe22:b3af')
5198            ('2001::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
5199            >>> IS_IPV6(is_multicast=True)('ff00::126c:8ffa:fe22:b3af')
5200            ('ff00::126c:8ffa:fe22:b3af', None)
5201            >>> IS_IPV6(is_routeable=True)('2001::126c:8ffa:fe22:b3af')
5202            ('2001::126c:8ffa:fe22:b3af', None)
5203            >>> IS_IPV6(is_routeable=True)('ff00::126c:8ffa:fe22:b3af')
5204            ('ff00::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
5205            >>> IS_IPV6(subnets='2001::/32')('2001::8ffa:fe22:b3af')
5206            ('2001::8ffa:fe22:b3af', None)
5207            >>> IS_IPV6(subnets='fb00::/8')('2001::8ffa:fe22:b3af')
5208            ('2001::8ffa:fe22:b3af', 'enter valid IPv6 address')
5209            >>> IS_IPV6(subnets=['fc00::/8','2001::/32'])('2001::8ffa:fe22:b3af')
5210            ('2001::8ffa:fe22:b3af', None)
5211            >>> IS_IPV6(subnets='invalidsubnet')('2001::8ffa:fe22:b3af')
5212            ('2001::8ffa:fe22:b3af', 'invalid subnet provided')
5213
5214    """
5215
5216    def __init__(
5217        self,
5218        is_private=None,
5219        is_link_local=None,
5220        is_reserved=None,
5221        is_multicast=None,
5222        is_routeable=None,
5223        is_6to4=None,
5224        is_teredo=None,
5225        subnets=None,
5226        error_message="Enter valid IPv6 address",
5227    ):
5228
5229        self.is_private = is_private
5230        self.is_link_local = is_link_local
5231        self.is_reserved = is_reserved
5232        self.is_multicast = is_multicast
5233        self.is_routeable = is_routeable
5234        self.is_6to4 = is_6to4
5235        self.is_teredo = is_teredo
5236        self.subnets = subnets
5237        self.error_message = error_message
5238
5239    def validate(self, value, record_id=None):
5240        try:
5241            ip = ipaddress.IPv6Address(to_unicode(value))
5242            ok = True
5243        except ipaddress.AddressValueError:
5244            raise ValidationError(self.translator(self.error_message))
5245
5246        if self.subnets:
5247            # iterate through self.subnets to see if value is a member
5248            ok = False
5249            if isinstance(self.subnets, str):
5250                self.subnets = [self.subnets]
5251            for network in self.subnets:
5252                try:
5253                    ipnet = ipaddress.IPv6Network(to_unicode(network))
5254                except (ipaddress.NetmaskValueError, ipaddress.AddressValueError):
5255                    raise ValidationError(self.translator("invalid subnet provided"))
5256                if ip in ipnet:
5257                    ok = True
5258
5259        if self.is_routeable:
5260            self.is_private = False
5261            self.is_reserved = False
5262            self.is_multicast = False
5263
5264        if ok and self.is_private is not None and self.is_private != ip.is_private:
5265            ok = False
5266        if (
5267            ok
5268            and self.is_link_local is not None
5269            and self.is_link_local != ip.is_link_local
5270        ):
5271            ok = False
5272        if ok and self.is_reserved is not None and self.is_reserved != ip.is_reserved:
5273            ok = False
5274        if (
5275            ok
5276            and self.is_multicast is not None
5277            and self.is_multicast != ip.is_multicast
5278        ):
5279            ok = False
5280        if ok and self.is_6to4 is not None and self.is_6to4 != bool(ip.sixtofour):
5281            ok = False
5282        if ok and self.is_teredo is not None and self.is_teredo != bool(ip.teredo):
5283            ok = False
5284
5285        if ok:
5286            return value
5287
5288        raise ValidationError(self.translator(self.error_message))
5289
5290
5291class IS_IPADDRESS(Validator):
5292    """
5293    Checks if field's value is an IP Address (v4 or v6). Can be set to force
5294    addresses from within a specific range. Checks are done with the correct
5295    IS_IPV4 and IS_IPV6 validators.
5296
5297    Uses the ipaddress from the Python 3 standard library
5298    and its Python 2 backport (in contrib/ipaddress.py).
5299
5300    Args:
5301        minip: lowest allowed address; accepts:
5302               str, eg. 192.168.0.1
5303               list or tuple of octets, eg. [192, 168, 0, 1]
5304        maxip: highest allowed address; same as above
5305        invert: True to allow addresses only from outside of given range; note
5306                that range boundaries are not matched this way
5307
5308    IPv4 specific arguments:
5309
5310        - is_localhost: localhost address treatment:
5311
5312            - None (default): indifferent
5313            - True (enforce): query address must match localhost address
5314              (127.0.0.1)
5315            - False (forbid): query address must not match localhost address
5316        - is_private: same as above, except that query address is checked against
5317          two address ranges: 172.16.0.0 - 172.31.255.255 and
5318          192.168.0.0 - 192.168.255.255
5319        - is_automatic: same as above, except that query address is checked against
5320          one address range: 169.254.0.0 - 169.254.255.255
5321        - is_ipv4: either:
5322
5323            - None (default): indifferent
5324            - True (enforce): must be an IPv4 address
5325            - False (forbid): must NOT be an IPv4 address
5326
5327    IPv6 specific arguments:
5328
5329        - is_link_local: Same as above but uses fe80::/10 range
5330        - is_reserved: Same as above but uses IETF reserved range
5331        - is_multicast: Same as above but uses ff00::/8 range
5332        - is_routeable: Similar to above but enforces not private, link_local,
5333          reserved or multicast
5334        - is_6to4: Same as above but uses 2002::/16 range
5335        - is_teredo: Same as above but uses 2001::/32 range
5336        - subnets: value must be a member of at least one from list of subnets
5337        - is_ipv6: either:
5338
5339            - None (default): indifferent
5340            - True (enforce): must be an IPv6 address
5341            - False (forbid): must NOT be an IPv6 address
5342
5343    Minip and maxip may also be lists or tuples of addresses in all above
5344    forms (str, int, list / tuple), allowing setup of multiple address ranges::
5345
5346        minip = (minip1, minip2, ... minipN)
5347                   |       |           |
5348                   |       |           |
5349        maxip = (maxip1, maxip2, ... maxipN)
5350
5351    Longer iterable will be truncated to match length of shorter one.
5352
5353        >>> IS_IPADDRESS()('192.168.1.5')
5354        ('192.168.1.5', None)
5355        >>> IS_IPADDRESS(is_ipv6=False)('192.168.1.5')
5356        ('192.168.1.5', None)
5357        >>> IS_IPADDRESS()('255.255.255.255')
5358        ('255.255.255.255', None)
5359        >>> IS_IPADDRESS()('192.168.1.5 ')
5360        ('192.168.1.5 ', 'enter valid IP address')
5361        >>> IS_IPADDRESS()('192.168.1.1.5')
5362        ('192.168.1.1.5', 'enter valid IP address')
5363        >>> IS_IPADDRESS()('123.123')
5364        ('123.123', 'enter valid IP address')
5365        >>> IS_IPADDRESS()('1111.2.3.4')
5366        ('1111.2.3.4', 'enter valid IP address')
5367        >>> IS_IPADDRESS()('0111.2.3.4')
5368        ('0111.2.3.4', 'enter valid IP address')
5369        >>> IS_IPADDRESS()('256.2.3.4')
5370        ('256.2.3.4', 'enter valid IP address')
5371        >>> IS_IPADDRESS()('300.2.3.4')
5372        ('300.2.3.4', 'enter valid IP address')
5373        >>> IS_IPADDRESS(minip='192.168.1.0', maxip='192.168.1.255')('192.168.1.100')
5374        ('192.168.1.100', None)
5375        >>> IS_IPADDRESS(minip='1.2.3.5', maxip='1.2.3.9', error_message='Bad ip')('1.2.3.4')
5376        ('1.2.3.4', 'bad ip')
5377        >>> IS_IPADDRESS(maxip='1.2.3.4', invert=True)('127.0.0.1')
5378        ('127.0.0.1', None)
5379        >>> IS_IPADDRESS(maxip='192.168.1.4', invert=True)('192.168.1.4')
5380        ('192.168.1.4', 'enter valid IP address')
5381        >>> IS_IPADDRESS(is_localhost=True)('127.0.0.1')
5382        ('127.0.0.1', None)
5383        >>> IS_IPADDRESS(is_localhost=True)('192.168.1.10')
5384        ('192.168.1.10', 'enter valid IP address')
5385        >>> IS_IPADDRESS(is_localhost=False)('127.0.0.1')
5386        ('127.0.0.1', 'enter valid IP address')
5387        >>> IS_IPADDRESS(maxip='100.0.0.0', is_localhost=True)('127.0.0.1')
5388        ('127.0.0.1', 'enter valid IP address')
5389
5390        >>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af')
5391        ('fe80::126c:8ffa:fe22:b3af', None)
5392        >>> IS_IPADDRESS(is_ipv4=False)('fe80::126c:8ffa:fe22:b3af')
5393        ('fe80::126c:8ffa:fe22:b3af', None)
5394        >>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af  ')
5395        ('fe80::126c:8ffa:fe22:b3af  ', 'enter valid IP address')
5396        >>> IS_IPADDRESS(is_ipv4=True)('fe80::126c:8ffa:fe22:b3af')
5397        ('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address')
5398        >>> IS_IPADDRESS(is_ipv6=True)('192.168.1.1')
5399        ('192.168.1.1', 'enter valid IP address')
5400        >>> IS_IPADDRESS(is_ipv6=True, error_message='Bad ip')('192.168.1.1')
5401        ('192.168.1.1', 'bad ip')
5402        >>> IS_IPADDRESS(is_link_local=True)('fe80::126c:8ffa:fe22:b3af')
5403        ('fe80::126c:8ffa:fe22:b3af', None)
5404        >>> IS_IPADDRESS(is_link_local=False)('fe80::126c:8ffa:fe22:b3af')
5405        ('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address')
5406        >>> IS_IPADDRESS(is_link_local=True)('2001::126c:8ffa:fe22:b3af')
5407        ('2001::126c:8ffa:fe22:b3af', 'enter valid IP address')
5408        >>> IS_IPADDRESS(is_multicast=True)('2001::126c:8ffa:fe22:b3af')
5409        ('2001::126c:8ffa:fe22:b3af', 'enter valid IP address')
5410        >>> IS_IPADDRESS(is_multicast=True)('ff00::126c:8ffa:fe22:b3af')
5411        ('ff00::126c:8ffa:fe22:b3af', None)
5412        >>> IS_IPADDRESS(is_routeable=True)('2001::126c:8ffa:fe22:b3af')
5413        ('2001::126c:8ffa:fe22:b3af', None)
5414        >>> IS_IPADDRESS(is_routeable=True)('ff00::126c:8ffa:fe22:b3af')
5415        ('ff00::126c:8ffa:fe22:b3af', 'enter valid IP address')
5416        >>> IS_IPADDRESS(subnets='2001::/32')('2001::8ffa:fe22:b3af')
5417        ('2001::8ffa:fe22:b3af', None)
5418        >>> IS_IPADDRESS(subnets='fb00::/8')('2001::8ffa:fe22:b3af')
5419        ('2001::8ffa:fe22:b3af', 'enter valid IP address')
5420        >>> IS_IPADDRESS(subnets=['fc00::/8','2001::/32'])('2001::8ffa:fe22:b3af')
5421        ('2001::8ffa:fe22:b3af', None)
5422        >>> IS_IPADDRESS(subnets='invalidsubnet')('2001::8ffa:fe22:b3af')
5423        ('2001::8ffa:fe22:b3af', 'invalid subnet provided')
5424    """
5425
5426    def __init__(
5427        self,
5428        minip="0.0.0.0",
5429        maxip="255.255.255.255",
5430        invert=False,
5431        is_localhost=None,
5432        is_private=None,
5433        is_automatic=None,
5434        is_ipv4=None,
5435        is_link_local=None,
5436        is_reserved=None,
5437        is_multicast=None,
5438        is_routeable=None,
5439        is_6to4=None,
5440        is_teredo=None,
5441        subnets=None,
5442        is_ipv6=None,
5443        error_message="Enter valid IP address",
5444    ):
5445
5446        self.minip = (minip,)
5447        self.maxip = (maxip,)
5448        self.invert = invert
5449        self.is_localhost = is_localhost
5450        self.is_private = is_private
5451        self.is_automatic = is_automatic
5452        self.is_ipv4 = is_ipv4 or is_ipv6 is False
5453        self.is_private = is_private
5454        self.is_link_local = is_link_local
5455        self.is_reserved = is_reserved
5456        self.is_multicast = is_multicast
5457        self.is_routeable = is_routeable
5458        self.is_6to4 = is_6to4
5459        self.is_teredo = is_teredo
5460        self.subnets = subnets
5461        self.is_ipv6 = is_ipv6 or is_ipv4 is False
5462        self.error_message = error_message
5463
5464    def validate(self, value, record_id=None):
5465        IPAddress = ipaddress.ip_address
5466        IPv6Address = ipaddress.IPv6Address
5467        IPv4Address = ipaddress.IPv4Address
5468
5469        try:
5470            ip = IPAddress(to_unicode(value))
5471        except ValueError:
5472            raise ValidationError(self.translator(self.error_message))
5473
5474        if self.is_ipv4 and isinstance(ip, IPv6Address):
5475            raise ValidationError(self.translator(self.error_message))
5476        elif self.is_ipv6 and isinstance(ip, IPv4Address):
5477            raise ValidationError(self.translator(self.error_message))
5478        elif self.is_ipv4 or isinstance(ip, IPv4Address):
5479            return IS_IPV4(
5480                minip=self.minip,
5481                maxip=self.maxip,
5482                invert=self.invert,
5483                is_localhost=self.is_localhost,
5484                is_private=self.is_private,
5485                is_automatic=self.is_automatic,
5486                error_message=self.error_message,
5487            ).validate(value, record_id)
5488        elif self.is_ipv6 or isinstance(ip, IPv6Address):
5489            return IS_IPV6(
5490                is_private=self.is_private,
5491                is_link_local=self.is_link_local,
5492                is_reserved=self.is_reserved,
5493                is_multicast=self.is_multicast,
5494                is_routeable=self.is_routeable,
5495                is_6to4=self.is_6to4,
5496                is_teredo=self.is_teredo,
5497                subnets=self.subnets,
5498                error_message=self.error_message,
5499            ).validate(value, record_id)
5500        else:
5501            raise ValidationError(self.translator(self.error_message))
Note: See TracBrowser for help on using the repository browser.