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

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