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