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