web2py.gluon.validators

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