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

Source Code for Module web2py.gluon.validators

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