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