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