Package web2py :: Package gluon :: Module validators |
[hide private]
[frames] | no frames]

# Source Code for Module web2py.gluon.validators

       1  #!/bin/env python
       2  # -*- coding: utf-8 -*-
       3
       4  """
       5  This file is part of the web2py Web Framework
       6  Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
       7  License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
       8
       9  Thanks to ga2arch for help with IS_IN_DB and IS_NOT_IN_DB on GAE
      10  """
      11
      12  import os
      13  import re
      14  import datetime
      15  import time
      16  import cgi
      17  import urllib
      18  import struct
      19  import decimal
      20  import unicodedata
      21  from cStringIO import StringIO
      22  from utils import simple_hash, hmac_hash
      23
      24  __all__ = [
      25      'CLEANUP',
      26      'CRYPT',
      27      'IS_ALPHANUMERIC',
      28      'IS_DATE_IN_RANGE',
      29      'IS_DATE',
      30      'IS_DATETIME_IN_RANGE',
      31      'IS_DATETIME',
      32      'IS_DECIMAL_IN_RANGE',
      33      'IS_EMAIL',
      34      'IS_EMPTY_OR',
      35      'IS_EXPR',
      36      'IS_FLOAT_IN_RANGE',
      37      'IS_IMAGE',
      38      'IS_IN_DB',
      39      'IS_IN_SET',
      40      'IS_INT_IN_RANGE',
      41      'IS_IPV4',
      42      'IS_LENGTH',
      43      'IS_LIST_OF',
      44      'IS_LOWER',
      45      'IS_MATCH',
      46      'IS_EQUAL_TO',
      47      'IS_NOT_EMPTY',
      48      'IS_NOT_IN_DB',
      49      'IS_NULL_OR',
      50      'IS_SLUG',
      51      'IS_STRONG',
      52      'IS_TIME',
      53      'IS_UPLOAD_FILENAME',
      54      'IS_UPPER',
      55      'IS_URL',
      56      ]
      57
      58 -def translate(text):
      59      if isinstance(text,(str,unicode)):
      60          from globals import current
      61          if hasattr(current,'T'):
      62              return current.T(text)
      63      return text
      64
      65 -def options_sorter(x,y):
      66      return (str(x[1]).upper()>str(y[1]).upper() and 1) or -1
      67
      68 -class Validator(object):
      69      """
      70      Root for all validators, mainly for documentation purposes.
      71
      72      Validators are classes used to validate input fields (including forms
      73      generated from database tables).
      74
      75      Here is an example of using a validator with a FORM::
      76
      77          INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10))
      78
      79      Here is an example of how to require a validator for a table field::
      80
      81          db.define_table('person', SQLField('name'))
      82          db.person.name.requires=IS_NOT_EMPTY()
      83
      84      Validators are always assigned using the requires attribute of a field. A
      85      field can have a single validator or multiple validators. Multiple
      86      validators are made part of a list::
      87
      88          db.person.name.requires=[IS_NOT_EMPTY(), IS_NOT_IN_DB(db, 'person.id')]
      89
      90      Validators are called by the function accepts on a FORM or other HTML
      91      helper object that contains a form. They are always called in the order in
      92      which they are listed.
      93
      94      Built-in validators have constructors that take the optional argument error
      95      message which allows you to change the default error message.
      96      Here is an example of a validator on a database table::
      97
      98          db.person.name.requires=IS_NOT_EMPTY(error_message=T('fill this'))
      99
     100      where we have used the translation operator T to allow for
     101      internationalization.
     102
     103      Notice that default error messages are not translated.
     104      """
     105
     106 -    def formatter(self, value):
     107          """
     108          For some validators returns a formatted version (matching the validator)
     109          of value. Otherwise just returns the value.
     110          """
     111          return value
     112
     113
     114 -class IS_MATCH(Validator):
     115      """
     116      example::
     117
     118          INPUT(_type='text', _name='name', requires=IS_MATCH('.+'))
     119
     120      the argument of IS_MATCH is a regular expression::
     121
     122          >>> IS_MATCH('.+')('hello')
     123          ('hello', None)
     124
     125          >>> IS_MATCH('.+')('')
     126          ('', 'invalid expression')
     127      """
     128
     129 -    def __init__(self, expression, error_message='invalid expression', strict=True):
     130          if strict:
     131              if not expression.endswith('$'):
     132                  expression = '(%s)$' % expression
     133          self.regex = re.compile(expression)
     134          self.error_message = error_message
     135
     136 -    def __call__(self, value):
     137          match = self.regex.match(value)
     138          if match:
     139              return (match.group(), None)
     140          return (value, translate(self.error_message))
     141
     142
     143 -class IS_EQUAL_TO(Validator):
     144      """
     145      example::
     146
     147          INPUT(_type='text', _name='password')
     148          INPUT(_type='text', _name='password2',
     149                requires=IS_EQUAL_TO(request.vars.password))
     150
     151      the argument of IS_EQUAL_TO is a string
     152
     153          >>> IS_EQUAL_TO('aaa')('aaa')
     154          ('aaa', None)
     155
     156          >>> IS_EQUAL_TO('aaa')('aab')
     157          ('aab', 'no match')
     158      """
     159
     160 -    def __init__(self, expression, error_message='no match'):
     161          self.expression = expression
     162          self.error_message = error_message
     163
     164 -    def __call__(self, value):
     165          if value == self.expression:
     166              return (value, None)
     167          return (value, translate(self.error_message))
     168
     169
     170 -class IS_EXPR(Validator):
     171      """
     172      example::
     173
     174          INPUT(_type='text', _name='name',
     175              requires=IS_EXPR('5 < int(value) < 10'))
     176
     177      the argument of IS_EXPR must be python condition::
     178
     179          >>> IS_EXPR('int(value) < 2')('1')
     180          ('1', None)
     181
     182          >>> IS_EXPR('int(value) < 2')('2')
     183          ('2', 'invalid expression')
     184      """
     185
     186 -    def __init__(self, expression, error_message='invalid expression'):
     187          self.expression = expression
     188          self.error_message = error_message
     189
     190 -    def __call__(self, value):
     191          environment = {'value': value}
     192          exec '__ret__=' + self.expression in environment
     193          if environment['__ret__']:
     194              return (value, None)
     195          return (value, translate(self.error_message))
     196
     197
     198 -class IS_LENGTH(Validator):
     199      """
     200      Checks if length of field's value fits between given boundaries. Works
     201      for both text and file inputs.
     202
     203      Arguments:
     204
     205      maxsize: maximum allowed length / size
     206      minsize: minimum allowed length / size
     207
     208      Examples::
     209
     210          #Check if text string is shorter than 33 characters:
     211          INPUT(_type='text', _name='name', requires=IS_LENGTH(32))
     212
     213          #Check if password string is longer than 5 characters:
     214          INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6))
     215
     216          #Check if uploaded file has size between 1KB and 1MB:
     217          INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024))
     218
     219          >>> IS_LENGTH()('')
     220          ('', None)
     221          >>> IS_LENGTH()('1234567890')
     222          ('1234567890', None)
     223          >>> IS_LENGTH(maxsize=5, minsize=0)('1234567890')  # too long
     224          ('1234567890', 'enter from 0 to 5 characters')
     225          >>> IS_LENGTH(maxsize=50, minsize=20)('1234567890')  # too short
     226          ('1234567890', 'enter from 20 to 50 characters')
     227      """
     228
     229 -    def __init__(self, maxsize=255, minsize=0,
     230                   error_message='enter from %(min)g to %(max)g characters'):
     231          self.maxsize = maxsize
     232          self.minsize = minsize
     233          self.error_message = error_message
     234
     235 -    def __call__(self, value):
     236          if isinstance(value, cgi.FieldStorage):
     237              if value.file:
     238                  value.file.seek(0, os.SEEK_END)
     239                  length = value.file.tell()
     240                  value.file.seek(0, os.SEEK_SET)
     241              else:
     242                  val = value.value
     243                  if val:
     244                      length = len(val)
     245                  else:
     246                      length = 0
     247              if self.minsize <= length <= self.maxsize:
     248                  return (value, None)
     249          elif isinstance(value, (str, unicode, list)):
     250              if self.minsize <= len(value) <= self.maxsize:
     251                  return (value, None)
     252          elif self.minsize <= len(str(value)) <= self.maxsize:
     253              try:
     254                  value.decode('utf8')
     255                  return (value, None)
     256              except:
     257                  pass
     258          return (value, translate(self.error_message) \
     259                      % dict(min=self.minsize, max=self.maxsize))
     260
     261
     262 -class IS_IN_SET(Validator):
     263      """
     264      example::
     265
     266          INPUT(_type='text', _name='name',
     267                requires=IS_IN_SET(['max', 'john'],zero=''))
     268
     269      the argument of IS_IN_SET must be a list or set
     270
     271          >>> IS_IN_SET(['max', 'john'])('max')
     272          ('max', None)
     273          >>> IS_IN_SET(['max', 'john'])('massimo')
     274          ('massimo', 'value not allowed')
     275          >>> IS_IN_SET(['max', 'john'], multiple=True)(('max', 'john'))
     276          (('max', 'john'), None)
     277          >>> IS_IN_SET(['max', 'john'], multiple=True)(('bill', 'john'))
     278          (('bill', 'john'), 'value not allowed')
     279          >>> IS_IN_SET(('id1','id2'), ['first label','second label'])('id1') # Traditional way
     280          ('id1', None)
     281          >>> IS_IN_SET({'id1':'first label', 'id2':'second label'})('id1')
     282          ('id1', None)
     283          >>> import itertools
     284          >>> IS_IN_SET(itertools.chain(['1','3','5'],['2','4','6']))('1')
     285          ('1', None)
     286          >>> IS_IN_SET([('id1','first label'), ('id2','second label')])('id1') # Redundant way
     287          ('id1', None)
     288      """
     289
     290 -    def __init__(
     291          self,
     292          theset,
     293          labels=None,
     294          error_message='value not allowed',
     295          multiple=False,
     296          zero='',
     297          sort=False,
     298          ):
     299          self.multiple = multiple
     300          if isinstance(theset, dict):
     301              self.theset = [str(item) for item in theset]
     302              self.labels = theset.values()
     303          elif theset and isinstance(theset, (tuple,list)) \
     304              and isinstance(theset[0], (tuple,list)) and len(theset[0])==2:
     305              self.theset = [str(item) for item,label in theset]
     306              self.labels = [str(label) for item,label in theset]
     307          else:
     308              self.theset = [str(item) for item in theset]
     309              self.labels = labels
     310          self.error_message = error_message
     311          self.zero = zero
     312          self.sort = sort
     313
     314 -    def options(self,zero=True):
     315          if not self.labels:
     316              items = [(k, k) for (i, k) in enumerate(self.theset)]
     317          else:
     318              items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)]
     319          if self.sort:
     320              items.sort(options_sorter)
     321          if zero and self.zero != None and not self.multiple:
     322              items.insert(0,('',self.zero))
     323          return items
     324
     325 -    def __call__(self, value):
     326          if self.multiple:
     327              ### if below was values = re.compile("[\w\-:]+").findall(str(value))
     328              if isinstance(value, (str,unicode)):
     329                  values = [value]
     330              elif isinstance(value, (tuple, list)):
     331                  values = value
     332              elif not value:
     333                  values = []
     334          else:
     335              values = [value]
     336          failures = [x for x in values if not x in self.theset]
     337          if failures and self.theset:
     338              if self.multiple and (value == None or value == ''):
     339                  return ([], None)
     340              return (value, translate(self.error_message))
     341          if self.multiple:
     342              if isinstance(self.multiple,(tuple,list)) and \
     343                      not self.multiple[0]<=len(values)<self.multiple[1]:
     344                  return (values, translate(self.error_message))
     345              return (values, None)
     346          return (value, None)
     347
     348
     349  regex1 = re.compile('[\w_]+\.[\w_]+')
     350  regex2 = re.compile('%\((?P<name>[^\)]+)\)s')
     351
     352
     353 -class IS_IN_DB(Validator):
     354      """
     355      example::
     356
     357          INPUT(_type='text', _name='name',
     358                requires=IS_IN_DB(db, db.mytable.myfield, zero=''))
     359
     360      used for reference fields, rendered as a dropbox
     361      """
     362
     363 -    def __init__(
     364          self,
     365          dbset,
     366          field,
     367          label=None,
     368          error_message='value not in database',
     369          orderby=None,
     370          groupby=None,
     371          cache=None,
     372          multiple=False,
     373          zero='',
     374          sort=False,
     375          _and=None,
     376          ):
     377          from dal import Table
     378          if isinstance(field,Table): field = field._id
     379
     380          if hasattr(dbset, 'define_table'):
     381              self.dbset = dbset()
     382          else:
     383              self.dbset = dbset
     384          self.field = field
     385          (ktable, kfield) = str(self.field).split('.')
     386          if not label:
     387              label = '%%(%s)s' % kfield
     388          if isinstance(label,str):
     389              if regex1.match(str(label)):
     390                  label = '%%(%s)s' % str(label).split('.')[-1]
     391              ks = regex2.findall(label)
     392              if not kfield in ks:
     393                  ks += [kfield]
     394              fields = ks
     395          else:
     396              ks = [kfield]
     397              fields = 'all'
     398          self.fields = fields
     399          self.label = label
     400          self.ktable = ktable
     401          self.kfield = kfield
     402          self.ks = ks
     403          self.error_message = error_message
     404          self.theset = None
     405          self.orderby = orderby
     406          self.groupby = groupby
     407          self.cache = cache
     408          self.multiple = multiple
     409          self.zero = zero
     410          self.sort = sort
     411          self._and = _and
     412
     413 -    def set_self_id(self, id):
     414          if self._and:
     415              self._and.record_id = id
     416
     417 -    def build_set(self):
     418          if self.fields == 'all':
     419              fields = [f for f in self.dbset.db[self.ktable]]
     420          else:
     421              fields = [self.dbset.db[self.ktable][k] for k in self.fields]
     422          if self.dbset.db._dbname != 'gae':
     423              orderby = self.orderby or reduce(lambda a,b:a|b,fields)
     424              groupby = self.groupby
     425              dd = dict(orderby=orderby, groupby=groupby, cache=self.cache)
     426              records = self.dbset.select(*fields, **dd)
     427          else:
     428              orderby = self.orderby or reduce(lambda a,b:a|b,(f for f in fields if not f.name=='id'))
     429              dd = dict(orderby=orderby, cache=self.cache)
     430              records = self.dbset.select(self.dbset.db[self.ktable].ALL, **dd)
     431          self.theset = [str(r[self.kfield]) for r in records]
     432          if isinstance(self.label,str):
     433              self.labels = [self.label % dict(r) for r in records]
     434          else:
     435              self.labels = [self.label(r) for r in records]
     436
     437 -    def options(self, zero=True):
     438          self.build_set()
     439          items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)]
     440          if self.sort:
     441              items.sort(options_sorter)
     442          if zero and self.zero != None and not self.multiple:
     443              items.insert(0,('',self.zero))
     444          return items
     445
     446 -    def __call__(self, value):
     447          if self.multiple:
     448              if isinstance(value,list):
     449                  values=value
     450              elif value:
     451                  values = [value]
     452              else:
     453                  values = []
     454              if isinstance(self.multiple,(tuple,list)) and \
     455                      not self.multiple[0]<=len(values)<self.multiple[1]:
     456                  return (values, translate(self.error_message))
     457              if not [x for x in values if not x in self.theset]:
     458                  return (values, None)
     459          elif self.theset:
     460              if value in self.theset:
     461                  if self._and:
     462                      return self._and(value)
     463                  else:
     464                      return (value, None)
     465          else:
     466              (ktable, kfield) = str(self.field).split('.')
     467              field = self.dbset.db[ktable][kfield]
     468              if self.dbset(field == value).count():
     469                  if self._and:
     470                      return self._and(value)
     471                  else:
     472                      return (value, None)
     473          return (value, translate(self.error_message))
     474
     475
     476 -class IS_NOT_IN_DB(Validator):
     477      """
     478      example::
     479
     480          INPUT(_type='text', _name='name', requires=IS_NOT_IN_DB(db, db.table))
     481
     482      makes the field unique
     483      """
     484
     485 -    def __init__(
     486          self,
     487          dbset,
     488          field,
     489          error_message='value already in database or empty',
     490          allowed_override=[],
     491          ):
     492
     493          from dal import Table
     494          if isinstance(field,Table): field = field._id
     495
     496          if hasattr(dbset, 'define_table'):
     497              self.dbset = dbset()
     498          else:
     499              self.dbset = dbset
     500          self.field = field
     501          self.error_message = error_message
     502          self.record_id = 0
     503          self.allowed_override = allowed_override
     504
     505 -    def set_self_id(self, id):
     506          self.record_id = id
     507
     508 -    def __call__(self, value):
     509          value=str(value)
     510          if not value.strip():
     511              return (value, translate(self.error_message))
     512          if value in self.allowed_override:
     513              return (value, None)
     514          (tablename, fieldname) = str(self.field).split('.')
     515          field = self.dbset.db[tablename][fieldname]
     516          rows = self.dbset(field == value).select(limitby=(0, 1))
     517          if len(rows) > 0:
     518              if isinstance(self.record_id, dict):
     519                  for f in self.record_id:
     520                      if str(getattr(rows[0], f)) != str(self.record_id[f]):
     521                          return (value, translate(self.error_message))
     522              elif str(rows[0].id) != str(self.record_id):
     523                  return (value, translate(self.error_message))
     524          return (value, None)
     525
     526
     527 -class IS_INT_IN_RANGE(Validator):
     528      """
     529      Determine that the argument is (or can be represented as) an int,
     530      and that it falls within the specified range. The range is interpreted
     531      in the Pythonic way, so the test is: min <= value < max.
     532
     533      The minimum and maximum limits can be None, meaning no lower or upper limit,
     534      respectively.
     535
     536      example::
     537
     538          INPUT(_type='text', _name='name', requires=IS_INT_IN_RANGE(0, 10))
     539
     540          >>> IS_INT_IN_RANGE(1,5)('4')
     541          (4, None)
     542          >>> IS_INT_IN_RANGE(1,5)(4)
     543          (4, None)
     544          >>> IS_INT_IN_RANGE(1,5)(1)
     545          (1, None)
     546          >>> IS_INT_IN_RANGE(1,5)(5)
     547          (5, 'enter an integer between 1 and 4')
     548          >>> IS_INT_IN_RANGE(1,5)(5)
     549          (5, 'enter an integer between 1 and 4')
     550          >>> IS_INT_IN_RANGE(1,5)(3.5)
     551          (3, 'enter an integer between 1 and 4')
     552          >>> IS_INT_IN_RANGE(None,5)('4')
     553          (4, None)
     554          >>> IS_INT_IN_RANGE(None,5)('6')
     555          (6, 'enter an integer less than or equal to 4')
     556          >>> IS_INT_IN_RANGE(1,None)('4')
     557          (4, None)
     558          >>> IS_INT_IN_RANGE(1,None)('0')
     559          (0, 'enter an integer greater than or equal to 1')
     560          >>> IS_INT_IN_RANGE()(6)
     561          (6, None)
     562          >>> IS_INT_IN_RANGE()('abc')
     563          ('abc', 'enter an integer')
     564      """
     565
     566 -    def __init__(
     567          self,
     568          minimum=None,
     569          maximum=None,
     570          error_message=None,
     571          ):
     572          self.minimum = self.maximum = None
     573          if minimum is None:
     574              if maximum is None:
     575                  self.error_message = error_message or 'enter an integer'
     576              else:
     577                  self.maximum = int(maximum)
     578                  if error_message is None:
     579                      error_message = 'enter an integer less than or equal to %(max)g'
     580                  self.error_message = translate(error_message) % dict(max=self.maximum-1)
     581          elif maximum is None:
     582              self.minimum = int(minimum)
     583              if error_message is None:
     584                  error_message = 'enter an integer greater than or equal to %(min)g'
     585              self.error_message = translate(error_message) % dict(min=self.minimum)
     586          else:
     587              self.minimum = int(minimum)
     588              self.maximum = int(maximum)
     589              if error_message is None:
     590                  error_message = 'enter an integer between %(min)g and %(max)g'
     591              self.error_message = translate(error_message) \
     592                  % dict(min=self.minimum, max=self.maximum-1)
     593
     594 -    def __call__(self, value):
     595          try:
     596              fvalue = float(value)
     597              value = int(value)
     598              if value != fvalue:
     599                  return (value, self.error_message)
     600              if self.minimum is None:
     601                  if self.maximum is None or value < self.maximum:
     602                      return (value, None)
     603              elif self.maximum is None:
     604                  if value >= self.minimum:
     605                      return (value, None)
     606              elif self.minimum <= value < self.maximum:
     607                      return (value, None)
     608          except ValueError:
     609              pass
     610          return (value, self.error_message)
     611
     612
     613 -class IS_FLOAT_IN_RANGE(Validator):
     614      """
     615      Determine that the argument is (or can be represented as) a float,
     616      and that it falls within the specified inclusive range.
     617      The comparison is made with native arithmetic.
     618
     619      The minimum and maximum limits can be None, meaning no lower or upper limit,
     620      respectively.
     621
     622      example::
     623
     624          INPUT(_type='text', _name='name', requires=IS_FLOAT_IN_RANGE(0, 10))
     625
     626          >>> IS_FLOAT_IN_RANGE(1,5)('4')
     627          (4.0, None)
     628          >>> IS_FLOAT_IN_RANGE(1,5)(4)
     629          (4.0, None)
     630          >>> IS_FLOAT_IN_RANGE(1,5)(1)
     631          (1.0, None)
     632          >>> IS_FLOAT_IN_RANGE(1,5)(5.25)
     633          (5.25, 'enter a number between 1 and 5')
     634          >>> IS_FLOAT_IN_RANGE(1,5)(6.0)
     635          (6.0, 'enter a number between 1 and 5')
     636          >>> IS_FLOAT_IN_RANGE(1,5)(3.5)
     637          (3.5, None)
     638          >>> IS_FLOAT_IN_RANGE(1,None)(3.5)
     639          (3.5, None)
     640          >>> IS_FLOAT_IN_RANGE(None,5)(3.5)
     641          (3.5, None)
     642          >>> IS_FLOAT_IN_RANGE(1,None)(0.5)
     643          (0.5, 'enter a number greater than or equal to 1')
     644          >>> IS_FLOAT_IN_RANGE(None,5)(6.5)
     645          (6.5, 'enter a number less than or equal to 5')
     646          >>> IS_FLOAT_IN_RANGE()(6.5)
     647          (6.5, None)
     648          >>> IS_FLOAT_IN_RANGE()('abc')
     649          ('abc', 'enter a number')
     650      """
     651
     652 -    def __init__(
     653          self,
     654          minimum=None,
     655          maximum=None,
     656          error_message=None,
     657          dot='.'
     658          ):
     659          self.minimum = self.maximum = None
     660          self.dot = dot
     661          if minimum is None:
     662              if maximum is None:
     663                  if error_message is None:
     664                      error_message = 'enter a number'
     665              else:
     666                  self.maximum = float(maximum)
     667                  if error_message is None:
     668                      error_message = 'enter a number less than or equal to %(max)g'
     669          elif maximum is None:
     670              self.minimum = float(minimum)
     671              if error_message is None:
     672                  error_message = 'enter a number greater than or equal to %(min)g'
     673          else:
     674              self.minimum = float(minimum)
     675              self.maximum = float(maximum)
     676              if error_message is None:
     677                  error_message = 'enter a number between %(min)g and %(max)g'
     678          self.error_message = translate(error_message) \
     679              % dict(min=self.minimum, max=self.maximum)
     680
     681 -    def __call__(self, value):
     682          try:
     683              if self.dot=='.':
     684                  fvalue = float(value)
     685              else:
     686                  fvalue = float(str(value).replace(self.dot,'.'))
     687              if self.minimum is None:
     688                  if self.maximum is None or fvalue <= self.maximum:
     689                      return (fvalue, None)
     690              elif self.maximum is None:
     691                  if fvalue >= self.minimum:
     692                      return (fvalue, None)
     693              elif self.minimum <= fvalue <= self.maximum:
     694                      return (fvalue, None)
     695          except (ValueError, TypeError):
     696              pass
     697          return (value, self.error_message)
     698
     699 -    def formatter(self,value):
     700          if self.dot=='.':
     701              return str(value)
     702          else:
     703              return str(value).replace('.',self.dot)
     704
     705
     706 -class IS_DECIMAL_IN_RANGE(Validator):
     707      """
     708      Determine that the argument is (or can be represented as) a Python Decimal,
     709      and that it falls within the specified inclusive range.
     710      The comparison is made with Python Decimal arithmetic.
     711
     712      The minimum and maximum limits can be None, meaning no lower or upper limit,
     713      respectively.
     714
     715      example::
     716
     717          INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10))
     718
     719          >>> IS_DECIMAL_IN_RANGE(1,5)('4')
     720          (Decimal('4'), None)
     721          >>> IS_DECIMAL_IN_RANGE(1,5)(4)
     722          (Decimal('4'), None)
     723          >>> IS_DECIMAL_IN_RANGE(1,5)(1)
     724          (Decimal('1'), None)
     725          >>> IS_DECIMAL_IN_RANGE(1,5)(5.25)
     726          (5.25, 'enter a number between 1 and 5')
     727          >>> IS_DECIMAL_IN_RANGE(5.25,6)(5.25)
     728          (Decimal('5.25'), None)
     729          >>> IS_DECIMAL_IN_RANGE(5.25,6)('5.25')
     730          (Decimal('5.25'), None)
     731          >>> IS_DECIMAL_IN_RANGE(1,5)(6.0)
     732          (6.0, 'enter a number between 1 and 5')
     733          >>> IS_DECIMAL_IN_RANGE(1,5)(3.5)
     734          (Decimal('3.5'), None)
     735          >>> IS_DECIMAL_IN_RANGE(1.5,5.5)(3.5)
     736          (Decimal('3.5'), None)
     737          >>> IS_DECIMAL_IN_RANGE(1.5,5.5)(6.5)
     738          (6.5, 'enter a number between 1.5 and 5.5')
     739          >>> IS_DECIMAL_IN_RANGE(1.5,None)(6.5)
     740          (Decimal('6.5'), None)
     741          >>> IS_DECIMAL_IN_RANGE(1.5,None)(0.5)
     742          (0.5, 'enter a number greater than or equal to 1.5')
     743          >>> IS_DECIMAL_IN_RANGE(None,5.5)(4.5)
     744          (Decimal('4.5'), None)
     745          >>> IS_DECIMAL_IN_RANGE(None,5.5)(6.5)
     746          (6.5, 'enter a number less than or equal to 5.5')
     747          >>> IS_DECIMAL_IN_RANGE()(6.5)
     748          (Decimal('6.5'), None)
     749          >>> IS_DECIMAL_IN_RANGE(0,99)(123.123)
     750          (123.123, 'enter a number between 0 and 99')
     751          >>> IS_DECIMAL_IN_RANGE(0,99)('123.123')
     752          ('123.123', 'enter a number between 0 and 99')
     753          >>> IS_DECIMAL_IN_RANGE(0,99)('12.34')
     754          (Decimal('12.34'), None)
     755          >>> IS_DECIMAL_IN_RANGE()('abc')
     756          ('abc', 'enter a decimal number')
     757      """
     758
     759 -    def __init__(
     760          self,
     761          minimum=None,
     762          maximum=None,
     763          error_message=None,
     764          dot='.'
     765          ):
     766          self.minimum = self.maximum = None
     767          self.dot = dot
     768          if minimum is None:
     769              if maximum is None:
     770                  if error_message is None:
     771                      error_message = 'enter a decimal number'
     772              else:
     773                  self.maximum = decimal.Decimal(str(maximum))
     774                  if error_message is None:
     775                      error_message = 'enter a number less than or equal to %(max)g'
     776          elif maximum is None:
     777              self.minimum = decimal.Decimal(str(minimum))
     778              if error_message is None:
     779                  error_message = 'enter a number greater than or equal to %(min)g'
     780          else:
     781              self.minimum = decimal.Decimal(str(minimum))
     782              self.maximum = decimal.Decimal(str(maximum))
     783              if error_message is None:
     784                  error_message = 'enter a number between %(min)g and %(max)g'
     785          self.error_message = translate(error_message) \
     786              % dict(min=self.minimum, max=self.maximum)
     787
     788 -    def __call__(self, value):
     789          try:
     790              if isinstance(value,decimal.Decimal):
     791                  v = value
     792              else:
     793                  v = decimal.Decimal(str(value).replace(self.dot,'.'))
     794              if self.minimum is None:
     795                  if self.maximum is None or v <= self.maximum:
     796                      return (v, None)
     797              elif self.maximum is None:
     798                  if v >= self.minimum:
     799                      return (v, None)
     800              elif self.minimum <= v <= self.maximum:
     801                      return (v, None)
     802          except (ValueError, TypeError, decimal.InvalidOperation):
     803              pass
     804          return (value, self.error_message)
     805
     806 -    def formatter(self, value):
     807          return str(value).replace('.',self.dot)
     808
     809 -def is_empty(value, empty_regex=None):
     810      "test empty field"
     811      if isinstance(value, (str, unicode)):
     812          value = value.strip()
     813          if empty_regex is not None and empty_regex.match(value):
     814              value = ''
     815      if value == None or value == '' or value == []:
     816          return (value, True)
     817      return (value, False)
     818
     819 -class IS_NOT_EMPTY(Validator):
     820      """
     821      example::
     822
     823          INPUT(_type='text', _name='name', requires=IS_NOT_EMPTY())
     824
     825          >>> IS_NOT_EMPTY()(1)
     826          (1, None)
     827          >>> IS_NOT_EMPTY()(0)
     828          (0, None)
     829          >>> IS_NOT_EMPTY()('x')
     830          ('x', None)
     831          >>> IS_NOT_EMPTY()(' x ')
     832          ('x', None)
     833          >>> IS_NOT_EMPTY()(None)
     834          (None, 'enter a value')
     835          >>> IS_NOT_EMPTY()('')
     836          ('', 'enter a value')
     837          >>> IS_NOT_EMPTY()('  ')
     838          ('', 'enter a value')
     839          >>> IS_NOT_EMPTY()(' \\n\\t')
     840          ('', 'enter a value')
     841          >>> IS_NOT_EMPTY()([])
     842          ([], 'enter a value')
     843          >>> IS_NOT_EMPTY(empty_regex='def')('def')
     844          ('', 'enter a value')
     845          >>> IS_NOT_EMPTY(empty_regex='de[fg]')('deg')
     846          ('', 'enter a value')
     847          >>> IS_NOT_EMPTY(empty_regex='def')('abc')
     848          ('abc', None)
     849      """
     850
     851 -    def __init__(self, error_message='enter a value', empty_regex=None):
     852          self.error_message = error_message
     853          if empty_regex is not None:
     854              self.empty_regex = re.compile(empty_regex)
     855          else:
     856              self.empty_regex = None
     857
     858 -    def __call__(self, value):
     859          value, empty = is_empty(value, empty_regex=self.empty_regex)
     860          if empty:
     861              return (value, translate(self.error_message))
     862          return (value, None)
     863
     864
     865 -class IS_ALPHANUMERIC(IS_MATCH):
     866      """
     867      example::
     868
     869          INPUT(_type='text', _name='name', requires=IS_ALPHANUMERIC())
     870
     871          >>> IS_ALPHANUMERIC()('1')
     872          ('1', None)
     873          >>> IS_ALPHANUMERIC()('')
     874          ('', None)
     875          >>> IS_ALPHANUMERIC()('A_a')
     876          ('A_a', None)
     877          >>> IS_ALPHANUMERIC()('!')
     878          ('!', 'enter only letters, numbers, and underscore')
     879      """
     880
     881 -    def __init__(self, error_message='enter only letters, numbers, and underscore'):
     882          IS_MATCH.__init__(self, '^[\w]*$', error_message)
     883
     884
     885 -class IS_EMAIL(Validator):
     886      """
     887      Checks if field's value is a valid email address. Can be set to disallow
     888      or force addresses from certain domain(s).
     889
     890      Email regex adapted from
     891      http://haacked.com/archive/2007/08/21/i-knew-how-to-validate-an-email-address-until-i.aspx,
     892      generally following the RFCs, except that we disallow quoted strings
     893      and permit underscores and leading numerics in subdomain labels
     894
     895      Arguments:
     896
     897      - banned: regex text for disallowed address domains
     898      - forced: regex text for required address domains
     899
     900      Both arguments can also be custom objects with a match(value) method.
     901
     902      Examples::
     903
     904          #Check for valid email address:
     905          INPUT(_type='text', _name='name',
     906              requires=IS_EMAIL())
     907
     908          #Check for valid email address that can't be from a .com domain:
     909          INPUT(_type='text', _name='name',
     910              requires=IS_EMAIL(banned='^.*\.com(|\..*)$'))
     911
     912          #Check for valid email address that must be from a .edu domain:
     913          INPUT(_type='text', _name='name',
     914              requires=IS_EMAIL(forced='^.*\.edu(|\..*)$'))
     915
     916          >>> IS_EMAIL()('a@b.com')
     917          ('a@b.com', None)
     918          >>> IS_EMAIL()('abc@def.com')
     919          ('abc@def.com', None)
     920          >>> IS_EMAIL()('abc@3def.com')
     921          ('abc@3def.com', None)
     922          >>> IS_EMAIL()('abc@def.us')
     923          ('abc@def.us', None)
     924          >>> IS_EMAIL()('abc@d_-f.us')
     925          ('abc@d_-f.us', None)
     926          >>> IS_EMAIL()('@def.com')           # missing name
     927          ('@def.com', 'enter a valid email address')
     928          >>> IS_EMAIL()('"abc@def".com')      # quoted name
     929          ('"abc@def".com', 'enter a valid email address')
     930          >>> IS_EMAIL()('abc+def.com')        # no @
     931          ('abc+def.com', 'enter a valid email address')
     932          >>> IS_EMAIL()('abc@def.x')          # one-char TLD
     933          ('abc@def.x', 'enter a valid email address')
     934          >>> IS_EMAIL()('abc@def.12')         # numeric TLD
     935          ('abc@def.12', 'enter a valid email address')
     936          >>> IS_EMAIL()('abc@def..com')       # double-dot in domain
     937          ('abc@def..com', 'enter a valid email address')
     938          >>> IS_EMAIL()('abc@.def.com')       # dot starts domain
     939          ('abc@.def.com', 'enter a valid email address')
     940          >>> IS_EMAIL()('abc@def.c_m')        # underscore in TLD
     941          ('abc@def.c_m', 'enter a valid email address')
     942          >>> IS_EMAIL()('NotAnEmail')         # missing @
     943          ('NotAnEmail', 'enter a valid email address')
     944          >>> IS_EMAIL()('abc@NotAnEmail')     # missing TLD
     945          ('abc@NotAnEmail', 'enter a valid email address')
     946          >>> IS_EMAIL()('customer/department@example.com')
     947          ('customer/department@example.com', None)
     948          >>> IS_EMAIL()('$A12345@example.com')
     949          ('$A12345@example.com', None)
     950          >>> IS_EMAIL()('!def!xyz%abc@example.com')
     951          ('!def!xyz%abc@example.com', None)
     952          >>> IS_EMAIL()('_Yosemite.Sam@example.com')
     953          ('_Yosemite.Sam@example.com', None)
     954          >>> IS_EMAIL()('~@example.com')
     955          ('~@example.com', None)
     956          >>> IS_EMAIL()('.wooly@example.com')       # dot starts name
     957          ('.wooly@example.com', 'enter a valid email address')
     958          >>> IS_EMAIL()('wo..oly@example.com')      # adjacent dots in name
     959          ('wo..oly@example.com', 'enter a valid email address')
     960          >>> IS_EMAIL()('pootietang.@example.com')  # dot ends name
     961          ('pootietang.@example.com', 'enter a valid email address')
     962          >>> IS_EMAIL()('.@example.com')            # name is bare dot
     963          ('.@example.com', 'enter a valid email address')
     964          >>> IS_EMAIL()('Ima.Fool@example.com')
     965          ('Ima.Fool@example.com', None)
     966          >>> IS_EMAIL()('Ima Fool@example.com')     # space in name
     967          ('Ima Fool@example.com', 'enter a valid email address')
     968          >>> IS_EMAIL()('localguy@localhost')       # localhost as domain
     969          ('localguy@localhost', None)
     970
     971      """
     972
     973      regex = re.compile('''
     974          ^(?!\.)                            # name may not begin with a dot
     975          (
     976            [-a-z0-9!\#$%&'*+/=?^_`{|}~]     # all legal characters except dot
     977            |
     978            (?<!\.)\.                        # single dots only
     979          )+
     980          (?<!\.)                            # name may not end with a dot
     981          @
     982          (
     983            localhost
     984            |
     985            (
     986              [a-z0-9]                         # [sub]domain begins with alphanumeric
     987              (
     988                [-\w]*                         # alphanumeric, underscore, dot, hyphen
     989                [a-z0-9]                       # ending alphanumeric
     990              )?
     991            \.                               # ending dot
     992            )+
     993            [a-z]{2,}                        # TLD alpha-only
     994         )$
     995      ''', re.VERBOSE|re.IGNORECASE)
     996
     997      regex_proposed_but_failed = re.compile('^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$',re.VERBOSE|re.IGNORECASE)
     998
     999 -    def __init__(self,
    1000                   banned=None,
    1001                   forced=None,
    1002                   error_message='enter a valid email address'):
    1003          if isinstance(banned, str):
    1004              banned = re.compile(banned)
    1005          if isinstance(forced, str):
    1006              forced = re.compile(forced)
    1007          self.banned = banned
    1008          self.forced = forced
    1009          self.error_message = error_message
    1010
    1011 -    def __call__(self, value):
    1012          match = self.regex.match(value)
    1013          if match:
    1014              domain = value.split('@')[1]
    1015              if (not self.banned or not self.banned.match(domain)) \
    1016                      and (not self.forced or self.forced.match(domain)):
    1017                  return (value, None)
    1018          return (value, translate(self.error_message))
    1019
    1020
    1021  # URL scheme source:
    1022  # <http://en.wikipedia.org/wiki/URI_scheme> obtained on 2008-Nov-10
    1023
    1024  official_url_schemes = [
    1025      'aaa',
    1026      'aaas',
    1027      'acap',
    1028      'cap',
    1029      'cid',
    1030      'crid',
    1031      'data',
    1032      'dav',
    1033      'dict',
    1034      'dns',
    1035      'fax',
    1036      'file',
    1037      'ftp',
    1038      'go',
    1039      'gopher',
    1040      'h323',
    1041      'http',
    1042      'https',
    1043      'icap',
    1044      'im',
    1045      'imap',
    1046      'info',
    1047      'ipp',
    1048      'iris',
    1049      'iris.beep',
    1050      'iris.xpc',
    1051      'iris.xpcs',
    1052      'iris.lws',
    1053      'ldap',
    1054      'mailto',
    1055      'mid',
    1056      'modem',
    1057      'msrp',
    1058      'msrps',
    1059      'mtqp',
    1060      'mupdate',
    1061      'news',
    1062      'nfs',
    1063      'nntp',
    1064      'opaquelocktoken',
    1065      'pop',
    1066      'pres',
    1067      'prospero',
    1068      'rtsp',
    1069      'service',
    1070      'shttp',
    1071      'sip',
    1072      'sips',
    1073      'snmp',
    1074      'soap.beep',
    1075      'soap.beeps',
    1076      'tag',
    1077      'tel',
    1078      'telnet',
    1079      'tftp',
    1080      'thismessage',
    1081      'tip',
    1082      'tv',
    1083      'urn',
    1084      'vemmi',
    1085      'wais',
    1086      'xmlrpc.beep',
    1087      'xmlrpc.beep',
    1088      'xmpp',
    1089      'z39.50r',
    1090      'z39.50s',
    1091      ]
    1092  unofficial_url_schemes = [
    1093      'about',
    1094      'adiumxtra',
    1095      'aim',
    1096      'afp',
    1097      'aw',
    1098      'callto',
    1099      'chrome',
    1100      'cvs',
    1101      'ed2k',
    1102      'feed',
    1103      'fish',
    1104      'gg',
    1105      'gizmoproject',
    1106      'iax2',
    1107      'irc',
    1108      'ircs',
    1109      'itms',
    1110      'jar',
    1111      'javascript',
    1112      'keyparc',
    1113      'lastfm',
    1114      'ldaps',
    1115      'magnet',
    1116      'mms',
    1117      'msnim',
    1118      'mvn',
    1119      'notes',
    1120      'nsfw',
    1121      'psyc',
    1122      'paparazzi:http',
    1123      'rmi',
    1124      'rsync',
    1125      'secondlife',
    1126      'sgn',
    1127      'skype',
    1128      'ssh',
    1129      'sftp',
    1130      'smb',
    1131      'sms',
    1132      'soldat',
    1133      'steam',
    1134      'svn',
    1135      'teamspeak',
    1136      'unreal',
    1137      'ut2004',
    1138      'ventrilo',
    1139      'view-source',
    1140      'webcal',
    1141      'wyciwyg',
    1142      'xfire',
    1143      'xri',
    1144      'ymsgr',
    1145      ]
    1146  all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes
    1147  http_schemes = [None, 'http', 'https']
    1148
    1149
    1150  # This regex comes from RFC 2396, Appendix B. It's used to split a URL into
    1151  # its component parts
    1152  # Here are the regex groups that it extracts:
    1153  #    scheme = group(2)
    1154  #    authority = group(4)
    1155  #    path = group(5)
    1156  #    query = group(7)
    1157  #    fragment = group(9)
    1158
    1159  url_split_regex = \
    1160      re.compile('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?')
    1161
    1162  # Defined in RFC 3490, Section 3.1, Requirement #1
    1163  # Use this regex to split the authority component of a unicode URL into
    1164  # its component labels
    1165  label_split_regex = re.compile(u'[\u002e\u3002\uff0e\uff61]')
    1166
    1167
    1168 -def escape_unicode(string):
    1169      '''
    1170      Converts a unicode string into US-ASCII, using a simple conversion scheme.
    1171      Each unicode character that does not have a US-ASCII equivalent is
    1172      converted into a URL escaped form based on its hexadecimal value.
    1173      For example, the unicode character '\u4e86' will become the string '%4e%86'
    1174
    1175      :param string: unicode string, the unicode string to convert into an
    1176          escaped US-ASCII form
    1177      :returns: the US-ASCII escaped form of the inputted string
    1178      :rtype: string
    1179
    1180      @author: Jonathan Benn
    1181      '''
    1182      returnValue = StringIO()
    1183
    1184      for character in string:
    1185          code = ord(character)
    1186          if code > 0x7F:
    1187              hexCode = hex(code)
    1188              returnValue.write('%' + hexCode[2:4] + '%' + hexCode[4:6])
    1189          else:
    1190              returnValue.write(character)
    1191
    1192      return returnValue.getvalue()
    1193
    1194
    1195 -def unicode_to_ascii_authority(authority):
    1196      '''
    1197      Follows the steps in RFC 3490, Section 4 to convert a unicode authority
    1198      string into its ASCII equivalent.
    1199      For example, u'www.Alliancefran\xe7aise.nu' will be converted into
    1200      'www.xn--alliancefranaise-npb.nu'
    1201
    1202      :param authority: unicode string, the URL authority component to convert,
    1203                        e.g. u'www.Alliancefran\xe7aise.nu'
    1204      :returns: the US-ASCII character equivalent to the inputed authority,
    1205               e.g. 'www.xn--alliancefranaise-npb.nu'
    1206      :rtype: string
    1207      :raises Exception: if the function is not able to convert the inputed
    1208          authority
    1209
    1210      @author: Jonathan Benn
    1211      '''
    1212      #RFC 3490, Section 4, Step 1
    1213      #The encodings.idna Python module assumes that AllowUnassigned == True
    1214
    1215      #RFC 3490, Section 4, Step 2
    1216      labels = label_split_regex.split(authority)
    1217
    1218      #RFC 3490, Section 4, Step 3
    1219      #The encodings.idna Python module assumes that UseSTD3ASCIIRules == False
    1220
    1221      #RFC 3490, Section 4, Step 4
    1222      #We use the ToASCII operation because we are about to put the authority
    1223      #into an IDN-unaware slot
    1224      asciiLabels = []
    1225      try:
    1226          import encodings.idna
    1227          for label in labels:
    1228              if label:
    1229                  asciiLabels.append(encodings.idna.ToASCII(label))
    1230              else:
    1231                   #encodings.idna.ToASCII does not accept an empty string, but
    1232                   #it is necessary for us to allow for empty labels so that we
    1233                   #don't modify the URL
    1234                  asciiLabels.append('')
    1235      except:
    1236          asciiLabels=[str(label) for label in labels]
    1237      #RFC 3490, Section 4, Step 5
    1238      return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels))
    1239
    1240
    1241 -def unicode_to_ascii_url(url, prepend_scheme):
    1242      '''
    1243      Converts the inputed unicode url into a US-ASCII equivalent. This function
    1244      goes a little beyond RFC 3490, which is limited in scope to the domain name
    1245      (authority) only. Here, the functionality is expanded to what was observed
    1246      on Wikipedia on 2009-Jan-22:
    1247
    1248         Component    Can Use Unicode?
    1249         ---------    ----------------
    1250         scheme       No
    1251         authority    Yes
    1252         path         Yes
    1253         query        Yes
    1254         fragment     No
    1255
    1256      The authority component gets converted to punycode, but occurrences of
    1257      unicode in other components get converted into a pair of URI escapes (we
    1258      assume 4-byte unicode). E.g. the unicode character U+4E2D will be
    1259      converted into '%4E%2D'. Testing with Firefox v3.0.5 has shown that it can
    1260      understand this kind of URI encoding.
    1261
    1262      :param url: unicode string, the URL to convert from unicode into US-ASCII
    1263      :param prepend_scheme: string, a protocol scheme to prepend to the URL if
    1264          we're having trouble parsing it.
    1265          e.g. "http". Input None to disable this functionality
    1266      :returns: a US-ASCII equivalent of the inputed url
    1267      :rtype: string
    1268
    1269      @author: Jonathan Benn
    1270      '''
    1271      #convert the authority component of the URL into an ASCII punycode string,
    1272      #but encode the rest using the regular URI character encoding
    1273
    1274      groups = url_split_regex.match(url).groups()
    1275      #If no authority was found
    1276      if not groups[3]:
    1277          #Try appending a scheme to see if that fixes the problem
    1278          scheme_to_prepend = prepend_scheme or 'http'
    1279          groups = url_split_regex.match(
    1280              unicode(scheme_to_prepend) + u'://' + url).groups()
    1281      #if we still can't find the authority
    1282      if not groups[3]:
    1283          raise Exception('No authority component found, '+ \
    1284              'could not decode unicode to US-ASCII')
    1285
    1286      #We're here if we found an authority, let's rebuild the URL
    1287      scheme = groups[1]
    1288      authority = groups[3]
    1289      path = groups[4] or ''
    1290      query = groups[5] or ''
    1291      fragment = groups[7] or ''
    1292
    1293      if prepend_scheme:
    1294          scheme = str(scheme) + '://'
    1295      else:
    1296          scheme = ''
    1297      return scheme + unicode_to_ascii_authority(authority) +\
    1298          escape_unicode(path) + escape_unicode(query) + str(fragment)
    1299
    1300
    1301 -class IS_GENERIC_URL(Validator):
    1302      """
    1303      Rejects a URL string if any of the following is true:
    1304         * The string is empty or None
    1305         * The string uses characters that are not allowed in a URL
    1306         * The URL scheme specified (if one is specified) is not valid
    1307
    1308      Based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html
    1309
    1310      This function only checks the URL's syntax. It does not check that the URL
    1311      points to a real document, for example, or that it otherwise makes sense
    1312      semantically. This function does automatically prepend 'http://' in front
    1313      of a URL if and only if that's necessary to successfully parse the URL.
    1314      Please note that a scheme will be prepended only for rare cases
    1315      (e.g. 'google.ca:80')
    1316
    1317      The list of allowed schemes is customizable with the allowed_schemes
    1318      parameter. If you exclude None from the list, then abbreviated URLs
    1319      (lacking a scheme such as 'http') will be rejected.
    1320
    1321      The default prepended scheme is customizable with the prepend_scheme
    1322      parameter. If you set prepend_scheme to None then prepending will be
    1323      disabled. URLs that require prepending to parse will still be accepted,
    1324      but the return value will not be modified.
    1325
    1326      @author: Jonathan Benn
    1327
    1328      >>> IS_GENERIC_URL()('http://user@abc.com')
    1329      ('http://user@abc.com', None)
    1330
    1331      """
    1332
    1333 -    def __init__(
    1334          self,
    1335          error_message='enter a valid URL',
    1336          allowed_schemes=None,
    1337          prepend_scheme=None,
    1338          ):
    1339          """
    1340          :param error_message: a string, the error message to give the end user
    1341              if the URL does not validate
    1342          :param allowed_schemes: a list containing strings or None. Each element
    1343              is a scheme the inputed URL is allowed to use
    1344          :param prepend_scheme: a string, this scheme is prepended if it's
    1345              necessary to make the URL valid
    1346          """
    1347
    1348          self.error_message = error_message
    1349          if allowed_schemes == None:
    1350              self.allowed_schemes = all_url_schemes
    1351          else:
    1352              self.allowed_schemes = allowed_schemes
    1353          self.prepend_scheme = prepend_scheme
    1354          if self.prepend_scheme not in self.allowed_schemes:
    1355              raise SyntaxError, \
    1356                  "prepend_scheme='%s' is not in allowed_schemes=%s" \
    1357                  % (self.prepend_scheme, self.allowed_schemes)
    1358
    1359 -    def __call__(self, value):
    1360          """
    1361          :param value: a string, the URL to validate
    1362          :returns: a tuple, where tuple[0] is the inputed value (possible
    1363              prepended with prepend_scheme), and tuple[1] is either
    1364              None (success!) or the string error_message
    1365          """
    1366          try:
    1367              # if the URL does not misuse the '%' character
    1368              if not re.compile(
    1369                  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]$"
    1370                                ).search(value):
    1371                  # if the URL is only composed of valid characters
    1372                  if re.compile(
    1373                      r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%#]+$").match(value):
    1374                      # Then split up the URL into its components and check on
    1375                      # the scheme
    1376                      scheme = url_split_regex.match(value).group(2)
    1377                      # Clean up the scheme before we check it
    1378                      if scheme != None:
    1379                          scheme = urllib.unquote(scheme).lower()
    1380                      # If the scheme really exists
    1381                      if scheme in self.allowed_schemes:
    1382                          # Then the URL is valid
    1383                          return (value, None)
    1384                      else:
    1385                          # else, for the possible case of abbreviated URLs with
    1386                          # ports, check to see if adding a valid scheme fixes
    1387                          # the problem (but only do this if it doesn't have
    1388                          # one already!)
    1389                          if not re.compile('://').search(value) and None\
    1390                               in self.allowed_schemes:
    1391                              schemeToUse = self.prepend_scheme or 'http'
    1392                              prependTest = self.__call__(schemeToUse
    1393                                       + '://' + value)
    1394                              # if the prepend test succeeded
    1395                              if prependTest[1] == None:
    1396                                  # if prepending in the output is enabled
    1397                                  if self.prepend_scheme:
    1398                                      return prependTest
    1399                                  else:
    1400                                      # else return the original,
    1401                                      #  non-prepended value
    1402                                      return (value, None)
    1403          except:
    1404              pass
    1405          # else the URL is not valid
    1406          return (value, translate(self.error_message))
    1407
    1408  # Sources (obtained 2008-Nov-11):
    1409  #    http://en.wikipedia.org/wiki/Top-level_domain
    1410  #    http://www.iana.org/domains/root/db/
    1411
    1412  official_top_level_domains = [
    1413      'ac',
    1414      'ad',
    1415      'ae',
    1416      'aero',
    1417      'af',
    1418      'ag',
    1419      'ai',
    1420      'al',
    1421      'am',
    1422      'an',
    1423      'ao',
    1424      'aq',
    1425      'ar',
    1426      'arpa',
    1427      'as',
    1428      'asia',
    1429      'at',
    1430      'au',
    1431      'aw',
    1432      'ax',
    1433      'az',
    1434      'ba',
    1435      'bb',
    1436      'bd',
    1437      'be',
    1438      'bf',
    1439      'bg',
    1440      'bh',
    1441      'bi',
    1442      'biz',
    1443      'bj',
    1444      'bl',
    1445      'bm',
    1446      'bn',
    1447      'bo',
    1448      'br',
    1449      'bs',
    1450      'bt',
    1451      'bv',
    1452      'bw',
    1453      'by',
    1454      'bz',
    1455      'ca',
    1456      'cat',
    1457      'cc',
    1458      'cd',
    1459      'cf',
    1460      'cg',
    1461      'ch',
    1462      'ci',
    1463      'ck',
    1464      'cl',
    1465      'cm',
    1466      'cn',
    1467      'co',
    1468      'com',
    1469      'coop',
    1470      'cr',
    1471      'cu',
    1472      'cv',
    1473      'cx',
    1474      'cy',
    1475      'cz',
    1476      'de',
    1477      'dj',
    1478      'dk',
    1479      'dm',
    1480      'do',
    1481      'dz',
    1482      'ec',
    1483      'edu',
    1484      'ee',
    1485      'eg',
    1486      'eh',
    1487      'er',
    1488      'es',
    1489      'et',
    1490      'eu',
    1491      'example',
    1492      'fi',
    1493      'fj',
    1494      'fk',
    1495      'fm',
    1496      'fo',
    1497      'fr',
    1498      'ga',
    1499      'gb',
    1500      'gd',
    1501      'ge',
    1502      'gf',
    1503      'gg',
    1504      'gh',
    1505      'gi',
    1506      'gl',
    1507      'gm',
    1508      'gn',
    1509      'gov',
    1510      'gp',
    1511      'gq',
    1512      'gr',
    1513      'gs',
    1514      'gt',
    1515      'gu',
    1516      'gw',
    1517      'gy',
    1518      'hk',
    1519      'hm',
    1520      'hn',
    1521      'hr',
    1522      'ht',
    1523      'hu',
    1524      'id',
    1525      'ie',
    1526      'il',
    1527      'im',
    1528      'in',
    1529      'info',
    1530      'int',
    1531      'invalid',
    1532      'io',
    1533      'iq',
    1534      'ir',
    1535      'is',
    1536      'it',
    1537      'je',
    1538      'jm',
    1539      'jo',
    1540      'jobs',
    1541      'jp',
    1542      'ke',
    1543      'kg',
    1544      'kh',
    1545      'ki',
    1546      'km',
    1547      'kn',
    1548      'kp',
    1549      'kr',
    1550      'kw',
    1551      'ky',
    1552      'kz',
    1553      'la',
    1554      'lb',
    1555      'lc',
    1556      'li',
    1557      'lk',
    1558      'localhost',
    1559      'lr',
    1560      'ls',
    1561      'lt',
    1562      'lu',
    1563      'lv',
    1564      'ly',
    1565      'ma',
    1566      'mc',
    1567      'md',
    1568      'me',
    1569      'mf',
    1570      'mg',
    1571      'mh',
    1572      'mil',
    1573      'mk',
    1574      'ml',
    1575      'mm',
    1576      'mn',
    1577      'mo',
    1578      'mobi',
    1579      'mp',
    1580      'mq',
    1581      'mr',
    1582      'ms',
    1583      'mt',
    1584      'mu',
    1585      'museum',
    1586      'mv',
    1587      'mw',
    1588      'mx',
    1589      'my',
    1590      'mz',
    1591      'na',
    1592      'name',
    1593      'nc',
    1594      'ne',
    1595      'net',
    1596      'nf',
    1597      'ng',
    1598      'ni',
    1599      'nl',
    1600      'no',
    1601      'np',
    1602      'nr',
    1603      'nu',
    1604      'nz',
    1605      'om',
    1606      'org',
    1607      'pa',
    1608      'pe',
    1609      'pf',
    1610      'pg',
    1611      'ph',
    1612      'pk',
    1613      'pl',
    1614      'pm',
    1615      'pn',
    1616      'pr',
    1617      'pro',
    1618      'ps',
    1619      'pt',
    1620      'pw',
    1621      'py',
    1622      'qa',
    1623      're',
    1624      'ro',
    1625      'rs',
    1626      'ru',
    1627      'rw',
    1628      'sa',
    1629      'sb',
    1630      'sc',
    1631      'sd',
    1632      'se',
    1633      'sg',
    1634      'sh',
    1635      'si',
    1636      'sj',
    1637      'sk',
    1638      'sl',
    1639      'sm',
    1640      'sn',
    1641      'so',
    1642      'sr',
    1643      'st',
    1644      'su',
    1645      'sv',
    1646      'sy',
    1647      'sz',
    1648      'tc',
    1649      'td',
    1650      'tel',
    1651      'test',
    1652      'tf',
    1653      'tg',
    1654      'th',
    1655      'tj',
    1656      'tk',
    1657      'tl',
    1658      'tm',
    1659      'tn',
    1660      'to',
    1661      'tp',
    1662      'tr',
    1663      'travel',
    1664      'tt',
    1665      'tv',
    1666      'tw',
    1667      'tz',
    1668      'ua',
    1669      'ug',
    1670      'uk',
    1671      'um',
    1672      'us',
    1673      'uy',
    1674      'uz',
    1675      'va',
    1676      'vc',
    1677      've',
    1678      'vg',
    1679      'vi',
    1680      'vn',
    1681      'vu',
    1682      'wf',
    1683      'ws',
    1684      'xn--0zwm56d',
    1685      'xn--11b5bs3a9aj6g',
    1686      'xn--80akhbyknj4f',
    1687      'xn--9t4b11yi5a',
    1688      'xn--deba0ad',
    1689      'xn--g6w251d',
    1690      'xn--hgbk6aj7f53bba',
    1691      'xn--hlcj6aya9esc7a',
    1692      'xn--jxalpdlp',
    1693      'xn--kgbechtv',
    1694      'xn--zckzah',
    1695      'ye',
    1696      'yt',
    1697      'yu',
    1698      'za',
    1699      'zm',
    1700      'zw',
    1701      ]
    1702
    1703
    1704 -class IS_HTTP_URL(Validator):
    1705      """
    1706      Rejects a URL string if any of the following is true:
    1707         * The string is empty or None
    1708         * The string uses characters that are not allowed in a URL
    1709         * The string breaks any of the HTTP syntactic rules
    1710         * The URL scheme specified (if one is specified) is not 'http' or 'https'
    1711         * The top-level domain (if a host name is specified) does not exist
    1712
    1713      Based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html
    1714
    1715      This function only checks the URL's syntax. It does not check that the URL
    1716      points to a real document, for example, or that it otherwise makes sense
    1717      semantically. This function does automatically prepend 'http://' in front
    1718      of a URL in the case of an abbreviated URL (e.g. 'google.ca').
    1719
    1720      The list of allowed schemes is customizable with the allowed_schemes
    1721      parameter. If you exclude None from the list, then abbreviated URLs
    1722      (lacking a scheme such as 'http') will be rejected.
    1723
    1724      The default prepended scheme is customizable with the prepend_scheme
    1725      parameter. If you set prepend_scheme to None then prepending will be
    1726      disabled. URLs that require prepending to parse will still be accepted,
    1727      but the return value will not be modified.
    1728
    1729      @author: Jonathan Benn
    1730
    1731      >>> IS_HTTP_URL()('http://1.2.3.4')
    1732      ('http://1.2.3.4', None)
    1733      >>> IS_HTTP_URL()('http://abc.com')
    1734      ('http://abc.com', None)
    1735      >>> IS_HTTP_URL()('https://abc.com')
    1736      ('https://abc.com', None)
    1737      >>> IS_HTTP_URL()('httpx://abc.com')
    1738      ('httpx://abc.com', 'enter a valid URL')
    1739      >>> IS_HTTP_URL()('http://abc.com:80')
    1740      ('http://abc.com:80', None)
    1741      >>> IS_HTTP_URL()('http://user@abc.com')
    1742      ('http://user@abc.com', None)
    1743      >>> IS_HTTP_URL()('http://user@1.2.3.4')
    1744      ('http://user@1.2.3.4', None)
    1745
    1746      """
    1747
    1748 -    def __init__(
    1749          self,
    1750          error_message='enter a valid URL',
    1751          allowed_schemes=None,
    1752          prepend_scheme='http',
    1753          ):
    1754          """
    1755          :param error_message: a string, the error message to give the end user
    1756              if the URL does not validate
    1757          :param allowed_schemes: a list containing strings or None. Each element
    1758              is a scheme the inputed URL is allowed to use
    1759          :param prepend_scheme: a string, this scheme is prepended if it's
    1760              necessary to make the URL valid
    1761          """
    1762
    1763          self.error_message = error_message
    1764          if allowed_schemes == None:
    1765              self.allowed_schemes = http_schemes
    1766          else:
    1767              self.allowed_schemes = allowed_schemes
    1768          self.prepend_scheme = prepend_scheme
    1769
    1770          for i in self.allowed_schemes:
    1771              if i not in http_schemes:
    1772                  raise SyntaxError, \
    1773                      "allowed_scheme value '%s' is not in %s" % \
    1774                      (i, http_schemes)
    1775
    1776          if self.prepend_scheme not in self.allowed_schemes:
    1777              raise SyntaxError, \
    1778                  "prepend_scheme='%s' is not in allowed_schemes=%s" % \
    1779                  (self.prepend_scheme, self.allowed_schemes)
    1780
    1781 -    def __call__(self, value):
    1782          """
    1783          :param value: a string, the URL to validate
    1784          :returns: a tuple, where tuple[0] is the inputed value
    1785              (possible prepended with prepend_scheme), and tuple[1] is either
    1786              None (success!) or the string error_message
    1787          """
    1788
    1789          try:
    1790              # if the URL passes generic validation
    1791              x = IS_GENERIC_URL(error_message=self.error_message,
    1792                                 allowed_schemes=self.allowed_schemes,
    1793                                 prepend_scheme=self.prepend_scheme)
    1794              if x(value)[1] == None:
    1795                  componentsMatch = url_split_regex.match(value)
    1796                  authority = componentsMatch.group(4)
    1797                  # if there is an authority component
    1798                  if authority:
    1799                      # if authority is a valid IP address
    1800                      if re.compile(
    1801                          "([\w.!~*'|;:&=+$,-]+@)?\d+\.\d+\.\d+\.\d+(:\d*)*$").match(authority):
    1802                          # Then this HTTP URL is valid
    1803                          return (value, None)
    1804                      else:
    1805                          # else if authority is a valid domain name
    1806                          domainMatch = \
    1807                              re.compile(
    1808                                  "([\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*)*$"
    1809                                  ).match(authority)
    1810                          if domainMatch:
    1811                              # if the top-level domain really exists
    1812                              if domainMatch.group(5).lower()\
    1813                                   in official_top_level_domains:
    1814                                  # Then this HTTP URL is valid
    1815                                  return (value, None)
    1816                  else:
    1817                      # else this is a relative/abbreviated URL, which will parse
    1818                      # into the URL's path component
    1819                      path = componentsMatch.group(5)
    1820                      # relative case: if this is a valid path (if it starts with
    1821                      # a slash)
    1822                      if re.compile('/').match(path):
    1823                          # Then this HTTP URL is valid
    1824                          return (value, None)
    1825                      else:
    1826                          # abbreviated case: if we haven't already, prepend a
    1827                          # scheme and see if it fixes the problem
    1828                          if not re.compile('://').search(value):
    1829                              schemeToUse = self.prepend_scheme or 'http'
    1830                              prependTest = self.__call__(schemeToUse
    1831                                       + '://' + value)
    1832                              # if the prepend test succeeded
    1833                              if prependTest[1] == None:
    1834                                  # if prepending in the output is enabled
    1835                                  if self.prepend_scheme:
    1836                                      return prependTest
    1837                                  else:
    1838                                      # else return the original, non-prepended
    1839                                      # value
    1840                                      return (value, None)
    1841          except:
    1842              pass
    1843          # else the HTTP URL is not valid
    1844          return (value, translate(self.error_message))
    1845
    1846
    1847 -class IS_URL(Validator):
    1848      """
    1849      Rejects a URL string if any of the following is true:
    1850         * The string is empty or None
    1851         * The string uses characters that are not allowed in a URL
    1852         * The string breaks any of the HTTP syntactic rules
    1853         * The URL scheme specified (if one is specified) is not 'http' or 'https'
    1854         * The top-level domain (if a host name is specified) does not exist
    1855
    1856      (These rules are based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html)
    1857
    1858      This function only checks the URL's syntax. It does not check that the URL
    1859      points to a real document, for example, or that it otherwise makes sense
    1860      semantically. This function does automatically prepend 'http://' in front
    1861      of a URL in the case of an abbreviated URL (e.g. 'google.ca').
    1862
    1863      If the parameter mode='generic' is used, then this function's behavior
    1864      changes. It then rejects a URL string if any of the following is true:
    1865         * The string is empty or None
    1866         * The string uses characters that are not allowed in a URL
    1867         * The URL scheme specified (if one is specified) is not valid
    1868
    1869      (These rules are based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html)
    1870
    1871      The list of allowed schemes is customizable with the allowed_schemes
    1872      parameter. If you exclude None from the list, then abbreviated URLs
    1873      (lacking a scheme such as 'http') will be rejected.
    1874
    1875      The default prepended scheme is customizable with the prepend_scheme
    1876      parameter. If you set prepend_scheme to None then prepending will be
    1877      disabled. URLs that require prepending to parse will still be accepted,
    1878      but the return value will not be modified.
    1879
    1880      IS_URL is compatible with the Internationalized Domain Name (IDN) standard
    1881      specified in RFC 3490 (http://tools.ietf.org/html/rfc3490). As a result,
    1882      URLs can be regular strings or unicode strings.
    1883      If the URL's domain component (e.g. google.ca) contains non-US-ASCII
    1884      letters, then the domain will be converted into Punycode (defined in
    1885      RFC 3492, http://tools.ietf.org/html/rfc3492). IS_URL goes a bit beyond
    1886      the standards, and allows non-US-ASCII characters to be present in the path
    1887      and query components of the URL as well. These non-US-ASCII characters will
    1888      be escaped using the standard '%20' type syntax. e.g. the unicode
    1889      character with hex code 0x4e86 will become '%4e%86'
    1890
    1891      Code Examples::
    1892
    1893          INPUT(_type='text', _name='name', requires=IS_URL())
    1894          >>> IS_URL()('abc.com')
    1895          ('http://abc.com', None)
    1896
    1897          INPUT(_type='text', _name='name', requires=IS_URL(mode='generic'))
    1898          >>> IS_URL(mode='generic')('abc.com')
    1899          ('abc.com', None)
    1900
    1901          INPUT(_type='text', _name='name',
    1902              requires=IS_URL(allowed_schemes=['https'], prepend_scheme='https'))
    1903          >>> IS_URL(allowed_schemes=['https'], prepend_scheme='https')('https://abc.com')
    1904          ('https://abc.com', None)
    1905
    1906          INPUT(_type='text', _name='name',
    1907              requires=IS_URL(prepend_scheme='https'))
    1908          >>> IS_URL(prepend_scheme='https')('abc.com')
    1909          ('https://abc.com', None)
    1910
    1911          INPUT(_type='text', _name='name',
    1912              requires=IS_URL(mode='generic', allowed_schemes=['ftps', 'https'],
    1913                  prepend_scheme='https'))
    1914          >>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], prepend_scheme='https')('https://abc.com')
    1915          ('https://abc.com', None)
    1916          >>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https', None], prepend_scheme='https')('abc.com')
    1917          ('abc.com', None)
    1918
    1919      @author: Jonathan Benn
    1920      """
    1921
    1922 -    def __init__(
    1923          self,
    1924          error_message='enter a valid URL',
    1925          mode='http',
    1926          allowed_schemes=None,
    1927          prepend_scheme='http',
    1928          ):
    1929          """
    1930          :param error_message: a string, the error message to give the end user
    1931              if the URL does not validate
    1932          :param allowed_schemes: a list containing strings or None. Each element
    1933              is a scheme the inputed URL is allowed to use
    1934          :param prepend_scheme: a string, this scheme is prepended if it's
    1935              necessary to make the URL valid
    1936          """
    1937
    1938          self.error_message = error_message
    1939          self.mode = mode.lower()
    1940          if not self.mode in ['generic', 'http']:
    1941              raise SyntaxError, "invalid mode '%s' in IS_URL" % self.mode
    1942          self.allowed_schemes = allowed_schemes
    1943
    1944          if self.allowed_schemes:
    1945              if prepend_scheme not in self.allowed_schemes:
    1946                  raise SyntaxError, \
    1947                      "prepend_scheme='%s' is not in allowed_schemes=%s" \
    1948                      % (prepend_scheme, self.allowed_schemes)
    1949
    1950          # if allowed_schemes is None, then we will defer testing
    1951          # prepend_scheme's validity to a sub-method
    1952
    1953          self.prepend_scheme = prepend_scheme
    1954
    1955 -    def __call__(self, value):
    1956          """
    1957          :param value: a unicode or regular string, the URL to validate
    1958          :returns: a (string, string) tuple, where tuple[0] is the modified
    1959              input value and tuple[1] is either None (success!) or the
    1960              string error_message. The input value will never be modified in the
    1961              case of an error. However, if there is success then the input URL
    1962              may be modified to (1) prepend a scheme, and/or (2) convert a
    1963              non-compliant unicode URL into a compliant US-ASCII version.
    1964          """
    1965
    1966          if self.mode == 'generic':
    1967              subMethod = IS_GENERIC_URL(error_message=self.error_message,
    1968                                         allowed_schemes=self.allowed_schemes,
    1969                                         prepend_scheme=self.prepend_scheme)
    1970          elif self.mode == 'http':
    1971              subMethod = IS_HTTP_URL(error_message=self.error_message,
    1972                                      allowed_schemes=self.allowed_schemes,
    1973                                      prepend_scheme=self.prepend_scheme)
    1974          else:
    1975              raise SyntaxError, "invalid mode '%s' in IS_URL" % self.mode
    1976
    1977          if type(value) != unicode:
    1978              return subMethod(value)
    1979          else:
    1980              try:
    1981                  asciiValue = unicode_to_ascii_url(value, self.prepend_scheme)
    1982              except Exception:
    1983                  #If we are not able to convert the unicode url into a
    1984                  # US-ASCII URL, then the URL is not valid
    1985                  return (value, translate(self.error_message))
    1986
    1987              methodResult = subMethod(asciiValue)
    1988              #if the validation of the US-ASCII version of the value failed
    1989              if methodResult[1] != None:
    1990                  # then return the original input value, not the US-ASCII version
    1991                  return (value, methodResult[1])
    1992              else:
    1993                  return methodResult
    1994
    1995
    1996  regex_time = re.compile(
    1997      '((?P<h>[0-9]+))([^0-9 ]+(?P<m>[0-9 ]+))?([^0-9ap ]+(?P<s>[0-9]*))?((?P<d>[ap]m))?')
    1998
    1999
    2000 -class IS_TIME(Validator):
    2001      """
    2002      example::
    2003
    2004          INPUT(_type='text', _name='name', requires=IS_TIME())
    2005
    2006      understands the following formats
    2007      hh:mm:ss [am/pm]
    2008      hh:mm [am/pm]
    2009      hh [am/pm]
    2010
    2011      [am/pm] is optional, ':' can be replaced by any other non-space non-digit
    2012
    2013          >>> IS_TIME()('21:30')
    2014          (datetime.time(21, 30), None)
    2015          >>> IS_TIME()('21-30')
    2016          (datetime.time(21, 30), None)
    2017          >>> IS_TIME()('21.30')
    2018          (datetime.time(21, 30), None)
    2019          >>> IS_TIME()('21:30:59')
    2020          (datetime.time(21, 30, 59), None)
    2021          >>> IS_TIME()('5:30')
    2022          (datetime.time(5, 30), None)
    2023          >>> IS_TIME()('5:30 am')
    2024          (datetime.time(5, 30), None)
    2025          >>> IS_TIME()('5:30 pm')
    2026          (datetime.time(17, 30), None)
    2027          >>> IS_TIME()('5:30 whatever')
    2028          ('5:30 whatever', 'enter time as hh:mm:ss (seconds, am, pm optional)')
    2029          >>> IS_TIME()('5:30 20')
    2030          ('5:30 20', 'enter time as hh:mm:ss (seconds, am, pm optional)')
    2031          >>> IS_TIME()('24:30')
    2032          ('24:30', 'enter time as hh:mm:ss (seconds, am, pm optional)')
    2033          >>> IS_TIME()('21:60')
    2034          ('21:60', 'enter time as hh:mm:ss (seconds, am, pm optional)')
    2035          >>> IS_TIME()('21:30::')
    2036          ('21:30::', 'enter time as hh:mm:ss (seconds, am, pm optional)')
    2037          >>> IS_TIME()('')
    2038          ('', 'enter time as hh:mm:ss (seconds, am, pm optional)')
    2039      """
    2040
    2041 -    def __init__(self, error_message='enter time as hh:mm:ss (seconds, am, pm optional)'):
    2042          self.error_message = error_message
    2043
    2044 -    def __call__(self, value):
    2045          try:
    2046              ivalue = value
    2047              value = regex_time.match(value.lower())
    2048              (h, m, s) = (int(value.group('h')), 0, 0)
    2049              if value.group('m') != None:
    2050                  m = int(value.group('m'))
    2051              if value.group('s') != None:
    2052                  s = int(value.group('s'))
    2053              if value.group('d') == 'pm' and 0 < h < 12:
    2054                  h = h + 12
    2055              if not (h in range(24) and m in range(60) and s
    2056                       in range(60)):
    2057                  raise ValueError\
    2058                      ('Hours or minutes or seconds are outside of allowed range')
    2059              value = datetime.time(h, m, s)
    2060              return (value, None)
    2061          except AttributeError:
    2062              pass
    2063          except ValueError:
    2064              pass
    2065          return (ivalue, translate(self.error_message))
    2066
    2067
    2068 -class IS_DATE(Validator):
    2069      """
    2070      example::
    2071
    2072          INPUT(_type='text', _name='name', requires=IS_DATE())
    2073
    2074      date has to be in the ISO8960 format YYYY-MM-DD
    2075      """
    2076
    2077 -    def __init__(self, format='%Y-%m-%d',
    2078                   error_message='enter date as %(format)s'):
    2079          self.format = str(format)
    2080          self.error_message = str(error_message)
    2081
    2082 -    def __call__(self, value):
    2083          if isinstance(value,datetime.date):
    2084              return (value,None)
    2085          try:
    2086              (y, m, d, hh, mm, ss, t0, t1, t2) = \
    2087                  time.strptime(value, str(self.format))
    2088              value = datetime.date(y, m, d)
    2089              return (value, None)
    2090          except:
    2091              return (value, translate(self.error_message) % IS_DATETIME.nice(self.format))
    2092
    2093 -    def formatter(self, value):
    2094          format = self.format
    2095          year = value.year
    2096          y = '%.4i' % year
    2097          format = format.replace('%y',y[-2:])
    2098          format = format.replace('%Y',y)
    2099          if year<1900:
    2100              year = 2000
    2101          d = datetime.date(year,value.month,value.day)
    2102          return d.strftime(format)
    2103
    2104
    2105 -class IS_DATETIME(Validator):
    2106      """
    2107      example::
    2108
    2109          INPUT(_type='text', _name='name', requires=IS_DATETIME())
    2110
    2111      datetime has to be in the ISO8960 format YYYY-MM-DD hh:mm:ss
    2112      """
    2113
    2114      isodatetime = '%Y-%m-%d %H:%M:%S'
    2115
    2116      @staticmethod
    2117 -    def nice(format):
    2118          code=(('%Y','1963'),
    2119                ('%y','63'),
    2120                ('%d','28'),
    2121                ('%m','08'),
    2122                ('%b','Aug'),
    2123                ('%b','August'),
    2124                ('%H','14'),
    2125                ('%I','02'),
    2126                ('%p','PM'),
    2127                ('%M','30'),
    2128                ('%S','59'))
    2129          for (a,b) in code:
    2130              format=format.replace(a,b)
    2131          return dict(format=format)
    2132
    2133 -    def __init__(self, format='%Y-%m-%d %H:%M:%S',
    2134                   error_message='enter date and time as %(format)s'):
    2135          self.format = str(format)
    2136          self.error_message = str(error_message)
    2137
    2138 -    def __call__(self, value):
    2139          if isinstance(value,datetime.datetime):
    2140              return (value,None)
    2141          try:
    2142              (y, m, d, hh, mm, ss, t0, t1, t2) = \
    2143                  time.strptime(value, str(self.format))
    2144              value = datetime.datetime(y, m, d, hh, mm, ss)
    2145              return (value, None)
    2146          except:
    2147              return (value, translate(self.error_message) % IS_DATETIME.nice(self.format))
    2148
    2149 -    def formatter(self, value):
    2150          format = self.format
    2151          year = value.year
    2152          y = '%.4i' % year
    2153          format = format.replace('%y',y[-2:])
    2154          format = format.replace('%Y',y)
    2155          if year<1900:
    2156              year = 2000
    2157          d = datetime.datetime(year,value.month,value.day,value.hour,value.minute,value.second)
    2158          return d.strftime(format)
    2159
    2160 -class IS_DATE_IN_RANGE(IS_DATE):
    2161      """
    2162      example::
    2163
    2164          >>> v = IS_DATE_IN_RANGE(minimum=datetime.date(2008,1,1), \
    2165                                   maximum=datetime.date(2009,12,31), \
    2166                                   format="%m/%d/%Y",error_message="oops")
    2167
    2168          >>> v('03/03/2008')
    2169          (datetime.date(2008, 3, 3), None)
    2170
    2171          >>> v('03/03/2010')
    2172          (datetime.date(2010, 3, 3), 'oops')
    2173
    2174          >>> v(datetime.date(2008,3,3))
    2175          (datetime.date(2008, 3, 3), None)
    2176
    2177          >>> v(datetime.date(2010,3,3))
    2178          (datetime.date(2010, 3, 3), 'oops')
    2179
    2180      """
    2181 -    def __init__(self,
    2182                   minimum = None,
    2183                   maximum = None,
    2184                   format='%Y-%m-%d',
    2185                   error_message = None):
    2186          self.minimum = minimum
    2187          self.maximum = maximum
    2188          if error_message is None:
    2189              if minimum is None:
    2190                  error_message = "enter date on or before %(max)s"
    2191              elif maximum is None:
    2192                  error_message = "enter date on or after %(min)s"
    2193              else:
    2194                  error_message = "enter date in range %(min)s %(max)s"
    2195          d = dict(min=minimum, max=maximum)
    2196          IS_DATE.__init__(self,
    2197                           format = format,
    2198                           error_message = error_message % d)
    2199
    2200 -    def __call__(self, value):
    2201          (value, msg) = IS_DATE.__call__(self,value)
    2202          if msg is not None:
    2203              return (value, msg)
    2204          if self.minimum and self.minimum > value:
    2205              return (value, translate(self.error_message))
    2206          if self.maximum and value > self.maximum:
    2207              return (value, translate(self.error_message))
    2208          return (value, None)
    2209
    2210
    2211 -class IS_DATETIME_IN_RANGE(IS_DATETIME):
    2212      """
    2213      example::
    2214
    2215          >>> v = IS_DATETIME_IN_RANGE(\
    2216                  minimum=datetime.datetime(2008,1,1,12,20), \
    2217                  maximum=datetime.datetime(2009,12,31,12,20), \
    2218                  format="%m/%d/%Y %H:%M",error_message="oops")
    2219          >>> v('03/03/2008 12:40')
    2220          (datetime.datetime(2008, 3, 3, 12, 40), None)
    2221
    2222          >>> v('03/03/2010 10:34')
    2223          (datetime.datetime(2010, 3, 3, 10, 34), 'oops')
    2224
    2225          >>> v(datetime.datetime(2008,3,3,0,0))
    2226          (datetime.datetime(2008, 3, 3, 0, 0), None)
    2227
    2228          >>> v(datetime.datetime(2010,3,3,0,0))
    2229          (datetime.datetime(2010, 3, 3, 0, 0), 'oops')
    2230      """
    2231 -    def __init__(self,
    2232                   minimum = None,
    2233                   maximum = None,
    2234                   format = '%Y-%m-%d %H:%M:%S',
    2235                   error_message = None):
    2236          self.minimum = minimum
    2237          self.maximum = maximum
    2238          if error_message is None:
    2239              if minimum is None:
    2240                  error_message = "enter date and time on or before %(max)s"
    2241              elif maximum is None:
    2242                  error_message = "enter date and time on or after %(min)s"
    2243              else:
    2244                  error_message = "enter date and time in range %(min)s %(max)s"
    2245          d = dict(min = minimum, max = maximum)
    2246          IS_DATETIME.__init__(self,
    2247                           format = format,
    2248                           error_message = error_message % d)
    2249
    2250 -    def __call__(self, value):
    2251          (value, msg) = IS_DATETIME.__call__(self, value)
    2252          if msg is not None:
    2253              return (value, msg)
    2254          if self.minimum and self.minimum > value:
    2255              return (value, translate(self.error_message))
    2256          if self.maximum and value > self.maximum:
    2257              return (value, translate(self.error_message))
    2258          return (value, None)
    2259
    2260
    2261 -class IS_LIST_OF(Validator):
    2262
    2263 -    def __init__(self, other):
    2264          self.other = other
    2265
    2266 -    def __call__(self, value):
    2267          ivalue = value
    2268          if not isinstance(value, list):
    2269              ivalue = [ivalue]
    2270          new_value = []
    2271          for item in ivalue:
    2272              (v, e) = self.other(item)
    2273              if e:
    2274                  return (value, e)
    2275              else:
    2276                  new_value.append(v)
    2277          return (new_value, None)
    2278
    2279
    2280 -class IS_LOWER(Validator):
    2281      """
    2282      convert to lower case
    2283
    2284      >>> IS_LOWER()('ABC')
    2285      ('abc', None)
    2286      >>> IS_LOWER()('Ñ')
    2287      ('\\xc3\\xb1', None)
    2288      """
    2289
    2290 -    def __call__(self, value):
    2291          return (value.decode('utf8').lower().encode('utf8'), None)
    2292
    2293
    2294 -class IS_UPPER(Validator):
    2295      """
    2296      convert to upper case
    2297
    2298      >>> IS_UPPER()('abc')
    2299      ('ABC', None)
    2300      >>> IS_UPPER()('ñ')
    2301      ('\\xc3\\x91', None)
    2302      """
    2303
    2304 -    def __call__(self, value):
    2305          return (value.decode('utf8').upper().encode('utf8'), None)
    2306
    2307
    2308 -def urlify(value, maxlen=80, keep_underscores=False):
    2309      """
    2310      Convert incoming string to a simplified ASCII subset.
    2311      if (keep_underscores): underscores are retained in the string
    2312      else: underscores are translated to hyphens (default)
    2313      """
    2314      s = value.lower()                     # to lowercase
    2315      s = s.decode('utf-8')                 # to utf-8
    2316      s = unicodedata.normalize('NFKD', s)  # normalize eg è => e, ñ => n
    2317      s = s.encode('ASCII', 'ignore')       # encode as ASCII
    2318      s = re.sub('&\w+;', '', s)            # strip html entities
    2319      if keep_underscores:
    2320          s = re.sub('\s+', '-', s)         # whitespace to hyphens
    2321          s = re.sub('[^\w\-]', '', s)      # strip all but alphanumeric/underscore/hyphen
    2322      else:
    2323          s = re.sub('[\s_]+', '-', s)      # whitespace & underscores to hyphens
    2324          s = re.sub('[^a-z0-9\-]', '', s)  # strip all but alphanumeric/hyphen
    2325      s = re.sub('[-_][-_]+', '-', s)       # collapse strings of hyphens
    2326      s = s.strip('-')                      # remove leading and trailing hyphens
    2327      return s[:maxlen]                     # enforce maximum length
    2328
    2329
    2330 -class IS_SLUG(Validator):
    2331      """
    2332      convert arbitrary text string to a slug
    2333
    2334      >>> IS_SLUG()('abc123')
    2335      ('abc123', None)
    2336      >>> IS_SLUG()('ABC123')
    2337      ('abc123', None)
    2338      >>> IS_SLUG()('abc-123')
    2339      ('abc-123', None)
    2340      >>> IS_SLUG()('abc--123')
    2341      ('abc-123', None)
    2342      >>> IS_SLUG()('abc 123')
    2343      ('abc-123', None)
    2344      >>> IS_SLUG()('abc\t_123')
    2345      ('abc-123', None)
    2346      >>> IS_SLUG()('-abc-')
    2347      ('abc', None)
    2348      >>> IS_SLUG()('--a--b--_ -c--')
    2349      ('a-b-c', None)
    2350      >>> IS_SLUG()('abc&amp;123')
    2351      ('abc123', None)
    2352      >>> IS_SLUG()('abc&amp;123&amp;def')
    2353      ('abc123def', None)
    2354      >>> IS_SLUG()('ñ')
    2355      ('n', None)
    2356      >>> IS_SLUG(maxlen=4)('abc123')
    2357      ('abc1', None)
    2358      >>> IS_SLUG()('abc_123')
    2359      ('abc-123', None)
    2360      >>> IS_SLUG(keep_underscores=False)('abc_123')
    2361      ('abc-123', None)
    2362      >>> IS_SLUG(keep_underscores=True)('abc_123')
    2363      ('abc_123', None)
    2364      >>> IS_SLUG(check=False)('abc')
    2365      ('abc', None)
    2366      >>> IS_SLUG(check=True)('abc')
    2367      ('abc', None)
    2368      >>> IS_SLUG(check=False)('a bc')
    2369      ('a-bc', None)
    2370      >>> IS_SLUG(check=True)('a bc')
    2371      ('a bc', 'must be slug')
    2372      """
    2373
    2374      @staticmethod
    2375 -    def urlify(value, maxlen=80, keep_underscores=False):
    2376          return urlify(value, maxlen, keep_underscores)
    2377
    2378 -    def __init__(self, maxlen=80, check=False, error_message='must be slug', keep_underscores=False):
    2379          self.maxlen = maxlen
    2380          self.check = check
    2381          self.error_message = error_message
    2382          self.keep_underscores = keep_underscores
    2383
    2384 -    def __call__(self, value):
    2385          if self.check and value != urlify(value, self.maxlen, self.keep_underscores):
    2386              return (value, translate(self.error_message))
    2387          return (urlify(value,self.maxlen, self.keep_underscores), None)
    2388
    2389 -class IS_EMPTY_OR(Validator):
    2390      """
    2391      dummy class for testing IS_EMPTY_OR
    2392
    2393      >>> IS_EMPTY_OR(IS_EMAIL())('abc@def.com')
    2394      ('abc@def.com', None)
    2395      >>> IS_EMPTY_OR(IS_EMAIL())('   ')
    2396      (None, None)
    2397      >>> IS_EMPTY_OR(IS_EMAIL(), null='abc')('   ')
    2398      ('abc', None)
    2399      >>> IS_EMPTY_OR(IS_EMAIL(), null='abc', empty_regex='def')('def')
    2400      ('abc', None)
    2401      >>> IS_EMPTY_OR(IS_EMAIL())('abc')
    2402      ('abc', 'enter a valid email address')
    2403      >>> IS_EMPTY_OR(IS_EMAIL())(' abc ')
    2404      ('abc', 'enter a valid email address')
    2405      """
    2406
    2407 -    def __init__(self, other, null=None, empty_regex=None):
    2408          (self.other, self.null) = (other, null)
    2409          if empty_regex is not None:
    2410              self.empty_regex = re.compile(empty_regex)
    2411          else:
    2412              self.empty_regex = None
    2413          if hasattr(other, 'multiple'):
    2414              self.multiple = other.multiple
    2415          if hasattr(other, 'options'):
    2416              self.options=self._options
    2417
    2418 -    def _options(self):
    2419          options = self.other.options()
    2420          if (not options or options[0][0]!='') and not self.multiple:
    2421              options.insert(0,('',''))
    2422          return options
    2423
    2424 -    def set_self_id(self, id):
    2425          if isinstance(self.other, (list, tuple)):
    2426              for item in self.other:
    2427                  if hasattr(item, 'set_self_id'):
    2428                      item.set_self_id(id)
    2429          else:
    2430              if hasattr(self.other, 'set_self_id'):
    2431                  self.other.set_self_id(id)
    2432
    2433 -    def __call__(self, value):
    2434          value, empty = is_empty(value, empty_regex=self.empty_regex)
    2435          if empty:
    2436              return (self.null, None)
    2437          if isinstance(self.other, (list, tuple)):
    2438              for item in self.other:
    2439                  value, error = item(value)
    2440                  if error: break
    2441              return value, error
    2442          else:
    2443              return self.other(value)
    2444
    2445 -    def formatter(self, value):
    2446          if hasattr(self.other, 'formatter'):
    2447              return self.other.formatter(value)
    2448          return value
    2449
    2450  IS_NULL_OR = IS_EMPTY_OR    # for backward compatibility
    2451
    2452
    2453 -class CLEANUP(Validator):
    2454      """
    2455      example::
    2456
    2457          INPUT(_type='text', _name='name', requires=CLEANUP())
    2458
    2459      removes special characters on validation
    2460      """
    2461
    2462 -    def __init__(self, regex='[^\x09\x0a\x0d\x20-\x7e]'):
    2463          self.regex = re.compile(regex)
    2464
    2465 -    def __call__(self, value):
    2466          v = self.regex.sub('',str(value).strip())
    2467          return (v, None)
    2468
    2469
    2470 -class CRYPT(object):
    2471      """
    2472      example::
    2473
    2474          INPUT(_type='text', _name='name', requires=CRYPT())
    2475
    2476      encodes the value on validation with a digest.
    2477
    2478      If no arguments are provided CRYPT uses the MD5 algorithm.
    2479      If the key argument is provided the HMAC+MD5 algorithm is used.
    2480      If the digest_alg is specified this is used to replace the
    2481      MD5 with, for example, SHA512. The digest_alg can be
    2482      the name of a hashlib algorithm as a string or the algorithm itself.
    2483      """
    2484
    2485 -    def __init__(self, key=None, digest_alg='md5'):
    2486          self.key = key
    2487          self.digest_alg = digest_alg
    2488
    2489 -    def __call__(self, value):
    2490          if self.key:
    2491              return (hmac_hash(value, self.key, self.digest_alg), None)
    2492          else:
    2493              return (simple_hash(value, self.digest_alg), None)
    2494
    2495
    2496 -class IS_STRONG(object):
    2497      """
    2498      example::
    2499
    2500          INPUT(_type='password', _name='passwd',
    2501              requires=IS_STRONG(min=10, special=2, upper=2))
    2502
    2503      enforces complexity requirements on a field
    2504      """
    2505
    2506 -    def __init__(self, min=8, max=20, upper=1, lower=1, number=1,
    2507                   special=1, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|',
    2508                   invalid=' "', error_message=None):
    2509          self.min = min
    2510          self.max = max
    2511          self.upper = upper
    2512          self.lower = lower
    2513          self.number = number
    2514          self.special = special
    2515          self.specials = specials
    2516          self.invalid = invalid
    2517          self.error_message = error_message
    2518
    2519 -    def __call__(self, value):
    2520          failures = []
    2521          if type(self.min) == int and self.min > 0:
    2522              if not len(value) >= self.min:
    2523                  failures.append("Minimum length is %s" % self.min)
    2524          if type(self.max) == int and self.max > 0:
    2525              if not len(value) <= self.max:
    2526                  failures.append("Maximum length is %s" % self.max)
    2527          if type(self.special) == int:
    2528              all_special = [ch in value for ch in self.specials]
    2529              if self.special > 0:
    2530                  if not all_special.count(True) >= self.special:
    2531                      failures.append("Must include at least %s of the following : %s" % (self.special, self.specials))
    2532          if self.invalid:
    2533              all_invalid = [ch in value for ch in self.invalid]
    2534              if all_invalid.count(True) > 0:
    2535                  failures.append("May not contain any of the following: %s" \
    2536                      % self.invalid)
    2537          if type(self.upper) == int:
    2538              all_upper = re.findall("[A-Z]", value)
    2539              if self.upper > 0:
    2540                  if not len(all_upper) >= self.upper:
    2541                      failures.append("Must include at least %s upper case" \
    2542                          % str(self.upper))
    2543              else:
    2544                  if len(all_upper) > 0:
    2545                      failures.append("May not include any upper case letters")
    2546          if type(self.lower) == int:
    2547              all_lower = re.findall("[a-z]", value)
    2548              if self.lower > 0:
    2549                  if not len(all_lower) >= self.lower:
    2550                      failures.append("Must include at least %s lower case" \
    2551                          % str(self.lower))
    2552              else:
    2553                  if len(all_lower) > 0:
    2554                      failures.append("May not include any lower case letters")
    2555          if type(self.number) == int:
    2556              all_number = re.findall("[0-9]", value)
    2557              if self.number > 0:
    2558                  numbers = "number"
    2559                  if self.number > 1:
    2560                      numbers = "numbers"
    2561                  if not len(all_number) >= self.number:
    2562                      failures.append("Must include at least %s %s" \
    2563                          % (str(self.number), numbers))
    2564              else:
    2565                  if len(all_number) > 0:
    2566                      failures.append("May not include any numbers")
    2567          if len(failures) == 0:
    2568              return (value, None)
    2569          if not translate(self.error_message):
    2570              from html import XML
    2571              return (value, XML('<br />'.join(failures)))
    2572          else:
    2573              return (value, translate(self.error_message))
    2574
    2575
    2576 -class IS_IN_SUBSET(IS_IN_SET):
    2577
    2578 -    def __init__(self, *a, **b):
    2579          IS_IN_SET.__init__(self, *a, **b)
    2580
    2581 -    def __call__(self, value):
    2582          values = re.compile("\w+").findall(str(value))
    2583          failures = [x for x in values if IS_IN_SET.__call__(self, x)[1]]
    2584          if failures:
    2585              return (value, translate(self.error_message))
    2586          return (value, None)
    2587
    2588
    2589 -class IS_IMAGE(Validator):
    2590      """
    2591      Checks if file uploaded through file input was saved in one of selected
    2592      image formats and has dimensions (width and height) within given boundaries.
    2593
    2594      Does *not* check for maximum file size (use IS_LENGTH for that). Returns
    2595      validation failure if no data was uploaded.
    2596
    2597      Supported file formats: BMP, GIF, JPEG, PNG.
    2598
    2599      Code parts taken from
    2600      http://mail.python.org/pipermail/python-list/2007-June/617126.html
    2601
    2602      Arguments:
    2603
    2604      extensions: iterable containing allowed *lowercase* image file extensions
    2605      ('jpg' extension of uploaded file counts as 'jpeg')
    2606      maxsize: iterable containing maximum width and height of the image
    2607      minsize: iterable containing minimum width and height of the image
    2608
    2609      Use (-1, -1) as minsize to pass image size check.
    2610
    2611      Examples::
    2612
    2613          #Check if uploaded file is in any of supported image formats:
    2614          INPUT(_type='file', _name='name', requires=IS_IMAGE())
    2615
    2616          #Check if uploaded file is either JPEG or PNG:
    2617          INPUT(_type='file', _name='name',
    2618              requires=IS_IMAGE(extensions=('jpeg', 'png')))
    2619
    2620          #Check if uploaded file is PNG with maximum size of 200x200 pixels:
    2621          INPUT(_type='file', _name='name',
    2622              requires=IS_IMAGE(extensions=('png'), maxsize=(200, 200)))
    2623      """
    2624
    2625 -    def __init__(self,
    2626                   extensions=('bmp', 'gif', 'jpeg', 'png'),
    2627                   maxsize=(10000, 10000),
    2628                   minsize=(0, 0),
    2629                   error_message='invalid image'):
    2630
    2631          self.extensions = extensions
    2632          self.maxsize = maxsize
    2633          self.minsize = minsize
    2634          self.error_message = error_message
    2635
    2636 -    def __call__(self, value):
    2637          try:
    2638              extension = value.filename.rfind('.')
    2639              assert extension >= 0
    2640              extension = value.filename[extension + 1:].lower()
    2641              if extension == 'jpg':
    2642                  extension = 'jpeg'
    2643              assert extension in self.extensions
    2644              if extension == 'bmp':
    2645                  width, height = self.__bmp(value.file)
    2646              elif extension == 'gif':
    2647                  width, height = self.__gif(value.file)
    2648              elif extension == 'jpeg':
    2649                  width, height = self.__jpeg(value.file)
    2650              elif extension == 'png':
    2651                  width, height = self.__png(value.file)
    2652              else:
    2653                  width = -1
    2654                  height = -1
    2655              assert self.minsize[0] <= width <= self.maxsize[0] \
    2656                  and self.minsize[1] <= height <= self.maxsize[1]
    2657              value.file.seek(0)
    2658              return (value, None)
    2659          except:
    2660              return (value, translate(self.error_message))
    2661
    2662 -    def __bmp(self, stream):
    2663          if stream.read(2) == 'BM':
    2664              stream.read(16)
    2665              return struct.unpack("<LL", stream.read(8))
    2666          return (-1, -1)
    2667
    2668 -    def __gif(self, stream):
    2669          if stream.read(6) in ('GIF87a', 'GIF89a'):
    2670              stream = stream.read(5)
    2671              if len(stream) == 5:
    2672                  return tuple(struct.unpack("<HHB", stream)[:-1])
    2673          return (-1, -1)
    2674
    2675 -    def __jpeg(self, stream):
    2676          if stream.read(2) == '\xFF\xD8':
    2677              while True:
    2678                  (marker, code, length) = struct.unpack("!BBH", stream.read(4))
    2679                  if marker != 0xFF:
    2680                      break
    2681                  elif code >= 0xC0 and code <= 0xC3:
    2682                      return tuple(reversed(
    2683                          struct.unpack("!xHH", stream.read(5))))
    2684                  else:
    2685                      stream.read(length - 2)
    2686          return (-1, -1)
    2687
    2688 -    def __png(self, stream):
    2689          if stream.read(8) == '\211PNG\r\n\032\n':
    2690              stream.read(4)
    2691              if stream.read(4) == "IHDR":
    2692                  return struct.unpack("!LL", stream.read(8))
    2693          return (-1, -1)
    2694
    2695
    2696 -class IS_UPLOAD_FILENAME(Validator):
    2697      """
    2698      Checks if name and extension of file uploaded through file input matches
    2699      given criteria.
    2700
    2701      Does *not* ensure the file type in any way. Returns validation failure
    2702      if no data was uploaded.
    2703
    2704      Arguments::
    2705
    2706      filename: filename (before dot) regex
    2707      extension: extension (after dot) regex
    2708      lastdot: which dot should be used as a filename / extension separator:
    2709               True means last dot, eg. file.png -> file / png
    2710               False means first dot, eg. file.tar.gz -> file / tar.gz
    2711      case: 0 - keep the case, 1 - transform the string into lowercase (default),
    2712            2 - transform the string into uppercase
    2713
    2714      If there is no dot present, extension checks will be done against empty
    2715      string and filename checks against whole value.
    2716
    2717      Examples::
    2718
    2719          #Check if file has a pdf extension (case insensitive):
    2720          INPUT(_type='file', _name='name',
    2721              requires=IS_UPLOAD_FILENAME(extension='pdf'))
    2722
    2723          #Check if file has a tar.gz extension and name starting with backup:
    2724          INPUT(_type='file', _name='name',
    2725              requires=IS_UPLOAD_FILENAME(filename='backup.*',
    2726                  extension='tar.gz', lastdot=False))
    2727
    2728          #Check if file has no extension and name matching README
    2729          #(case sensitive):
    2730          INPUT(_type='file', _name='name',
    2731              requires=IS_UPLOAD_FILENAME(filename='^README$',
    2732                  extension='^$', case=0))
    2733      """
    2734
    2735 -    def __init__(self, filename=None, extension=None, lastdot=True, case=1,
    2736              error_message='enter valid filename'):
    2737          if isinstance(filename, str):
    2738              filename = re.compile(filename)
    2739          if isinstance(extension, str):
    2740              extension = re.compile(extension)
    2741          self.filename = filename
    2742          self.extension = extension
    2743          self.lastdot = lastdot
    2744          self.case = case
    2745          self.error_message = error_message
    2746
    2747 -    def __call__(self, value):
    2748          try:
    2749              string = value.filename
    2750          except:
    2751              return (value, translate(self.error_message))
    2752          if self.case == 1:
    2753              string = string.lower()
    2754          elif self.case == 2:
    2755              string = string.upper()
    2756          if self.lastdot:
    2757              dot = string.rfind('.')
    2758          else:
    2759              dot = string.find('.')
    2760          if dot == -1:
    2761              dot = len(string)
    2762          if self.filename and not self.filename.match(string[:dot]):
    2763              return (value, translate(self.error_message))
    2764          elif self.extension and not self.extension.match(string[dot + 1:]):
    2765              return (value, translate(self.error_message))
    2766          else:
    2767              return (value, None)
    2768
    2769
    2770 -class IS_IPV4(Validator):
    2771      """
    2772      Checks if field's value is an IP version 4 address in decimal form. Can
    2773      be set to force addresses from certain range.
    2774
    2775      IPv4 regex taken from: http://regexlib.com/REDetails.aspx?regexp_id=1411
    2776
    2777      Arguments:
    2778
    2779      minip: lowest allowed address; accepts:
    2780             str, eg. 192.168.0.1
    2781             list or tuple of octets, eg. [192, 168, 0, 1]
    2782      maxip: highest allowed address; same as above
    2783      invert: True to allow addresses only from outside of given range; note
    2784              that range boundaries are not matched this way
    2785      is_localhost: localhost address treatment:
    2786                    None (default): indifferent
    2787                    True (enforce): query address must match localhost address
    2788                                    (127.0.0.1)
    2789                    False (forbid): query address must not match localhost
    2790                                    address
    2791      is_private: same as above, except that query address is checked against
    2792                  two address ranges: 172.16.0.0 - 172.31.255.255 and
    2793                  192.168.0.0 - 192.168.255.255
    2794      is_automatic: same as above, except that query address is checked against
    2795                    one address range: 169.254.0.0 - 169.254.255.255
    2796
    2797      Minip and maxip may also be lists or tuples of addresses in all above
    2798      forms (str, int, list / tuple), allowing setup of multiple address ranges:
    2799
    2800          minip = (minip1, minip2, ... minipN)
    2801                     |       |           |
    2802                     |       |           |
    2803          maxip = (maxip1, maxip2, ... maxipN)
    2804
    2805      Longer iterable will be truncated to match length of shorter one.
    2806
    2807      Examples::
    2808
    2809          #Check for valid IPv4 address:
    2810          INPUT(_type='text', _name='name', requires=IS_IPV4())
    2811
    2812          #Check for valid IPv4 address belonging to specific range:
    2813          INPUT(_type='text', _name='name',
    2814              requires=IS_IPV4(minip='100.200.0.0', maxip='100.200.255.255'))
    2815
    2816          #Check for valid IPv4 address belonging to either 100.110.0.0 -
    2817          #100.110.255.255 or 200.50.0.0 - 200.50.0.255 address range:
    2818          INPUT(_type='text', _name='name',
    2819              requires=IS_IPV4(minip=('100.110.0.0', '200.50.0.0'),
    2820                               maxip=('100.110.255.255', '200.50.0.255')))
    2821
    2822          #Check for valid IPv4 address belonging to private address space:
    2823          INPUT(_type='text', _name='name', requires=IS_IPV4(is_private=True))
    2824
    2825          #Check for valid IPv4 address that is not a localhost address:
    2826          INPUT(_type='text', _name='name', requires=IS_IPV4(is_localhost=False))
    2827
    2828      >>> IS_IPV4()('1.2.3.4')
    2829      ('1.2.3.4', None)
    2830      >>> IS_IPV4()('255.255.255.255')
    2831      ('255.255.255.255', None)
    2832      >>> IS_IPV4()('1.2.3.4 ')
    2833      ('1.2.3.4 ', 'enter valid IPv4 address')
    2834      >>> IS_IPV4()('1.2.3.4.5')
    2835      ('1.2.3.4.5', 'enter valid IPv4 address')
    2836      >>> IS_IPV4()('123.123')
    2837      ('123.123', 'enter valid IPv4 address')
    2838      >>> IS_IPV4()('1111.2.3.4')
    2839      ('1111.2.3.4', 'enter valid IPv4 address')
    2840      >>> IS_IPV4()('0111.2.3.4')
    2841      ('0111.2.3.4', 'enter valid IPv4 address')
    2842      >>> IS_IPV4()('256.2.3.4')
    2843      ('256.2.3.4', 'enter valid IPv4 address')
    2844      >>> IS_IPV4()('300.2.3.4')
    2845      ('300.2.3.4', 'enter valid IPv4 address')
    2846      >>> IS_IPV4(minip='1.2.3.4', maxip='1.2.3.4')('1.2.3.4')
    2847      ('1.2.3.4', None)
    2848      >>> IS_IPV4(minip='1.2.3.5', maxip='1.2.3.9', error_message='bad ip')('1.2.3.4')
    2849      ('1.2.3.4', 'bad ip')
    2850      >>> IS_IPV4(maxip='1.2.3.4', invert=True)('127.0.0.1')
    2851      ('127.0.0.1', None)
    2852      >>> IS_IPV4(maxip='1.2.3.4', invert=True)('1.2.3.4')
    2853      ('1.2.3.4', 'enter valid IPv4 address')
    2854      >>> IS_IPV4(is_localhost=True)('127.0.0.1')
    2855      ('127.0.0.1', None)
    2856      >>> IS_IPV4(is_localhost=True)('1.2.3.4')
    2857      ('1.2.3.4', 'enter valid IPv4 address')
    2858      >>> IS_IPV4(is_localhost=False)('127.0.0.1')
    2859      ('127.0.0.1', 'enter valid IPv4 address')
    2860      >>> IS_IPV4(maxip='100.0.0.0', is_localhost=True)('127.0.0.1')
    2861      ('127.0.0.1', 'enter valid IPv4 address')
    2862      """
    2863
    2864      regex = re.compile(
    2865          '^(([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])$')
    2866      numbers = (16777216, 65536, 256, 1)
    2867      localhost = 2130706433
    2868      private = ((2886729728L, 2886795263L), (3232235520L, 3232301055L))
    2869      automatic = (2851995648L, 2852061183L)
    2870
    2871 -    def __init__(
    2872          self,
    2873          minip='0.0.0.0',
    2874          maxip='255.255.255.255',
    2875          invert=False,
    2876          is_localhost=None,
    2877          is_private=None,
    2878          is_automatic=None,
    2879          error_message='enter valid IPv4 address'):
    2880          for n, value in enumerate((minip, maxip)):
    2881              temp = []
    2882              if isinstance(value, str):
    2883                  temp.append(value.split('.'))
    2884              elif isinstance(value, (list, tuple)):
    2885                  if len(value) == len(filter(lambda item: isinstance(item, int), value)) == 4:
    2886                      temp.append(value)
    2887                  else:
    2888                      for item in value:
    2889                          if isinstance(item, str):
    2890                              temp.append(item.split('.'))
    2891                          elif isinstance(item, (list, tuple)):
    2892                              temp.append(item)
    2893              numbers = []
    2894              for item in temp:
    2895                  number = 0
    2896                  for i, j in zip(self.numbers, item):
    2897                      number += i * int(j)
    2898                  numbers.append(number)
    2899              if n == 0:
    2900                  self.minip = numbers
    2901              else:
    2902                  self.maxip = numbers
    2903          self.invert = invert
    2904          self.is_localhost = is_localhost
    2905          self.is_private = is_private
    2906          self.is_automatic = is_automatic
    2907          self.error_message = error_message
    2908
    2909 -    def __call__(self, value):
    2910          if self.regex.match(value):
    2911              number = 0
    2912              for i, j in zip(self.numbers, value.split('.')):
    2913                  number += i * int(j)
    2914              ok = False
    2915              for bottom, top in zip(self.minip, self.maxip):
    2916                  if self.invert != (bottom <= number <= top):
    2917                      ok = True
    2918              if not (self.is_localhost == None or self.is_localhost == \
    2919                  (number == self.localhost)):
    2920                      ok = False
    2921              if not (self.is_private == None or self.is_private == \
    2922                  (sum([number[0] <= number <= number[1] for number in self.private]) > 0)):
    2923                      ok = False
    2924              if not (self.is_automatic == None or self.is_automatic == \
    2925                  (self.automatic[0] <= number <= self.automatic[1])):
    2926                      ok = False
    2927              if ok:
    2928                  return (value, None)
    2929          return (value, translate(self.error_message))
    2930
    2931  if __name__ == '__main__':
    2932      import doctest
    2933      doctest.testmod()
    2934